A Shopify checkout UI extension renders inside checkout at one of about thirty named targets, declared in shopify.extension.toml and built from Polaris web components. It can read the cart, write cart attributes and query the Storefront API. It cannot change prices or shipping rates, and as of the 2026-07 API version it can no longer block the buyer’s progress on its own.
That last part is recent, and it is a deprecation rather than a removal, so the old call still builds and still runs while you plan the move. The rest of checkout customisation went earlier. checkout.liquid is unsupported for the Information, Shipping and Payment steps, and Shopify sunset it along with additional scripts for the Thank you and Order status pages on 28 August 2025, with script tags following on 26 August 2026 for stores not on Plus.
Where can a checkout UI extension render?
Targets come in two shapes, and the difference decides how defensively you write the component.
Static targets attach to a specific part of checkout. purchase.checkout.delivery-address.render-after sits under the shipping address, purchase.checkout.payment-method-list.render-before above the payment methods, purchase.checkout.cart-line-item.render-after under each line in the order summary. If the section is not on the page, neither is your extension.
Block targets work the other way round. purchase.checkout.block.render and purchase.thank-you.block.render are positioned by the merchant in the checkout and accounts editor, and Shopify’s targets reference is blunt about the consequence: block extensions “are always rendered, even if the section they’re positioned in is hidden”. An extension the merchant parked above the shipping address still appears on a digital-only order that has no address at all, so a block extension has to check the state it depends on and return nothing when that state is missing.
Set default_placement in the TOML to pick where merchants first meet your block. To check the others, run shopify app dev and append ?placement-reference=INFORMATION1 to your dev store’s checkout URL.
One constraint sits above all of this. Extensions for the information, shipping and payment steps require a Shopify Plus plan. Thank you and Order status targets are open to every plan, which is why the post-purchase surveys and delivery trackers cluster there.
What do the four capabilities allow?
Capabilities are opt-in permissions in the TOML. Declaring one is not the same as being granted it.
api_version = "2026-07"
[[extensions]]
type = "ui_extension"
name = "delivery-notes"
handle = "delivery-notes"
[[extensions.targeting]]
module = "./src/Checkout.jsx"
target = "purchase.checkout.delivery-address.render-after"
[extensions.capabilities]
api_access = true
network_access = true
block_progress = false
[extensions.capabilities.collect_buyer_consent]
sms_marketing = true
customer_privacy = true
api_access opens the Storefront API with seven unauthenticated scopes: product and collection publications, product listings, product tags, selling plans, collection listings and metaobjects. Shopify signs the requests, so there is no token to hold. Reading a metaobject from checkout is the useful one if you already keep merchandising copy there, which is the pattern behind our note on choosing between metafields and metaobjects.
network_access covers calls to your own backend, and the capabilities guide says you “must request access in order to publish your extension” with it on. Your endpoint has to answer CORS with Access-Control-Allow-Origin: *. Since that means anyone can call it from anywhere, authenticate with a session token rather than a query parameter.
collect_buyer_consent unlocks applyTrackingConsentChange() and the consent components, split into sms_marketing and customer_privacy.
block_progress is the one to read twice. Merchants can decline it in the checkout editor, and when they do, an intercept returning behavior: 'block' is treated as behavior: 'allow'. No error is raised and the extension renders exactly as before, so read useExtensionCapability('block_progress') and warn the merchant in the checkout editor when it comes back false.
How do you block checkout now that intercept is deprecated?
Move the rule to a validation function. The Buyer Journey API reference for 2026-07 marks useBuyerJourneyIntercept and buyerJourney.intercept deprecated, says to “use a cart and checkout validation function instead”, and warns they will be removed in a future version of the API.
The replacement runs on Shopify’s servers at the cart.validations.generate.run target, so a buyer cannot get past it by driving the Storefront API directly.
export function cartValidationsGenerateRun(input) {
const errors = input.cart.lines
.filter((line) => line.quantity > 2)
.map(() => ({
message: 'Limit two per order on this item',
target: '$.cart',
}));
return {
operations: errors.length ? [{validationAdd: {errors}}] : [],
};
}
target decides where the message lands. $.cart puts it at page level; $.cart.deliveryGroups[0].deliveryAddress.zip or $.cart.buyerIdentity.email attach it to a field. Validations apply to online store carts and carts built for custom storefronts as well as to checkout itself, and a store can have 25 of them active at once.
The rest of the Buyer Journey API is untouched. steps, activeStep, completed and canBlockProgress still work, so an extension can render only on the payment step, or only once the order is placed. It just cannot be the thing that says no.
If you have already ported discounts, the split will be familiar: the Function makes the decision, the extension explains it. Same division as in the Discount Function API migration.
What changed when Polaris web components arrived?
API version 2025-10 and later use web components by default, and the React package is off the happy path. Imports move from @shopify/ui-extensions-react/checkout to @shopify/ui-extensions/checkout/preact. react in package.json gives way to preact. reactExtension('purchase.checkout.block.render', () => <Extension />) becomes a plain Preact render into document.body. Components are s- prefixed custom elements with camelCase attributes, and the extension APIs hang off a global shopify object instead of arriving as a hook argument.
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useState} from 'preact/hooks';
export default function extension() {
render(<Extension />, document.body);
}
function Extension() {
const [note, setNote] = useState('');
async function save(event) {
const value = event.currentTarget.value;
setNote(value);
const result = await shopify.applyAttributeChange({
type: 'updateAttribute',
key: 'deliveryNote',
value,
});
if (result.type !== 'success') {
console.error('Could not save the delivery note', result);
}
}
return (
<s-text-field
label="Delivery instructions"
name="deliveryNote"
value={note}
onChange={save}
/>
);
}
The hooks survive the move. useAttributeValues, useExtensionCapability and the others are re-exported from the Preact entry point, so most components port with an import rewrite and a tag swap. shopify app dev writes a shopify.d.ts into the extension directory, which is what types the global.
Scaffold with shopify app generate extension --template checkout_ui rather than assembling the TOML by hand, then set api_version deliberately and re-read the reference each quarter, since this area has already changed twice: components at 2025-10, blocking at 2026-07. Put anything that must hold into a validation function and keep the extension for what the buyer reads. If the customisation changes what an order costs, or which delivery or payment options appear, no UI extension will do it, and you want to know that before you quote for it. Same for the plan: on a store that is not Plus, the three main checkout steps are closed to you.
We build Shopify apps and checkout extensions for UK merchants, including the awkward moves off checkout.liquid and Scripts. There is more about that work on our Shopify development page.