> 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-mob-ui.md).

# Custom MOB UI

This guide walks through building a fully custom Multi-Option Bundle UI using the Elite Bundle Builder SDK.

MOB bundles present a fixed set of options (e.g. "Choose a shirt", "Choose a hat") where the customer selects one item per option. The SDK handles all cart logic — you just render the picker UI and call `selectItem` when the customer makes a choice.

***

## Prerequisites

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

***

## 1. Add your HTML scaffold

```html
<div id="my-mob-root" style="display: none;">
  <div id="my-mob-options"></div>

  <div id="my-mob-summary">
    <p id="my-mob-price"></p>
    <button id="my-mob-add-btn" disabled>Add to cart</button>
    <p id="my-mob-error" style="display: none; color: red;"></p>
  </div>
</div>
<div id="my-mob-loading">Loading bundle…</div>
```

***

## 2. Wait for the SDK to be ready

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

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

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

onMobReady(initMyMobUI);
```

***

## 3. Render options (products mode)

Most MOB options use `"products"` mode — a flat list of product/variant combos the customer picks from.

```js
function initMyMobUI() {
  const sdk = window.eliteBundle.sdk.mob;

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

  const optionsContainer = document.querySelector('#my-mob-options');

  sdk.options.forEach((option, i) => {
    const optionEl = document.createElement('div');
    optionEl.className = 'mob-option';
    optionEl.dataset.optionIndex = i;
    optionEl.innerHTML = `<h3>${option.title}</h3>`;

    const items = sdk.selectableItems[i] ?? [];
    items.forEach(item => {
      const card = createItemCard(item, i);
      optionEl.appendChild(card);
    });

    optionsContainer.appendChild(optionEl);
  });

  // Checkout button
  document.querySelector('#my-mob-add-btn').addEventListener('click', () => {
    sdk.addToCart('checkout');
  });
}

function createItemCard(item, optionIndex) {
  const card = document.createElement('div');
  card.className = 'mob-item-card';
  card.dataset.itemKey = item.key;
  card.dataset.optionIndex = optionIndex;

  if (!item.available) {
    card.classList.add('unavailable');
  }

  card.innerHTML = `
    <img src="${item.imageUrl}" alt="${item.label}" />
    <p>${item.label}</p>
    <p>${item.priceFormatted}</p>
  `;

  card.addEventListener('click', () => {
    if (!item.available) return;
    window.eliteBundle.sdk.mob.selectItem(optionIndex, item.key);
  });

  return card;
}
```

***

## 4. Handle single\_product\_options mode

When an option uses `"single_product_options"` mode, render a set of dropdowns — one per Shopify option (Size, Color, etc.).

```js
function renderSingleProductOption(option, optionIndex, product) {
  const sdk = window.eliteBundle.sdk.mob;
  const container = document.createElement('div');
  container.className = 'mob-option';
  container.innerHTML = `<h3>${option.title}</h3>`;

  product.options.forEach(shopifyOption => {
    const label = document.createElement('label');
    label.textContent = shopifyOption.name;

    const select = document.createElement('select');
    shopifyOption.values.forEach(val => {
      const opt = document.createElement('option');
      opt.value = val;
      opt.textContent = val;
      select.appendChild(opt);
    });

    select.addEventListener('change', (e) => {
      sdk.setOptionSelection(optionIndex, shopifyOption.name, e.target.value);
    });

    container.appendChild(label);
    container.appendChild(select);
  });

  return container;
}
```

To determine which mode to use for each option, check `option.optionMode`:

```js
sdk.options.forEach((option, i) => {
  if (option.optionMode === 'single_product_options') {
    const product = sdk.optionProducts[i]?.[0];
    if (product) {
      optionsContainer.appendChild(renderSingleProductOption(option, i, product));
    }
  } else {
    // default "products" mode — render flat item list
    optionsContainer.appendChild(renderProductsOption(option, i));
  }
});
```

***

## 5. Keep the UI in sync with selections

Listen for `elite:mob:selection-change` to update selected state and prices after every selection.

```js
document.addEventListener('elite:mob:selection-change', (event) => {
  const {
    selectedItemKeys,
    allSelected,
    totalPriceFormatted,
    discountedPriceFormatted,
  } = event.detail;

  // Enable/disable add-to-cart button
  document.querySelector('#my-mob-add-btn').disabled = !allSelected;

  // Update price display
  const priceEl = document.querySelector('#my-mob-price');
  if (discountedPriceFormatted) {
    priceEl.innerHTML = `<s>${totalPriceFormatted}</s> <strong>${discountedPriceFormatted}</strong>`;
  } else {
    priceEl.textContent = totalPriceFormatted;
  }

  // Highlight selected cards
  document.querySelectorAll('.mob-item-card').forEach(card => {
    const optionIndex = Number(card.dataset.optionIndex);
    const key = card.dataset.itemKey;
    card.classList.toggle('selected', selectedItemKeys[optionIndex] === key);
  });
});
```

***

## 6. Handle add-to-cart states

```js
// Loading
document.addEventListener('elite:mob:cart-add-start', () => {
  const btn = document.querySelector('#my-mob-add-btn');
  btn.textContent = 'Adding…';
  btn.disabled = true;
});

// Error
document.addEventListener('elite:mob:cart-add-error', (event) => {
  const btn = document.querySelector('#my-mob-add-btn');
  btn.textContent = 'Add to cart';
  btn.disabled = false;

  const errorEl = document.querySelector('#my-mob-error');
  errorEl.textContent = event.detail.error;
  errorEl.style.display = '';
});

// Success (page navigates if redirectTarget !== "stay_on_page")
document.addEventListener('elite:mob:cart-add-success', () => {
  document.querySelector('#my-mob-add-btn').textContent = 'Added!';
});
```

***

## 7. Show discount info (optional)

```js
document.addEventListener('elite:mob:selection-change', (event) => {
  const { discountedPrice, discountedPriceFormatted } = event.detail;
  const sdk = window.eliteBundle.sdk.mob;

  const discountEl = document.querySelector('#discount-badge');
  if (!discountEl) return;

  if (discountedPrice !== null && sdk.discountPercent > 0) {
    discountEl.textContent = `${sdk.discountPercent}% bundle discount applied`;
    discountEl.style.display = '';
  } else {
    discountEl.style.display = 'none';
  }
});
```

***

## 8. Handle load errors

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

***

## Hiding the default UI

The default MOB template renders inside a `[data-mob-bundle]` element. Hide it with CSS if you're replacing it entirely:

```css
[data-mob-bundle] {
  display: none !important;
}
```


---

# 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-mob-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.
