> For the complete documentation index, see [llms.txt](https://docs.elitebundleapp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.elitebundleapp.com/guides/custom-byob-ui.md).

# Custom BYOB UI

This guide walks through building a fully custom Build Your Own Bundle UI from scratch, using the Elite Bundle Builder SDK for data and actions while rendering your own HTML.

The default Puck-rendered template continues to exist — you can replace individual parts or the whole thing.

***

## Prerequisites

* SDK access enabled in **App → Settings → Developer SDK**
* A bundle product set up with at least one step
* Access to your Shopify theme files

***

## 1. Add your HTML scaffold

In your theme, add the container elements where your custom bundle UI will render. Place these on the bundle product page — either in the product template or a theme section.

```html
<!-- _theme/sections/custom-byob.liquid or similar -->
<div id="my-bundle-root" style="display: none;">
  <div id="my-steps"></div>
  <div id="my-cart-drawer">
    <ul id="my-cart-items"></ul>
    <p>Total: <span id="my-cart-total">$0.00</span></p>
    <button id="my-checkout-btn" disabled>Checkout</button>
  </div>
</div>
<div id="my-bundle-loading">Loading bundle…</div>
<div id="my-bundle-error" style="display: none;"></div>
```

***

## 2. Wait for the SDK to be ready

The SDK populates `window.eliteBundle.sdk.byob` after React mounts and products load. Listen for the ready event before reading any state.

```js
function onByobReady(callback) {
  if (!window.eliteBundle?.sdk) return; // SDK not enabled, abort

  document.addEventListener('elite:byob:ready', callback);

  // Handle the case where ready fired before this script loaded
  const sdk = window.eliteBundle.sdk.byob;
  if (sdk && !sdk.isLoading && !sdk.error) callback();
}

onByobReady(initMyBundleUI);
```

***

## 3. Render steps and products

```js
function initMyBundleUI() {
  const sdk = window.eliteBundle.sdk.byob;

  document.querySelector('#my-bundle-loading').style.display = 'none';
  document.querySelector('#my-bundle-root').style.display = '';

  const stepsContainer = document.querySelector('#my-steps');

  sdk.steps.forEach(step => {
    const stepEl = document.createElement('div');
    stepEl.className = 'bundle-step';
    stepEl.innerHTML = `<h2>${step.title}</h2>`;

    // Add a products grid
    const grid = document.createElement('div');
    grid.className = 'products-grid';

    step.products.forEach(product => {
      product.variants.forEach(variant => {
        const card = document.createElement('div');
        card.className = 'product-card';
        card.dataset.variantId = variant.id;
        card.dataset.stepId = step.id;

        const image = product.featuredImage?.url ?? '';
        const price = sdk.formatPrice(parseFloat(variant.price));

        card.innerHTML = `
          <img src="${image}" alt="${product.title}" />
          <p class="product-title">${product.title}</p>
          ${variant.title !== 'Default Title' ? `<p class="variant-title">${variant.title}</p>` : ''}
          <p class="price">${price}</p>
          <div class="qty-controls" style="display: none;">
            <button class="qty-dec">−</button>
            <span class="qty-display">0</span>
            <button class="qty-inc">+</button>
          </div>
          <button class="add-btn" ${!variant.available ? 'disabled' : ''}>
            ${variant.available ? 'Add' : 'Sold out'}
          </button>
        `;

        // Add to cart
        card.querySelector('.add-btn').addEventListener('click', () => {
          sdk.addToCart(product, variant.id, step.id);
        });

        // Quantity controls
        card.querySelector('.qty-inc').addEventListener('click', () => {
          sdk.updateQuantity(variant.id, 1, step.id);
        });
        card.querySelector('.qty-dec').addEventListener('click', () => {
          sdk.updateQuantity(variant.id, -1, step.id);
        });

        grid.appendChild(card);
      });
    });

    stepEl.appendChild(grid);
    stepsContainer.appendChild(stepEl);
  });
}
```

***

## 4. Keep the cart in sync

Listen for `elite:byob:cart-change` to update your UI after every cart mutation.

```js
document.addEventListener('elite:byob:cart-change', (event) => {
  const { cart, cartTotalFormatted, cartCount, canCheckout } = event.detail;
  const sdk = window.eliteBundle.sdk.byob;

  // Update total and checkout button
  document.querySelector('#my-cart-total').textContent = cartTotalFormatted;
  document.querySelector('#my-checkout-btn').disabled = !canCheckout;

  // Update cart items list
  const itemsList = document.querySelector('#my-cart-items');
  itemsList.innerHTML = '';
  cart.forEach(item => {
    const li = document.createElement('li');
    li.textContent = `${item.title} × ${item.quantity} — ${sdk.formatPrice(item.price * item.quantity)}`;
    itemsList.appendChild(li);
  });

  // Update per-card quantity displays and step limits
  document.querySelectorAll('.product-card').forEach(card => {
    const variantId = card.dataset.variantId;
    const stepId = card.dataset.stepId;
    const qty = sdk.getItemQuantity(variantId, stepId);

    const qtyControls = card.querySelector('.qty-controls');
    const addBtn = card.querySelector('.add-btn');
    const qtyDisplay = card.querySelector('.qty-display');

    qtyControls.style.display = qty > 0 ? '' : 'none';
    qtyDisplay.textContent = qty;

    const stepFull = sdk.isStepFull(stepId);
    addBtn.disabled = stepFull && qty === 0;
  });
});
```

***

## 5. Handle checkout

```js
const checkoutBtn = document.querySelector('#my-checkout-btn');

checkoutBtn.addEventListener('click', () => {
  window.eliteBundle.sdk.byob.checkout('checkout');
});

// Loading state while checkout is in flight
document.addEventListener('elite:byob:checkout-start', () => {
  checkoutBtn.textContent = 'Processing…';
  checkoutBtn.disabled = true;
});

document.addEventListener('elite:byob:checkout-error', (event) => {
  checkoutBtn.textContent = 'Checkout';
  checkoutBtn.disabled = false;
  alert('Checkout failed: ' + event.detail.error);
});
```

***

## 6. Show a discount tier indicator (optional)

```js
function renderTierProgress() {
  const { discountConfig, cartCount, isTierMaxReached } = window.eliteBundle.sdk.byob;

  if (discountConfig.discountType === 'none') return;

  const tiers = discountConfig.tiers;
  const nextTier = tiers.find(t => t.minQuantity > cartCount);

  const progressEl = document.querySelector('#discount-progress');

  if (isTierMaxReached || !nextTier) {
    progressEl.textContent = `Maximum discount applied!`;
    return;
  }

  const remaining = nextTier.minQuantity - cartCount;
  progressEl.textContent =
    `Add ${remaining} more item${remaining !== 1 ? 's' : ''} for ${nextTier.discountValue}% off`;
}

document.addEventListener('elite:byob:cart-change', renderTierProgress);
```

***

## 7. Handle errors

```js
document.addEventListener('elite:byob:error', (event) => {
  document.querySelector('#my-bundle-loading').style.display = 'none';
  const errorEl = document.querySelector('#my-bundle-error');
  errorEl.textContent = 'Failed to load bundle: ' + event.detail.error;
  errorEl.style.display = '';
});
```

***

## Hiding the default UI

If you want to replace (not augment) the default Puck-rendered template, hide it with CSS. The default bundle renders inside a `[data-byob-bundle]` element.

```css
/* In your theme CSS */
[data-byob-bundle] {
  display: none !important;
}
```

Or hide it in JavaScript after the ready event, so there's no flash:

```js
document.addEventListener('elite:byob:ready', () => {
  const defaultUI = document.querySelector('[data-byob-bundle]');
  if (defaultUI) defaultUI.style.display = 'none';
});
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.elitebundleapp.com/guides/custom-byob-ui.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
