> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom templates

> Override how any Aftersell Cart block renders with your own JSX: what a template replaces, what's in scope, how to style it, and where to find each block's props.

A **custom template** lets you override how an individual block renders. Instead of the block's built-in UI, the cart renders your own JSX, using the same data the block would normally use. It's a cross-cutting capability rather than a block of its own: most blocks expose it from their **Code** tab.

This page covers what applies to **every** block. For the props a specific block hands you, jump to [the block's own reference](#props-for-each-block).

## Custom template vs. Custom code block

These sound similar but do different things:

* A **custom template** *replaces the rendering of an existing block* with your own markup, and hands you that block's own data (the Header's title and item count, the Summary's totals, and so on). It doesn't add anything new; it restyles one block.
* The **[Custom code](/aftersell/cart/custom-code-blocks)** block *adds a new block* of arbitrary HTML or React anywhere in the cart.

Reach for a custom template when the built-in block is almost right but you need a different layout or markup. Reach for a Custom code block when you want to add something the built-in blocks don't cover.

## Using a custom template

1. Select a block in the editor and open its **Code** tab.
2. Edit the default template. Custom templates are **JSX only** (the HTML-or-JSX choice is exclusive to the Custom code block).
3. Click **Compile**. Compiling strips the types and transpiles the JSX, so it catches **syntax** errors. Type errors don't stop a compile — the editor flags those inline as you type, with the same IntelliSense that autocompletes the block's props.
4. Turn the template on to make the cart use it instead of the built-in rendering.
5. **Reset to default** restores the block's original template at any time.

## Writing a template with AI

The Code tab includes a **Copy AI prompt** button (✦ wand icon). Clicking it copies a self-contained brief to your clipboard that you can paste directly into an AI chat session (Claude, ChatGPT, or similar).

The prompt includes everything the AI needs to write a valid template for that specific block:

* The compile rules (single expression, no `export default`, no imports)
* The exact props the block receives, matching what the editor's IntelliSense shows
* The locked function signature the editor enforces
* Block-specific rules (money formats, which handlers to wire, accessibility requirements)
* A fill-in section where you paste your current template and describe the change you want

After copying, open an AI session, paste the prompt, fill in the two blanks at the bottom (your current template and the change you want), and send. The AI returns a complete template you can paste back into the editor and compile.

<Tip>
  Paste your existing template into the fill-in section rather than leaving it blank. The AI uses it as the starting point, so any customization you've already made carries forward rather than being replaced by the default.
</Tip>

<Note>
  The prompt is specific to each block. The **Copy AI prompt** button only appears on blocks that support custom templates.
</Note>

<Tip>
  The default template you start from is a **working copy of the block's built-in markup**, so you always have a correct, rendering reference to modify rather than a blank page. Reach for **Reset to default** whenever you want that reference back.

  It isn't always a byte-for-byte match. The Header's default template also renders `logoUrl`, which the built-in markup has no placement for, so turning that template on is how an uploaded header image first appears.
</Tip>

## What your template replaces

A template replaces the block's rendering **entirely**. There's no wrapper left around your JSX, which has consequences worth knowing before you start deleting things:

| You lose                            | What that means                                                                                                                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The block's wrapper element         | Nothing wraps your markup. Any padding, alignment, or layout the block provided is now yours to supply.                                                                           |
| **The block's Design tab settings** | Design settings are applied as inline styles on the built-in wrapper, and that wrapper is gone. Colors, spacing, and radii set in the Design tab **stop applying** to this block. |
| Built-in accessibility affordances  | `aria-label`s, focus handling, and semantic elements only exist if your JSX includes them.                                                                                        |

<Warning>
  **The Design tab is the one people are caught by.** While a custom template is active, the Design tab's fields are disabled and a warning icon appears next to the "Design" heading. Hover over the icon to see why. Style the block from your template instead, either [inline or with your own CSS](#styling-a-custom-template). The fields re-enable as soon as you turn the custom template off.
</Warning>

What you keep: the block's position in the cart, its visibility toggle, its settings (which still feed the props you receive), the cart's [Custom CSS](/aftersell/cart/custom-css) panel, and **the built-in loading skeleton**.

That last one surprises people. The block checks whether the cart is still loading *before* it reaches your template, so the built-in skeleton renders during load and your template only runs once the cart is ready. You don't have to build a loading state.

## What's available inside a template

Your template is a single function component. It compiles from **TSX**, so type annotations are allowed and stripped at compile time. That's why the default templates are written with them:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props: HeaderProps) {
  return <div>{/* … */}</div>;
}
```

**The signature line and the closing brace are locked** — the editor won't let you edit either, and hovering shows "Locked — this line can't be edited." You write the body between them. **Reset to default** is the one thing that can replace them.

What else matters:

* **You get five hooks:** `useState`, `useEffect`, `useMemo`, `useRef`, and `useCallback`. Plus `Fragment`, for `<>…</>`.
* **There are no imports.** You can't `import` anything, and there's no `React` object in scope, so no `React.useReducer`, no `React.Children`. If a hook isn't in the list above, it isn't available.
* **Props are read-only.** Mutating a prop won't do anything useful. To change the cart, use the handler props the block gives you (`onClose`, `increment`, `selectPlan`, and so on) rather than writing to props directly.
* **`window` is reachable**, so a template can call the [Cart SDK](/aftersell/cart/sdk-overview) via `window.aftersell.cart` when it needs something the block's props don't cover.

## Conventions across every block

Three rules hold everywhere, and knowing them removes most of the guesswork:

* **`*Html` props are pre-sanitized rich text.** Render them with `dangerouslySetInnerHTML`. They've already been through the cart's sanitizer, and merchant tokens like `{{total_price}}` are already resolved.
* **Prices that arrive as `string` are already formatted** in the shop's money format. Prices as `number` are in cents. A block gives you one or the other, and each block's table says which.
* **`isLoading` is always `false` inside a template.** The block renders its built-in skeleton and only calls your template once the cart has loaded, so the prop is passed for completeness rather than for you to branch on.

<Note>
  A few blocks return nothing at all in certain states, so your template is never called with empty data. The Rewards template never sees an empty `milestones`, and the Subscription upgrade template never sees a null `view`. Each block's reference notes where this applies, so you can skip the empty-state branch.
</Note>

## Styling a custom template

The default template you start from carries the block's classnames. How you style your edits depends on how far you move from that starting point.

### The two class families

Every element in a default template carries a paired classname, and they do very different jobs:

| Family            | What it does                                                                                                        | Write CSS against it?                                                                   |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `cart-internal-*` | **Carries the block's built-in styling.** Every rule in the cart's stylesheet targets this family.                  | No. It's the cart's own plumbing, and the Custom CSS editor flags selectors against it. |
| `cart-external-*` | **A hook with no styling of its own.** Nothing in the cart's stylesheet targets it; it exists for your CSS to grab. | Yes. This is the supported way to restyle a block.                                      |

So `cart-internal-header__title` is what makes the title *look* like the built-in title, and `cart-external-header__title` is the handle you're meant to grab when you want to change how it looks.

### Small changes: keep both classnames

If you're reordering elements, relabelling, or adding something inside the existing structure, leave the classnames alone. You keep the built-in look for free, and you restyle through [Custom CSS](/aftersell/cart/custom-css) targeting the `cart-external-*` hooks.

### Restructuring: drop both classnames

Once you're changing the DOM structure rather than tweaking it, take **both** families off your markup and use [your own classnames](#option-1-your-own-classnames-plus-custom-css) instead. There's a separate reason for each.

**Drop `cart-internal-*` because the built-in CSS was written for the built-in DOM.** Keep those classes on restructured markup and you inherit layout rules that assume elements you no longer have: flex containers expecting different children, spacing between elements that moved, positioning relative to something you removed. This usually surfaces as your own CSS "not working" when the built-in rules are the ones winning.

<Warning>
  **Drop `cart-external-*` because it's a shared name, not yours.** Those classnames mean something specific on the built-in markup, and your Custom CSS is written once for the whole cart. If a restructured template reuses them, any rule you write targets both your structure and the built-in one.

  That goes wrong the moment you turn the custom template off: the block reverts to its built-in markup, and your CSS is still pointing at it, now styling a DOM it was never written for. Your own prefix keeps the two cleanly separated, so toggling a template off is a clean revert.
</Warning>

Two ways to style what you've built:

#### Option 1: your own classnames plus Custom CSS

Best for anything you'll maintain or reuse. Give your classes a prefix nobody else will collide with, usually your store or brand name:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props) {
  return (
    <div className="northwind-custom-header">
      <div className="northwind-custom-header__title" dangerouslySetInnerHTML={{ __html: props.title }} />
      <button type="button" className="northwind-custom-header__close" onClick={props.onClose}>
        &times;
      </button>
    </div>
  );
}
```

Then in the cart editor, select **Cart settings** in the left panel and open the **Custom CSS** tab on the right:

```css theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.northwind-custom-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
}

.northwind-custom-header__title {
  font-size: 18px;
  font-weight: 600;
}

.northwind-custom-header__close:hover {
  opacity: 0.6;
}
```

A prefix matters more than it looks. Without one, a class like `.header` or `.title` risks colliding with the cart's own classes, another app's template, or a future block.

#### Option 2: inline styles

No CSS panel round-trip, and everything lives in one place:

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
```

Good for layout scaffolding and one-offs. Its limits are the usual ones: no `:hover` or other pseudo-classes, no media queries, and no reuse across blocks. Reach for Option 1 once you want any of those.

### Picking an approach

| Situation                                  | Do this                                                                          |
| ------------------------------------------ | -------------------------------------------------------------------------------- |
| Same structure, different wording or order | Keep both classnames, restyle via Custom CSS on `cart-external-*`                |
| New structure, styling you'll maintain     | Your own prefixed classes, both cart families dropped                            |
| New structure, a few quick layout rules    | Inline styles, both cart families dropped                                        |
| Lots of custom code across several blocks  | Your own prefixed classes everywhere, so any template can be toggled off cleanly |

<Note>
  The cart renders in a shadow root, so your theme's stylesheet can't reach inside it. Styles for a custom template have to come from the cart's own **Custom CSS** panel or from inline styles, not from your theme. See [Custom CSS](/aftersell/cart/custom-css).
</Note>

## When a template fails

A broken template never breaks the cart. The block renders **nothing** and everything around it keeps working, which is safe but easy to miss: a blank space where your block should be is the symptom.

| Failure                 | When you'll see it           | Where it reports                                                                                                        |
| ----------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Type error              | As you type                  | An inline squiggle in the editor. It does **not** block compiling — the compiler strips types rather than checking them |
| Syntax error            | When you click **Compile**   | The editor, before it can reach your storefront                                                                         |
| A crash while rendering | On the storefront, once live | `console.error('[aftersell-cart] module crashed: …')`                                                                   |

Because the block silently disappears rather than erroring visibly, always check a template in [preview](/aftersell/cart/previewing-carts) before publishing. If a block has gone missing, open the browser console first.

Two things worth guarding for, since both crash a template that assumes otherwise:

* **Nullable props.** Many props are `null` in normal conditions (`logoUrl` with no logo, `imageUrl` with no image, `variantTitle` on a single-variant product). Check before you use them.
* **Arrays that can be empty.** `discountTags` and `discountCodes` are `[]` far more often than not.

## Limitations

* **Custom templates are display overrides.** To run logic against the cart (subscribe to events, add items, react to changes), use [Custom scripts](/aftersell/cart/custom-scripts) and the [Cart SDK](/aftersell/cart/sdk-overview).
* **Almost every block supports one.** The exceptions are the **[Express payments](/aftersell/cart/express-payments-block)** block, which hosts Shopify's own payment buttons, and the **[Cart items](/aftersell/cart/cart-items-block)** container itself, though the **Product** row inside it does support a custom template.
* **A template can't change what a block fundamentally does.** It changes how the block's data is presented, not the data or the behavior behind it.

## Props for each block

Every block passes its own data. The full prop table, with types and a worked example, lives on that block's page:

| Block                                                                              | Props it receives                                                                                                                  |
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| [Header](/aftersell/cart/header-block#custom-template)                             | `title`, `logoUrl`, `leftSection`, `rightSection`, `itemCount`, `onClose`, `isLoading`                                             |
| [Banner](/aftersell/cart/banner-block#custom-template)                             | `text`, `shouldUseTimer`, `isTimerExpiredAndShouldHide`, `isLoading`                                                               |
| [Rewards](/aftersell/cart/rewards-block#custom-template)                           | `milestones`, `rewardsMessageHtml`, `showIcons`, `isLoading`                                                                       |
| [Cart items · Product](/aftersell/cart/cart-items-block#custom-template)           | 25 props: per-line content, identifiers, and quantity controls                                                                     |
| [Subscription upgrade](/aftersell/cart/subscription-upgrade-block#custom-template) | `view`, `selectPlan`, `onChange`, `oneTimeValue`, and more                                                                         |
| [Summary](/aftersell/cart/summary-block#custom-template)                           | `leftHtml`, `rightHtml`, `discountCodes`, `totalPrice`, `savings`, and more                                                        |
| [Checkout button](/aftersell/cart/checkout-button-block#custom-template)           | `label`, `href`, `isLoading`                                                                                                       |
| [Discount code](/aftersell/cart/discount-code-block#custom-template)               | `discountCodeInput`, `placeholder`, `buttonText`, `isValidating`, `isInvalid`, `setDiscountCodeInput`, `handleSubmit`, `isLoading` |
| [Empty cart](/aftersell/cart/empty-cart-block#custom-template)                     | `text`, `cta`, `href`                                                                                                              |
| [Image](/aftersell/cart/image-block#custom-template)                               | `imageUrl`, `altText`, `maxHeight`, `fullWidth`                                                                                    |
| [Notes](/aftersell/cart/notes-block#custom-template)                               | `titleHtml`, `placeholder`, `noteInput`, `status`, `isExpanded`, `onNoteChange`, `onNoteBlur`, `onToggle`, and more                |
| [Product add-on](/aftersell/cart/product-add-on-block#custom-template)             | `addonTitleHtml`, `descriptionHtml`, `priceHtml`, `imageUrl`, `format`, `isEnabled`, `handleAdd`, `handleToggle`, and more         |
| [Shipping protection](/aftersell/cart/shipping-protection-block#custom-template)   | `titleHtml`, `descriptionHtml`, `priceHtml`, `imageUrl`, `format`, `isEnabled`, `handleAdd`, `handleToggle`, and more              |
| [Upsells](/aftersell/cart/upsells-block#custom-template)                           | `title`, `addButtonText`, `layout`, `upsells`, `selectVariant`, `handleAdd`, and the carousel controls                             |

The [Custom code](/aftersell/cart/custom-code-blocks) block is the one surface that **adds** markup rather than replacing a block's rendering, so its props are different: the whole cart, plus an add-to-cart action. See [Custom code blocks → Props](/aftersell/cart/custom-code-blocks#props).
