OxifyOxify

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.

PropTypeWhat 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.isFirstInCategorybooleanTrue 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.selectedVariantstringId of the variant currently chosen on this card.
props.onVariantChange(variantId: string) => voidSelect a different variant. Re-resolves price, stock and displayImage.
props.displayImagestring | nullImage 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 | nullThe variant's own image, or null when it has none — for swatches with per-variant thumbnails. Treat null as "use the product image".
props.displayPricenumber | nullUnit price for the selected variant in the shopper's market. null while market pricing resolves.
props.compareAtPricenumber | nullCompare-at price, or null when the variant isn't on sale.
props.formattedPricestringdisplayPrice already formatted in the store's currency — use this instead of formatting yourself. An empty string while pricing resolves.
props.formattedComparePricestring | nullFormatted compare-at price, or null when there's nothing to strike through.
props.discountPercentagenumberRounded percent off, or 0 when not discounted. Ready for a sale badge.
props.isPriceLoadingbooleanTrue while market pricing resolves — show a placeholder rather than a possibly-wrong price.
props.quantitynumberHow many of the selected variant are already in the kit.
props.onAddToKit / props.onRemoveFromKit() => voidAdd or remove one unit of the selected variant.
props.isAddButtonDisabledbooleanHonor 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.isOutOfStockbooleanTrue when the selected variant can't be purchased.
props.showQuantitySelectorbooleanThe Enable quantity selector setting from Product Card Template. On unless the merchant switched it off.
props.buttonText / props.outOfStockTextstringThe merchant's Button Text (default Add) and Out of Stock Label (default Out of Stock).
props.currencyFormatstringThe store's money format string, for example ${{amount}}.
props.formatPrice(amount: number) => stringFormat any number in the store's currency, for totals you work out yourself.

Recipe — a minimal card with a sale badge:

<div className="oxify-bb-product-card">
  <img src={props.displayImage} alt={props.product.title} />
 
  {props.discountPercentage > 0 && (
    <span className="my-badge">Save {props.discountPercentage}%</span>
  )}
 
  <div>{props.product.title}</div>
  <div>{props.isPriceLoading ? "…" : props.formattedPrice}</div>
 
  <button onClick={props.onAddToKit} disabled={props.isAddButtonDisabled}>
    {props.isOutOfStock ? props.outOfStockText : props.buttonText}
  </button>
</div>

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.

PropTypeWhat it is
props.tabsArray<{ 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.categoriesArray<{ id, name }>The raw category list without the All tab, if you want to build tabs yourself.
props.activeCategoryIdstringId of the selected category, or "all".
props.onCategoryChange(categoryId: string) => voidSwitch 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.categoryImagesRecord<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.hideAllTabbooleanAlready applied to tabs; only relevant if you build tabs from categories.
props.arrowStylestringThe Stepper arrow setting: arrow, chevron, double, line or none.
props.categoryTitlestringDisplay name of the active category (All Products on the All tab).

Recipe — starting template:

<div className="oxify-bb-tabs" data-alignment={props.alignment}>
  {props.tabs.map((tab) => (
    <button
      key={tab.id}
      className={"oxify-bb-tab " + (tab.isActive ? "active" : "")}
      onClick={() => props.onCategoryChange(tab.id)}
    >
      {tab.image && <img src={tab.image} alt={tab.name} />}
      <span>{tab.name}</span>
    </button>
  ))}
</div>

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.

PropTypeWhat it is
props.kitItemsArray<{ 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.itemsCountnumberkitItems.length, for convenience.
props.maxItemsnumberHow 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.remainingSlotsnumbermaxItems - itemsCount, never below 0.
props.isCompletebooleanTrue once the kit has reached maxItems. For the add-to-cart button, use isAddToCartDisabled.
props.onRemoveItem(index: number) => voidRemove the kit item at that index.
props.onAddToCart() => voidAdd the whole bundle to the cart, then open the cart — or go straight to checkout when skipCartCheckout is true.
props.isAddToCartDisabledbooleanHonor 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.originalPricenumber | nullUndiscounted kit total, or null when there's no discount.
props.finalPricenumberKit total after the box discount.
props.formattedOriginalPrice / props.formattedFinalPricestring | null / stringThe same two prices already formatted, ready to strike through the original. formattedFinalPrice is always the one-time price.
props.boxesArray<{ 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.selectedBoxobject | nullThe box currently selected. Can be null, so guard before reading from it.
props.selectedBoxIdstringId of the selected box.
props.onBoxChange(boxId: string) => voidSwitch box. Kit contents stay where they fit; items beyond a smaller box's size are dropped from the end.
props.showBoxSelectorbooleanTrue when the merchant enabled Enable quantity as an option — that is, when a box picker should be shown at all.
props.getBoxBadgeText(box) => stringRenders 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.titlestringThe merchant's sidebar title (empty when left blank).
props.instructionTextstringThe 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.emptyTextstringWhat 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.buttonTextstringThe merchant's add-to-cart label (default Add to Cart).
props.showBadgebooleanThe merchant's Discount Badge setting.
props.skipCartCheckoutbooleanTrue when the merchant ticked Skip cart and go to checkout. See the callout below.
props.checkoutUrlstringThe checkout path to put in that anchor's href — currently "/checkout".
props.currencyFormatstringThe store's money format string.
props.formatPrice(amount: number) => stringFormat any number in the store's currency.
props.isAdminPreviewbooleanTrue 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:

<a
  href={props.checkoutUrl}
  onClick={(e) => {
    e.preventDefault();
    if (!props.isAddToCartDisabled) props.onAddToCart();
  }}
>
  {props.buttonText}
</a>

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.

PropTypeWhat it is
props.subscriptionEnabledbooleanTrue 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.subscriptionBlockReactNode | nullThe ready-made checkbox card. null when subscriptions are off.
props.isSubscribedbooleanWhether the shopper has ticked subscribe. Decides which price is charged, so pair it with formattedSubscriptionPrice.
props.onSubscriptionToggle(subscribed: boolean) => voidTick or untick subscribe — for drawing your own control instead of subscriptionBlock.
props.subscribeTitlestringThe merchant's checkbox label (default Subscribe & Save).
props.subscribeSubtitlestring | nullThe optional line under the label; null when left blank.
props.subscriptionPlanNamestringThe chosen delivery plan's name.
props.subscriptionDiscountPercentagenumberRounded percent the plan takes off, or 0 for plans that aren't percentage-based. Ready for a "Save 10%" badge.
props.formattedSubscriptionPricestring | nullBundle total with the box discount and the plan's adjustment, formatted. null when subscriptions are off. Show this one while isSubscribed.
props.subscriptionSavingsnumberThe extra amount saved by subscribing versus a one-time purchase (0 when none). Pair it with formatPrice.

Recipe — box tabs with savings badges:

<div className="oxify-bb-kit-sidebar">
  {props.showBoxSelector && (
    <div className="my-boxes">
      {props.boxes.map((box) => (
        <button
          key={box.id}
          className={"my-box " + (box.id === props.selectedBoxId ? "is-selected" : "")}
          onClick={() => props.onBoxChange(box.id)}
        >
          <span>{box.title}</span>
          {props.getBoxBadgeText(box) && (
            <span className="my-box-badge">{props.getBoxBadgeText(box)}</span>
          )}
        </button>
      ))}
    </div>
  )}
 
  {/* …kit items and the add-to-cart button… */}
</div>

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.

PropTypeWhat it is
props.messagestringThe 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.isAdminPreviewbooleanTrue in the admin Preview panel.

Recipe — starting template:

<div className="oxify-bb-toast">
  <div className="oxify-bb-toast-content">{props.message}</div>
</div>

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.

On this page