The five templates
Every prop each template receives, plus a starting recipe for each one.
Each template receives a props object with everything it needs already prepared — plain values, formatted strings, and ready-to-use functions. This page lists every prop for each of the five templates. Values marked honor it exist for a reason — the built-in buttons follow them, so a button that ignores them looks clickable when the built-in one wouldn't be.
The exact prop list for the template you're editing is also shown in the app, right inside the editor.
Handlers: pass the id, not the event
onVariantChange, onBoxChange, onCategoryChange and onRemoveItem expect an id (or an index), so wrap them: onClick={() => props.onCategoryChange(tab.id)}. Handlers that take nothing — onAddToKit, onRemoveFromKit, onAddToCart — can be passed straight to onClick.
Full layout — the whole arrangement
Root class: oxify-bundle-builder-layout
Replaces where every piece sits rather than what any one of them looks like. It's the biggest of the five and has its own guide, covering the props.slots pieces, the wrapper class that decides whether the sidebar is a sticky column or a full-width block, and ready-to-paste recipes.
Product card — one card per product
Root class: oxify-bb-product-card
Rendered once per product in the active category — and on the All tab, once per product per category, grouped so each category's products stay together. Column count (including the mobile Cards Per Row (Mobile) setting) is controlled by the grid, so keep your card fluid — avoid fixed widths.
| Prop | Type | What it is |
|---|---|---|
props.product | { id, title, handle, image, variants[], prices[], categoryId, categoryName } | The product being rendered. image can be null. Each entry in variants has id, title, price, compareAtPrice, sku and availableForSale; those prices are saved snapshot values (strings) — for what the shopper actually pays, use displayPrice or formattedPrice. categoryId / categoryName are the category this card sits under — on the All tab, the category the product came from. |
props.isFirstInCategory | boolean | True on the first card of each category run in the grid. Print a heading when it is true and every category gets its own titled section — see Recipes. |
props.selectedVariant | string | Id of the variant currently chosen on this card. |
props.onVariantChange | (variantId: string) => void | Select a different variant. Re-resolves price, stock and displayImage. |
props.displayImage | string | null | Image URL for the selected variant, falling back to the product image when the variant has no photo of its own. Bind your <img> to this and picking a variant swaps the picture, whenever that variant has a photo of its own. |
props.getVariantImage | (variantId: string) => string | null | The variant's own image, or null when it has none — for swatches with per-variant thumbnails. Treat null as "use the product image". |
props.displayPrice | number | null | Unit price for the selected variant in the shopper's market. null while market pricing resolves. |
props.compareAtPrice | number | null | Compare-at price, or null when the variant isn't on sale. |
props.formattedPrice | string | displayPrice already formatted in the store's currency — use this instead of formatting yourself. An empty string while pricing resolves. |
props.formattedComparePrice | string | null | Formatted compare-at price, or null when there's nothing to strike through. |
props.discountPercentage | number | Rounded percent off, or 0 when not discounted. Ready for a sale badge. |
props.isPriceLoading | boolean | True while market pricing resolves — show a placeholder rather than a possibly-wrong price. |
props.quantity | number | How many of the selected variant are already in the kit. |
props.onAddToKit / props.onRemoveFromKit | () => void | Add or remove one unit of the selected variant. |
props.isAddButtonDisabled | boolean | Honor it. True when the kit is full, the variant is unavailable, the category's selection rule is at its limit, or pricing hasn't resolved. |
props.isOutOfStock | boolean | True when the selected variant can't be purchased. |
props.showQuantitySelector | boolean | The Enable quantity selector setting from Product Card Template. On unless the merchant switched it off. |
props.buttonText / props.outOfStockText | string | The merchant's Button Text (default Add) and Out of Stock Label (default Out of Stock). |
props.currencyFormat | string | The store's money format string, for example ${{amount}}. |
props.formatPrice | (amount: number) => string | Format any number in the store's currency, for totals you work out yourself. |
Recipe — a minimal card with a sale badge:
Category tabs — the switcher above the grid
Root class: oxify-bb-tabs
Rendered once — and only while the Tab Button Template row's own switch is on.
| Prop | Type | What it is |
|---|---|---|
props.tabs | Array<{ id, name, image, isActive, isAllTab }> | Every tab to render, in order, including the leading All tab (id: "all", isAllTab: true) unless the merchant turned on Hide "All Products" Tab. image comes from the Category Images setting; the All tab's image is always null, so read props.categoryImages.all for its picture. Prefer this over categories. |
props.categories | Array<{ id, name }> | The raw category list without the All tab, if you want to build tabs yourself. |
props.activeCategoryId | string | Id of the selected category, or "all". |
props.onCategoryChange | (categoryId: string) => void | Switch the products grid to a category. Pass "all" for all products — unless Hide "All Products" Tab is on, in which case the builder moves straight back to the first category. |
props.categoryImages | Record<string, string> | Category id → image URL, from Category Images (shown for the Category image and Stepper tab templates). The All tab's image, if there is one, is under the key "all". |
props.alignment | "left" | "center" | "right" | The merchant's Tab Button Alignment setting. Put it on your root element as data-alignment — oxify-bb-tabs uses it to align the row. |
props.hideAllTab | boolean | Already applied to tabs; only relevant if you build tabs from categories. |
props.arrowStyle | string | The Stepper arrow setting: arrow, chevron, double, line or none. |
props.categoryTitle | string | Display name of the active category (All Products on the All tab). |
Recipe — starting template:
Sidebar — kit summary and add-to-cart
Root class: oxify-bb-kit-sidebar
Rendered once, beside the products grid. This template also replaces the Buy Box sidebar (Sidebar Widget → Buy Box) when active.
| Prop | Type | What it is |
|---|---|---|
props.kitItems | Array<{ productId, variantId, title, variant, image, price, quantity }> | Everything currently in the kit, in the order it was added. One entry per unit, so quantity is always 1. |
props.itemsCount | number | kitItems.length, for convenience. |
props.maxItems | number | How many items the selected box holds. With Allow customers to add more items than the selected quantity on, boxes have no ceiling, so this is the selected box's quantity or the item count, whichever is larger — never below itemsCount. |
props.remainingSlots | number | maxItems - itemsCount, never below 0. |
props.isComplete | boolean | True once the kit has reached maxItems. For the add-to-cart button, use isAddToCartDisabled. |
props.onRemoveItem | (index: number) => void | Remove the kit item at that index. |
props.onAddToCart | () => void | Add the whole bundle to the cart, then open the cart — or go straight to checkout when skipCartCheckout is true. |
props.isAddToCartDisabled | boolean | Honor it. True while the kit is below the minimum for the selected box (the smallest box's minimum when extra items are allowed, or when there's no box picker). onAddToCart itself only re-checks the smallest box's minimum. |
props.originalPrice | number | null | Undiscounted kit total, or null when there's no discount. |
props.finalPrice | number | Kit total after the box discount. |
props.formattedOriginalPrice / props.formattedFinalPrice | string | null / string | The same two prices already formatted, ready to strike through the original. formattedFinalPrice is always the one-time price. |
props.boxes | Array<{ id, title, minItems, maxItems, price, pricingType, discountType, discountValue, selectedByDefault }> | Every configured box size. discountType is no-discount, percentage, amount or fixed-price, and discountValue is a string. See Box sizes and discounts. |
props.selectedBox | object | null | The box currently selected. Can be null, so guard before reading from it. |
props.selectedBoxId | string | Id of the selected box. |
props.onBoxChange | (boxId: string) => void | Switch box. Kit contents stay where they fit; items beyond a smaller box's size are dropped from the end. |
props.showBoxSelector | boolean | True when the merchant enabled Enable quantity as an option — that is, when a box picker should be shown at all. |
props.getBoxBadgeText | (box) => string | Renders the merchant's savings badge for a box, with {{discount_amount}} / {{discount_percentage}} / {{discount_value}} already substituted. Returns "" when the badge is off or the box has no discount. |
props.title | string | The merchant's sidebar title (empty when left blank). |
props.instructionText | string | The merchant's instruction text with its tokens already filled in — {maxItems}, {remainingItems}, {selectedItems}, {{discount_value}}, {{discount_percentage}}, {{discount_amount}}, {{total_discount}} and {{total_saving}}. |
props.emptyText | string | What to show when the kit is empty: the instruction text, or Your kit is empty. Add items to get started! when that's blank. |
props.buttonText | string | The merchant's add-to-cart label (default Add to Cart). |
props.showBadge | boolean | The merchant's Discount Badge setting. |
props.skipCartCheckout | boolean | True when the merchant ticked Skip cart and go to checkout. See the callout below. |
props.checkoutUrl | string | The checkout path to put in that anchor's href — currently "/checkout". |
props.currencyFormat | string | The store's money format string. |
props.formatPrice | (amount: number) => string | Format any number in the store's currency. |
props.isAdminPreview | boolean | True in the admin Preview panel, false on a live storefront. |
The merchant's text settings reach you through props — title, buttonText, instructionText and the badge text. Only instructionText, emptyText and the badge text have tokens, and those are already filled in for you.
Honor skipCartCheckout
When props.skipCartCheckout is true, render the call-to-action as an anchor pointing at props.checkoutUrl rather than a plain <button> — some checkout providers only detect a real link. onAddToCart still performs the redirect for you; you only need to change the element:
Subscribe & Save props
These carry the Subscriptions row's checkbox. They're additive — a template written without them renders exactly as before. For the quick route, drop {props.subscriptionBlock} anywhere in your markup: it's the ready-made checkbox card, already wired to the cart.
| Prop | Type | What it is |
|---|---|---|
props.subscriptionEnabled | boolean | True when the checkbox should render: Subscriptions is on with a delivery plan chosen, Show products as individual lines in the cart is on, and every product in the builder offers that plan. In the admin Preview the last check is skipped. False on any store that hasn't turned it on — gate your markup on it. |
props.subscriptionBlock | ReactNode | null | The ready-made checkbox card. null when subscriptions are off. |
props.isSubscribed | boolean | Whether the shopper has ticked subscribe. Decides which price is charged, so pair it with formattedSubscriptionPrice. |
props.onSubscriptionToggle | (subscribed: boolean) => void | Tick or untick subscribe — for drawing your own control instead of subscriptionBlock. |
props.subscribeTitle | string | The merchant's checkbox label (default Subscribe & Save). |
props.subscribeSubtitle | string | null | The optional line under the label; null when left blank. |
props.subscriptionPlanName | string | The chosen delivery plan's name. |
props.subscriptionDiscountPercentage | number | Rounded percent the plan takes off, or 0 for plans that aren't percentage-based. Ready for a "Save 10%" badge. |
props.formattedSubscriptionPrice | string | null | Bundle total with the box discount and the plan's adjustment, formatted. null when subscriptions are off. Show this one while isSubscribed. |
props.subscriptionSavings | number | The extra amount saved by subscribing versus a one-time purchase (0 when none). Pair it with formatPrice. |
Recipe — box tabs with savings badges:
Toast — the confirmation message
Root class: oxify-bb-toast
Rendered only while a message is active — and only if the merchant's Add to Cart Message switch is on. The message disappears by itself after about three seconds.
| Prop | Type | What it is |
|---|---|---|
props.message | string | The message to show. Never empty when this template renders. It's usually the merchant's Toast Message text, but it can also be a selection rule message — shown for about four seconds when a shopper tries to go past a category's limit — so don't style it as pure good news. |
props.isAdminPreview | boolean | True in the admin Preview panel. |
Recipe — starting template:
More worked examples — variant swatches, category headings, kit slots, your own subscribe checkbox — are in Recipes.
What happens if the app adds new props later?
Existing templates keep working — new capabilities are added to props, never removed. New props show up in the in-app props reference and here.