shopify-checkout-extensions
Shopify Checkout Extensions
Overview
Shopify Checkout Extensions allow apps to render custom UI blocks inside Shopify's checkout without forking the checkout template. Shopify Functions let you replace backend logic — discounts, shipping, payment methods, and order validation — with custom WebAssembly modules that run inside Shopify's infrastructure. Together they replace the deprecated checkout.liquid customization approach and work with both Shopify Plus and non-Plus stores (UI extensions) or Plus-only (some Functions targets).
When to Use This Skill
- When adding custom UI blocks to checkout (upsells, trust badges, gift message fields, warranty options)
- When implementing custom discount logic beyond native Shopify discount rules (e.g., tiered discounts, B2B pricing)
- When creating custom shipping method filtering or renaming based on cart contents
- When building payment customization to hide/rename payment methods for specific customers
- When validating order contents before checkout completes (e.g., quantity limits, region restrictions)
- When replacing deprecated
checkout.liquidcustomizations for Shopify Plus stores
Core Instructions
-
Scaffold an extension with Shopify CLI
# Inside an existing Shopify app directory shopify app generate extension # Choose: Checkout UI extension OR Shopify Function # Name: my-checkout-extensionThis creates an
extensions/my-checkout-extension/directory withsrc/index.tsx(UI) orsrc/index.ts(Function). -
Build a Checkout UI extension
Checkout UI extensions use a React-like component API from
@shopify/ui-extensions-react/checkout:// extensions/order-upsell/src/index.tsx import { reactExtension, useCartLines, useApplyCartLinesChange, useSettings, BlockStack, Button, Image, Text, InlineStack, Divider, } from "@shopify/ui-extensions-react/checkout"; const CheckoutBlock = reactExtension( "purchase.checkout.block.render", () => <OrderUpsell /> ); export { CheckoutBlock }; function OrderUpsell() { const cartLines = useCartLines(); const applyCartLinesChange = useApplyCartLinesChange(); const { upsell_variant_id: upsellVariantId } = useSettings(); // Only show upsell if a variant is configured and cart doesn't already contain it if (!upsellVariantId) return null; const alreadyInCart = cartLines.some( (line) => line.merchandise.id === upsellVariantId ); if (alreadyInCart) return null; const handleAddUpsell = async () => { await applyCartLinesChange({ type: "addCartLine", merchandiseId: upsellVariantId, quantity: 1, }); }; return ( <BlockStack spacing="base"> <Divider /> <InlineStack blockAlignment="center" spacing="base"> <Image source="https://cdn.shopify.com/s/files/..." aspectRatio={1} /> <BlockStack> <Text emphasis="bold">Add a gift bag for $3.99</Text> <Text appearance="subdued">Beautiful packaging for your order</Text> </BlockStack> <Button onPress={handleAddUpsell}>Add</Button> </InlineStack> </BlockStack> ); } -
Configure the extension in
shopify.extension.tomlapi_version = "2025-01" [[extensions]] type = "ui_extension" name = "Order Upsell" handle = "order-upsell" [[extensions.targeting]] module = "./src/index.tsx" target = "purchase.checkout.block.render" [extensions.settings] [[extensions.settings.fields]] key = "upsell_variant_id" type = "variant_reference" name = "Upsell Product Variant" -
Build a Shopify Function for custom discounts
Shopify Functions compile to WebAssembly. Use Rust or JavaScript:
// extensions/volume-discount/src/index.ts import type { RunInput, FunctionRunResult, CartLineInput, } from "../generated/api"; const NO_CHANGES: FunctionRunResult = { discounts: [], discountApplicationStrategy: "FIRST" }; export function run(input: RunInput): FunctionRunResult { const { cart } = input; // Calculate total quantity across all lines const totalQuantity = cart.lines.reduce( (sum, line) => sum + line.quantity, 0 ); // Tiered volume discount let discountPercent = 0; if (totalQuantity >= 20) discountPercent = 20; else if (totalQuantity >= 10) discountPercent = 10; else if (totalQuantity >= 5) discountPercent = 5; if (discountPercent === 0) return NO_CHANGES; return { discounts: [ { value: { percentage: { value: discountPercent.toString() }, }, targets: [{ orderSubtotal: { excludedVariantIds: [] } }], message: `${discountPercent}% volume discount (${totalQuantity} items)`, }, ], discountApplicationStrategy: "FIRST", }; }Function
shopify.extension.toml:api_version = "2025-01" [[extensions]] type = "function" name = "Volume Discount" handle = "volume-discount" runtime = "javascript" [[extensions.input.variables]] name = "cart" type = "Cart" [extensions.build] command = "npm run build" path = "dist/index.wasm" -
Test and deploy extensions
# Run local dev preview (UI extension hot-reloads in checkout) shopify app dev # Open the checkout preview URL shown in terminal # Deploy all extensions to Shopify shopify app deployAfter deploying, go to Admin → Checkout → Customize to add the UI extension block to a checkout template. Functions are activated by creating a discount with the function from Admin → Discounts.
Examples
Gift message field using useApplyMetafieldsChange
import {
reactExtension,
TextField,
useApplyMetafieldsChange,
useMetafield,
BlockStack,
Text,
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension(
"purchase.checkout.shipping-option-list.render-after",
() => <GiftMessage />
);
function GiftMessage() {
const giftMessage = useMetafield({ namespace: "custom", key: "gift_message" });
const applyMetafieldsChange = useApplyMetafieldsChange();
return (
<BlockStack spacing="tight">
<Text emphasis="bold">Gift message (optional)</Text>
<TextField
label="Message"
value={giftMessage?.value ?? ""}
multiline={3}
onChange={(value) =>
applyMetafieldsChange({
type: "updateMetafield",
namespace: "custom",
key: "gift_message",
valueType: "string",
value,
})
}
/>
</BlockStack>
);
}
Payment customization Function (hide cash on delivery for international orders)
// extensions/payment-customization/src/index.ts
import type { RunInput, FunctionRunResult } from "../generated/api";
export function run(input: RunInput): FunctionRunResult {
const country = input.cart.buyerIdentity?.countryCode;
// Hide "Cash on Delivery" for non-domestic orders
const hideOperations = input.paymentMethods
.filter((pm) => pm.name.toLowerCase().includes("cash on delivery") && country !== "US")
.map((pm) => ({
hide: { paymentMethodId: pm.id },
}));
return { operations: hideOperations };
}
Best Practices
- Use the
purchase.checkout.block.rendertarget for maximum placement flexibility — merchants can drag the block anywhere in the checkout editor - Keep Function execution under 5ms — Shopify enforces a strict execution time limit; avoid network calls inside Functions (use metafields or Function input variables for configuration)
- Use
useSettings()hook to read merchant-configured values from the extension settings schema — avoids hardcoded IDs in extension code - Never read DOM or use browser APIs in UI extensions — they run in a sandboxed Worker environment without DOM access
- Use
@shopify/ui-extensions-react/checkoutcomponents only — native HTML and other UI libraries are not available in the extension sandbox - Test payment and shipping Functions with real checkout sessions — the local dev preview only works for UI extensions; Functions need to be deployed to test
- Version-pin your extension API version — increment the
api_versioninshopify.extension.tomlto access new APIs while keeping backward compatibility - Handle async operations with loading states —
useApplyCartLinesChangeis async; show a spinner while the mutation is in flight
Common Pitfalls
| Problem | Solution |
|---|---|
| Extension not appearing in checkout editor | Ensure the extension is deployed (shopify app deploy) and the correct checkout template is selected in Admin → Checkout |
Function returns FUNCTION_EXECUTION_TIMEOUT |
Move configuration out of runtime logic into Function input metafields; avoid complex loops on large catalogs |
useCartLines returns stale data after cart update |
Use the returned promise from applyCartLinesChange to wait for checkout to re-evaluate before reading cart lines again |
| Extension crashes with "Cannot use browser APIs" | Remove document, window, localStorage references — the extension runs in a Worker sandbox |
| Discount Function not applying | Verify the Function-based discount is active in Admin → Discounts and the customer qualifies per any eligibility rules |
| Checkout UI extension settings not saving | Settings fields require handle values that match the keys referenced by useSettings() in the extension code |
Related Skills
- @shopify-app-development
- @shopify-storefront-api
- @shopify-metafields
- @checkout-flow-optimization
- @shopify-admin-api