About shopify-checkout-extensions
A guide for Shopify app developers building checkout extensions, covering UI blocks and backend rules implemented with Shopify Functions. It describes setup, configuration, testing, and deployment as an alternative to deprecated `checkout.liquid` customizations.
Key features
- 01Generate Checkout UI extensions
- 02Implement rules with Shopify Functions
- 03Configure extension targets and settings
- 04Preview locally and deploy extensions
- 05Work within checkout sandbox and runtime limits
Use cases
- 01Add a gift message field to checkout
- 02Filter shipping methods by cart contents
- 03Apply custom discount rules to orders
- 04Validate order contents before checkout completes
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-checkout-extensionsAfter installing, type/shopify-checkout-extensionsin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
tile.jsonevals/checkout-metafield-persistence-with-ui-e/criteria.jsontask.mdevals/configurable-ui-extension-with-settings-/criteria.jsontask.mdevals/shopify-function-configuration-and-wasm-/criteria.jsontask.mdDetails
From the project READMEShopify 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