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.liquid customizations 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-extension
This creates an extensions/my-checkout-extension/ directory with src/index.tsx (UI) or src/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
</InlineStack>
</BlockStack>
);
}
Configure the extension in shopify.extension.toml
api_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 deploy
After 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}
=>
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.render target 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/checkout components 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_version in shopify.extension.toml to access new APIs while keeping backward compatibility
- Handle async operations with loading states —
useApplyCartLinesChange is 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
1---2name: shopify-checkout-extensions3description: Customize Shopify's checkout with UI extensions for upsells and custom fields, plus Shopify Functions for serverless discount and shipping logic4---56# Shopify Checkout Extensions78## Overview910Shopify 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).1112## When to Use This Skill1314- When adding custom UI blocks to checkout (upsells, trust badges, gift message fields, warranty options)15- When implementing custom discount logic beyond native Shopify discount rules (e.g., tiered discounts, B2B pricing)16- When creating custom shipping method filtering or renaming based on cart contents17- When building payment customization to hide/rename payment methods for specific customers18- When validating order contents before checkout completes (e.g., quantity limits, region restrictions)19- When replacing deprecated `checkout.liquid` customizations for Shopify Plus stores2021## Core Instructions22231. **Scaffold an extension with Shopify CLI**2425 ```bash26 # Inside an existing Shopify app directory27 shopify app generate extension28 # Choose: Checkout UI extension OR Shopify Function29 # Name: my-checkout-extension30 ```3132 This creates an `extensions/my-checkout-extension/` directory with `src/index.tsx` (UI) or `src/index.ts` (Function).33342. **Build a Checkout UI extension**3536 Checkout UI extensions use a React-like component API from `@shopify/ui-extensions-react/checkout`:3738 ```typescript39 // extensions/order-upsell/src/index.tsx40 import {41 reactExtension,42 useCartLines,43 useApplyCartLinesChange,44 useSettings,45 BlockStack,46 Button,47 Image,48 Text,49 InlineStack,50 Divider,51 } from "@shopify/ui-extensions-react/checkout";5253 const CheckoutBlock = reactExtension(54 "purchase.checkout.block.render",55 () => <OrderUpsell />56 );57 export { CheckoutBlock };5859 function OrderUpsell() {60 const cartLines = useCartLines();61 const applyCartLinesChange = useApplyCartLinesChange();62 const { upsell_variant_id: upsellVariantId } = useSettings();6364 // Only show upsell if a variant is configured and cart doesn't already contain it65 if (!upsellVariantId) return null;6667 const alreadyInCart = cartLines.some(68 (line) => line.merchandise.id === upsellVariantId69 );7071 if (alreadyInCart) return null;7273 const handleAddUpsell = async () => {74 await applyCartLinesChange({75 type: "addCartLine",76 merchandiseId: upsellVariantId,77 quantity: 1,78 });79 };8081 return (82 <BlockStack spacing="base">83 <Divider />84 <InlineStack blockAlignment="center" spacing="base">85 <Image source="https://cdn.shopify.com/s/files/..." aspectRatio={1} />86 <BlockStack>87 <Text emphasis="bold">Add a gift bag for $3.99</Text>88 <Text appearance="subdued">Beautiful packaging for your order</Text>89 </BlockStack>90 <Button onPress={handleAddUpsell}>Add</Button>91 </InlineStack>92 </BlockStack>93 );94 }95 ```96973. **Configure the extension in `shopify.extension.toml`**9899 ```toml100 api_version = "2025-01"101102 [[extensions]]103 type = "ui_extension"104 name = "Order Upsell"105 handle = "order-upsell"106107 [[extensions.targeting]]108 module = "./src/index.tsx"109 target = "purchase.checkout.block.render"110111 [extensions.settings]112 [[extensions.settings.fields]]113 key = "upsell_variant_id"114 type = "variant_reference"115 name = "Upsell Product Variant"116 ```1171184. **Build a Shopify Function for custom discounts**119120 Shopify Functions compile to WebAssembly. Use Rust or JavaScript:121122 ```typescript123 // extensions/volume-discount/src/index.ts124 import type {125 RunInput,126 FunctionRunResult,127 CartLineInput,128 } from "../generated/api";129130 const NO_CHANGES: FunctionRunResult = { discounts: [], discountApplicationStrategy: "FIRST" };131132 export function run(input: RunInput): FunctionRunResult {133 const { cart } = input;134135 // Calculate total quantity across all lines136 const totalQuantity = cart.lines.reduce(137 (sum, line) => sum + line.quantity,138 0139 );140141 // Tiered volume discount142 let discountPercent = 0;143 if (totalQuantity >= 20) discountPercent = 20;144 else if (totalQuantity >= 10) discountPercent = 10;145 else if (totalQuantity >= 5) discountPercent = 5;146147 if (discountPercent === 0) return NO_CHANGES;148149 return {150 discounts: [151 {152 value: {153 percentage: { value: discountPercent.toString() },154 },155 targets: [{ orderSubtotal: { excludedVariantIds: [] } }],156 message: `${discountPercent}% volume discount (${totalQuantity} items)`,157 },158 ],159 discountApplicationStrategy: "FIRST",160 };161 }162 ```163164 Function `shopify.extension.toml`:165166 ```toml167 api_version = "2025-01"168169 [[extensions]]170 type = "function"171 name = "Volume Discount"172 handle = "volume-discount"173 runtime = "javascript"174175 [[extensions.input.variables]]176 name = "cart"177 type = "Cart"178179 [extensions.build]180 command = "npm run build"181 path = "dist/index.wasm"182 ```1831845. **Test and deploy extensions**185186 ```bash187 # Run local dev preview (UI extension hot-reloads in checkout)188 shopify app dev189 # Open the checkout preview URL shown in terminal190191 # Deploy all extensions to Shopify192 shopify app deploy193 ```194195 After 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.196197## Examples198199### Gift message field using useApplyMetafieldsChange200201```typescript202import {203 reactExtension,204 TextField,205 useApplyMetafieldsChange,206 useMetafield,207 BlockStack,208 Text,209} from "@shopify/ui-extensions-react/checkout";210211export default reactExtension(212 "purchase.checkout.shipping-option-list.render-after",213 () => <GiftMessage />214);215216function GiftMessage() {217 const giftMessage = useMetafield({ namespace: "custom", key: "gift_message" });218 const applyMetafieldsChange = useApplyMetafieldsChange();219220 return (221 <BlockStack spacing="tight">222 <Text emphasis="bold">Gift message (optional)</Text>223 <TextField224 label="Message"225 value={giftMessage?.value ?? ""}226 multiline={3}227 onChange={(value) =>228 applyMetafieldsChange({229 type: "updateMetafield",230 namespace: "custom",231 key: "gift_message",232 valueType: "string",233 value,234 })235 }236 />237 </BlockStack>238 );239}240```241242### Payment customization Function (hide cash on delivery for international orders)243244```typescript245// extensions/payment-customization/src/index.ts246import type { RunInput, FunctionRunResult } from "../generated/api";247248export function run(input: RunInput): FunctionRunResult {249 const country = input.cart.buyerIdentity?.countryCode;250251 // Hide "Cash on Delivery" for non-domestic orders252 const hideOperations = input.paymentMethods253 .filter((pm) => pm.name.toLowerCase().includes("cash on delivery") && country !== "US")254 .map((pm) => ({255 hide: { paymentMethodId: pm.id },256 }));257258 return { operations: hideOperations };259}260```261262## Best Practices263264- **Use the `purchase.checkout.block.render` target** for maximum placement flexibility — merchants can drag the block anywhere in the checkout editor265- **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)266- **Use `useSettings()` hook** to read merchant-configured values from the extension settings schema — avoids hardcoded IDs in extension code267- **Never read DOM or use browser APIs** in UI extensions — they run in a sandboxed Worker environment without DOM access268- **Use `@shopify/ui-extensions-react/checkout` components only** — native HTML and other UI libraries are not available in the extension sandbox269- **Test payment and shipping Functions with real checkout sessions** — the local dev preview only works for UI extensions; Functions need to be deployed to test270- **Version-pin your extension API version** — increment the `api_version` in `shopify.extension.toml` to access new APIs while keeping backward compatibility271- **Handle async operations with loading states** — `useApplyCartLinesChange` is async; show a spinner while the mutation is in flight272273## Common Pitfalls274275| Problem | Solution |276|---------|----------|277| Extension not appearing in checkout editor | Ensure the extension is deployed (`shopify app deploy`) and the correct checkout template is selected in Admin → Checkout |278| Function returns `FUNCTION_EXECUTION_TIMEOUT` | Move configuration out of runtime logic into Function input metafields; avoid complex loops on large catalogs |279| `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 |280| Extension crashes with "Cannot use browser APIs" | Remove `document`, `window`, `localStorage` references — the extension runs in a Worker sandbox |281| Discount Function not applying | Verify the Function-based discount is active in Admin → Discounts and the customer qualifies per any eligibility rules |282| Checkout UI extension settings not saving | Settings fields require `handle` values that match the keys referenced by `useSettings()` in the extension code |283284## Related Skills285286- @shopify-app-development287- @shopify-storefront-api288- @shopify-metafields289- @checkout-flow-optimization290- @shopify-admin-api