Shopify Plus checkout extensions: a freelancer's playbook
Checkout UI Extensions, Functions, and Pixels — how I scope, build, and ship checkout customization without violating PCI scope or wrecking conversion.
Shopify Plus is the only Shopify tier with meaningful checkout customization. Even on Plus, the checkout itself is hosted by Shopify — you cannot replace the form, you cannot bypass the payment surface. What you can do is extend it: UI Extensions to add fields and content, Functions to customize discount, shipping, and payment logic, and Pixels for analytics that does not block the checkout flow.
What checkout extensibility actually replaces
If you remember the Liquid checkout from before Shopify Plus's checkout extensibility migration, you remember a lot of things that no longer apply. Custom Liquid in checkout.liquid is gone. Script Editor is gone. The new model is component-based, server-validated, and runs in a sandboxed extension surface that Shopify can update without breaking your customizations.
The tradeoff is constraint. You cannot do anything you want anymore. You can do specific things, well, within bounds Shopify has thought about. For most stores this is a net positive — the old model produced unmaintainable Liquid that broke at every checkout update. The new model is stable, scoped, and ships with type-safe APIs in TypeScript.
When to use UI Extensions
Custom fields above the cart, between the cart and contact step, between contact and shipping, between shipping and payment, or on the order status page. Examples: a gift message field, a delivery instructions textarea, a B2B PO number capture, a 'leave this on my doorstep' checkbox.
Custom content blocks on the same surfaces. Examples: a trust banner, a referral incentive, a 'still available' upsell, an order-specific message ('this product ships separately').
Post-purchase upsells on the thank-you page. Examples: 'add a 12-month warranty', 'subscribe and save', 'one-click bundle add'. Post-purchase is the highest-converting checkout surface I have shipped — well over 10% take rate on the right offer.
When to use Functions
Discount Functions for any discount logic the native discount engine cannot express. Examples: tiered volume discounts, B2B-specific pricing, free gift-with-purchase based on cart contents, exclusive-product-bundle pricing.
Shipping Functions for custom shipping methods, hide-show logic, freight thresholds. Examples: 'free shipping over $50 except for this product category', 'oversized item surcharge for west-coast addresses', 'lead-time flagging for backorder products'.
Payment Customization Functions for hide-show logic on payment methods. Examples: 'hide PayPal for B2B customers', 'show purchase orders only for tagged customers', 'hide afterpay over a cart threshold'.
When to use Pixels
Customer Events (formerly Pixels) for analytics, ad tracking, and any third-party script that wants to observe the checkout. The point of Pixels is sandboxing — your tag manager runs in a separate context that cannot block or modify the checkout. Conversion tracking that used to fight the checkout for main thread time now coexists peacefully.
If you are migrating from script editor or from theme.liquid script tags, every analytics script needs to move to a Pixel. This is one of the largest sources of slow checkout I see on Plus stores that have not finished the migration.
Project shape and scoping
A typical checkout extension engagement has three phases: discovery, build, monitoring.
Discovery: I read the existing checkout customizations, list every custom field and every script firing in checkout, and produce a target architecture. The output is a written proposal with the specific Shopify primitives (UI Extension, Function, or Pixel) that replace each existing customization.
Build: usually two to four weeks for a typical store. Each extension is a separate commit, deployed to a development store, validated against representative orders, and then promoted to production. UI Extensions and Functions deploy via the Shopify CLI; Pixels can be installed via the admin or via the CLI for theme-app extensions.
Monitoring: 30 days post-launch I watch checkout conversion, abandoned cart rate, and order-to-shipped time for any regressions. If something dropped, I have time and contractual obligation to fix it.
Common pitfalls
Treating UI Extensions like full React apps. They are sandboxed components with a limited API surface. You cannot fetch from arbitrary URLs (without a backend you provide). You cannot mutate the cart from a UI Extension (use Functions for that). The constraints are intentional.
Writing Functions in JavaScript when Rust is faster. Functions can be written in JavaScript or compiled from Rust, AssemblyScript, or any language that targets WebAssembly. For high-traffic stores, the latency difference between a JavaScript Function and a Rust-compiled WASM Function is meaningful — sometimes the difference between a Function timing out and completing successfully on a complex cart.
Skipping the Function input query optimization. Each Function declares an input query in GraphQL that limits what data is loaded. If your input query asks for cart.lines including every metafield on every product, your Function is slow before it does any logic. Trim the input query to exactly the fields you need.
Forgetting that customers can revisit the order status page. Your post-purchase extension renders on the thank-you page once. Your order status extension renders every time the customer visits the order. The extensions are separate. Pick the right one for the use case.
Real-world example: a B2B PO capture
Requirement: B2B customers with the 'wholesale' tag must enter a PO number at checkout. The PO number must appear on the order, the order confirmation email, and the packing slip. Customers without the wholesale tag should not see the field at all.
Solution: a UI Extension on the checkout::cart::after surface that reads the customer's tags and conditionally renders a PO number field. The field uses BlockStack and TextField components from the @shopify/ui-extensions React bindings. On change, the value is written to a cart attribute via the applyAttributeChange API. The cart attribute then maps to an order attribute, which is rendered in the email template (Liquid for emails is alive and well) and printed on the packing slip via a small Liquid edit.
Validation: the field uses the buyerJourney intercept API to block checkout submission if the field is empty for a wholesale customer. The buyer sees a clear error message; the checkout cannot be submitted without a PO.
Total time: about a day to build, a day to test, a day to ship and document. The extension is 80 lines of TypeScript. It replaces a previous solution that lived in checkout.liquid and broke every time Shopify updated the checkout.
Closing
Checkout extensibility on Plus is the most powerful e-commerce customization I have access to, with the cleanest deployment story I have used in any e-commerce engagement. The constraints are real and worth working within. If you are shipping a Plus store with custom checkout in 2026, you should be on extensions. If you are not, the migration is overdue.