From Reading About Extensibility to Actually Shipping Code
You have until August 26, 2026 before Shopify auto-upgrades every non-Plus store's Thank You and Order Status pages and strips whatever legacy customizations are still clinging to them. That date turns Shopify checkout UI extensions from a nice-to-know into the only sanctioned way to put custom UI back into checkout — and this guide gets you from zero to a deployed extension, with real commands and real code.
This is a build tutorial, not a concept explainer. If you want the background on why checkout.liquid died and what replaced it, read our checkout extensibility explainer first. If you need the full countdown on the deadline itself, we covered the 2026 migration deadline in depth. Here, we assume you know what extensibility is and you want to build with it.
By the end you will have scaffolded an extension with the Shopify CLI, picked the right target, rendered UI with Polaris web components, read and written checkout state, exposed merchant settings, and deployed — all while staying under the 64KB bundle limit.
What You Need Before Starting
- Node.js 20+ and the latest Shopify CLI (
npm install -g @shopify/cli) - A Partner account and a development store. To preview extensions on the information, shipping, and payment steps, your dev store needs the Checkout and Customer Accounts Extensibility preview (dev stores get Plus features for testing)
- Chrome or Firefox for the Dev Console preview
- Basic comfort with JSX — extensions render with Preact, not full React
Who This Guide Is For
Developers and technical merchants on any plan. Post-purchase surfaces — the Thank You and Order Status pages — accept checkout UI extensions on every plan except Starter, so you do not need Plus to ship something real today. We flag the Plus-only parts explicitly as we go.
How Long This Takes
Scaffolding takes five minutes. A working banner takes fifteen. A production-ready extension with settings, validation, and deploy review takes an afternoon. Budget accordingly and build the banner first — momentum matters.
How Shopify Checkout UI Extensions Work Under the Hood

Before writing code, understand the three-part architecture, because it explains every constraint you are about to hit. Shopify checkout UI extensions consist of targets (where your UI appears), target APIs (what data you can touch), and web components (what you can render). The official checkout UI extensions reference documents all three, currently at API version 2026-04.
Sandboxed by Design
Your extension runs in an isolated web worker, completely separate from the checkout page's DOM. You never get direct access to checkout HTML, other extensions, or payment fields. That isolation is the entire point: it is why extensions survive platform upgrades and why Shopify lets third-party code anywhere near its highest-converting page.
The practical consequences:
- No `document`, no `window` access to the host page — you describe UI, Shopify renders it
- No arbitrary CSS or script injection — styling happens through component properties
- Network calls require an explicit capability flag in your config file
Polaris Web Components and Remote-DOM
You build UI from Shopify-supplied web components — s-banner, s-text-field, s-stack, s-button, and dozens more — that follow the Polaris design system. Under the hood they are powered by remote-dom, Shopify's library for mirroring a component tree from your sandboxed worker into the real checkout page. The full API definitions live in the open-source Shopify/ui-extensions repository if you want to read the source.
The mental model: you compose approved building blocks; Shopify owns the rendering, the accessibility, and the visual consistency with the merchant's brand settings.
The Global shopify Object
Inside your extension, a global shopify object exposes checkout state as reactive signals — shopify.cost, shopify.lines, shopify.buyerIdentity, shopify.settings — plus methods like applyAttributeChange for writes. Reading a signal's current value uses .value; in Preact components, accessing signals makes the component re-render automatically when checkout state changes.
Scaffold Your First Extension With the Shopify CLI

Everything starts with the CLI. The flow for checkout UI extensions is identical whether you are building a private customization for one store or a public app.
Create or Reuse an App
Extensions live inside an app, so create one if you do not have one:
shopify app init
cd your-app-namePick the "Start by adding your first extension" template when prompted — you do not need the full Remix app scaffold for a checkout-only customization.
Generate the Extension
shopify app generate extension --name checkout-bannerThe CLI asks which extension type you want — choose Checkout UI — and which language flavor (JavaScript or TypeScript, with JSX). It then writes a new directory under extensions/.
Tour the Generated Files
extensions/checkout-banner/
├── shopify.extension.toml # config: targets, capabilities, settings
├── src/
│ └── Checkout.jsx # your extension code
├── locales/
│ └── en.default.json # translatable strings
└── package.jsonThe two files you will live in are shopify.extension.toml (where the extension renders, what it can access, what merchants can configure) and Checkout.jsx (what it does). A minimal config looks like this:
api_version = "2026-04"
[[extensions]]
type = "ui_extension"
name = "Checkout banner"
handle = "checkout-banner"
[[extensions.targeting]]
module = "./src/Checkout.jsx"
target = "purchase.checkout.block.render"Choose Your Extension Target
The target value is the single most consequential line in your config. It determines where your checkout UI extension can appear, which APIs you get, and which plans can use it.
Block Targets vs Static Targets
Block targets (like purchase.checkout.block.render) are merchant-positionable: the merchant drags your extension anywhere supported in the checkout editor. Use these for banners, badges, and content that does not depend on a specific form.
Static targets render at a fixed location tied to a specific piece of checkout UI — for example, purchase.checkout.delivery-address.render-after always appears directly below the delivery address form. Use these when your UI only makes sense next to one element, like a delivery note under the address fields.
The Target Map
A non-exhaustive map of the targets you will reach for most:
| Target | Renders | Typical Use |
|---|---|---|
| purchase.checkout.block.render | Anywhere the merchant places it (checkout steps) | Banners, trust badges, custom fields |
| purchase.checkout.delivery-address.render-after | Below the delivery address | Delivery instructions, address warnings |
| purchase.checkout.shipping-option-list.render-after | Below shipping options | Delivery date pickers, carbon-offset opt-ins |
| purchase.checkout.payment-option-list.render-after | Below payment options | Financing explainers, payment trust copy |
| purchase.thank-you.block.render | Thank You page | Surveys, referral offers, order FAQs |
| customer-account.order-status.block.render | Order Status page | Tracking info, support links, upsells |
What Is Plus-Gated vs Open to All Plans
This is the question that derails most projects, so get it straight before you build. Per Shopify's checkout app extensions overview: extensions that render on the information, shipping, and payment steps are available only to stores on Shopify Plus. Extensions that appear after purchase — Thank You and Order Status pages — are available to all plans except Starter.
So a trust badge in the payment step is a Plus feature. The same badge on the Thank You page works on Basic. If you are deciding whether checkout-step customization justifies the Plus price tag, our Shopify Plus checkout customization guide walks through that math.
Build the UI With Polaris Web Components

Open src/Checkout.jsx. The scaffold gives you a Preact component rendered into the sandbox:
import '@shopify/ui-extensions/preact';
import { render } from 'preact';
export default function extension() {
render(<Extension />, document.body);
}
function Extension() {
return (
<s-banner heading="checkout-banner">
<s-text>Welcome to your first extension.</s-text>
</s-banner>
);
}Note the imports: @shopify/ui-extensions/preact, not React. The s- prefixed elements are the Polaris web components.
Layout Primitives
Compose structure with s-stack (vertical or inline flow with gap control), s-grid for column layouts, and s-box for padding, borders, and backgrounds. There is no custom CSS file — spacing, tone, and emphasis are all component properties, which is how Shopify guarantees your UI inherits the merchant's brand settings automatically.
Forms and Feedback
The form components mirror native checkout inputs: s-text-field, s-select, s-checkbox, s-choice-list. Feedback components include s-banner (with tone values like info, warning, critical), s-spinner, and s-icon. Every component ships with accessibility built in — labels, focus management, and screen reader support come free.
Styling Inside the System
You will be tempted to fight the design constraints. Don't. The constraint is the feature:
- Use
toneandemphasisprops instead of wishing for hex codes - Use
s-headingands-textsize variants instead of font-size overrides - Test against a dev store with customized branding to confirm your extension adapts
Merchants change their checkout branding without warning. Extensions that lean on the system keep looking native; anything clever breaks.
Read and Write Checkout State

Static banners are fine, but the real power of checkout UI extensions is reacting to what the buyer is doing.
Reading Cart Lines, Cost, and Buyer Identity
The shopify global exposes the live checkout as signals:
function Extension() {
// Re-renders automatically when the cart changes
const lines = shopify.lines.value;
const total = shopify.cost.totalAmount.value;
const hasSubscription = lines.some(
(line) => line.merchandise?.sellingPlan != null
);
if (!hasSubscription) return null;
return (
<s-banner tone="info" heading="Subscription in cart">
<s-text>
Your subscription renews automatically. Manage it anytime from
your account. Order total: {total.amount} {total.currencyCode}
</s-text>
</s-banner>
);
}Writing Attributes and Metafields
Writes go through apply* methods that return a result you must check:
const result = await shopify.applyAttributeChange({
type: 'updateAttribute',
key: 'gift_wrap',
value: 'yes',
});
if (result.type === 'error') {
console.error(result.message);
}Cart attributes flow through to the order, so anything you write here is visible in the admin, in webhooks, and to fulfillment apps. applyMetafieldChange works the same way for structured data, and applyCartLinesChange can add or update line items — the API behind in-checkout upsells.
Intercepting the Buyer Journey
shopify.buyerJourney.intercept lets you validate before the buyer advances a step — blocking progress on a missing field, an invalid address, or a business rule:
shopify.buyerJourney.intercept(({ canBlockProgress }) => {
const age = shopify.attributes.value?.find(
(attr) => attr.key === 'age_confirmed'
);
if (canBlockProgress && age?.value !== 'yes') {
return {
behavior: 'block',
reason: 'Age confirmation required',
errors: [{ message: 'Please confirm you are over 18 to continue.' }],
};
}
return { behavior: 'allow' };
});Blocking requires block_progress = true in your capabilities config, and always check canBlockProgress — on some surfaces blocking is not permitted, and your extension must degrade gracefully.
Add Merchant Settings
Hardcoded values make checkout UI extensions disposable. Settings make them products: merchants configure your extension in the checkout editor without touching code.
Define Fields in the TOML
[extensions.settings]
[[extensions.settings.fields]]
key = "banner_title"
type = "single_line_text_field"
name = "Banner title"
description = "Headline shown at the top of the banner"
[[extensions.settings.fields.validations]]
name = "max"
value = "80"
[[extensions.settings.fields]]
key = "show_icon"
type = "boolean"
name = "Show icon"Supported field types include single_line_text_field, multi_line_text_field, boolean, number_integer, number_decimal, date, date_time, and variant_reference — enough to cover most configuration needs without building an admin UI.
Read Settings in Code
function Extension() {
const settings = shopify.settings.value;
const title = settings?.banner_title ?? 'Free shipping on orders over $75';
return <s-banner heading={title} tone="info" />;
}Always supply a fallback — settings are empty until the merchant saves values in the editor, and your extension must render sensibly on first install.
Validate at the Edge
Use the TOML validations (min/max length, regex) to stop bad config before it reaches your code. Server-side-style validation in the extension itself is your second line of defense, not your first.
Three Mini-Builds You Can Ship Today
Theory done. Here are three complete, deployable checkout UI extensions — each one maps to a real request we see weekly from store owners.
Mini-Build 1: Custom Announcement Banner
Target: purchase.checkout.block.render. The merchant writes the message in settings; you render it. This is the "hello world" that is actually useful — shipping cutoffs, holiday notices, backorder warnings.
import '@shopify/ui-extensions/preact';
import { render } from 'preact';
export default function extension() {
render(<Banner />, document.body);
}
function Banner() {
const settings = shopify.settings.value;
const title = settings?.banner_title;
if (!title) return null;
return (
<s-banner heading={title} tone={settings?.banner_tone ?? 'info'}>
{settings?.banner_body ? <s-text>{settings.banner_body}</s-text> : null}
</s-banner>
);
}Pair it with banner_title, banner_body, and banner_tone settings fields and the merchant never files a "can you change the banner text" ticket again.
Mini-Build 2: Delivery Instructions Field
Target: purchase.checkout.delivery-address.render-after (Plus-only, since it lives on a checkout step). Captures a note and writes it to an order attribute:
function DeliveryInstructions() {
const handleChange = async (event) => {
const result = await shopify.applyAttributeChange({
type: 'updateAttribute',
key: 'delivery_instructions',
value: event.currentTarget.value,
});
if (result.type === 'error') console.error(result.message);
};
return (
<s-text-field
label="Delivery instructions (optional)"
details="Gate codes, safe drop-off spots, anything the driver needs."
onChange={handleChange}
/>
);
}The attribute lands on the order, where your 3PL or fulfillment app picks it up. Write on onChange (fires on blur) rather than every keystroke to avoid hammering the attribute API.
Mini-Build 3: Trust Badges
Target: purchase.checkout.block.render, typically placed near the payment area (Plus) or on the Thank You page (any plan). No state needed — just disciplined layout:
function TrustBadges() {
const badges = [
{ icon: 'lock', label: 'SSL-encrypted checkout' },
{ icon: 'return', label: '30-day free returns' },
{ icon: 'delivery', label: 'Ships within 24 hours' },
];
return (
<s-stack direction="inline" gap="large" justifyContent="center">
{badges.map((badge) => (
<s-stack key={badge.label} direction="inline" gap="small-200" alignItems="center">
<s-icon type={badge.icon} tone="subdued" size="small" />
<s-text tone="subdued" size="small">{badge.label}</s-text>
</s-stack>
))}
</s-stack>
);
}Resist the urge to upload third-party badge images — the icon set plus subdued text reads as native trust, while an off-brand image strip reads as a banner ad.
Stay Under 64KB, Then Preview and Deploy

Your compiled extension bundle cannot exceed 64KB — Shopify enforces the limit at deploy time, full stop. This surprises developers used to megabyte-scale web apps, but it is the contract that keeps checkout fast. Clear that bar, and shipping is two commands away.
The 64KB Ceiling and How to Stay Under It
Every extension on the page downloads and executes before it renders. Checkout speed is conversion-critical, and Shopify would rather reject your deploy than let a bloated bundle tax every buyer. As Ralf Elfving notes in his 20-minute checkout extension walkthrough, the constraint pushes you toward small, single-purpose extensions — which are also easier to maintain.
- Skip heavy dependencies. No moment.js, no lodash, no UI kits — Preact and the component library are already provided by the platform
- Check before you import. A single careless
importof a large library can blow the budget instantly - Split unrelated features into separate extensions within the same app rather than one mega-extension
- Inspect the build output — the CLI reports bundle size at deploy, and CI should fail loudly when you are close to the line
Size is necessary but not sufficient. Avoid waterfalls of network calls (each requires the network_access capability and adds latency), render a sensible default before remote data arrives, and never block first paint of your component on an external API. Gadget's overview of checkout UI extension architecture is a good companion read on how the sandbox model shapes performance behavior.
Local Dev Against a Dev Store
shopify app devThe CLI builds your extension, connects to your dev store, and hot-reloads on every save. Press p to open the Dev Console, then click your extension's preview link — it opens a checkout with your extension injected. Iterate here until the behavior is right; this loop is seconds, not minutes.
shopify app deployDeploy bundles every checkout UI extension in the app, enforces the 64KB limit, and creates a new app version. For a custom app this releases to the store immediately; for a public app it goes through app review.
Place It in the Checkout Editor
Deployment makes the extension available — the merchant still has to add it. In the Shopify admin: Settings → Checkout → Customize opens the checkout editor, where block-target extensions appear in the "Apps" section and can be dragged into position, configured via your settings fields, and saved. For Thank You and Order Status placements on non-Plus stores, the same editor handles it — which is exactly the surface Shopify's upgrade guide for non-Plus stores tells merchants to migrate to before the August 26, 2026 auto-upgrade.
Common Mistakes That Sink Checkout Extensions
We see the same checkout UI extension failure patterns over and over in our Shopify developer community — most are avoidable with a checklist.
| Best Practice | Common Mistake |
|---|---|
| Verify plan gating before scoping the project | Building a payment-step extension for a non-Plus merchant, discovering the gate at deploy |
| Check canBlockProgress before blocking | Assuming block always works, silently failing on surfaces where it doesn't |
| Provide fallbacks for empty settings | Rendering undefined as the banner headline on first install |
| Write attributes on blur/change events | Firing applyAttributeChange on every keystroke |
| Test with customized merchant branding | Shipping UI that only looks right on default checkout styling |
| Track bundle size in CI | Finding out at deploy time that a dependency added 80KB |
Build-Phase Mistakes
The most expensive error is target misselection: building against purchase.checkout.block.render for a Basic-plan store, then learning checkout steps are Plus-only after the work is done. Decide the surface first, confirm the plan, then write code. The second most expensive is treating the sandbox as an obstacle — trying to smuggle in custom fonts, raw HTML, or third-party scripts. Those paths are closed by design; budget your effort inside the component system.
Launch-Phase Mistakes
Deploying is not launching. Extensions sitting un-placed in the checkout editor render nothing, and merchants frequently do not know the placement step exists. Hand off with a one-paragraph instruction (or a screen recording) showing where to drag the block and which settings to fill in. And re-test after the merchant customizes branding — your "perfect" spacing may collapse under their typography choices.
Shopify Checkout UI Extensions FAQ
Do I Need Shopify Plus to Use Checkout UI Extensions?
Only for the information, shipping, and payment steps. Thank You and Order Status page extensions work on every plan except Starter — which is exactly where the August 26, 2026 auto-upgrade pressure applies. Non-Plus merchants who currently rely on legacy thank-you-page scripts should rebuild them as extensions now.
Can I Use React Instead of Preact?
The current component model renders with Preact and Polaris web components (the s- element set). Earlier extension versions used React-style components from @shopify/ui-extensions-react; new builds on API version 2026-04 should follow the Preact + web components pattern in the current docs. Either way, your JSX knowledge transfers almost entirely.
Can an Extension Call My Own API?
Yes — declare network_access = true under [extensions.capabilities] in the TOML, then fetch as usual. Keep calls minimal and non-blocking: every external request adds latency to the most conversion-sensitive page the merchant owns.
Ship One Extension This Week
The fastest way to learn Shopify checkout UI extensions is to deploy the banner build above to a dev store today — scaffold with shopify app init, generate the extension, render an s-banner, run shopify app dev, and you will have working checkout UI before lunch. From there, layering in state, settings, and buyer-journey logic is incremental, and the August 26, 2026 deadline gives every non-Plus store a concrete reason to start now rather than later.
Keep building from here with our Shopify development tutorials and the broader payments and checkout archive on Talk Shop's blog. And when you hit the inevitable "why is my target not rendering" moment, bring it to the Talk Shop Discord dev community — there is almost always someone who shipped the same extension last month.
What is the first thing you would add to your checkout — a delivery field, an upsell, or trust badges? Join the Discord and tell us what you are building.

About Talk Shop
The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.
