What Checkout UI Extensions actually are
If you've worked with Shopify themes for a while, you know checkout has always been the locked room. On Shopify Plus you could edit checkout.liquid, but it was fragile, largely unsupported, and Shopify deprecated it in 2024. For everyone else, checkout was completely off-limits to theme code.
Checkout UI Extensions are the official replacement. They're React-based components (using a Shopify-specific UI library called Checkout Components) that render inside the checkout at specific extension targets — predefined slots in the checkout page where your UI appears. You deploy them as part of a Shopify app or a theme app extension, and merchants install them through the Shopify admin.
Key mental model: Extensions don't inject arbitrary HTML into checkout. They render Shopify's own components (Text, Button, BlockStack, Banner, etc.) at specific targets. You control the logic and content — Shopify controls the rendering, styling, and security boundary.
This architecture has real implications for what you can build. You get access to live cart data, customer info, and checkout APIs. You can't load external scripts, make arbitrary fetch calls to third-party servers, or add custom CSS that overrides checkout styles. The sandbox is intentional and non-negotiable.
Setting up your development environment
You'll need:
- Node.js 18+
- Shopify CLI 3+ (
npm install -g @shopify/cli) - A development store with a Shopify Partner account
- A Shopify app to attach the extension to (can be a new app created purely for this)
Create a new app or navigate to an existing one, then generate the extension:
# From inside your Shopify app directory shopify app generate extension # Choose: UI extension → Checkout UI # Give it a name, e.g. "checkout-gift-note"
This scaffolds an extensions/checkout-gift-note/ folder with a src/Checkout.jsx entry point and an extension.toml config file. Everything you need to start is already wired up.
Extension targets — where your UI goes
The target is the most important configuration decision. It determines which step of checkout your extension renders on, and where on the page it appears. You set it in extension.toml.
The most commonly used targets:
| Target | Where it renders | Best for |
|---|---|---|
| purchase.checkout.block.render | Flexible — merchant places it in checkout editor | Most use cases. Custom blocks the merchant positions. |
| purchase.checkout.cart-line-item.render-after | After each cart line item | Per-item messaging, gift options, personalisation |
| purchase.checkout.shipping-option-item.render-after | After each shipping option | Additional info about delivery, estimated dates |
| purchase.checkout.contact.render-after | After the contact info section | Newsletter opt-in, loyalty programme prompts |
| purchase.checkout.payment-method-list.render-after | After payment methods | Trust signals, instalment explanations |
| purchase.checkout.order-summary.render-after | After the order summary | Upsells, cross-sells, bundle offers |
| purchase.thank-you.block.render | Thank you / order confirmation page | Post-purchase upsells, referral prompts, surveys |
Use purchase.checkout.block.render by default. It gives merchants flexibility to place your extension in the checkout editor, and it works across checkout and thank-you page. Reach for the more specific targets only when placement must be tied to a specific UI element (e.g., per-line-item).
The component model — what you can render
You're not writing arbitrary JSX. The Checkout Components library is a closed set — only components exported from @shopify/ui-extensions-react/checkout are available. Shopify renders them using the checkout's own design system, which automatically respects the merchant's branding configuration.
The components you'll use most:
BlockStack,InlineStack— layout primitives, vertical/horizontal stackingText— body text withsize,emphasis,appearancepropsHeading— section titleButton— styled action buttonBanner— informational or error messages withstatusvariantsTextField— input field with label, validation, and change handlerCheckbox,Select— form controlsDivider,Image,Icon— utility components
You can't add custom HTML, load images from arbitrary URLs without declaring them in the extension config, or use any component not in this library. Working within these constraints is a skill — but the constraints also mean your extension will always look consistent with the merchant's checkout.
Reading checkout data with hooks
The real power of checkout extensions is read access to live checkout state via hooks. All hooks are imported from @shopify/ui-extensions-react/checkout.
import { reactExtension, useCartLines, useTotalAmount, useShippingAddress, useAttributeValues, useApplyAttributeChange, BlockStack, Text, TextField, Button, } from '@shopify/ui-extensions-react/checkout'; export default reactExtension( 'purchase.checkout.block.render', () => <GiftNoteExtension /> ); function GiftNoteExtension() { const lines = useCartLines(); const total = useTotalAmount(); const [giftNote] = useAttributeValues(['gift_note']); const applyChange = useApplyAttributeChange(); return ( <BlockStack> <Text>{lines.length} item(s) · {total.amount} {total.currencyCode}</Text> <TextField label="Gift note" value={giftNote ?? ''} onChange={async (value) => { await applyChange({ type: 'updateAttribute', key: 'gift_note', value, }); }} /> </BlockStack> ); }
The applyChange pattern is how you write data back to the checkout. Every mutation goes through useApply* hooks — there's one for cart line attributes, order attributes, discount codes, and more. The API is deliberately narrow: you can only change what Shopify allows extensions to change.
Useful hooks reference
useCartLines()— array of cart line items with variant, quantity, priceuseTotalAmount()— current order total with currencyuseShippingAddress()— customer's shipping address (available after address step)useBuyerIdentity()— email, phone, customer identity if logged inuseAttributeValues(keys)— read cart attributes by keyuseApplyAttributeChange()— write to cart attributesuseApplyCartLinesChange()— add, remove, or update quantitiesuseApplyDiscountCodeChange()— apply or remove discount codesuseSettings()— read merchant-configured settings from the checkout editor
Merchant-configurable settings
One of the most useful features: you can expose settings that merchants configure directly in the checkout editor, without any code changes. Define them in extension.toml:
[[extensions.settings.fields]] key = "heading_text" type = "single_line_text_field" name = "Heading" [[extensions.settings.fields]] key = "show_for_orders_above" type = "number_integer" name = "Show for orders above (USD)"
Then in your component:
const { heading_text, show_for_orders_above } = useSettings(); const total = useTotalAmount(); if (show_for_orders_above && total.amount < show_for_orders_above) { return null; // don't render below threshold } return <Text>{heading_text ?? 'Add a gift note'}</Text>;
This is powerful. The same extension can behave differently on different stores without any code forking. Merchants self-serve the configuration. The pattern I used most in agency work: expose the heading, enable/disable the feature, and a threshold amount — no merchant needs to call a developer to tweak copy.
Making external API calls — the right way
You can't make arbitrary fetch() calls to external URLs from an extension. Any external URL your extension fetches must be declared in the network_access section of extension.toml, and even then only HTTPS GET requests to explicitly allowlisted domains are permitted.
[extensions.capabilities] network_access = true [[extensions.settings.fields]] # ... your settings
import { useEffect, useState } from 'react'; import { useCartLines } from '@shopify/ui-extensions-react/checkout'; function UpsellExtension() { const lines = useCartLines(); const [offer, setOffer] = useState(null); useEffect(() => { const variantIds = lines.map(l => l.merchandise.id); // fetch must be to a declared, allowlisted domain fetch(`https://your-app.myshopify.com/api/offers?ids=${variantIds.join(',')}`) .then(r => r.json()) .then(setOffer); }, [lines]); if (!offer) return null; return <Text>{offer.message}</Text>; }
Performance note: Network requests in checkout extensions slow down the checkout experience. Keep them to a minimum, cache aggressively on your API side, and always handle the loading and error states — the extension must not break checkout if your API is down.
Local development and preview
Start the dev server from your app root:
shopify app dev
This opens a tunnel to your local machine and gives you a preview URL. Navigate to your development store's checkout, add ?dev to the URL, and your extension renders live with hot reload. Changes to your JSX appear in the checkout in real time — no redeploy needed.
The workflow is genuinely good. You can iterate on UI changes in seconds. Where you'll spend more time is in the deploy-and-verify cycle for settings changes (those require pushing to Shopify's servers) and for any changes to extension.toml.
Deploying and activating
When you're ready to deploy:
shopify app deploy
This pushes the extension to Shopify. But deploying doesn't make it live — merchants still need to activate it in their checkout editor. For your own store:
- Go to Shopify Admin → Settings → Checkout → Customize
- In the checkout editor, find your extension in the "Apps" section in the left panel
- Drag it to the target position — or if it's a
purchase.checkout.block.rendertype, it will appear in the available blocks list - Configure any settings you exposed
- Save
For client stores in an agency context, the merchant installs your app and follows the same steps. You can also set a default layout in your extension config so the block appears automatically in a suggested position on first install.
What you can't do — the real constraints
Before you commit to building something as a checkout extension, verify these constraints won't block you:
- No arbitrary HTML or CSS. Only Checkout Components. You can't add a custom font, override the checkout's colour scheme, or use flexbox/grid directly.
- No external scripts. No GTM, no Klaviyo tracking, no pixel fires. Any tracking integration needs to go through Shopify's built-in Customer Events system, not the extension.
- No access to
windowordocument. The extension runs in a sandboxed environment — no DOM access outside your rendered component tree. - Images must be declared. Any
<Image>src must point to a URL you've declared in the extension config. Ad-hoc image loading doesn't work. - Available only on Shopify's checkout. This doesn't work on the cart page, in the theme, or anywhere outside the checkout and post-purchase flow.
When not to use an extension: if the feature requires custom styling that must precisely match a non-standard design, injecting arbitrary scripts, or deeply integrating with third-party systems at checkout — you're looking at a Shopify Plus app or a post-checkout solution instead.
Real use cases from production
Features I've built or seen work well as checkout UI extensions:
- Gift note field — the canonical use case. Text field writes to order attributes, picked up in the order admin and fulfilment flow.
- Delivery date picker — Select component with available dates, writes to order attributes.
- Newsletter opt-in — Checkbox at the contact step, handled by a backend webhook on order creation.
- Loyalty points display — Banner showing how many points the customer earns on this order (read from a loyalty API call).
- Order upsell block — Banner with a single product offer after the cart summary; uses
useApplyCartLinesChangeto add the item when accepted. - Trust signals block — Static content: icons + copy about returns policy, security badges, support contact. Merchant-configured copy via settings.
The pattern across all of these: keep the extension focused on one job, expose the copy and threshold logic as settings, and make sure the fallback (when the extension has no data or errors) is a clean no-render rather than a broken UI.