← All Shopify articles
11 min read

Life after checkout.liquid: building with Checkout UI Extensions

Life after checkout.liquid — building with Shopify Checkout UI Extensions

For years, customizing a Shopify checkout meant editing one file: checkout.liquid. It was a Shopify Plus superpower and a Shopify Plus footgun. You could inject any HTML, any CSS, any <script> you liked into the highest-stakes page in commerce — and you could also break it silently, slow it to a crawl, or leave it stranded the next time Shopify shipped a checkout change.

That era is over. Shopify deprecated checkout.liquid in two waves and replaced it with something structurally different: Checkout Extensibility. This is the migration story — what actually changed, what you can and can’t do now, and how the React-based extension model works once you stop thinking in templates.

The deadlines that forced the move

This wasn’t a soft “please consider migrating.” Shopify set hard cutoffs, after which un-migrated checkouts were upgraded off checkout.liquid automatically and lost their customizations.

Surfacecheckout.liquid sunsetWhat replaced it
Checkout steps — Information, Shipping, PaymentAugust 13, 2024Checkout UI extensions + Functions + Branding API
Thank-you page & Order-status pageAugust 28, 2025Thank-you / Order-status UI extensions
“Additional scripts” boxSame wavesWeb pixels (customer-events sandbox)

If you maintain a Plus store or build apps for one, the migration is no longer optional or upcoming — it’s the baseline. The interesting question is no longer whether to move but how to think in the new model.

Why Shopify killed the most flexible file it had

checkout.liquid was infinitely flexible, and that was exactly the problem. Flexibility at checkout came at the cost of three things Shopify cares about more than your custom layout:

Checkout Extensibility is Shopify’s answer to all three at once: give developers real power, but inside guardrails the platform can keep upgrading underneath you.

What replaced it: four pillars, not one file

“Checkout Extensibility” is an umbrella over four distinct tools. Most migrations touch more than one, so it helps to know which lever does what.

PillarWhat it’s forOld checkout.liquid equivalent
Checkout UI extensionsAdd UI — fields, banners, custom content — at sanctioned pointsCustom HTML / Liquid in the template
FunctionsBackend logic: discounts, delivery & payment customization, validationScripts (Ruby) + manual hacks
Branding API + checkout editorColors, fonts, corners, layout — declarativelyHand-written CSS
Web pixelsAnalytics & marketing tags in a sandboxThe “Additional scripts” box

This article is about the first pillar — UI extensions — because that’s where the mental model breaks hardest from checkout.liquid, and where the “React-based extension model” actually lives.

The mental shift: you don’t own the page anymore

Here is the single idea that makes everything else click. With checkout.liquid you rendered the page. With UI extensions Shopify renders the page, and you contribute components into slots it exposes.

And your code doesn’t even run where the page does. A checkout UI extension executes in a sandboxed web worker, off the main thread, with no access to the DOM. You never touch document, you can’t query an element, you can’t inject a stylesheet. Instead you build a tree of Shopify-provided components, and Shopify mirrors that tree onto the real page on your behalf (a “remote DOM” bridge). It’s React — but React rendering into a worker, not into the browser document.

That one constraint explains every “why can’t I just…” you’re about to have. No DOM means no arbitrary HTML, no third-party widget that expects a mount node, no CSS overrides. In exchange you get an extension that keeps working when Shopify redesigns checkout next quarter.

What’s possible

The sanctioned surface is broader than first impressions suggest. With UI extensions (and their Function siblings) you can:

What isn’t (and the workarounds)

Be honest with stakeholders early: some things checkout.liquid did are simply gone, by design.

You used to…NowThe sanctioned path
Drop arbitrary HTML/CSSNot possibleCompose Shopify’s component set; style via Branding API
Run third-party scripts on the pageNot possibleWeb pixels for analytics; declared network access for your own APIs
Query/mutate the checkout DOMNo DOM at allReact state + the extension APIs/hooks
Add UI anywhere on the pageFixed extension targets onlyPick from Shopify’s published targets
Free-form layout of the stepsLimitedBranding API + editor; sections, not pixels

The honest summary: you trade unbounded control for durable, fast, upgrade-safe control. For most real checkout requirements that’s a good trade. For the rare pixel-perfect redesign, it’s a genuine limitation you should scope before promising it.

The React extension model, concretely

An extension is two things: a config file that declares where it plugs in and what it’s allowed to do, and a React module that says what to render. Start by scaffolding one with the CLI:

shopify app generate extension --template checkout_ui

1 — The config: shopify.extension.toml

This is where the guardrails are declared. The target is the slot you render into; capabilities are the permissions you must opt into (and which surface to merchants); settings are the fields a merchant can configure in the checkout editor.

api_version = "2025-07"

[[extensions]]
name = "Delivery instructions"
handle = "delivery-instructions"
type = "ui_extension"

  [[extensions.targeting]]
  # WHERE this renders. Pick from Shopify's published targets.
  target = "purchase.checkout.delivery-address.render-after"
  module = "./src/Checkout.jsx"

  [extensions.capabilities]
  network_access = true     # call your own backend
  api_access = true         # read cart/customer via the query API
  block_progress = true     # allowed to stop the buyer journey

  # Merchant-configurable fields, shown in the checkout editor.
  [[extensions.settings.fields]]
  key = "label"
  type = "single_line_text_field"
  name = "Field label"

Note the target string. Targets come in two flavors: static targets like purchase.checkout.block.render, which a merchant drags into place in the editor, and dynamic targets like the one above, anchored to a specific part of the page (here, just after the delivery address). You don’t invent targets — you choose from Shopify’s published list.

2 — The component: composing Shopify’s primitives

The entry point is reactExtension, which binds your component to the target. Everything you render — BlockStack, TextField, Banner — comes from @shopify/ui-extensions-react/checkout, not from HTML. State and data flow through hooks.

import {
  reactExtension,
  BlockStack,
  TextField,
  useApplyAttributeChange,
  useSettings,
} from '@shopify/ui-extensions-react/checkout';

// Bind the component to the target declared in the .toml.
export default reactExtension(
  'purchase.checkout.delivery-address.render-after',
  () => <Extension />,
);

function Extension() {
  const { label } = useSettings();              // merchant config
  const applyAttributeChange = useApplyAttributeChange();

  return (
    <BlockStack>
      <TextField
        label={label || 'Delivery instructions'}
        multiline={3}
        onChange={(value) =>
          // Write onto the order — no DOM, no form post.
          applyAttributeChange({
            type: 'updateAttribute',
            key: 'Delivery instructions',
            value,
          })
        }
      />
    </BlockStack>
  );
}

There is no <div>, no className, nofetch of the DOM. applyAttributeChange is how the value reaches the order; the merchant later sees “Delivery instructions” on the order in admin. This is the whole shape of a UI extension.

3 — Validation: blocking the buyer journey

The single most common checkout.liquid hack — “don’t let them continue until X” — has a first-class API now. useBuyerJourneyIntercept runs every time the buyer tries to advance, and you return whether to allow or block:

import { useState } from 'react';
import {
  reactExtension,
  Checkbox,
  useBuyerJourneyIntercept,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
  'purchase.checkout.block.render',
  () => <Terms />,
);

function Terms() {
  const [accepted, setAccepted] = useState(false);

  useBuyerJourneyIntercept(({ canBlockProgress }) =>
    canBlockProgress && !accepted
      ? {
          behavior: 'block',
          reason: 'Terms not accepted',
          errors: [{ message: 'Please accept the terms to continue.' }],
        }
      : { behavior: 'allow' },
  );

  return (
    <Checkbox checked={accepted} onChange={setAccepted}>
      I agree to the terms and conditions
    </Checkbox>
  );
}

Crucially, canBlockProgress reflects whether your extension actually holds the block_progress capability — the same one you declared in the .toml. Guardrails, declared up front, enforced at runtime.

Plan availability — read this before you promise anything

Not every part of Checkout Extensibility is available on every plan, and this trips up scoping more than the code does.

CapabilityAvailability
The three checkout steps (Info/Shipping/Payment) — UI extensions, editor, brandingShopify Plus
Functions on checkout (discounts, delivery/payment customization)Plan-dependent; broadly available to apps
Thank-you & Order-status page extensionsAvailable beyond Plus
Web pixelsAll plans

If you’re building a public app, design for the fact that a non-Plus merchant may only be able to use your Thank-you / Order-status targets, not your in-checkout ones.

A migration playbook

  1. Inventory the old customizations. List everything checkout.liquid (and the scripts box) was doing. Tag each as UI, logic, styling, or analytics.
  2. Map each to a pillar. UI → UI extension; logic → Function; styling → Branding API; analytics → web pixel. Items that map to nothing are your scope risks — surface them now.
  3. Match UI to targets. For each piece of UI, find the extension target closest to where it lived. No target? Rethink the placement; you can’t force one.
  4. Build behind a draft. Develop against a dev store, preview in the checkout editor, and only publish the new checkout profile when it’s at parity.
  5. Re-validate analytics. The scripts box is gone; confirm conversion tracking fires through web pixels before you cut over.

The takeaway

Losing checkout.liquid feels like losing power, and at the raw level of “can I put any HTML I want on the page,” you did. But the trade is deliberate: a sandboxed, component-based, declaratively-permissioned model where your work survives Shopify’s upgrades instead of fighting them. Once you stop trying to render the page and start contributing components to it, Checkout UI extensions stop feeling like a cage and start feeling like a contract — one that finally lets the platform and your customizations move forward together.

If your goal at checkout is less about layout and more about conversion — answering the last-minute question that makes a buyer hesitate — that’s exactly where an AI assistant earns its place alongside these extensions. That’s the problem WisWes is built to solve.

Turn questions into checkout.

WisWes drops into your store and guides shoppers from browsing to buying. 14-day free trial — no card.