TL;DR — Shopify split checkout into three layers. What buyers see → UI Extensions. What Shopify decides → Functions. How it looks → Branding. Scripts, the thing that used to do all three at once, stops running on 30 June 2026. Pick your surface by what you're actually changing, not by what you used to write a Script for.
If you customise Shopify checkouts, the question you ask in 2026 is no longer "how do I write this?" It's "where does this belong?" That's a better question, and the platform now forces you to answer it before you write a line of code.
For a decade, the answer was Scripts. One Ruby box on a Plus store, and you could change a price, hide a payment method, and inject a banner from the same place. That era is days from over: as of 15 April 2026 you can no longer add or edit Shopify Scripts, and on 30 June 2026 they stop executing entirely. Shopify has said the date won't move.
What replaced Scripts isn't one thing. It's a deliberate split into three surfaces, each with one job. This post is the decision framework we use at BrainFeed to route any checkout request to the right one — built as a tree, a lookup table, and a migration map, because the same idea lands differently depending on whether you're a merchant, an engineer, or an architect.
This article is for:
- Developers and technical PMs who have a specific checkout change to make and aren't sure which surface owns it
- Teams mid-migration off Scripts before the 30 June deadline who need to know where each piece lands
- Anyone who just searched "how do I hide a payment method in Shopify checkout"
This article is NOT for:
- Plus stores needing a full Checkout Extensibility migration runbook — that's a separate post in our Migration & Scaling pillar
- Anyone wanting an exhaustive API reference; Shopify's docs do that better than any blog can
- Readers hoping for a "Shopify is locking everything down" hot take — the split is an upgrade, and I'll argue why
Why Scripts disappeared
Scripts was a single Ruby file that ran inside checkout on Plus stores. It could rewrite line-item prices, reorder or hide shipping and payment methods, and drop markup onto the Thank You and Order Status pages. One surface, total reach — the appeal and the problem in one.
That reach is what made checkout impossible for Shopify to improve. If any Script could run arbitrary code against the page, Shopify couldn't upgrade or harden checkout without risking every customisation built on top of it — performance and security held hostage by a feature designed to do anything, anywhere.
So Shopify drew boundaries. The timeline, precisely:
- 28 August 2025 —
checkout.liquidfor the Information, Shipping, and Payment steps was sunset. Stores that hadn't moved are being auto-upgraded. - 15 April 2026 — Scripts became read-only. No new Scripts, no edits to existing ones.
- 30 June 2026 — Scripts stop running. Any pricing, shipping, payment, or bundle logic still living in a Script breaks on 1 July.
- 26 August 2026 — non-Plus stores lose the Additional Scripts, Thank You, and Order Status script boxes too.
The lesson worth carrying out of the Scripts era isn't "Shopify took my toy away." It's that Scripts let you put logic and presentation in the same box, and that convenience became a category error. The new model makes you separate the two — and the rest of this article is why that separation is the point.
The three modern layers
Checkout UI Extensions are how you change what the buyer sees. They're React (or JS) components that render at fixed extension points in the checkout, customer account, and post-purchase flows. They run in a sandboxed Web Worker with no access to the DOM, the window, or sensitive payment data — they render through Shopify's component APIs, not into the page directly. This is the layer for fields, messages, banners, upsells, and anything informational or lightly interactive.
Shopify Functions are how you change what Shopify decides. They're small programs compiled to WebAssembly (Rust, or JavaScript via Javy) that run on Shopify's servers as part of checkout's own logic: computing discounts, hiding or reordering delivery and payment methods, validating the cart, and transforming line items for bundles. They take a GraphQL input query and return a set of operations. They are pure logic — no UI, and crucially no network access.
Branding is how you change how it looks. Colours, fonts, logo, corner radius, button styles — configured through the checkout editor and the Branding API, not written as code. If the request is "make it match our brand," this is the layer, and reaching for an extension to do it is a mistake.
The capability matrix
If you remember one image from this article, make it this. Five seconds tells you where anything belongs:
| Capability | UI Extensions | Functions | Branding |
|---|---|---|---|
| Show / add content to checkout | ✓ | ✗ | ✗ |
| Run business logic | ✗ | ✓ | ✗ |
| Modify shipping / delivery options | ✗ | ✓ | ✗ |
| Modify payment options | ✗ | ✓ | ✗ |
| Apply / compute discounts | ✗ | ✓ | ✗ |
| Block / validate checkout | ✗ | ✓ | ✗ |
| Make network calls | ✓ (opt-in) | ✗ | ✗ |
| Change colours / fonts / logo | Limited | ✗ | ✓ |
The matrix is also the argument. Almost every row has exactly one ✓. The surfaces barely overlap — and where they do (network calls in UI Extensions, limited styling), it's deliberate, not redundant.
The decision tree
Route by the job you're trying to do:
- Changing what the buyer sees — a field, a message, a banner, an upsell → UI Extension
- Changing a discount — tiered, bundle, conditional → Discount Function
- Changing shipping/delivery — hide, rename, reorder methods; pincode rules → Delivery Customization Function
- Changing payment — hide, rename, reorder methods; threshold rules → Payment Customization Function
- Blocking or validating the cart — min order, address rules, B2B gates → Cart & Checkout Validation Function
- Bundling — merge or expand line items → Cart Transform Function
- Look-and-feel only — colours, fonts, logo → Branding (no code)
The last branch is the one people miss. Logic and its explanation are now two surfaces by design: the Function decides, the UI Extension narrates. That feels like extra work until the first time you change the logic without touching the copy, or vice versa.
Ask the right question first
The whole framework turns on the question you ask before you build:
Wrong question: "How do I hide Cash on Delivery?" Right question: "Am I changing payment logic or checkout UI?"
Hiding COD is a payment-logic change → Payment Function. Name the surface first and the implementation follows. Ask the wrong question and you'll lose an afternoon trying to hide a payment method from a UI Extension that was never allowed to touch one.
Where your old Scripts go
If you're migrating off Scripts before 30 June, this is the map. Every Script use case has exactly one new home:
| Old Script use case | New home |
|---|---|
| Line-item / order discounts | Discount Function |
| Tiered or conditional shipping rates | Delivery Customization Function |
| Hiding or reordering payment methods | Payment Customization Function |
| Bundle / "buy X get Y" logic | Discount Function + Cart Transform |
| Cart validation / purchase rules | Cart & Checkout Validation Function |
| Checkout messaging / banners | UI Extension |
checkout.liquid styling | Branding |
Common requests, mapped
This is the lookup the rest of the article exists to support. Nobody searches for "checkout surfaces" — they search for the thing they need to ship today. Find the row, ship the right layer.
| Requirement | Surface |
|---|---|
| Add a delivery-note or gift-message field | UI Extension |
| Show loyalty points / store-credit balance | UI Extension |
| Show a custom upsell or cross-sell block | UI Extension |
| Display a shipping-ETA or trust message | UI Extension |
| Explain why a discount applied | UI Extension (+ Function) |
| Hide Cash on Delivery above ₹10,000 | Payment Function |
| Reorder or rename payment methods | Payment Function |
| Hide express shipping for certain pincodes | Delivery Function |
| Rename a shipping option | Delivery Function |
| Tiered / volume discounts | Discount Function |
| Buy-X-get-Y or bundle pricing | Discount Function + Cart Transform |
| Block PO box / restricted addresses | Validation Function |
| Enforce a minimum order value | Validation Function |
| Validate GST / VAT number format | Validation Function |
| Restrict checkout by customer tag (B2B) | Validation Function (+ UI Extension) |
| Merge or expand bundle line items | Cart Transform Function |
| Change checkout colours, fonts, or logo | Branding |
| Adjust button style or corner radius | Branding |
Bookmark-worthy, and worth saying once: if a row pulls you toward two surfaces, that's not the framework failing — it's the seen-versus-decided split showing through.
The scorecard
The tree tells you which. This tells you why, and where each surface stops:
| Surface | Runs where | Best for | Hard limits | Wrong tool when |
|---|---|---|---|---|
| UI Extensions | Buyer's browser, sandboxed Web Worker | What the buyer sees; light interaction | No DOM/window; checkout-step extensions are Plus-only; can't touch pricing/shipping/payment logic | You need to change a rule or a price |
| Functions | Shopify's servers, WebAssembly | Discounts, delivery/payment rules, validation, bundles | No network/IO; deterministic; per-store count limits; renders nothing | You need to show or explain something |
| Branding | Checkout editor / Branding API | Colours, fonts, logo, radius | Style only, within Shopify's tokens; no logic, no new content | You need behaviour, not appearance |
| Scripts (RIP) | Legacy Plus checkout | — historical — | Read-only since 15 Apr 2026; stops 30 Jun 2026 | Always, now — migrate off |
What bites you in production
These constraints trip up teams arriving from the Scripts world. Read them not as complaints but as the shape of the bargain: each limit is the price of a checkout Shopify can keep making faster and safer underneath you.
Functions can't reach the network — on purpose. No fetch, no database, no external pricing service mid-checkout. A Function takes its GraphQL input and returns operations; that's the whole contract. It feels restrictive until you realise it's why a Function can run inside checkout's hot path without becoming a latency or reliability risk. If a decision genuinely needs external data, you precompute it (metafields, tags, cart attributes set earlier) and let the Function read that. Determinism isn't a tax here — it's what makes the checkout fast.
UI Extensions can't see the page — also on purpose. Sandboxed in a Web Worker, no DOM, no window, no access to payment fields. You don't style the checkout from an extension and you can't scrape it; you compose through Shopify's component and API surface. That isolation is exactly what lets Shopify restyle and re-lay-out checkout without breaking your extension. UI Extensions can make network calls, but only opt-in: you set network_access = true in the extension's capabilities, and the call goes to your app backend, not arbitrary origins.
Functions have count limits, and an execution order. As of mid-2026 a store runs at most 5 Discount Functions, 1 Cart Transform, and 5 Validation Functions, and they execute in a fixed order — transforms and pricing first, then discounts, then validation. Architect for that order; two Functions fighting over the same outcome is a self-inflicted wound, not a platform bug.
The Plus line moved, and it's the most common surprise. Functions — discounts, delivery, payment, validation — work on every plan. But UI Extensions on the checkout steps (Information, Shipping, Payment) are Plus-only; non-Plus stores get extensions on the Thank You and Order Status pages. So a non-Plus merchant can hide COD over a threshold with a Function but can't add a field to the payment step. Confirm the plan before you promise the UI.
The "just do it in Liquid" reflex is gone. There is no checkout.liquid to drop a snippet into anymore. The muscle memory of "I'll just edit the template" has no target. That reflex is the single biggest source of wasted hours for teams that haven't internalised the new model — which is the whole reason a decision framework beats another how-to.
What the code actually looks like
Two short examples to make the split concrete. First, a UI Extension that adds a delivery-note field — pure presentation, written in React against the 2026-04 API:
// extensions/delivery-note/src/Checkout.jsx
import {
reactExtension,
TextField,
useApplyAttributeChange,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.delivery-address.render-after",
() => <DeliveryNote />,
);
function DeliveryNote() {
const applyAttributeChange = useApplyAttributeChange();
return (
<TextField
label="Delivery note (optional)"
multiline={3}
onChange={(value) =>
applyAttributeChange({ type: "updateAttribute", key: "delivery_note", value })
}
/>
);
}
Notice what it doesn't do: it doesn't price anything, doesn't touch payment, doesn't read the DOM. It renders and writes a cart attribute. That's the ceiling of a UI Extension, by design.
Now the logic half — a Payment Function that hides Cash on Delivery above ₹10,000. The decision lives entirely server-side:
# extensions/hide-cod/input.graphql
query Input {
cart { cost { totalAmount { amount } } }
paymentMethods { id name }
}
// extensions/hide-cod/src/run.js
export function run(input) {
const total = parseFloat(input.cart.cost.totalAmount.amount);
const cod = input.paymentMethods.find((m) => m.name === "Cash on Delivery");
if (total <= 10000 || !cod) return { operations: [] };
return { operations: [{ hide: { paymentMethodId: cod.id } }] };
}
No fetch, no database, no UI — input in, operations out. If you also wanted to tell the buyer why COD vanished, that's the other surface: a UI Extension reading the same threshold. Two files, two layers, one behaviour. That's not duplication; that's the architecture doing its job.
When you shouldn't customise at all
The strongest checkout decision is often don't. A few cases where the right surface is "none":
Don't customise because you can. The most common failure we see isn't picking the wrong surface — it's building a customisation that shouldn't exist. The delivery-note field nobody reads. Six upsells stacked into the payment step. A validation rule that blocks 2% of legitimate orders to stop 0.1% of bad ones. Every element you add to checkout is friction, and the highest-converting checkout is almost always the simplest one. "Which surface?" should always be preceded by "should this exist?"
Don't hand-roll what an app already does well. If a battle-tested App Store app covers the discount or validation, a custom Function is maintenance you've volunteered for. Build custom when the logic is genuinely yours; install when it isn't.
Don't customise before you've migrated. If you're still on Scripts, the prerequisite isn't a new feature — it's getting onto Checkout Extensibility before 30 June. Build the new thing after the old thing is safely moved, not on top of a surface that's about to stop running.
Don't write code to change a colour. If the request is visual, it's Branding. Reaching for an extension to restyle checkout is the inverted version of the old Scripts mistake — using a logic/UI tool for a job the styling layer already owns.
The thesis
Scripts hid a category error. It let you keep logic, presentation, and styling in one box, and because it was convenient, nobody noticed the cost — until Shopify couldn't improve checkout without permission from a million bespoke Ruby files.
The three-layer split fixes that at the root. What buyers see, what Shopify decides, and how it looks are genuinely different concerns, and they now live in genuinely different places. The constraints people grumble about — Functions with no network, extensions with no DOM — aren't friction bolted on. They're the boundaries that let Shopify make checkout faster, safer, and upgradeable underneath you, without ever asking what you built on top.
So the skill in 2026 isn't knowing the Functions API by heart. It's answering "where does this belong?" before you write anything — and trusting that if the answer feels like two surfaces, that's the platform telling you you've got two concerns. Pick by the axis, not by habit.
If you'd rather hand the whole migration-and-customisation problem to a team that already thinks this way, that's the work we do: Shopify development for SMB merchants.
Next in this series: the full Scripts-to-Functions migration — what we move first, how we test a Function before it touches live checkout, and the order-of-operations traps. After that: a Functions deep-dive on the input-query pattern and how to keep logic deterministic when the business rules aren't.




