Skip to main content

How to use Custom Templates

Build custom JSX templates for the 12 cart drawer modules and the Product Page Upsell (FBT) widget: how the editor works, every prop, code examples, and troubleshooting.

A
Written by Aditya Singh

Follow this guide to customize the Cart Drawer and the Product Page Upsell (FBT) widget with your own code.

You will:

  1. Turn on the custom template editors (cart modules only)

  2. Open the module you want to customize

  3. Edit the template code (JSX)

  4. Compile → Activate → Save

  5. Check the cart preview and your live store

There are 13 modules with a custom template editor — 12 in the Cart editor and 1 on the Product Page Upsell funnel — covering 30 editable tabs in total.


Before you start

Who this is for

Custom templates are written in React (JSX) with HTML and CSS.

  • If you (or your developer) are comfortable editing JSX → continue.

  • If not → hire a Shopify developer. Do not guess at the code.

Support note

Our support team cannot help write, debug, or fix custom template code.

This is the same warning shown on the Settings toggle: "Requires React and JSX knowledge. Our support team cannot assist with custom template development or debugging." We can help with settings, billing, and bugs in the built-in features — not with your custom code.

What this feature covers

Module

Where to open it

Tabs you can customize

Upsells

Cart editor → Upsells

Custom template, Bottom sheet

Upsell with Discount

Cart editor → Upsell with Discount

Custom template, Bottom sheet

Add-ons

Cart editor → Add-ons

Add-ons, Loading skeleton

Cart items

Cart editor → Cart items

Skeleton, Product tile, Variant, Properties, Price, Volume tiers

Announcement bar

Cart editor → Announcement

Main, Skeleton

Rewards

Cart editor → Rewards

Full, Skeleton, Rewards to be achieved, Pick Free Gift

Discount codes

Cart editor → Discount codes

Basic input, Advanced banner, Advanced sheet

Empty cart

Cart editor → Empty cart

Empty state, Recommendations

Split payments

Cart editor → Split payments

Split payments

Trust badges

Cart editor → Trust badges

Trust badges

Additional notes

Cart editor → Additional notes

Notes

Cart design

Cart editor → Cart design

Header, Footer

Product Page Upsell (FBT)

Product Page Upsells → funnel → Design

6 bases (offer type × layout)

Product Page Upsell (FBT) now has a full JSX custom template that replaces the entire offer block — see Part 11. It lives on the funnel Design tab and does not need the cart Settings toggle.


Part 0 — How custom templates actually work

Reading this once will save you most of the trouble people run into. Five facts:

1. Your code becomes one function

When you press Compile, the app wraps your code like this and converts it to JavaScript:

(props) => (
  <>  … your JSX …  </>
)

That means your template must be one single JSX expression: a fragment <>…</> or one root element. It also means:

  • No import or export

  • No React hooks (useState, useEffect, …)

  • No TypeScript types

  • No statements before the JSX — you cannot write const x = 1; at the top

  • props is your only input

2. React is available

Your compiled template runs with React in scope, so React.Fragment works. This is the correct way to render two sibling elements per loop item:

{props.products.map((p, index) => (
  <React.Fragment key={p.id}>
    {index > 0 && <div className="divider" />}
    <div className="card">{p.title}</div>
  </React.Fragment>
))}

You still cannot use hooks — there is no component state to hold.

3. Every tab stores three things

For each tab, the app saves your source, the compiled output, and the Active switch separately:

What is saved

Set by

Your code

Typing in the box (and Format)

The compiled version

The Compile button

Active / Inactive

The Active switch

Your storefront uses the template only when it is Active AND has been compiled. This is why turning on Active without pressing Compile does nothing at all — and why the order Compile → Activate → Save matters.

Reset restores the original starter code into the box. It does not compile — always press Compile again after Reset.

4. Mistakes fail safely

A broken template makes that one block disappear. It does not break your cart or your store.

What went wrong

What you see

Code will not compile

Red banner in the editor: Compile error: …. Nothing is saved.

Compiled code is invalid

The block renders nothing. Browser console shows [IA_CART] custom template compile failed:

Your code crashes while rendering

The block renders nothing and recovers. Browser console shows [IA_CART_ERR] cart drawer render crash

If a block vanished after you activated a template, open your browser console (F12 → Console) and look for those two messages. They tell you whether the problem is the compile step or your rendering code.

5. The app prepares your data

You never fetch products, format currency, call the Shopify cart API, or write analytics. The app does all of that and hands you finished values and ready-to-call functions. Your template only displays what it receives and calls the functions it is given.


Part 1 — Turn on custom templates (cart modules)

  1. Open the app.

  2. Go to Settings.

  3. Find Custom templates (marked Technical knowledge required).

  4. Tick Enable custom template editors.

  5. Click Save.

A Custom template card now appears inside the cart modules listed above.

Turning this off later does not disable live templates. Anything already Active keeps running on your storefront. To actually remove a template, set that tab to Inactive and Save.

Product Page Upsell (FBT) does not need this toggle — its editor is always visible on the funnel Design tab.


Part 2 — Open a template editor (cart modules)

  1. Go to Cart editor.

  2. Open the module you want (for example Upsells).

  3. Scroll to the Custom template card.

  4. Pick a tab if the module has more than one.

Each module — and each duplicated upsell instance — has its own template. Each tab has its own Compile button and its own Active switch.


Part 3 — Use the editor buttons (in this order)

Edit code  →  Format (optional)  →  Compile  →  Activate  →  Save

Button / control

What it does

Code box

Where you type your JSX.

Format

Tidies indentation and spacing. Does not compile or publish.

Compile

Required. Wait for the green "Successfully compiled at …" message.

Active / Inactive

Switches your template on or off for the storefront.

Reset

Restores the original starter code. Compile again afterwards.

Save

Saves the cart (or the funnel, for FBT). Nothing goes live until you save.

Rules to remember

  • Compile every tab you edit. Compiling one tab does not compile the others.

  • Activate every tab you want live. Each tab has its own switch.

  • Always Save at the end. Compile + Active without Save changes nothing.

  • After Reset, Compile again — Reset only refills the code box.


Part 4 — First success (do this before customizing)

Prove the pipeline works before you write any code of your own.

  1. Open Cart editor → Upsells → Custom template.

  2. Click Reset to load the starter code.

  3. Click Compile — wait for the green message.

  4. Turn the switch to Active.

  5. Click Save.

  6. Look at the cart preview — the upsell should look exactly as before.

That is correct. The starter code is a faithful copy of the built-in design, so nothing visibly changes. You have just confirmed that Compile → Activate → Save works. Now you can start editing.


Part 5 — How to write a template

What you are typing

One block of JSX. Not a React project, not a component file.

<>
  {/* your UI goes here */}
</>

Rule 1 — Wrap everything

Start and end with a fragment <></>, or use a single root <div>. Two elements side by side at the top level will not compile.

Rule 2 — Use props for data

Everything the app gives you lives on props.

<div dangerouslySetInnerHTML={{ __html: props.titleHtml }} />

Rule 3 — Loop over lists with a key

{props.products.map((p) => (
  <div key={p.id}>
    {p.title}
  </div>
))}

Every looped item needs a unique key. Use p.id — or item.rowKey for add-ons.

Rule 4 — Show HTML fields the safe way

Anything ending in Html is already-formatted HTML (prices, titles, savings). Render it like this:

<div dangerouslySetInnerHTML={{ __html: p.priceHtml }} />

Writing {p.priceHtml} directly would print the raw HTML tags on the page.

Rule 5 — Wire actions exactly as shown

Call the functions the app gives you. Do not build your own cart logic.

<button
  type="button"
  disabled={p.isLoading}
  onClick={() => {
    if (!props.isPreview) p.onAddToCart();
  }}
>
  {p.isLoading ? "Adding…" : p.buttonText}
</button>

The isPreview guard stops the admin preview from adding real items to a cart.

Rule 6 — JSX basics (quick reference)

Instead of…

Write…

class="card"

className="card"

style="color:red"

style={{ color: "red" }}

Merchant colours

style={p.buttonStyleVars}

Override one property

style={{ ...p.priceStyleVars, fontWeight: 700 }}

Show only sometimes

{condition && ( <div>…</div> )}

Either/or

{condition ? ( <A /> ) : ( <B /> )}

A comment

{/* note */}

A loading spinner

<span className="ia-circular-loader" />

Custom CSS

<style>{` .my-card:hover { … } `}</style>

Go back to the stock design

{props.builtInUpsell}


Part 6 — Build your first custom layout: Upsells

Follow these steps in order. Compile → Active → Save after each one so you can see what changed.

Step A — Start from a simple Upsell layout

Open Cart editor → Upsells → Custom template, replace the code with this, then Compile → Active → Save.

<>
  <div id="ia-upsell-component">
    <div
      style={{ fontSize: 18, fontWeight: 700, marginBottom: 12 }}
      dangerouslySetInnerHTML={{ __html: props.titleHtml }}
    />    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      {props.products.map((p) => (
        <div
          key={p.id}
          style={{
            display: "flex",
            gap: 12,
            alignItems: "center",
            padding: 8,
            border: "1px solid #eee",
            borderRadius: 8,
          }}
        >
          <img
            src={p.imageUrl}
            alt={p.title}
            width={72}
            height={72}
            style={{ objectFit: "cover", borderRadius: 6 }}
          />          <div style={{ flex: 1 }}>
            <a href={p.productUrl} style={{ fontWeight: 600, color: "inherit" }}>
              {p.title}
            </a>
            <div dangerouslySetInnerHTML={{ __html: p.priceHtml }} />            <button
              type="button"
              disabled={p.isLoading}
              onClick={() => {
                if (!props.isPreview) p.onAddToCart();
              }}
            >
              {p.isLoading ? "Adding…" : p.buttonText}
            </button>
          </div>
        </div>
      ))}
    </div>
  </div>
</>

Step B — Add reviews and compare-at price

Add these two blocks inside the <div style={{ flex: 1 }}>, just above the price:

{p.reviews}{p.showCompareAtPrice && p.compareAtPriceHtml && (
  <div
    style={{ textDecoration: "line-through", opacity: 0.6 }}
    dangerouslySetInnerHTML={{ __html: p.compareAtPriceHtml }}
  />
)}

p.reviews is a ready-made element — render it as-is. Never wrap it in JSON.stringify.

Step C — Add your own colours with CSS

Put a <style> block inside your fragment and scope it under the root id so it cannot leak into the rest of your theme:

<>
  <style>{`
    #ia-upsell-component .my-cta {
      background: #111;
      color: #fff;
      border: 0;
      border-radius: 999px;
      padding: 10px 16px;
      cursor: pointer;
      width: 100%;
    }
    #ia-upsell-component .my-cta:hover {
      background: #333;
    }
  `}</style>  <div id="ia-upsell-component">
    {/* …your layout… add className="my-cta" to the button… */}
  </div>
</>

Step D — Keep merchant settings working

Spread the style objects the app gives you (buttonStyleVars, titleStyleVars, priceStyleVars, …) so the colours chosen in the module settings still apply:

<button type="button" style={p.buttonStyleVars}>{p.buttonText}</button>

Step E — Fall back to the stock design

If you need to bail out quickly without deactivating, render the built-in UI:

<>{props.builtInUpsell}</>

Handle loading

While products are still loading, props.showSkeleton is true and props.products is empty. Show placeholders:

{props.showSkeleton ? (
  <div id="ia-upsell-component">
    <div style={{ height: 14, width: "35%", background: "#eee", marginBottom: 16 }} />
    <div style={{ display: "flex", gap: 16 }}>
      <div style={{ width: 80, height: 80, background: "#eee" }} />
      <div style={{ flex: 1, height: 60, background: "#eee" }} />
    </div>
  </div>
) : (
  /* your real layout */
)}

Customize the variant bottom sheet

Products with several variants open a bottom sheet instead of adding straight away. Do not build your own variant <select> on the card — call p.onAddToCart() and customize the sheet on its own tab.

  1. In the same card, open the Bottom sheet tab.

  2. Edit the code (props are in Appendix A3).

  3. Compile → Active → Save — this tab has its own switch.

<>
  <div className="ia-choose-variant-bs-details">
    <img height={60} width={60} src={props.imageUrl} alt="" />
    <div>
      <a href={props.productUrl}>{props.title}</a>
      <div>{props.variantTitle}</div>
    </div>
    <div dangerouslySetInnerHTML={{ __html: props.priceHtml }} />
  </div>  {props.options.map((option, index) => (
    <div key={index}>
      <div>{option.name}</div>
      <select
        className="ia-choose-variant-bs-variant-select"
        value={option.selectedValue}
        onChange={(e) => option.onChange(e.target.value)}
      >
        {option.values.map((value, valueIndex) => (
          <option key={valueIndex} value={value}>{value}</option>
        ))}
      </select>
    </div>
  ))}  {props.isOutOfStock && <div>This product is out of stock</div>}  <button
    type="button"
    disabled={props.isOutOfStock}
    style={props.buttonStyleVars}
    onClick={props.onAddToCart}
  >
    {props.isAddToCartLoading ? (
      <span className="ia-circular-loader" />
    ) : (
      props.buttonText
    )}
  </button>
</>

Fallback: {props.builtInBottomSheetContent}


Part 7 — Upsell with Discount

Same flow, same two tabs (Custom template + Bottom sheet). The bottom sheet props are identical to Upsells.

Root id: id="ia-upsell-discount-component"

Each card gets extra discount fields on top of the normal ones:

Extra prop

Meaning

priceHtml

The discounted price

originalPriceHtml

Price before the discount

showOriginalPrice

Whether to show the strikethrough price

saveTextHtml

The "Save 15%" badge

savingsStyleVars

Styles for the savings text

savingsAmount

Savings as a number

discountType / discountValue

The offer discount configuration

id

Composite productId-configId — use it as the key

<>
  <div id="ia-upsell-discount-component" style={{ padding: 12, ...props.containerStyleVars }}>
    <div
      style={{ marginBottom: 12, ...props.titleStyleVars }}
      dangerouslySetInnerHTML={{ __html: props.titleHtml }}
    />    <div style={{ display: "flex", gap: 12, overflowX: "auto" }}>
      {props.products.map((p) => (
        <div key={p.id} style={{ minWidth: 160, flex: "0 0 auto", ...p.containerStyleVars }}>
          <img
            src={p.imageUrl}
            alt={p.title}
            style={{ width: "100%", height: 120, objectFit: "cover", ...p.imageStyleVars }}
          />
          <div style={p.titleStyleVars}>{p.title}</div>
          <div style={p.priceStyleVars} dangerouslySetInnerHTML={{ __html: p.priceHtml }} />          {p.showOriginalPrice && p.originalPriceHtml && (
            <div
              style={{ textDecoration: "line-through", opacity: 0.65, ...p.originalPriceStyleVars }}
              dangerouslySetInnerHTML={{ __html: p.originalPriceHtml }}
            />
          )}          <div style={p.savingsStyleVars} dangerouslySetInnerHTML={{ __html: p.saveTextHtml }} />          <button
            type="button"
            style={{ width: "100%", marginTop: 8, ...p.buttonStyleVars }}
            disabled={p.isLoading}
            onClick={() => {
              if (!props.isPreview) p.onAddToCart();
            }}
          >
            {p.isLoading ? "…" : p.buttonText}
          </button>
        </div>
      ))}
    </div>
  </div>
</>

Fallback: {props.builtInUpsellDiscount}. Note that props.showSkeleton is always false for this module.


Part 8 — Cart Add-ons

Cart Add-ons (shipping protection, gift wrap, small extras) use toggles, not Add-to-cart buttons.

This is not the same as FBT's "Product Addon" offer type on the product page — those are two separate systems.

Two tabs

Tab

When it shows

Add-ons

The normal loaded list of add-on rows

Loading skeleton

While add-ons load on your live store — never in the admin preview

How the toggle works

item.toggle.state        // "enabled" | "disabled" | "loading"
item.toggle.onEnable()   // turn the add-on on
item.toggle.onDisable()  // turn the add-on off

toggle.state becomes "loading" in two situations: the row itself is being changed, or props.autoAddInProgress is true and the row is isEnabledByDefault. Just read toggle.state and both cases are handled.

Always guard the click:

onClick={() => {
  if (props.isPreview || props.autoAddInProgress || item.toggle.state === "loading") {
    return;
  }
  item.toggle.state === "enabled"
    ? item.toggle.onDisable()
    : item.toggle.onEnable();
}}

Starter — Add-ons tab

<div id="ia-add-ons-component" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
  {props.items.map((item) => (
    <div
      key={item.rowKey}
      style={{
        display: "flex",
        gap: 12,
        alignItems: "center",
        padding: 10,
        border: "1px solid #eee",
        borderRadius: 8,
      }}
    >
      <img src={item.imageUrl} alt="" width={56} height={56} style={{ borderRadius: 6 }} />      <div style={{ flex: 1 }}>
        <div dangerouslySetInnerHTML={{ __html: item.titleHtml }} />
        <div dangerouslySetInnerHTML={{ __html: item.priceHtml }} />
      </div>      <button
        type="button"
        onClick={() => {
          if (props.isPreview || props.autoAddInProgress || item.toggle.state === "loading") return;
          item.toggle.state === "enabled" ? item.toggle.onDisable() : item.toggle.onEnable();
        }}
      >
        {item.toggle.state === "loading"
          ? "…"
          : item.toggle.state === "enabled"
            ? "On"
            : "Off"}
      </button>
    </div>
  ))}
</div>

For the real pill-and-knob switch markup, press Reset in the editor — the starter code contains it.

Starter — Loading skeleton tab

<div id="ia-add-ons-component">
  {Array.from({ length: props.skeletonCount }).map((_, i) => (
    <div key={i} className="ia-add-on-margin-bottom">
      <div className="ia-flex-gap-16">
        <div style={{ ...props.imageStyleVars, background: "#eee", borderRadius: 4, flexShrink: 0 }} />
        <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
          <div style={{ background: "#eee", borderRadius: 4, height: 14, width: "70%" }} />
          <div style={{ background: "#eee", borderRadius: 4, height: 12, width: "50%" }} />
        </div>
      </div>
    </div>
  ))}
</div>

Add-ons props

Whole section

Meaning

items

The rows to render

autoAddInProgress

Blocks toggles while default add-ons are being added

builtInAddOns

Stock UI fallback

isPreview / moneyFormat / styles

Shared values

Each item

Meaning

id / rowKey

Identifiers — use key={item.rowKey}

imageUrl / imageStyleVars

Image and its styles

titleHtml / titleStyleVars

Title (HTML)

subtitleHtml / subtitleStyleVars

Optional subtitle (HTML)

priceHtml / priceStyleVars

Price (HTML)

switchStyleVars

Toggle track styles

isEnabledByDefault

Whether this add-on starts switched on

toggle.state

"enabled" | "disabled" | "loading"

toggle.onEnable() / toggle.onDisable()

Switch it on or off

Skeleton tab props: styles, skeletonCount, imageStyleVars, builtInAddOnsSkeleton.


Part 9 — Optional: carousel with arrows

The arrow helpers only work if you keep these names. The app finds the scroll container by class.

Piece

Required name

Root element

id="ia-upsell-component"

Scroll wrapper

ia-upsell-custom-carousel

Track

ia-carousel-list

Each slide

ia-carousel-item

Arrow button

ia-carousel-button

<div className="ia-carousel-button" onClick={props.onCarouselPrevious}>‹</div>
{/* your list using ia-carousel-list / ia-carousel-item */}
<div className="ia-carousel-button" onClick={props.onCarouselNext}>›</div>

The equivalent classes for other modules:

Module

Scroll container class

Upsells / Upsell with Discount

ia-upsell-custom-carousel

Empty cart recommendations

ia-empty-cart-custom-carousel

Product Page Upsell (FBT)

oxify-fbt-custom-carousel


Part 10 — All other cart modules

Every module below follows the same flow: enable editors, open the module, edit the Custom template, Compile → Activate → Save. Press Reset in any tab to load a complete working starter that reproduces the built-in design exactly.

Cart items (6 tabs)

Path: Cart editor → Cart items → Custom template

Tab

Replaces

Skeleton

Loading placeholder for line items

Product tile

The whole line-item row

Variant

Variant selector / text

Properties

Line-item properties

Price

The price block

Volume tiers

Per-line volume discount tiers

The four sub-slots (Variant, Properties, Price, Volume tiers) are composed into the product tile. If you activate only Price, the built-in tile keeps rendering and simply uses your price block.

Skeleton tab props: isPreview — that is the whole contract.

Product tile props:

Prop

Meaning

title, imageUrl, productUrl

Line basics

quantity, showQuantitySelector

Quantity UI (false for reward lines)

price, compareAtPrice, savings

Formatted currency strings — use dangerouslySetInnerHTML

formattedPrice, formattedCompareAtPrice, savingsLabel

The same values under explicit names

unitPriceText

Unit-price label, or null when disabled

discountCodes

Line-level discount titles

onIncrease(), onDecrease()

Quantity + 1 / − 1

onDeleteProduct()

Remove the line

onQuantityChange(val), handleInputChange(e)

Set quantity directly

isLoading, isDeleteLoading, isVariantLoading

What is currently in flight

cartItemLoading, setCartItemLoading

Whole-line overlay control

loadingOperation, setLoadingOperation

Per-button spinner: "increase" | "decrease" | "remove" | "inputChange"

variant, properties, subscription, volume

The resolved sub-slots

variantSwitcher, showVariantSelector, replaceItemWithVariant

Variant switching

conversionFactor, styles, savingsStyle, rawItem

Extra data

bundle

Always null — no bundle feature in this app

isPreview, shopLocale

Shared

builtInProductTile

Exact stock tile fallback

builtInVariant, builtInVariantText, builtInProperties, builtInPrice, builtInSubscription, builtInVolumeDiscount

Slot fallbacks

Loading behaviour: quantity buttons use per-button spinners — call props.setLoadingOperation("increase") on click and the app clears it for you. Delete and variant swaps use a whole-line overlay that the app draws around your template automatically.

Variant tab props: item / rawItem, styles, variantSwitcher, showVariantSelector, the loading flags, builtInVariant, builtInVariantSwitcher, and builtInVariantText (a ready-made read-only "Name: Value" node).

variantSwitcher field

Meaning

isActive

false when this line has no switchable variants

isSingleVariant

Only one option available

fetchingOptions

Options are still loading

selectedVariantId, selectedVariantLabel

Current selection

variantOptions[]

Each: { id, label }

onVariantChange(variantId)

Swap this line to another variant

styles

Dropdown styles

Match the built-in dropdown with class ia-cart-item-variant-select and disable it while busy:

disabled={props.isVariantLoading || props.variantSwitcher.fetchingOptions || props.isLoading}

Properties tab props: productProperties[] (each: key, value, isUrl — underscore-prefixed keys are already filtered out), subscriptionLabel, styles, builtInProperties. Render image-valued properties with class ia-cart-item-property-value-image. The subscription label is drawn by the app before this slot — do not repeat it.

Price tab props: price, compareAtPrice, savings, formattedPrice, formattedCompareAtPrice, savingsLabel, unitPriceText, styles, savingsStyle, rawItem, builtInPrice.

Volume tiers tab props: isPreview, volume, builtInVolumeDiscount.

volume field

Meaning

isActive

True only when volume discounts apply and unfulfilled tiers exist

tiers[]

Each: id, minQuantity, discountType, discountValue, title, titleHtml

currentQuantity

The line quantity — on volume, not on each tier

displayType

"BUTTONS" or "BARS"

styles

Volume discount module styles

onTierClick(minQty, tierId)

Jump the line to that tier

Announcement bar (2 tabs)

Path: Cart editor → Announcement → Custom template. Tabs: Main, Skeleton.

Main prop

Meaning

announcementTemplate

The localized announcement text/HTML

timer

A snapshot like "05:00" — for a live countdown use defaultContent

shouldUseTimer

True when the text contains the timer placeholder

isTimerExpiredAndShouldHide

Hide once the timer ends

type

DEFAULT | CAROUSEL | MOVING_LINE

slides

Carousel slide texts

marqueeSpeed, marqueeTextGap

Moving-line settings

containerStyleVars, hideClassName

Shell styles and hide class

defaultContent

Prebuilt DEFAULT content including the live countdown

builtInAnnouncement

The full inner tree — required for CAROUSEL / MOVING_LINE

styles, shopLocale, isPreview

Shared

Skeleton props: isPreview, skeletonHeight, styles.

Keep the shell and delegate the inside. Carousel and marquee animations cannot be rebuilt in a template — they live in React components:

<>
  {props.shouldUseTimer && props.isTimerExpiredAndShouldHide ? null : (
    <div
      className={"ia-announcement-bar-parent " + (props.hideClassName || "")}
      style={props.containerStyleVars}
    >
      {props.type === "CAROUSEL" || props.type === "MOVING_LINE"
        ? props.builtInAnnouncement
        : props.defaultContent}
    </div>
  )}
</>

Rewards (4 tabs)

Path: Cart editor → Rewards → Custom template

Tab

Replaces

Full

The rewards progress bar

Skeleton

Loading placeholder

Rewards to be achieved

The not-yet-earned tiers carousel

Pick Free Gift

The manual free-gift picker

Shared by all four tabs: rewardsModule, styles, cartTotal, cartItemCount, conversionRate, moneyFormat, currency, formatMoney(cents), refreshCart(), incredibleCartMarketGId, shopLocale, isPreview.

Full tab: rewardsMessageHtml, barFillPercentage, milestonesCompleted, milestones[] (each: id, isAchieved, leftPosition, spacerLeftPosition, icon, descriptionHtml, reward), builtInRewards. Confetti, automatic gift adds, and analytics stay with the app and keep firing with your template active.

Skeleton tab: styles, isPreview.

Rewards to be achieved: rewardsToBeAchieved[] (each: id, imageUrl, titleHtml, descriptionHtml, reward), cardStyleVars, imageStyleVars, titleStyleVars, descriptionStyleVars, carouselItemWidth, arrowColor, onCarouselPrevious(), onCarouselNext(), builtInRewardsToBeAchieved. Arrows work from your code, but swipe gestures do not — use the built-in fallback if you need them.

Pick Free Gift: headerTitleHtml, headerStyleVars, gifts[] (each: id, product, title, imageUrl, originalPriceHtml, freeText, buttonText, isLoading, onSelect), onSelectGift(product), onSliderPrevious(), onSliderNext(), leftArrow, rightArrow, arrowColor, itemStyleVars, freeText, buttonText, showProgress, progressText, progressColor, selectedCount, maxFreeGiftUserCanChoose, achievedTier, builtInFreeGiftSelection. Selecting a gift opens the variant sheet when needed and does nothing in preview.

Discount codes (3 tabs)

Path: Cart editor → Discount codes → Custom template

Tab

Replaces

Basic input

The plain code input (BASIC mode)

Advanced banner

The "Coupons & offers" banner (visual only)

Advanced sheet

The sheet content only — the sliding shell stays with the app

Basic props: discountCodeInput, setDiscountCodeInput(v), onApply() (takes no arguments), handleSubmit(e?) (use only if you wrap it in a form), indicator ("idle" | "validating" | "invalid"), showInvalidCross, placeholder, buttonText, applyButtonStyleVars, builtInBasic, isPreview.

<div className="ia-flex-gap-8">
  <input
    type="text"
    value={props.discountCodeInput}
    onChange={(e) => props.setDiscountCodeInput(e.target.value)}
    placeholder={props.placeholder}
  />
  <button type="button" onClick={props.onApply} style={props.applyButtonStyleVars}>
    {props.indicator === "validating" ? (
      <span className="ia-circular-loader" />
    ) : (
      props.buttonText
    )}
  </button>
</div>

Advanced banner props: icon, iconColor, iconSize, title, subtitle, offersCount, offersCountText, containerStyleVars, titleStyleVars, subtitleStyleVars, countStyleVars, onOpen(), builtInAdvancedOuter, isPreview.

Important: this banner is purely visual — the app already handles the click that opens the sheet. Do not call props.onOpen() yourself or the sheet will open twice.

Advanced sheet props: headerTitle, backIcon, onClose(), placeholder, buttonText, inputValue, setInputValue, onApplyTyped(), isApplying, showError, errorText, availableCouponsTitle, coupons[] (each: couponCode, description, isApplied, applyButtonText, onApply()), builtInAdvancedBottomSheetContent, isPreview.

Empty cart (2 tabs)

Path: Cart editor → Empty cart → Custom template

Empty state props: titleText, descriptionText, showButton, buttonText, buttonHref, buttonAction ("open_link" | "close_cart"), onButtonClick(), titleStyleVars, descriptionStyleVars, buttonStyle, builtInEmptyShell, isPreview.

<div id="ia-cart-full-container">
  <div style={props.titleStyleVars}>{props.titleText}</div>
  <div style={props.descriptionStyleVars}>{props.descriptionText}</div>
  {props.showButton && (
    <button type="button" style={props.buttonStyle} onClick={props.onButtonClick}>
      {props.buttonText}
    </button>
  )}
</div>

The Rewards banner and the "empty cart" custom HTML block are drawn by the app around this template — do not add them here.

Recommendations props: recommendationsTitle, titleStyleVars, products[], arrowColor, carouselItemWidth, carouselPadding, onCarouselPrevious(), onCarouselNext(), builtInRecommendations, isPreview.

Each recommendation product: id, imageUrl, productUrl, title, priceHtml, compareAtPriceHtml, buttonText, isLoading, onAdd(), styleVars, imageStyleVars, titleStyleVars, priceStyleVars, originalPriceStyleVars, buttonStyleVars.

Split payments & Trust badges

Split payments props: options[] (each: title as a ready element, titleHtml as a string, quantity, installmentAmount, imageUrl, rowStyleVars, titleStyleVars), containerStyleVars, titleStyleVars, builtInSplitPayments, isPreview.

The installment placeholders are already replaced inside options — do not try to parse them again.

<div style={props.containerStyleVars}>
  {props.options.map((o, i) => (
    <div key={i} className="ia-split-payment-parent" style={o.rowStyleVars}>
      <span style={o.titleStyleVars}>{o.title}</span>
      <img src={o.imageUrl} alt="" className="ia-split-payment-logo" />
    </div>
  ))}
</div>

Trust badges props: mode ("list" | "single"), badges[] (each { imageUrl }), badgeUrl, iconWidth, listStyleVars, showBadge, builtInTrustBadges, isPreview. Raw escape hatches: fileUrl, selectedPredefinedTrustBadgeIconUrl, listOfSelectedTrustBadgeIconsUrls, styles.

<div className="trust-badge-parent-container">
  {props.mode === "list" ? (
    <div style={props.listStyleVars}>
      {props.badges.map((badge, i) => (
        <img key={i} src={badge.imageUrl} alt="" style={{ width: props.iconWidth + "px" }} />
      ))}
    </div>
  ) : (
    <img src={props.badgeUrl} alt="" width="100%" style={{ objectFit: "contain" }} />
  )}
</div>

Additional notes

Props: notesTitle (a ready element), chevronIcon, isExpanded, toggleCollapsible(), cartNote, handleCartNoteChange(v), notesPlaceholder, isLoading, titleStyleVars, iconStyleVars, expanderStyleVars, builtInNotes, isPreview.

<div id="additional-notes">
  <div style={props.expanderStyleVars} onClick={props.toggleCollapsible}>
    <div style={props.titleStyleVars}>{props.notesTitle}</div>
    {props.chevronIcon}
  </div>
  {props.isExpanded ? (
    <textarea
      value={props.cartNote}
      rows={3}
      placeholder={props.notesPlaceholder}
      onChange={(e) => props.handleCartNoteChange(e.target.value)}
    />
  ) : null}
</div>

Cart design — Header & Footer

Path: Cart editor → Cart design → Custom template

Header props: cartItemCount, titleType, brandLogoUrl, brandLogoSize, titleContent, titleBackgroundColor, titleFontSize, titleTextColor, actualTextAlign, builtInButtonsGroup, onCartClose, shareEnabled, builtInHeader, isPreview, shopLocale.

<div className="ia-top-title-bar">
  {props.titleType === "BRAND_LOGO" && props.brandLogoUrl ? (
    <img src={props.brandLogoUrl} alt="Brand Logo" style={{ height: props.brandLogoSize + "px" }} />
  ) : (
    <h2>{props.titleContent}</h2>
  )}
  {props.builtInButtonsGroup}
</div>

Always keep the close control — either {props.builtInButtonsGroup} or your own button wired to props.onCartClose. Without it shoppers cannot dismiss the cart.

Footer: the footer is best used to reorder the built-in blocks rather than rebuild them:

<div className="ia-checkout-layout-parent-container">
  {props.builtInHtmlAboveAddOns}
  {props.builtInAddOns}
  {props.builtInAdditionalNotes}
  {props.builtInDiscountDisplay}
  {props.builtInDiscountInput}
  {props.builtInSplitPayments}
  {props.builtInTerms}
  {props.builtInSubtotal}
  {props.builtInHtmlAboveCheckout}
  {props.builtInCheckoutButton}
  {props.builtInAcceleratedCheckout}
  {props.builtInHtmlBelowCheckout}
  {props.builtInGoodApi}
  {props.builtInContinueShopping}
  {props.builtInTrustBadges}
</div>

Never rebuild the checkout button or the terms checkbox. Terms validation and checkout blocking are handled by the app — if you replace them, shoppers can check out without accepting your terms. Always use builtInCheckoutButton and builtInTerms.

Other footer props: footerBackgroundColor, containerStyleVars, finalTotalPrice, displayOriginalPrice, displaySavings, displayHasSavings, isTermsAndConditionBoxChecked plus its setters, showTermsAndConditionError, builtInFooter.


Part 11 — Product Page Upsell (FBT)

This module powers product-page offers: Cross-sell, Frequently Bought Together, and Product Addon.

Path: Product Page Upsells → open or create a funnel → Design tab

Three ways to customize

Method

Replaces the whole widget?

Skill needed

Custom template (JSX)

Yes — full control, same Compile → Activate flow as the cart

React / JSX

Custom HTML & CSS

No — injects HTML at fixed slots around the built-in widget

HTML / CSS

Layout + Template presets, colours, fonts

No — styling only

None

The JSX custom template is new. If you previously read that FBT could not be fully replaced, that is no longer the case — you can now rebuild the entire offer block, exactly like the cart modules.

No Settings toggle needed

The FBT Custom template card is always visible on the funnel Design tab. You do not need to turn on "Enable custom template editors" in Settings — that toggle only controls the cart modules.

Six bases — offer type × layout

FBT does not have one template. It has six, and the storefront runs the one matching this funnel's offer type and Design layout.

Offer segment

Layout

Base

Cross-sell

Carousel

crossSellCarousel

Cross-sell

Stacked

crossSellStacked

Add-on

Carousel

addonCarousel

Add-on

Stacked

addonStacked

FBT

Stacked

fbtStacked

FBT

Horizontal

fbtHorizontal

When the base you are editing is the one this funnel will actually use, a green In use for this funnel badge appears at the top of the card. If that badge is missing, the base you are editing will not run — either switch to the matching base, or change the funnel's Design layout.

Publish an FBT template (follow along)

  1. Open Product Page Upsells → your funnel.

  2. Open the Design tab.

  3. Scroll to Custom template.

  4. Choose the offer segment: Cross-sell, Add-on, or FBT.

  5. Choose the Layout base.

  6. Edit the code → Format (optional) → Compile → turn Active.

  7. Save the funnel (not the cart).

  8. Hard-refresh a product page that triggers this offer.

Instant fallback to the built-in design at any time:

<>{props.builtInModule}</>

Where the products come from

Products are not written into your JSX. The app builds props.products for you from the Offer tab:

  1. The trigger matches and the widget is placed on the page.

  2. Specific products → the products you selected. Frequently bought together → Shopify's related recommendations.

  3. Cross-sell and Add-on remove the current page product. The FBT offer type puts the current product first and marks it isTrigger.

  4. Non-carousel layouts are capped by your stacked products count.

Your template only renders that list. If the offer resolves to zero products, the widget does not render at all — that is an Offer/trigger issue, not a template issue.

Block-level props

Prop

Meaning

titleHtml

The offer title (HTML)

buttonTextHtml

The CTA / Claim button HTML, with variables already resolved

containerStyleVars

Root spacing and font styles

titleStyleVars

Title styles

navStyleVars

Carousel arrow styles

gridStyleVars

Product grid / carousel row styles

horizontalWrapperStyleVars

Shell styles for the FBT horizontal layout

claimButtonStyleVars, claimButtonIconStyleVars

Claim Offer button and its icon

plusIconStyleVars

The "+" between products

showArrows

Show previous / next arrows

showCheckboxes

Show selection checkboxes

showAddButtons

Show a per-product Add button

showClaimOffer

Show the Claim Offer button

showPlusIcon

Show the plus between cards

claimDisabled

Claim is not available yet (e.g. only the trigger product is selected)

isClaimLoading, isClaimSuccess

Claim button state

onCarouselPrevious(), onCarouselNext()

Scroll the carousel

onClaimOffer()

Add the selected bundle to the cart

summary

Totals block, mainly for the horizontal layout

offerType, layoutType, customBaseKey

Which base is running

moneyFormat, shopLocale, itemsPerPage

Helpers

isPreview

True in the admin preview

builtInModule

The exact built-in widget as a fallback

products

The array of product cards

Each product

Prop

Meaning

id, title, handle, productUrl, imageUrl

Basics

isSelected

Whether the checkbox is ticked

isTrigger

The current page product — normally not deselectable

isLoading, isSuccess

Per-product add state

hasDiscount

Whether to show the discounted price

priceHtml, discountedPriceHtml

Prices (HTML)

variants[]

Each: { id, title, available }

selectedVariantId

The currently chosen variant

itemStyleVars, titleStyleVars, variantStyleVars, priceStyleVars, discountedPriceStyleVars, checkboxStyleVars, buttonStyleVars

Style objects from your Design settings

variantIconStyleVars, buttonIconStyleVars

Icon colours

onToggleSelect(checked)

Select or deselect this product

onAddToCart()

Add just this product

onVariantChange(variantId)

Change the variant

The summary block (horizontal layout)

props.summary contains: titleHtml, savingsBadgeHtml, discountedPriceHtml, originalPriceHtml, showTitle, showSavingsBadge, hasTotals, and the matching style objects (titleStyleVars, discountedPriceStyleVars, originalPriceStyleVars, savingsBadgeStyleVars).

Check both flags together, the way the starter code does:

{props.summary.showTitle && props.summary.hasTotals && ( … )}

Starter patterns

Title

<div
  style={props.titleStyleVars}
  dangerouslySetInnerHTML={{ __html: props.titleHtml }}
/>

Product list

{props.products.map((p) => (
  <div key={p.id} style={p.itemStyleVars}>
    {props.showCheckboxes && (
      <input
        type="checkbox"
        checked={p.isSelected}
        disabled={p.isTrigger}
        onChange={(e) => p.onToggleSelect(e.target.checked)}
      />
    )}    <img src={p.imageUrl} alt={p.title} />
    <div dangerouslySetInnerHTML={{ __html: p.priceHtml }} />    {p.hasDiscount && (
      <div dangerouslySetInnerHTML={{ __html: p.discountedPriceHtml }} />
    )}    {props.showAddButtons && (
      <button type="button" onClick={p.onAddToCart} style={p.buttonStyleVars}>
        Add
      </button>
    )}
  </div>
))}

A "+" between cards

{props.products.map((p, index) => (
  <React.Fragment key={p.id}>
    {index > 0 && props.showPlusIcon && (
      <div className="oxify-fbt-upsell-plus-icon" style={props.plusIconStyleVars}>+</div>
    )}
    <div className="oxify-fbt-upsell-card">{/* card */}</div>
  </React.Fragment>
))}

Claim Offer button

{props.showClaimOffer && (
  <button
    type="button"
    disabled={props.claimDisabled}
    onClick={props.onClaimOffer}
    style={props.claimButtonStyleVars}
  >
    <span dangerouslySetInnerHTML={{ __html: props.buttonTextHtml }} />
  </button>
)}

Carousel arrows: keep the class oxify-fbt-custom-carousel on your scrolling container or the arrows will do nothing.

Useful FBT classes

oxify-fbt-upsell-container, oxify-fbt-upsell-header, oxify-fbt-upsell-title, oxify-fbt-upsell-navigation, oxify-fbt-upsell-nav-button, oxify-fbt-upsell-products-grid, oxify-fbt-upsell-carousel, oxify-fbt-custom-carousel, oxify-fbt-upsell-single-column, oxify-fbt-upsell-product-item, oxify-fbt-upsell-checkbox, oxify-fbt-upsell-horizontal-wrapper, oxify-fbt-upsell-card, oxify-fbt-upsell-plus-icon, oxify-fbt-upsell-summary, oxify-fbt-upsell-savings-badge.

Design presets still apply

Your Design → Template choice (Dawn, Minimal, Neon, and the rest) and all the colour controls feed the style objects your template receives. Spread them and your merchant settings keep working:

<button style={props.claimButtonStyleVars}>…</button>

Button text variables are resolved before your template runs, so props.buttonTextHtml already contains the final text:

Variable

Meaning

Example

{discount_price}

Total after discount

$450

{original_price}

Total before discount

$500

{saved_percentage}

Percent off

10%

{saved_amount_total}

Amount saved

$50

Custom HTML & CSS (the lighter option)

Still available on the Design tab for small additions — badges, trust copy, dividers — without replacing anything.

  1. FBT funnel → Design tab.

  2. Scroll to Custom HTML & CSS.

  3. Tick Enable custom HTML & CSS.

  4. Choose a location in Select HTML location, paste your HTML, and optionally add global CSS.

  5. Save the funnel, then refresh a product page.

Location

Where the HTML appears

Above title

Before the offer title

Below title

After the offer title

Above products list

Before the product cards

Between each product

Between product rows / cards

Below products list

After the product cards

Above button

Before the main CTA

Below button

After the main CTA

Example — badge above the title

<div class="oxify-fbt-badge">Bundle &amp; save — limited time</div>

.oxify-fbt-upsell-container .oxify-fbt-badge {
  display: inline-block;
  margin-bottom: 8px;
  padding: 4px 10px;
  border-radius: 999px;
  background: #111;
  color: #fff;
  font-size: 12px;
  font-weight: 600;
}

Always scope your CSS under .oxify-fbt-upsell-container so it cannot leak into the rest of your storefront.

Choose one approach for a big redesign. Custom HTML does not replace product cards or the Add button — if that is what you want, use the JSX custom template instead.

Placement reminder

Nothing renders unless the widget is actually mounted:

  • Placement tab — product page / cart page rules

  • Theme editor — App embed or the Product page upsell block is active

  • The funnel is Active and its triggers match the product


Part 12 — Test before you go live

Cart modules

  • Custom template editors enabled in Settings

  • Every tab you edited shows the green compile message

  • Every tab you want live is Active

  • Cart editor Saved

  • The cart preview looks right

  • On your live store: add, remove, change quantity, toggle add-ons, apply a discount, close the cart, and reach checkout

  • Browser console is free of [IA_CART] and [IA_CART_ERR] messages

  • Checked on a phone-width screen

Product Page Upsell (FBT)

  • Funnel is Active

  • Offer tab has products (specific products, or FBT recommendations enabled)

  • Trigger matches the product page you are testing

  • The Design layout matches the base you activated

  • The base shows the In use for this funnel badge

  • Custom template Compiled and Active

  • Funnel Saved

  • Placement / app embed / app block are correct

  • The product page shows your design, the products, and working CTAs


Something wrong? Fix it here

Nothing appears

What you see

What to do

No Custom template card in a cart module

Settings → Enable custom template editors → Save

Your edits do not show on the storefront

You need all three: CompileActiveSave. Activating without compiling does nothing.

You activated right after pressing Reset

Reset only refills the code box. Press Compile again, then Save.

The block renders nothing at all

Open the browser console (F12). [IA_CART] custom template compile failed: means the compiled code is broken; [IA_CART_ERR] cart drawer render crash means your code threw while rendering — usually reading a field on something undefined.

Red Compile error banner

Fix the code and Compile again. Most common causes: class= instead of className=, a missing } or />, two root elements, or a statement outside the JSX.

"Unexpected token" when compiling

You used TypeScript syntax, an import, or a statement. Only one JSX expression is allowed.

It renders but behaves wrong

What you see

What to do

Add-on toggles do nothing in the preview

Normal — test on your live cart

The variant sheet still looks stock

Open the Bottom sheet tab → Compile → Active → Save. It has its own switch.

Carousel arrows do nothing

Keep the required classes — ia-upsell-custom-carousel, ia-empty-cart-custom-carousel, or oxify-fbt-custom-carousel

Announcement carousel or marquee is broken

Use {props.builtInAnnouncement} for CAROUSEL and MOVING_LINE types

The announcement countdown is frozen

props.timer is only a snapshot — use {props.defaultContent} for a live countdown

Rewards swipe stopped working

Swipe lives in the built-in carousel — use {props.builtInRewardsToBeAchieved}

The quantity spinner shows on the wrong button

Call props.setLoadingOperation("increase" / "decrease" / "remove" / "inputChange") on click

The coupons sheet opens twice

Your Advanced banner calls props.onOpen() — remove it, the app already handles the click

Volume tiers disappeared from the line item

Render {props.builtInVolumeDiscount} in your product tile

Shoppers can check out without accepting terms

You replaced the checkout button — use {props.builtInCheckoutButton} and {props.builtInTerms}

The cart cannot be closed

Your header dropped the close control — restore {props.builtInButtonsGroup} or wire props.onCartClose

You turned the Settings toggle off but custom UI is still live

By design. Set each tab to Inactive and Save.

Product Page Upsell (FBT)

What you see

What to do

You cannot find the FBT Custom template card

Product Page Upsells → funnel → Design tab. It is always shown — no Settings toggle needed.

Your FBT code changes do not show

Compile → Active → Save the funnel, then hard-refresh the product page

FBT still shows the built-in design

You activated the wrong base. Check the In use for this funnel badge and confirm the Design layout matches.

FBT arrows do nothing

Keep the class oxify-fbt-custom-carousel on the scrolling container

FBT custom HTML is missing

Enable the checkbox, Save the funnel, and check Placement + app embed

The FBT widget is missing entirely

The offer has zero products, or the trigger / placement / app embed is wrong — see Part 11

Custom HTML shows but there are no product cards

Expected — HTML slots do not build cards. Use the JSX custom template.

You customized the wrong thing

Cart Add-ons and FBT Product Addon are two different systems


Appendix A — Upsell props

Use these names in your code. The app provides the values.

A1. Whole section

Prop

Meaning

isPreview

true in the admin preview — guard cart changes with it

titleHtml

Section title (HTML)

titleStyleVars

Title styles from your settings

products

The array of product cards

showSkeleton

True while products are still loading

onCarouselPrevious() / onCarouselNext()

Arrow helpers

arrowColor

Arrow colour

carouselItemWidth / carouselPadding

Carousel sizing

builtInUpsell

The full built-in UI as a fallback

styles / shopLocale / moneyFormat

Extra settings (rarely needed)

Keep id="ia-upsell-component" on the root element if you use carousel arrows.

A2. Each product

Prop

Meaning

id

Unique key

title

Product name

productUrl

Link to the product page ("/" in preview)

imageUrl

Image URL

priceHtml

Formatted price (HTML)

compareAtPriceHtml

Compare-at price (HTML)

showCompareAtPrice

Whether to show the compare-at price

buttonText

The Add / Choose options label

isLoading

An add is in progress

onAddToCart()

Add it — or open the variant sheet for multi-variant products

reviews

Reviews widget — render as {p.reviews}, never stringify it

containerStyleVars, imageStyleVars, titleStyleVars, priceStyleVars, originalPriceStyleVars, buttonStyleVars

Style objects from your settings

A3. Bottom sheet tab (Upsell + Upsell with Discount)

Customizes the inside of the multi-variant sheet. The dimmed overlay and slide animation stay with the app.

Prop

Meaning

imageUrl

Product image

title

Product title

productUrl

Product link

variantTitle

Current variant title

priceHtml

Current price

originalPriceHtml

Compare-at / pre-discount price

showCompareAtPrice

Whether compare-at is enabled in settings

hasDiscount

Marks the discount-upsell path

options

Variant option rows

buttonText

Add button label

isAddToCartLoading

Spinner on the CTA

isOutOfStock

Show out-of-stock and disable the CTA

onAddToCart()

Confirm the add with the selected options

onClose()

Close the sheet

buttonStyleVars

CTA styles

builtInBottomSheetContent

Fallback

Each option in props.options: name (e.g. Size, Colour), values (an array for your <option> list), selectedValue, and onChange(value).

Useful classes: ia-choose-variant-bs-details, ia-choose-variant-bs-details-image, ia-choose-variant-bs-pricing, ia-choose-variant-bs-original-price, ia-choose-variant-bs-variant-select, ia-choose-variant-bs-out-of-stock, ia-choose-variant-bs-button.


Appendix B — Every fallback prop

Every module gives you the built-in design as a ready-made element. Render it to fall back instantly, or to keep part of the stock UI inside your own layout.

Module / tab

Fallback prop

Upsell — Custom template

builtInUpsell

Upsell with Discount — Custom template

builtInUpsellDiscount

Bottom sheet (both modules)

builtInBottomSheetContent

Add-ons — Add-ons tab

builtInAddOns

Add-ons — Loading skeleton

builtInAddOnsSkeleton

Cart items — Product tile

builtInProductTile

Cart items — sub-slots

builtInVariant, builtInVariantText, builtInProperties, builtInPrice, builtInSubscription, builtInVolumeDiscount

Announcement — Main

builtInAnnouncement (and defaultContent)

Rewards — Full

builtInRewards

Rewards — To be achieved

builtInRewardsToBeAchieved

Rewards — Pick Free Gift

builtInFreeGiftSelection

Discount codes — Basic

builtInBasic

Discount codes — Advanced banner

builtInAdvancedOuter

Discount codes — Advanced sheet

builtInAdvancedBottomSheetContent

Empty cart — Empty state

builtInEmptyShell

Empty cart — Recommendations

builtInRecommendations

Split payments

builtInSplitPayments

Trust badges

builtInTrustBadges

Additional notes

builtInNotes

Cart design — Header

builtInHeader (and builtInButtonsGroup)

Cart design — Footer

builtInFooter (and the 15 block nodes)

Product Page Upsell (FBT)

builtInModule


Appendix C — Every module at a glance

#

Module

Tabs

Read

1

Upsells

2

Part 6 + Appendix A

2

Upsell with Discount

2

Part 7

3

Add-ons

2

Part 8

4

Cart items

6

Part 10

5

Announcement bar

2

Part 10

6

Rewards

4

Part 10

7

Discount codes

3

Part 10

8

Empty cart

2

Part 10

9

Split payments

1

Part 10

10

Trust badges

1

Part 10

11

Additional notes

1

Part 10

12

Cart design (Header / Footer)

2

Part 10

13

Product Page Upsell (FBT)

6 bases

Part 11


Appendix D — Do / Don't

Do

Don't

Press Reset to start from the real built-in code

Start from a blank box and guess at prop names

Use className

Use class

Use style={{ color: "red" }}

Use style="color:red"

Render HTML fields with dangerouslySetInnerHTML

Print {p.priceHtml} directly

Give every looped item a key

Leave keys out of a .map()

Guard actions with if (!props.isPreview)

Let the admin preview change a real cart

Call p.onAddToCart() for multi-variant products

Build your own variant dropdown on the card

Spread the *StyleVars objects

Hard-code colours and break the merchant settings

Compile and Activate and Save every tab you touch

Assume one Compile covers all tabs

Compile again after Reset

Activate straight after Reset

Keep builtInCheckoutButton and builtInTerms

Rebuild the checkout button yourself

Keep builtInAnnouncement for carousel / marquee

Rebuild the marquee animation in JSX

Scope your CSS under the module root id

Write global selectors that leak into the theme

Test on your live store and on mobile

Ship after checking the admin preview only


Quick path (summary)

Cart modules

Settings → Enable custom template editors → Save
   ↓
Cart editor → your module → Custom template
   ↓
Edit → Format → Compile → Active → Save

Product Page Upsell (FBT)

Product Page Upsells → your funnel
   ↓
Offer tab (which products)  →  Design tab (layout)
   ↓
Design → Custom template → pick offer segment + layout base
   ↓
Edit → Compile → Active → Save funnel → hard-refresh a product page

Stuck? Render the fallback ({props.builtInUpsell}, {props.builtInModule}, and so on), Compile, Save — you are back to the built-in design instantly, without losing your code.

Did this answer your question?