Shopify checkout used to be a locked box. For years, only Shopify Plus merchants could edit checkout.liquid, and even then the work felt brittle. That era is over. Shopify checkout UI extensions now give you a supported, upgrade-safe way to inject custom components directly into the checkout flow.
This guide walks you through the full lifecycle. You will scaffold a project, understand extension targets, build real components, handle data, and deploy. By the end, you will know how to customize Shopify checkout without the fragility of legacy liquid overrides.
Why Checkout UI Extensions Replaced checkout.liquid
Shopify deprecated checkout.liquid customizations for the Information, Shipping, and Payment steps. The hard deadline pushed every merchant toward Checkout Extensibility.
The change matters for three reasons.
- Upgrade safety. Extensions survive Shopify platform updates automatically.
- Security. Your code runs in a sandboxed web worker, isolated from the main checkout thread.
- Performance. Shopify controls rendering, so your extension cannot slow checkout or break PCI compliance.
The tradeoff is control. You no longer write raw HTML and CSS into the page. Instead, you compose Shopify’s design-system components into fixed extension points. That constraint keeps checkout fast and consistent, which protects conversion.
If you have lived inside theme code, think of this as a cleaner contract. The same discipline you apply in a Shopify Liquid tutorial applies here, except the rendering surface is managed and typed.
Prerequisites Before You Start
You need a few things in place before writing any code.
| Requirement | Details |
|---|---|
| Shopify Partner account | Create a development store for testing |
| Node.js | Version 18 or higher recommended |
| Shopify CLI | Version 3.x for scaffolding and deployment |
| Checkout Extensibility | Enabled on the store (default for new Plus stores) |
| Basic React knowledge | Extensions use a React-based rendering model |
Checkout UI extensions ship inside an app. So the broader workflow mirrors how to build a Shopify app from scratch. If you have never deployed an app, read that first, then return here.
Understanding Extension Targets
A target is a named location in checkout where your extension renders. Shopify defines dozens of them. You pick one in your configuration file, and Shopify slots your component into that exact spot.
Common static targets include the following.
| Target | Location |
|---|---|
purchase.checkout.block.render |
Flexible block placed by the merchant |
purchase.checkout.delivery-address.render-before |
Above the shipping address form |
purchase.checkout.payment-method-list.render-after |
After payment options |
purchase.checkout.reductions.render-after |
Near discount fields |
purchase.thank-you.block.render |
Thank you page |
The block.render target is special. It lets merchants position your extension anywhere using the checkout editor, which makes your app far more flexible than a hardcoded slot.
Scaffolding Your First Extension
Start by generating an app if you do not already have one.
npm init @shopify/app@latest
Move into the app directory, then generate the checkout extension.
shopify app generate extension --type=checkout_ui
The CLI asks for a name and language. Choose TypeScript for type safety. Shopify creates a folder under extensions/ with a predictable structure.
extensions/
my-checkout-ext/
shopify.extension.toml
src/
Checkout.tsx
package.json
The shopify.extension.toml file controls where your extension appears and what it can access.
Configuring the Extension
Open shopify.extension.toml. This file is the contract between your code and the checkout.
api_version = "2024-10" [[extensions]] name = "custom-checkout-banner" handle = "custom-checkout-banner" type = "ui_extension" [[extensions.targeting]] target = "purchase.checkout.block.render" module = "./src/Checkout.tsx" [extensions.capabilities] network_access = true block_progress = false api_access = true
A few fields deserve attention.
network_accesslets your extension call external APIs. Request it only when needed.block_progresscan stop a buyer from advancing. Use it carefully, because it affects conversion.api_accessenables the Storefront API from within checkout.
Keep capabilities minimal. Every permission you request appears during app review and affects merchant trust.
Building a Real Component
Let us build a component that shows a free shipping progress bar. It reads the cart total, compares it to a threshold, and nudges buyers toward a larger order.
Open src/Checkout.tsx.
import {
reactExtension,
Banner,
BlockStack,
Text,
useSubtotalAmount,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.block.render",
() => <FreeShippingNudge />
);
function FreeShippingNudge() {
const subtotal = useSubtotalAmount();
const threshold = 50;
if (!subtotal) return null;
const remaining = threshold - subtotal.amount;
if (remaining <= 0) {
return (
<Banner status="success" title="You qualify for free shipping" />
);
}
return (
<BlockStack>
<Text emphasis="bold">
Add {remaining.toFixed(2)} {subtotal.currencyCode} more for free shipping
</Text>
</BlockStack>
);
}
Notice what you do not do. You never write CSS. You compose Banner, Text, and BlockStack, which inherit the merchant’s checkout styling automatically. That consistency is the whole point of the platform.
Running the Extension Locally
Start the dev server with one command.
shopify app dev
The CLI prints a preview URL. Open it, add products to a cart, and reach checkout. Your extension renders live. Hot reload updates the component as you edit, so iteration stays fast.
During development, keep your dev store data clean. The same version control habits from Shopify theme testing with Git apply to extension code. Commit early, branch per feature, and review before merge.
Reading Checkout Data with Hooks
Shopify exposes checkout state through React hooks. These are the backbone of any dynamic extension.
| Hook | Purpose |
|---|---|
useSubtotalAmount |
Current cart subtotal |
useCartLines |
Line items in the cart |
useShippingAddress |
Buyer shipping details |
useApplyCartLinesChange |
Add or update line items |
useApplyAttributeChange |
Write custom checkout attributes |
useBuyerJourneyIntercept |
Validate and block progress |
Hooks are reactive. When the buyer changes a quantity, your component re-renders with fresh data. You never poll or refetch manually.
Writing Data Back to Checkout
Reading is half the story. Extensions can also modify the order. Here is a component that lets buyers add gift wrapping.
import {
reactExtension,
Checkbox,
useApplyCartLinesChange,
useCartLines,
} from "@shopify/ui-extensions-react/checkout";
import { useState } from "react";
export default reactExtension(
"purchase.checkout.block.render",
() => <GiftWrap />
);
function GiftWrap() {
const applyChange = useApplyCartLinesChange();
const lines = useCartLines();
const [checked, setChecked] = useState(false);
const wrapVariantId = "gid://shopify/ProductVariant/1234567890";
async function toggle(value: boolean) {
setChecked(value);
if (value) {
await applyChange({
type: "addCartLine",
merchandiseId: wrapVariantId,
quantity: 1,
});
}
}
return (
<Checkbox checked={checked} onChange={toggle}>
Add gift wrapping for 4.99
</Checkbox>
);
}
This pattern turns checkout into an upsell surface without redirecting the buyer. The change applies instantly and updates the order summary.
Validating Input and Blocking Progress
Sometimes you need to stop checkout until a condition is met. The useBuyerJourneyIntercept hook handles this.
import {
reactExtension,
useBuyerJourneyIntercept,
useShippingAddress,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.delivery-address.render-after",
() => <PoBoxBlock />
);
function PoBoxBlock() {
const address = useShippingAddress();
useBuyerJourneyIntercept(({ canBlockProgress }) => {
const isPoBox = /p\.?o\.? box/i.test(address?.address1 ?? "");
if (canBlockProgress && isPoBox) {
return {
behavior: "block",
reason: "PO boxes are not supported",
errors: [
{
message: "We cannot ship to PO boxes. Please use a street address.",
},
],
};
}
return { behavior: "allow" };
});
return null;
}
Set block_progress = true in the TOML file for this to work. Use interception sparingly. Every block is a potential drop-off point, so reserve it for real business rules.
Connecting to External Data
Many extensions need data that does not live in checkout. Loyalty points, inventory from a warehouse, or a personalized message all require a network call.
When network_access is enabled, you can fetch from your app backend.
import { useEffect, useState } from "react";
import {
reactExtension,
Text,
useApi,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.block.render",
() => <LoyaltyPoints />
);
function LoyaltyPoints() {
const { sessionToken } = useApi();
const [points, setPoints] = useState<number | null>(null);
useEffect(() => {
async function load() {
const token = await sessionToken.get();
const res = await fetch("https://your-app.com/api/points", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
setPoints(data.points);
}
load();
}, []);
if (points === null) return null;
return <Text>You have {points} loyalty points to redeem.</Text>;
}
Always verify the session token on your backend. That token proves the request came from a genuine checkout session.
Backend calls add latency, so design for speed. The techniques in building high-performance Shopify API clients help here, and Shopify GraphQL caching can shave off repeated lookups. For heavier read patterns, a dedicated caching layer keeps checkout snappy.
Storing Custom Data
Checkout extensions often capture information you need after the sale. A delivery date, a gift note, or a build-your-own-bundle config all need a home.
You have two options.
- Cart and order attributes via
useApplyAttributeChange. Simple key-value data attached to the order. - Metafields for structured, reusable data tied to products, customers, or orders.
For anything beyond basic strings, metafields win. They give you typed, queryable storage. The Shopify metafields guide covers definitions, access scopes, and how to read them back through the Admin API.
Here is an attribute example.
import {
reactExtension,
TextField,
useApplyAttributeChange,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.block.render",
() => <GiftNote />
);
function GiftNote() {
const applyAttributeChange = useApplyAttributeChange();
return (
<TextField
label="Gift note"
multiline={3}
onChange={(value) =>
applyAttributeChange({
type: "updateAttribute",
key: "gift_note",
value,
})
}
/>
);
}
That note now rides along with the order and appears in the Admin.
Reacting to the Completed Order
After checkout completes, your app often needs to act. Sync the order to a CRM, trigger fulfillment, or update loyalty balances.
Do not try to do this inside the extension. The extension is a UI layer, not a durable backend. Instead, lean on Shopify webhooks to catch the orders/create event.
Design those consumers to be resilient. A robust setup follows the patterns in building reliable Shopify webhook consumers and guards against duplicate webhook events. For failures, a dead letter queue keeps lost events recoverable.
If you push order data outward, something like CRM synchronization strategies and solid data mapping prevents drift between systems. High-volume stores should also review async event processing architectures to keep the pipeline from backing up.
Deploying to Production
When your extension is ready, deploy it.
shopify app deploy
This pushes a new app version. Merchants then add your extension through the checkout editor and position any block.render targets where they want.
Deployment creates a versioned release. If something breaks, you roll back to a prior version from the Partner Dashboard. Treat every deploy as a release, not a hotfix.
Testing and Quality Checks
A checkout bug costs real revenue. Test thoroughly before you ship.
| Test area | What to verify |
|---|---|
| Empty cart states | Component hides or degrades gracefully |
| Currency and locale | Amounts and text adapt correctly |
| Network failure | Extension does not crash on failed fetch |
| Progress blocking | Buyers can resolve the block and continue |
| Mobile rendering | Components stack cleanly on small screens |
Run through each on a real device. Shopify’s preview is accurate, but nothing beats a live checkout on a phone.
Common Pitfalls to Avoid
A few mistakes show up repeatedly.
- Overusing
block_progress. Every block risks abandonment. - Slow network calls. A 2-second fetch feels broken at checkout.
- Ignoring localization. Hardcoded strings break multi-market stores.
- Requesting unnecessary capabilities. It slows app review and erodes trust.
- Treating the extension as a backend. Keep heavy logic server-side.
Stores running at serious volume should also study high-traffic Shopify architecture patterns, since checkout traffic spikes expose weak backends fast.
Performance and Scale Considerations
Checkout is the most performance-sensitive page on any store. Shopify enforces limits, but your backend is still your responsibility.
Keep these principles front of mind.
- Cache aggressively on reads that rarely change.
- Batch backend work instead of firing many small calls.
- Use async architectures so slow jobs never block the buyer.
- Return minimal payloads from your API.
For teams integrating with ERPs or fulfillment, the extension is only the visible tip. The durable work lives in your middleware. Review resilient Shopify middleware and fault-tolerant integration before wiring checkout into critical systems.
Wrapping Up
Shopify checkout UI extensions give you a modern, supported path to a tailored buying experience. You scaffold through the CLI, pick a target, compose Shopify components, read and write checkout state with hooks, and deploy as a versioned app.
The constraints are a feature, not a limitation. They keep checkout fast, secure, and consistent while still letting you add real value. Start with one small extension, measure its impact on conversion, then expand. That disciplined approach turns this Shopify checkout customization guide into measurable results.
FAQs
What are Shopify checkout UI extensions?
They are sandboxed, React-based components that render inside Shopify checkout at defined targets. They let you add custom content and logic without editing checkout.liquid, and they survive platform updates automatically.
Do I need Shopify Plus to use checkout extensions?
Checkout UI extensions work on all plans for the Thank You and Order Status pages. Full customization of the Information, Shipping, and Payment steps requires Shopify Plus with Checkout Extensibility enabled.
Can checkout extensions call external APIs?
Yes. Enable network_access in the extension config, then fetch from your app backend using a verified session token. Keep calls fast, since latency directly affects checkout conversion.
How do I store custom data collected at checkout?
Use cart and order attributes for simple key-value data, or metafields for structured, queryable storage. Attributes attach to the order and appear in the Admin after purchase.
Can an extension stop a buyer from completing checkout?
Yes, through the useBuyerJourneyIntercept hook with block_progress enabled. Use it only for real validation rules, because every block increases the risk of cart abandonment.
How do I deploy a checkout UI extension?
Run shopify app deploy to publish a new app version. Merchants then add and position the extension through the checkout editor, and you can roll back to earlier versions from the Partner Dashboard.
