Shopify Checkout Extensibility vs checkout.liquid: A Migration Field Report
Shopify killed checkout.liquid for the Plus crowd and pushed everyone to Checkout Extensibility. Here's what actually broke when we migrated three stores, and the patterns that survived the transition.

Shopify set a hard deadline for checkout.liquid deprecation on Plus stores, and most teams we talked to in late 2025 pushed the migration to the last possible sprint. That was a mistake. We ran three of these migrations back-to-back — a mid-size apparel brand, a B2B industrial parts catalog, and a DTC supplements store — and the pattern of surprises was consistent enough to write down.
This is the field report: what Checkout Extensibility actually gives you, what you lose, and the architectural choices that saved us from rework.
Why the migration isn't optional anymore
Shopify's position is clear: checkout.liquid and the old Scripts app are being replaced by Checkout Extensibility, which is a combination of Checkout UI Extensions (React or vanilla JS components rendered in defined extension points), Shopify Functions (server-side logic for discounts, shipping, payment customizations), and the Branding API for visual theming.
The upside Shopify advertises — PCI scope reduction, upgrade-safe customizations, faster checkout rendering — is real. In our experience, the new checkout renders meaningfully faster on mobile, often shaving several hundred milliseconds off time-to-interactive compared to a heavily customized checkout.liquid. We won't quote exact numbers because yours will vary with region and app load, but the direction is consistent.
The downside is also real: you cannot do everything you used to do. If your old checkout had a custom step, a bespoke address form, or a non-standard payment flow glued in with Liquid and jQuery, some of that will not have a one-to-one replacement.
The four categories of customization — and which ones hurt
Before writing a line of code, we audited each store's existing checkout customizations and sorted them into four buckets. This sort predicted roughly 80% of the migration effort.
1. Cosmetic changes
Colors, fonts, logo, button radii, field styling. These move cleanly to the Branding API or the Checkout Editor. Low risk, usually a half-day of work including QA across breakpoints.
2. Informational additions
Trust badges, delivery estimates, custom line-item messaging, gift messages, upsell blocks. These map well to Checkout UI Extensions at predefined extension targets like purchase.checkout.block.render or purchase.checkout.cart-line-item.render-after. Expect one to three days per extension including review.
3. Logic customizations
Dynamic discounts, shipping rate manipulation, payment method hiding based on cart contents. These move to Shopify Functions. The ergonomics are different — you're writing Rust or JavaScript compiled to WASM, running on Shopify's infrastructure with strict input schemas — but the model is powerful once you accept the constraints.
4. Flow changes
Adding a custom step, replacing the address form, injecting a third-party identity verification mid-checkout. This is where migrations die. Checkout Extensibility does not let you add new steps or replace core components. If your old checkout did this, you need a product conversation before an engineering one.
On the B2B store we migrated, the old checkout had a custom "PO number and approval email" step. We replaced it with a UI extension at the information step that collected the PO number into a cart attribute, plus a post-purchase extension for the approval email trigger. Different UX, same business outcome. The merchant signed off, but only after we built a Figma prototype first.
The UI extension pattern that held up
Most tutorials show you a trivial "Hello world" extension. Real extensions need to read cart state, call your backend, handle loading and error states, and respect the checkout's own validation lifecycle.
Here's the skeleton we standardized on, written against the current @shopify/ui-extensions-react/checkout API:
import {
reactExtension,
Banner,
BlockStack,
Text,
useApi,
useApplyAttributeChange,
useAttributeValues,
useCartLines,
} from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.block.render',
() => <DeliveryWindow />,
);
function DeliveryWindow() {
const { query } = useApi();
const lines = useCartLines();
const [selected] = useAttributeValues(['delivery_window']);
const applyAttribute = useApplyAttributeChange();
const hasPerishable = lines.some((l) =>
l.merchandise.product.productType === 'Fresh',
);
if (!hasPerishable) return null;
return (
<BlockStack spacing="base">
<Text emphasis="bold">Choose a delivery window</Text>
{!selected && (
<Banner status="warning">
Select a window to continue to payment.
</Banner>
)}
{/* window picker omitted for brevity */}
</BlockStack>
);
}
Three things to notice:
- The extension returns
nullwhen irrelevant. Do not render empty containers; they still take layout space and hurt perceived performance. - State is stored in cart attributes, not component state. Checkout can re-render or navigate between steps, and your extension will be remounted. Attributes survive; local state does not.
- Validation is declarative via
useBuyerJourneyIntercept(not shown above). Do not try to block the Continue button by hiding it — intercept the journey and return acanBlockProgressreason.
Shopify Functions: the mental model shift
Functions are the part most Liquid-era developers underestimate. You are not writing a request handler. You are writing a pure function that takes a typed input (the cart, the discount, the delivery options) and returns a typed set of operations.
For the supplements store, we replaced a tangled Script Editor rule — "5% off when cart contains two or more items from the Immunity collection, stacking with subscription discount" — with a product discount function. The migration took about a day and a half, most of which was writing GraphQL input queries to request only the cart fields we needed. Keep your input query minimal; it affects both performance and the schema contract you're committing to.
One trap: Functions run on every relevant cart mutation. If your function is slow or your input query is bloated, you will feel it in cart latency, not just checkout. Profile with the Shopify CLI's function run command against realistic cart fixtures before shipping.
The app audit nobody wants to do
Every store we migrated had between four and eleven apps that touched checkout. Some had already shipped Checkout Extensibility-compatible versions. Some had a "compatibility mode" that technically worked but silently disabled features. Two apps on the apparel store had been abandoned by their developers entirely.
Before you plan the migration, open each checkout-touching app and verify:
- Does the vendor have an Extensibility-compatible release?
- Does it require a plan upgrade or a new app install (not just an update)?
- Does it preserve your existing configuration, or do you reconfigure from scratch?
- Are there feature gaps versus the Liquid version?
For the two abandoned apps, we rebuilt their functionality as in-house UI extensions. Cheaper than finding replacement apps and reconfiguring data.
What we tracked during cutover
You cannot A/B test checkout.liquid against the new checkout on the same store — Shopify toggles it globally. So we instrumented aggressively before the switch and watched for regressions in the week after.
The metrics that caught real problems:
- Checkout-to-purchase conversion rate, segmented by device and country
- Payment method distribution (we lost a wallet option on one store because the app wasn't ready)
- Average checkout duration from first interaction to thank-you page
- Support ticket volume tagged "checkout" — the leading indicator before conversion data stabilizes
On two of three stores, conversion was flat or slightly up within two weeks. On the B2B store, it dipped for ten days while buyers learned the new PO flow, then recovered. We would not have known the recovery was happening without the daily segmentation.
Where we'd start
If you are staring at this migration right now, do these four things this week, in order:
- Export your current checkout customizations as a written inventory, sorted into the four buckets above. If anything lands in bucket four (flow changes), schedule a product conversation before touching code.
- Audit your checkout apps against the vendor's Extensibility roadmap. Decide what you'll replace, rebuild, or drop.
- Stand up a development store with a copy of your theme and reinstall your apps there. This is your migration lab — do not test on production.
- Instrument your current checkout with the metrics above so you have a baseline. Without one, you'll argue about vibes for a month after cutover.
If you want a second pair of eyes on your migration plan or help rebuilding a bucket-four customization as a UI extension, that's the kind of work our e-commerce team does regularly. The migration is finite. The checkout you end up with will outlive the project by years — spend the planning time.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Cart Abandonment Emails That Actually Recover Revenue in 2026
Most abandoned cart flows are three generic emails and a 10% coupon. Here's how we rebuild them into something that recovers real revenue — segmented, event-driven, and worth the deliverability risk.

Shopify Metaobjects for Product Variants: When They Save You and When They Trap You
Shopify's 100-variant cap pushes teams toward metaobjects for configurable products. They solve real problems — and quietly create new ones around inventory, search, and checkout. Here's the honest breakdown.
Faceted Search on Shopify at 100k SKUs: Where the Native Filters Break
Shopify's built-in filtering is fine until your catalog crosses six figures. Here's where it falls apart, what Search & Discovery actually indexes, and how we rebuild filtered collection pages without wrecking SEO.
