Shopify checkout UI extension development means building a small, sandboxed React or Preact component that Shopify renders inside a fixed slot in checkout, such as a block on the Thank you page or a static area next to the cart line items. This guide scaffolds one from a real Shopify CLI project, reads and writes checkout data through the current shopify global object, tests it locally, and deploys it under the platform’s actual constraints. By the end you’ll have a working delivery-instructions extension and a clear list of the places these extensions tend to fail once real customers start using them.
What You’re Building (and What This Guide Skips)
The extension in this walkthrough adds a text field to checkout where a customer can leave a note for the courier, and it shows the current order subtotal above the field. That’s deliberately small. It exercises the three things almost every checkout UI extension needs: a target, a read from the shopify object, and a write back to the order.
This guide does not cover Shopify Functions (server-side discount, shipping, and payment logic), the Branding API (colors, fonts, and layout), or post-purchase upsell offers. Those are real parts of checkout extensibility, but they solve different problems and deserve their own walkthroughs. Shopify maintains its own custom fields tutorial covering a similar delivery-instructions pattern, worth a look once you want the officially maintained reference version alongside this one. The next section explains how to tell which one you actually need.
Checkout UI Extensions vs. Shopify Functions: Which One Do You Actually Need?
If the customer needs to see or interact with something, you want a UI extension. If you need to change a price, a discount, a shipping rate, or which payment methods appear, you want a Function. UI extensions render interface inside a sandboxed environment; Functions run server-side with no rendering at all, according to Shopify’s documentation on apps in checkout.
Checkout UI Extensions Shopify Functions Renders visible UI Yes No Runs where Client-side, inside a Web Worker sandbox Server-side, on Shopify’s infrastructure Typical use Custom fields, banners, upsell blocks, trust badges Discount logic, shipping rate changes, payment method ordering Data access Reads/writes cart and checkout data through theshopify object
Reads structured input, returns structured output, no direct cart mutation
Plan requirement
Available on all plans; the information, shipping, and payment steps still require Shopify Plus
Available broadly, most often paired with Plus-tier checkout customization
Teams often reach for a UI extension when the real problem is a Function, which is why a custom field ends up trying to also silently change a shipping rate through a side channel. Keep the two separate. If your logic doesn’t need to be seen, it probably belongs in a Function instead.
It’s also worth knowing what these two tools replaced. Shopify Scripts, the old way to write custom discount and shipping logic for Plus stores, stopped executing on June 30, 2026, after running alongside checkout extensions during a transition window that Shopify’s own documentation confirms. Anything still living in an old Script needs to move to a Function now, not a UI extension.
Prerequisites
You’ll need:
- Node.js and a recent Shopify CLI install
- A Partner account and a development store with checkout extensibility enabled
- An existing custom or public app to attach the extension to (or scaffold a new one with
shopify app init)
Set the API version to the latest stable release in your configuration file rather than pinning to whatever version a tutorial used months ago. Each stable version stays supported for a minimum of 12 months, and the CLI blocks deploys against versions older than that window.
Scaffold the Extension
From inside your app directory, generate the extension:
cd my-app
shopify app generate extension --template checkout_ui
Enter fullscreen mode Exit fullscreen mode
This creates a folder with a shopify.extension.toml configuration file and a templated JSX entry point. Edit the TOML to point at a block target, which is the placement type merchants can position themselves using the checkout editor:
api_version = "2026-07"
[[extensions]]
type = "ui_extension"
name = "Delivery Notes"
handle = "delivery-notes"
uid = "generate-this-with-shopify-cli"
[[extensions.targeting]]
target = "purchase.checkout.block.render"
module = "./src/DeliveryNotes.jsx"
Enter fullscreen mode Exit fullscreen mode
Block targets are one of three target types. Static targets render automatically in a fixed spot, like right after the cart line items, and can’t be repositioned. Runnable targets don’t render anything at all; they fire in response to an event, such as a keystroke in an address field, and return data. Block targets sit in between: merchants place them through the checkout editor, and up to three extensions can share the same slot. The Shopify CLI documentation covers the full set of scaffold and dev-server flags if you need to target a specific store or app configuration.
Read Checkout Data With the shopify Global Object
Inside the extension, Shopify injects a global shopify object that exposes checkout data as properties. There’s no import needed for it: it’s simply available at runtime, similar to how window behaves in a normal browser page, except this one runs inside a Web Worker with no DOM access.
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useState} from 'preact/hooks';
export default function extension() {
render(<DeliveryNotes />, document.body);
}
function DeliveryNotes() {
const [note, setNote] = useState('');
const subtotal = shopify.cost.subtotalAmount.value;
return (
<s-stack border="base" padding="base">
<s-heading>Delivery instructions</s-heading>
<s-text type="small">
Subtotal: {subtotal.amount} {subtotal.currencyCode}
</s-text>
<s-text-field
label="Leave a note for the courier"
value={note}
onChange={(event) => setNote(event.target.value)}
/>
</s-stack>
);
}
Enter fullscreen mode Exit fullscreen mode
shopify.cost.subtotalAmount updates automatically as the cart changes, and reading it doesn’t cost you anything in terms of rate limits since it’s a read, not a write. The s-stack, s-heading, s-text, and s-text-field elements are web components from Shopify’s Polaris-based checkout component library, not raw HTML, so they inherit the store’s checkout styling without extra CSS work on your part.
Run shopify app dev at this point to preview the extension live on your dev store before writing a single line of persistence logic. Confirming the field renders and the subtotal updates is the cheapest bug you’ll ever catch.
Write Changes Back to the Order
Reading data is half the job. The other half is persisting what the customer typed, which happens through applyAttributeChange:
async function handleChange(event) {
const value = event.target.value;
setNote(value);
await shopify.applyAttributeChange({
type: 'updateAttribute',
key: 'deliveryInstructions',
value,
});
}
Enter fullscreen mode Exit fullscreen mode
Swap this in for the inline onChange in the component above. applyAttributeChange returns a promise that resolves once Shopify has applied the change and the corresponding property has updated, and the attribute becomes visible on the order in the admin once it’s placed.
Two boundaries matter here. First, extensions that fire too many attribute or metafield changes in a short window get rate-limited for the rest of that buyer’s session, so batch related writes with Promise.all instead of firing one call per keystroke. Second, an attribute set this way lands on the order object, not inside the payment step; if you need something reflected in pricing or payment method availability, that’s a Function’s job, not this one.
Test the Extension Before It Touches Real Checkouts
shopify app dev gives you a live preview against a real dev store, and it reloads automatically as you edit. That covers manual testing. As of API version 2026-04, Shopify also ships @shopify/ui-extensions-tester for writing actual unit tests against your extension code, which matters once the component grows past a single field and you want regression coverage without opening a browser every time.
Two checkpoints are worth running before you touch a production store:
- Confirm the field appears in the correct slot using the checkout editor’s preview, since block target placement is merchant-configurable and easy to get wrong during first setup.
- Place a real test order and check that the attribute shows up on the order detail page in the admin. If it’s missing, the write silently failed or the promise was never awaited, not a rendering problem.
Deploy It, and Respect the 64 KB Ceiling
Deployment is a single command:
shopify app deploy
Enter fullscreen mode Exit fullscreen mode
The CLI builds your extension bundle and uploads it to Shopify. There’s a hard constraint worth planning around early: a compiled UI extension bundle can’t exceed 64 KB, and Shopify enforces this at deploy time. A single text field extension won’t come close, but adding a chart library or a large icon set will get you there faster than expected. Analyze your bundle size before you’re surprised by a failed deploy the day before launch.
If your extension needs more than the default sandbox allows, such as calling your own backend or collecting SMS marketing consent, you declare that explicitly in the TOML under [extensions.capabilities] (network_access, api_access, collect_buyer_consent, block_progress). Requesting a capability you don’t end up using is a common reason review takes longer than expected, so keep the list matched to what the code actually does.
Where Do Checkout Extensions Actually Break in Production?
Two failure patterns show up more than any other.
The first is a mismatch between an extension’s declared api_version and the version a linked extension expects, most commonly on payment-related targets. One developer on the Shopify community forum hit a crash where checkout failed with a TypeError before their React component even rendered, tied to a payment extension linked to a UI extension through ui_extension_handle. The lesson generalizes: when two extensions are wired together, verify the target string and API version match exactly, and don’t assume a typo in a config field will surface as a readable error. It often surfaces as a crash somewhere upstream of your own code.
The second is the sandbox itself. The extension runs inside a Web Worker with no access to window or the DOM, which means a third-party error-reporting tool that assumes a normal browser environment will silently fail to initialize unless you disable its default integrations and attach error and unhandledrejection listeners manually. If you’re not seeing errors you know are happening, this is usually why.
Neither failure mode is exotic. Both come from treating a sandboxed, upgrade-safe environment as if it were a regular webpage, which it deliberately isn’t.
Build It Yourself or Bring in a Shopify Team?
A single custom field like the one in this guide is a reasonable first project for a developer who’s comfortable with React and hasn’t touched Shopify’s extension model before; budget a day or two including the review cycle. The calculation changes once you’re coordinating multiple targets, metafields across international markets, post-purchase offers, or a Scripts-to-Functions migration against a hard deadline. That’s less a coding problem than a project-management one, and it’s the same reasoning Shopify gives store owners directly in its own upgrade guidance: build the extension yourself, or bring in a service partner to build it for you.
For merchants in that second situation, Lucent Innovation’s Shopify development team handles exactly this kind of checkout migration and extension work as part of its Shopify Plus practice. Either path is legitimate. The point of this guide is that you now have enough to make that call with a working extension in front of you, not a guess.
What’s Next
You’ve got a checkout UI extension that reads live cart data and writes an attribute back to the order, scaffolded, tested, and deployed under the real constraints Shopify enforces at build time. The natural next step is a Shopify Function if your use case needs to touch pricing or shipping logic rather than just displaying something.
Have you run into the Web Worker sandbox limitations while wiring up error tracking, or found a cleaner pattern for requesting capabilities without triggering a longer review? Drop it in the comments, it’s the kind of detail that doesn’t make it into the docs.
답글 남기기