Shopify's checkout is the most constrained design surface in modern e-commerce. That's deliberate. Shopify processes billions of dollars in transactions, and they've learned — through years of A/B testing across millions of stores — that every pixel of the checkout flow affects conversion. So when they opened the checkout to third-party developers through the Checkout UI Extensions API, they opened it with guardrails. Thick ones.
These notes come from six Shopify Plus builds we've done in the last eighteen months. They're the things we wish someone had told us before the first one.
§ 01The sandbox you're working in
The first thing to understand about checkout extensions is that you don't control the checkout. You contribute to it. Shopify renders the checkout. Your extension renders inside a sandbox within the checkout. You cannot access the DOM. You cannot inject CSS. You cannot run arbitrary JavaScript. You get a set of React-like components provided by Shopify, and you compose them into a UI that Shopify renders on your behalf.
This feels restrictive coming from a standard web development context, and it is. But the restrictions exist for conversion and security reasons that are hard to argue with. Every checkout extension runs in a Web Worker, isolated from the main thread and from other extensions. Your code cannot crash the checkout, cannot read payment information, and cannot modify the layout outside your designated extension point.
The extension points are specific locations in the checkout where your UI can appear: before the shipping method, after the order summary, beside the payment form. You pick a target, you build a component, and Shopify slots it in. If you're used to building freeform UIs, this is a significant mental shift. You're not designing a page. You're designing a widget that lives inside someone else's page.
§ 02What the checkout UI extensions API gives you
The component library is intentionally small. You get Banner, BlockStack, InlineStack, Text, TextField, Checkbox, Select, Button, Divider, Image, and a handful of others. No custom styling. No CSS classes. The components accept a size prop for text and a few layout props for spacing, and that's it.
You also get a set of APIs for reading checkout state: the current cart, the customer's shipping address, the selected shipping method, applied discounts. You can subscribe to changes in these values and update your UI reactively. You can also write to certain parts of the state — adding attributes to the order, applying discount codes, or updating metafields — which gives you meaningful control over the transaction even if you can't control the visual presentation.
// A minimal checkout extension that adds a gift note import { extension, TextField, BlockStack, Text } from "@shopify/ui-extensions/checkout"; export default extension( "purchase.checkout.block.render", (root, { applyAttributeChange }) => { const stack = root.createComponent(BlockStack); const label = root.createComponent(Text, { size: "small" }, "Add a gift note"); const input = root.createComponent(TextField, { label: "Gift message", onChange: (val) => applyAttributeChange({ key: "gift_note", value: val, type: "updateAttribute" }) }); stack.appendChild(label); stack.appendChild(input); root.appendChild(stack); } );
You're not designing a page. You're designing a widget that lives inside someone else's page, and that changes everything about how you think.
§ 03Design constraints that matter
The biggest design constraint is that you cannot control typography, color, or spacing in any meaningful way. Your extension inherits the merchant's checkout branding settings. If the merchant has chosen a blue accent color and Helvetica, your extension uses a blue accent color and Helvetica. This is good for consistency. It's frustrating if you're building a branded upsell widget that needs to stand out.
The second constraint is space. Extension points give you a column-width slot, and you cannot overflow it. There is no mechanism for modals, popovers, or overlays inside a checkout extension. If your design requires a popover to show additional product details, you need to rethink the design. We've found that progressive disclosure through collapsible sections (using Disclosure) works, but the user interaction is different from what most designers assume.
The third constraint is performance. Shopify enforces strict performance budgets on checkout extensions. If your extension takes too long to render or makes too many network requests, it gets killed. The timeout is generous enough for reasonable use cases, but it rules out anything that requires heavy computation or large data fetches at render time.
§ 04Theme extensions vs app blocks
This is where most teams get confused. Shopify has two systems that sound similar but work completely differently:
- Checkout UI extensions render inside the checkout flow itself. They run in a sandbox, use Shopify's component library, and are restricted to Shopify Plus merchants. This is what we've been discussing.
- Theme app extensions (app blocks) render inside the storefront theme — product pages, collection pages, the cart page. They're built with Liquid, HTML, CSS, and JavaScript. They have full control over their rendering. They work on any Shopify plan.
The confusion arises because both are called "extensions" and both are distributed through Shopify apps. But the development experience is completely different. Theme app extensions are essentially Liquid snippets that merchants can drag and drop into their theme editor. They have no sandbox restrictions. You can write whatever HTML and CSS you want.
Use theme extensions for the storefront, checkout extensions for the checkout. Don't mix them up.
— Team decision log, project #3
Our rule of thumb: if the feature lives before the "Buy" button, use a theme app extension. If it lives after, use a checkout UI extension. The cart page is a grey area — it's technically the storefront, but it's functionally the beginning of checkout. We usually build cart features as theme extensions because the design flexibility matters more at that stage.
§ 05What works and what doesn't
Things that work well in checkout extensions:
- Order-level upsells. "Add gift wrapping for $5" with a checkbox. Simple, high-converting, easy to build. The
applyCartLinesChangeAPI handles adding the product variant to the cart. - Custom form fields. Delivery instructions, gift notes, company tax IDs. These map directly to order attributes or metafields and the API is straightforward.
- Loyalty program display. Showing the customer their points balance and what they'll earn from this order. Read-only, simple layout, high perceived value.
- Dynamic banners. Free shipping thresholds, limited-time offers, trust badges. The
Bannercomponent handles these natively.
Things that don't work well:
- Complex product selectors. If the upsell requires choosing a size, color, and quantity, the checkout extension component library doesn't have enough richness to make this feel good. Push this upstream to the cart page.
- Anything requiring external auth. OAuth flows, third-party login, redirect-based integrations. The sandbox won't let you navigate away from the checkout, and iframes are not available.
- Heavy personalization. If you need to call a recommendation engine, wait for a response, and render a carousel of products, you'll likely hit the performance budget. Pre-compute recommendations and cache them.
§ 06Practical advice from six builds
Start with the Shopify CLI and the extension template. Don't try to set up the build pipeline yourself — the CLI generates a working extension with hot reloading against a development store in under two minutes. The development experience is genuinely good once you're inside it.
Test on real devices early. The checkout looks different on mobile than on desktop, and mobile is where 70%+ of Shopify traffic happens. Extension points render in a single column on mobile, and space is even more constrained than on desktop.
Use metafields for configuration instead of environment variables. Metafields are editable by the merchant through the Shopify admin, which means you can ship configuration changes without redeploying the extension. We store things like upsell product IDs, banner copy, and feature flags in shop metafields.
Version your extensions aggressively. Shopify's extension versioning system lets you have multiple versions deployed simultaneously. We keep the stable version active and deploy the next version to a draft checkout profile for testing. Rollback is instant — switch the active version in the admin.
Finally, accept the constraints. The developers who struggle most with checkout extensions are the ones fighting the sandbox. The ones who ship fastest are the ones who design within the lines from the start. Shopify's checkout converts well because it's consistent, fast, and familiar. Your extension should enhance that, not fight it.
— End of essay. Building on Shopify Plus? We've done this. Start a project →