Follow this guide to customize the Cart Drawer and the Product Page Upsell (FBT) widget with your own code.
You will:
Turn on the custom template editors (cart modules only)
Open the module you want to customize
Edit the template code (JSX)
Compile → Activate → Save
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
importorexportNo React hooks (
useState,useEffect, …)No TypeScript types
No statements before the JSX — you cannot write
const x = 1;at the toppropsis 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: |
Compiled code is invalid | The block renders nothing. Browser console shows |
Your code crashes while rendering | The block renders nothing and recovers. Browser console shows |
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)
Open the app.
Go to Settings.
Find Custom templates (marked Technical knowledge required).
Tick Enable custom template editors.
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)
Go to Cart editor.
Open the module you want (for example Upsells).
Scroll to the Custom template card.
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.
Open Cart editor → Upsells → Custom template.
Click Reset to load the starter code.
Click Compile — wait for the green message.
Turn the switch to Active.
Click Save.
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… |
|
|
|
|
Merchant colours |
|
Override one property |
|
Show only sometimes |
|
Either/or |
|
A comment |
|
A loading spinner |
|
Custom CSS |
|
Go back to the stock design |
|
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.
In the same card, open the Bottom sheet tab.
Edit the code (props are in Appendix A3).
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 |
| The discounted price |
| Price before the discount |
| Whether to show the strikethrough price |
| The "Save 15%" badge |
| Styles for the savings text |
| Savings as a number |
| The offer discount configuration |
| Composite |
<>
<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 |
| The rows to render |
| Blocks toggles while default add-ons are being added |
| Stock UI fallback |
| Shared values |
Each item | Meaning |
| Identifiers — use |
| Image and its styles |
| Title (HTML) |
| Optional subtitle (HTML) |
| Price (HTML) |
| Toggle track styles |
| Whether this add-on starts switched on |
|
|
| 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 |
|
Scroll wrapper |
|
Track |
|
Each slide |
|
Arrow 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 |
|
Empty cart recommendations |
|
Product Page Upsell (FBT) |
|
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 |
| Line basics |
| Quantity UI ( |
| Formatted currency strings — use |
| The same values under explicit names |
| Unit-price label, or |
| Line-level discount titles |
| Quantity + 1 / − 1 |
| Remove the line |
| Set quantity directly |
| What is currently in flight |
| Whole-line overlay control |
| Per-button spinner: |
| The resolved sub-slots |
| Variant switching |
| Extra data |
| Always |
| Shared |
| Exact stock tile fallback |
| 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).
| Meaning |
|
|
| Only one option available |
| Options are still loading |
| Current selection |
| Each: |
| Swap this line to another variant |
| 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.
| Meaning |
| True only when volume discounts apply and unfulfilled tiers exist |
| Each: |
| The line quantity — on |
|
|
| Volume discount module styles |
| Jump the line to that tier |
Announcement bar (2 tabs)
Path: Cart editor → Announcement → Custom template. Tabs: Main, Skeleton.
Main prop | Meaning |
| The localized announcement text/HTML |
| A snapshot like |
| True when the text contains the timer placeholder |
| Hide once the timer ends |
|
|
| Carousel slide texts |
| Moving-line settings |
| Shell styles and hide class |
| Prebuilt DEFAULT content including the live countdown |
| The full inner tree — required for CAROUSEL / MOVING_LINE |
| 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 |
|
Cross-sell | Stacked |
|
Add-on | Carousel |
|
Add-on | Stacked |
|
FBT | Stacked |
|
FBT | Horizontal |
|
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)
Open Product Page Upsells → your funnel.
Open the Design tab.
Scroll to Custom template.
Choose the offer segment: Cross-sell, Add-on, or FBT.
Choose the Layout base.
Edit the code → Format (optional) → Compile → turn Active.
Save the funnel (not the cart).
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:
The trigger matches and the widget is placed on the page.
Specific products → the products you selected. Frequently bought together → Shopify's related recommendations.
Cross-sell and Add-on remove the current page product. The FBT offer type puts the current product first and marks it
isTrigger.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 |
| The offer title (HTML) |
| The CTA / Claim button HTML, with variables already resolved |
| Root spacing and font styles |
| Title styles |
| Carousel arrow styles |
| Product grid / carousel row styles |
| Shell styles for the FBT horizontal layout |
| Claim Offer button and its icon |
| The "+" between products |
| Show previous / next arrows |
| Show selection checkboxes |
| Show a per-product Add button |
| Show the Claim Offer button |
| Show the plus between cards |
| Claim is not available yet (e.g. only the trigger product is selected) |
| Claim button state |
| Scroll the carousel |
| Add the selected bundle to the cart |
| Totals block, mainly for the horizontal layout |
| Which base is running |
| Helpers |
| True in the admin preview |
| The exact built-in widget as a fallback |
| The array of product cards |
Each product
Prop | Meaning |
| Basics |
| Whether the checkbox is ticked |
| The current page product — normally not deselectable |
| Per-product add state |
| Whether to show the discounted price |
| Prices (HTML) |
| Each: |
| The currently chosen variant |
| Style objects from your Design settings |
| Icon colours |
| Select or deselect this product |
| Add just this product |
| 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 |
| Total after discount | $450 |
| Total before discount | $500 |
| Percent off | 10% |
| 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.
FBT funnel → Design tab.
Scroll to Custom HTML & CSS.
Tick Enable custom HTML & CSS.
Choose a location in Select HTML location, paste your HTML, and optionally add global CSS.
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 & 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]messagesChecked 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: Compile → Active → Save. 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). |
Red Compile error banner | Fix the code and Compile again. Most common causes: |
"Unexpected token" when compiling | You used TypeScript syntax, an |
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 — |
Announcement carousel or marquee is broken | Use |
The announcement countdown is frozen |
|
Rewards swipe stopped working | Swipe lives in the built-in carousel — use |
The quantity spinner shows on the wrong button | Call |
The coupons sheet opens twice | Your Advanced banner calls |
Volume tiers disappeared from the line item | Render |
Shoppers can check out without accepting terms | You replaced the checkout button — use |
The cart cannot be closed | Your header dropped the close control — restore |
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 |
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 |
|
|
| Section title (HTML) |
| Title styles from your settings |
| The array of product cards |
| True while products are still loading |
| Arrow helpers |
| Arrow colour |
| Carousel sizing |
| The full built-in UI as a fallback |
| Extra settings (rarely needed) |
Keep id="ia-upsell-component" on the root element if you use carousel arrows.
A2. Each product
Prop | Meaning |
| Unique key |
| Product name |
| Link to the product page ("/" in preview) |
| Image URL |
| Formatted price (HTML) |
| Compare-at price (HTML) |
| Whether to show the compare-at price |
| The Add / Choose options label |
| An add is in progress |
| Add it — or open the variant sheet for multi-variant products |
| Reviews widget — render as |
| 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 |
| Product image |
| Product title |
| Product link |
| Current variant title |
| Current price |
| Compare-at / pre-discount price |
| Whether compare-at is enabled in settings |
| Marks the discount-upsell path |
| Variant option rows |
| Add button label |
| Spinner on the CTA |
| Show out-of-stock and disable the CTA |
| Confirm the add with the selected options |
| Close the sheet |
| CTA styles |
| 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 |
|
Upsell with Discount — Custom template |
|
Bottom sheet (both modules) |
|
Add-ons — Add-ons tab |
|
Add-ons — Loading skeleton |
|
Cart items — Product tile |
|
Cart items — sub-slots |
|
Announcement — Main |
|
Rewards — Full |
|
Rewards — To be achieved |
|
Rewards — Pick Free Gift |
|
Discount codes — Basic |
|
Discount codes — Advanced banner |
|
Discount codes — Advanced sheet |
|
Empty cart — Empty state |
|
Empty cart — Recommendations |
|
Split payments |
|
Trust badges |
|
Additional notes |
|
Cart design — Header |
|
Cart design — Footer |
|
Product Page Upsell (FBT) |
|
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 | Use |
Use | Use |
Render HTML fields with | Print |
Give every looped item a | Leave keys out of a |
Guard actions with | Let the admin preview change a real cart |
Call | Build your own variant dropdown on the card |
Spread the | 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 | Rebuild the checkout button yourself |
Keep | 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.
