Shopify Checkout Extensibility: What Broke and How to Migrate

checkout.liquid is gone. Here is what actually replaces each customisation and which ones have no replacement at all.

Share
Shopify Checkout Extensibility: What Broke and How to Migrate. Abstract shopify illustration in orange and dark grey on debugly.dev

Disclosure: I spent four years building Shopify themes at Debutify, finishing as CDO. This is platform migration advice rather than product advice.

The short answer

Shopify deprecated checkout.liquid and additional scripts. Custom checkout code now runs as checkout UI extensions, small sandboxed components rendered at defined extension points, plus Shopify Functions for discount, delivery, and payment logic.

The migration is not a port. The extension model is deliberately more restrictive, and some things people did in checkout.liquid have no equivalent by design.

Map your customisations to one of four buckets before you write anything: UI extension, Function, app, or drop it.

Why Shopify did this

Worth understanding, because it explains which of your customisations will survive.

checkout.liquid let merchants inject arbitrary Liquid and JavaScript into the highest value page in ecommerce. That meant a badly written snippet could slow checkout down, break it on mobile, or fail silently in a way that lost orders. It also made it impossible for Shopify to change checkout, since any change could break arbitrary merchant code.

The new model runs your code in a sandboxed worker with a fixed API surface. You cannot touch the DOM, you cannot inject arbitrary scripts, and you can only render into places Shopify has defined.

That is genuinely more restrictive and it is the right trade for a checkout. It also means "I want to move this element" is frequently not possible, and no amount of searching will find a workaround.

Mapping your customisations

Go through your existing checkout code and sort each piece.

Checkout UI extensions

For anything that adds content or captures input at checkout.

Available extension points include areas near the contact form, shipping address, delivery options, payment section, and order summary. The exact target names change as the platform evolves, so check the current documentation rather than trusting a blog post, including this one.

import {
  reactExtension,
  Checkbox,
  BlockStack,
  Text,
  useApplyAttributeChange,
  useAttributeValues,
} from "@shopify/ui-extensions-react/checkout";

export default reactExtension(
  "purchase.checkout.delivery-address.render-after",
  () => <GiftOptions />
);

function GiftOptions() {
  const applyAttributeChange = useApplyAttributeChange();
  const [isGift] = useAttributeValues(["is_gift"]);

  return (
    <BlockStack>
      <Checkbox
        checked={isGift === "yes"}
        onChange={(checked) =>
          applyAttributeChange({
            type: "updateAttribute",
            key: "is_gift",
            value: checked ? "yes" : "no",
          })
        }
      >
        This order is a gift
      </Checkbox>
      {isGift === "yes" && <Text size="small">Prices will be hidden on the packing slip.</Text>}
    </BlockStack>
  );
}

Three constraints to internalise:

You use Shopify's components, not HTML. BlockStack, Text, Checkbox, Banner. No divs, no custom CSS, no CSS framework. This is what keeps checkout consistent and accessible, and it is the constraint people find hardest.

Network calls have to be declared. Any external endpoint must be listed in your extension's TOML configuration under network_access, and requests go through Shopify's proxy.

Extensions are async and can fail. Your extension might not render. Checkout must still work if it does not, which is a design requirement rather than an edge case.

Shopify Functions

For logic that changes what checkout calculates: discounts, shipping option filtering and renaming, payment method filtering, cart validation.

Functions run server side in WebAssembly, compiled from Rust or JavaScript. They receive an input document and return a list of operations.

export function run(input) {
  const b2b = input.cart.buyerIdentity?.customer?.hasTags?.some(t => t.tag === "wholesale");
  if (!b2b) return { operations: [] };

  return {
    operations: [{
      hide: {
        deliveryOptionHandle: input.cart.deliveryGroups
          .flatMap(g => g.deliveryOptions)
          .filter(o => o.title.includes("Express"))
          .map(o => o.handle),
      },
    }],
  };
}

Functions have hard limits: an instruction budget, no network access at all, and a strict execution time. If your logic needs to call an external pricing service, a Function cannot do it. That is a common discovery halfway through a migration.

Things with no direct replacement

Be honest about these early rather than searching for workarounds.

Arbitrary DOM manipulation. Gone. No jQuery in checkout.

Custom CSS beyond branding. You get the Branding API for colours, fonts, corner radius, and button styles. You do not get arbitrary CSS. If your checkout had a bespoke layout, it will now look like Shopify's checkout with your brand applied.

Third party scripts injected directly. Analytics goes through Web Pixels, which run in their own sandbox. Anything expecting direct DOM access will not work.

Reordering or removing standard checkout sections. Not supported.

Additional scripts. Replaced by Web Pixels for tracking and by Functions for logic.

The pixel migration is its own project

Most merchants had conversion tracking in additional scripts. That now runs as a Web Pixel, in a sandboxed iframe, subscribing to events:

analytics.subscribe("checkout_completed", (event) => {
  const c = event.data.checkout;
  fetch("https://analytics.example.com/collect", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      order_id: c.order?.id,
      value: c.totalPrice?.amount,
      currency: c.currencyCode,
      items: c.lineItems.map(li => ({
        id: li.variant?.sku,
        qty: li.quantity,
        price: li.variant?.price?.amount,
      })),
    }),
  });
});

The sandbox means your pixel cannot read cookies from the main document or access the DOM. Attribution setups that relied on reading a cookie set elsewhere on the site need rethinking, usually by passing the value through cart attributes.

Verify the numbers after migrating. The most common outcome I have seen is tracking that appears to work and under-reports by ten to twenty percent, because an event fires in a different place in the flow than it used to. Compare against Shopify's own order count for a full week before you trust the new setup, and keep the old numbers to compare against.

Testing

Extensions run in checkout, which makes them awkward to test.

shopify app dev

This gives you a preview URL that loads your extension against a real checkout on a development store. It is the only realistic way to iterate.

What to test specifically:

  • A cart with one item and a cart with twenty
  • A customer with an account and a guest
  • An international address, since some extension points behave differently
  • A discount code applied
  • A gift card as partial payment
  • Mobile viewport, because component layout differs
  • Your extension failing to load, by blocking its request in DevTools

That last one matters more than it sounds. An extension that throws should degrade to checkout working without it. If your gift message field fails to render and the order still completes, fine. If it fails and the customer cannot proceed, you have made checkout less reliable than it was.

A migration order that works

  1. Inventory everything in checkout.liquid, additional scripts, and any app that touches checkout. Every distinct behaviour on a list.
  2. Sort each item into UI extension, Function, Web Pixel, or drop.
  3. Do the pixels first. Tracking is the highest risk item because failure is silent and you lose data you cannot recover.
  4. Then Functions, since discount and shipping logic is testable in isolation.
  5. Then UI extensions, which are the most visible and the easiest to iterate on.
  6. Run both in parallel where possible and compare, particularly for anything numeric.
  7. Watch conversion rate hourly for the first few days after going live. A checkout change that costs two percent conversion is a large amount of money and it will not be obvious from error logs.

The honest assessment

For most merchants this is a downgrade in flexibility and an upgrade in reliability. Checkout is now something Shopify can improve without breaking your store, and it is much harder for a bad customisation to cost you orders.

If your checkout customisation was cosmetic, the migration is a week and the result is fine. If your business depended on genuinely custom checkout logic, this is the moment to find out whether Shopify is still the right platform, which is a bigger conversation I have written about in the platform comparison.

Either way, do not leave it until the deprecation date. The discovery phase, where you find out which of your customisations have no replacement, is the part that takes time.