Checkout UI extensions let you inject your own UI into Shopify's checkout page without touching the checkout template itself. Rather than forking checkout.liquid — a path that disqualifies you from future Shopify checkout improvements — you write a typed, sandboxed extension that Shopify renders inside the checkout. In this tutorial you'll build an extension that reads the delivery groups from the checkout and displays a custom message when an order contains perishable items that need expedited shipping.
What you'll build
A checkout UI extension that mounts in the purchase.checkout.delivery-address.render-before extension point, inspects the cart line items for products tagged 'perishable', and if found, renders a Banner component with a message telling the customer that their order will be shipped refrigerated and must be received within 24 hours of delivery.
Before you start
- Shopify CLI 3.x installed globally (npm install -g @shopify/cli)
- A Shopify development store with checkout extensibility enabled (Checkout.liquid-free, or a store on the Shopify Plus plan)
- Node.js 18+ and a Shopify Partner account with a connected app
- At least one product tagged 'perishable' in your development store
Scaffold the extension
If you don't already have a Shopify app, run shopify app init to scaffold one. Then add a new checkout UI extension with the generate command. Choose TypeScript when prompted — the type definitions for extension points and API hooks will save you from many runtime errors.
# Inside your existing Shopify app directory
shopify app generate extension
# When prompted:
# Extension type: Checkout UI
# Name: perishable-delivery-message
# Language: TypeScript ReactExpected outcome: A new directory extensions/perishable-delivery-message/ is created with a shopify.extension.toml config file and a src/Checkout.tsx entry point.
Configure the extension point in shopify.extension.toml
The shopify.extension.toml file declares which extension point your UI mounts in and what capabilities it needs. Set the target to purchase.checkout.delivery-address.render-before so your message appears directly above the delivery address form — a highly visible position that customers see before they commit to a shipping method.
You also need to declare the API access your extension requires. Reading cart line items falls under the read_cart and read_products capabilities. Without these declarations, the useCartLines() and useApplyAttributeChange() hooks will return empty data.
# extensions/perishable-delivery-message/shopify.extension.toml
api_version = "2024-04"
[[extensions]]
type = "ui_extension"
name = "Perishable Delivery Message"
handle = "perishable-delivery-message"
[[extensions.targeting]]
module = "./src/Checkout.tsx"
target = "purchase.checkout.delivery-address.render-before"Expected outcome: Running shopify app dev should show the extension in the list of loaded extensions without any config errors.
Read cart lines and detect perishable items
The @shopify/ui-extensions-react/checkout package exposes hooks that read live checkout state. useCartLines() returns an array of CartLine objects. Each CartLine has a merchandise property whose Product has a productType and a hasTag function — but note that product tags are not directly available on CartLine in all API versions. The stable way to check tags is to include them in the metafield approach or to use the product.tags field if your API version exposes it.
An alternative pattern — and a more reliable one — is to store a metafield on the product variant (namespace: custom, key: perishable, type: boolean). You can then read that metafield directly from the CartLine.merchandise.metafield() call inside your extension. This approach also works for merchants who use multiple tags for other purposes and don't want to rely on a specific tag name.
import {
useCartLines,
Banner,
Text,
reactExtension,
} from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.delivery-address.render-before',
() => <PerishableMessage />
);
function PerishableMessage() {
const lines = useCartLines();
// Check each line for a metafield indicating it's perishable
const hasPerishable = lines.some((line) => {
const meta = line.merchandise.metafield({
namespace: 'custom',
key: 'perishable',
});
return meta?.value === 'true';
});
if (!hasPerishable) return null;
return (
<Banner status="warning">
<Text>
Your order contains perishable items and will be shipped refrigerated.
Please ensure someone is available to receive the package within 24 hours
of the estimated delivery date.
</Text>
</Banner>
);
}Expected outcome: When a cart contains a line item whose variant has the custom.perishable metafield set to 'true', the yellow warning banner appears above the delivery address form.
Style the banner to match your brand
Checkout UI components adapt to the merchant's branding settings configured in the Customise editor — fonts, colors, corner radii. Your extension inherits these styles automatically. The Banner component accepts a status prop with values 'info', 'success', 'warning', and 'critical'. For a delivery message, 'warning' is appropriate.
If you need more layout control, wrap your content in BlockStack or InlineStack layout primitives. BlockStack stacks children vertically; InlineStack places them side by side. Both accept spacing and alignment props. Never use raw HTML elements — checkout extensions enforce a whitelist of approved UI components.
import {
Banner,
BlockStack,
Heading,
Text,
Icon,
InlineStack,
} from '@shopify/ui-extensions-react/checkout';
function PerishableBanner() {
return (
<Banner status="warning">
<BlockStack spacing="tight">
<InlineStack spacing="tight">
<Icon source="truck" />
<Heading level={3}>Refrigerated shipping required</Heading>
</InlineStack>
<Text>
This order contains perishable goods. Our cold-chain courier will
deliver between 8 am – 6 pm. Ensure someone is home to receive the
package.
</Text>
</BlockStack>
</Banner>
);
}Expected outcome: The banner now has a heading with a truck icon and a descriptive body text, inheriting the store's brand colors.
Preview locally with shopify app dev
Checkout UI extensions have a dedicated live-preview mode. Running shopify app dev will start your app server and tunnel it to a public URL. It will also print a preview link directly to the checkout of your development store with your extension loaded. Open that URL in a browser.
Add a product tagged with a custom.perishable metafield set to true to the cart, proceed to checkout, and navigate to the delivery address step. You should see your banner. Use the browser's developer tools — the extension host renders a shadow DOM so you won't see styles leak in either direction.
cd your-app-directory
shopify app dev
# The CLI will output a preview URL like:
# Preview URL: https://your-store.myshopify.com/cart/add?id=...&return_to=...Expected outcome: You can see your banner live in the checkout without deploying to production. Hot-reload is active — changes to src/Checkout.tsx refresh the preview automatically.
Deploy and activate the extension
When you're happy with the extension, deploy it with shopify app deploy. This bundles your extension code and uploads it to Shopify's CDN. The extension won't be live until a merchant activates it from their checkout editor in the Themes section of their Admin.
In the checkout editor (Online Store > Themes > Customise > Checkout), merchants drag your extension from the App blocks panel onto the checkout layout. This gives merchants control over placement without needing developer involvement. If your extension should always appear in the same position, document the recommended placement clearly in your app's onboarding flow.
shopify app deploy
# Confirm the prompts. You'll see:
# ✓ perishable-delivery-message deployed to partner dashboard
# Version created: 1.0.0Expected outcome: The extension version appears in your Partner dashboard under Extensions. Merchants who have your app installed can now activate it from their checkout editor.
What's next
Checkout UI extensions are capable of much more than banners. You can use useApplyCartLinesChange to add upsell products, useApplyAttributeChange to tag the order with custom attributes, and useShippingAddress to read or pre-fill address fields. The full extension API reference is at shopify.dev/docs/api/checkout-ui-extensions.
For more complex conditional logic — like showing different messages based on the selected shipping rate — look into the useDeliveryGroups hook. It exposes the selected delivery option, delivery strategy (ship, pickup, local delivery), and estimated delivery range, giving you everything you need to tailor the checkout experience to the actual fulfillment path.
Performance considerations for checkout extensions
Shopify measures and enforces performance budgets for checkout UI extensions. Your extension's JavaScript bundle must be under 10KB gzipped, and it must render within 50ms of being called. These constraints exist because checkout performance directly impacts conversion rates — Shopify's data shows that each additional 100ms of checkout load time reduces conversion by approximately 0.3%.
Keep your extension bundle small by avoiding large third-party libraries. All the UI primitives you need (Banner, Text, BlockStack, InlineStack, Icon, Button) are imported from @shopify/ui-extensions-react/checkout, which is provided by the Shopify host and doesn't count against your bundle size. Only code you write yourself or third-party packages you import count against the 10KB limit. The Shopify CLI will warn you if your extension bundle exceeds the limit when you run shopify app build.
Testing across checkout themes
Checkout UI components inherit the merchant's checkout branding — fonts, colors, button styles, and corner radius settings defined in the checkout editor. This means your extension will look different on a store with a dark theme versus one with a light theme. Test your extension against multiple theme configurations before submitting. In the Shopify CLI preview mode, you can switch between stored checkout profiles in the checkout editor to verify your extension renders correctly in each configuration.
One common rendering gotcha: if a merchant has set their checkout's accent color to a dark hue and your Banner uses status='warning' (which is typically yellow), the contrast between yellow text and a dark background may be insufficient for WCAG compliance. The Polaris checkout components handle this automatically by adjusting their contrast ratios based on the host's color scheme — another reason to use the provided components rather than custom divs with hardcoded color values.
Frequently asked questions
- Can a checkout UI extension block the customer from proceeding?
Yes, using the checkout validation API. Extensions targeting the purchase.checkout.block.render extension point can block checkout progression by returning a validation error. Use this sparingly — customers who are blocked unexpectedly abandon their carts. The checkout validation extension point is separate from the UI extension points and has its own API surface for reading cart state and returning block/proceed decisions.
- Does the extension run on mobile checkout?
Yes. Checkout UI extensions render in Shopify's checkout across all surfaces where that checkout is available: desktop web, mobile web, the Shop app, and Shopify's native mobile checkout SDK. The UI components adapt automatically to the screen size. You don't write separate code for mobile — the responsive layout is handled by the BlockStack and InlineStack primitives.
