Skip to content

Custom UI: Complete Example

A full custom UI with profile selector, material swatches, width slider, diamond options, and live price display.

Back to Headless API guide

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Custom Wedding Band Configurator</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Segoe UI', system-ui, sans-serif; background: #fafafa; }

    .app { display: flex; flex-direction: column; height: 100vh; }

    #viewer { flex: 1; min-height: 300px; }

    .controls {
      background: #fff;
      border-top: 1px solid #e0e0e0;
      padding: 20px 24px;
      overflow-x: auto;
    }

    .control-row {
      display: flex;
      align-items: center;
      gap: 16px;
      margin-bottom: 16px;
    }
    .control-row:last-child { margin-bottom: 0; }

    .control-label {
      font-size: 12px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: #888;
      width: 80px;
      flex-shrink: 0;
    }

    .btn-group { display: flex; gap: 8px; flex-wrap: wrap; }

    .btn {
      padding: 8px 16px;
      border: 1px solid #ddd;
      border-radius: 6px;
      background: #fff;
      cursor: pointer;
      font-size: 13px;
      transition: all 0.15s;
    }
    .btn:hover { border-color: #999; }
    .btn.active { background: #333; color: #fff; border-color: #333; }

    .swatch {
      width: 36px; height: 36px;
      border-radius: 50%;
      border: 2px solid #ddd;
      cursor: pointer;
      transition: all 0.15s;
    }
    .swatch:hover { border-color: #999; }
    .swatch.active { border-color: #333; box-shadow: 0 0 0 2px #333; }
    .swatch-white { background: linear-gradient(135deg, #e8e8e8, #d0d0d0); }
    .swatch-yellow { background: linear-gradient(135deg, #f0d060, #c8a840); }
    .swatch-rose { background: linear-gradient(135deg, #e8b8a8, #d09888); }

    .slider { width: 200px; accent-color: #333; }
    .slider-value { font-size: 14px; color: #333; min-width: 60px; }

    .price-bar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 16px 24px;
      background: #333;
      color: #fff;
    }
    .price { font-size: 24px; font-weight: 600; }
    .band-toggle { display: flex; gap: 8px; }
    .band-btn {
      padding: 6px 16px;
      border: 1px solid rgba(255,255,255,0.3);
      border-radius: 4px;
      background: transparent;
      color: #fff;
      cursor: pointer;
      font-size: 13px;
    }
    .band-btn.active { background: rgba(255,255,255,0.2); border-color: #fff; }
  </style>
</head>
<body>
  <div class="app">
    <div id="viewer"></div>

    <div class="controls">
      <div class="control-row">
        <span class="control-label">Profile</span>
        <div id="profiles" class="btn-group"></div>
      </div>

      <div class="control-row">
        <span class="control-label">Metal</span>
        <div id="metals" class="btn-group"></div>
      </div>

      <div class="control-row">
        <span class="control-label">Width</span>
        <input id="width" class="slider" type="range" min="2" max="10" step="0.1" value="4" />
        <span id="width-val" class="slider-value">4.0 mm</span>
      </div>

      <div class="control-row">
        <span class="control-label">Diamonds</span>
        <div id="diamonds" class="btn-group"></div>
      </div>
    </div>

    <div class="price-bar">
      <div class="band-toggle">
        <button class="band-btn active" onclick="switchBand('her')">Her Ring</button>
        <button class="band-btn" onclick="switchBand('his')">His Ring</button>
      </div>
      <div class="price" id="price">-</div>
    </div>
  </div>

  <script src="https://releases.ijewel3d.com/libs/mini-viewer/latest/bundle.iife.js"></script>

  <script>
    let api;

    // Initialize viewer in headless mode
    new ijewelViewer.Viewer(document.getElementById('viewer'), {
      name: 'Wedding Band Builder',
      version: 'v5',
      basePath: 'https://your-cdn.com/wbb-assets/',
      plugins: {
        WeddingBandBuilder: {
          manifestUrl: 'wedding-band-project.json',
          showUI: false,
        },
      },
    }, {
      showCard: false, showSwitchNode: false, showUiButtons: false,
      showConfigurator: false, showZoomButtons: false, enableZoom: true,
    });

    window.addEventListener('ijewel-viewer-ready', (e) => {
      api = e.detail.viewer.getPluginByType('WeddingBandBuilder').controller;

      // Build profile buttons
      const profilesEl = document.getElementById('profiles');
      api.getAvailableProfiles().forEach((p, i) => {
        const btn = document.createElement('button');
        btn.className = `btn${i === 0 ? ' active' : ''}`;
        btn.textContent = p.name;
        btn.onclick = async () => {
          await api.setProfile(p.index);
          profilesEl.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
          btn.classList.add('active');
        };
        profilesEl.appendChild(btn);
      });

      // Build metal swatches
      const metalsEl = document.getElementById('metals');
      const colorMap = { White: 'swatch-white', Yellow: 'swatch-yellow', Rose: 'swatch-rose' };
      api.getAvailableMetals().forEach((m, i) => {
        const sw = document.createElement('button');
        sw.className = `swatch ${colorMap[m.id] || ''}${i === 0 ? ' active' : ''}`;
        sw.title = m.name;
        sw.onclick = () => {
          api.setMaterial(1, m.id, 'Polished');
          metalsEl.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
          sw.classList.add('active');
        };
        metalsEl.appendChild(sw);
      });

      // Build diamond buttons
      const diamondsEl = document.getElementById('diamonds');
      ['None', ...api.getAvailableSettingTypes().filter(t => t !== 'none')].forEach((t, i) => {
        const btn = document.createElement('button');
        btn.className = `btn${i === 0 ? ' active' : ''}`;
        btn.textContent = t;
        btn.onclick = () => {
          if (t === 'None') api.setDiamonds(null);
          else api.setDiamonds({ settingType: t });
          diamondsEl.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
          btn.classList.add('active');
        };
        diamondsEl.appendChild(btn);
      });

      // Width slider
      const widthSlider = document.getElementById('width');
      const widthVal = document.getElementById('width-val');
      const dims = api.getDimensions();
      widthSlider.value = dims.widthMm;
      widthVal.textContent = `${dims.widthMm.toFixed(1)} mm`;

      widthSlider.addEventListener('input', (e) => {
        const mm = parseFloat(e.target.value);
        api.setWidth(mm);
        widthVal.textContent = `${mm.toFixed(1)} mm`;
      });

      // Price updates
      api.events.on('price:updated', (data) => {
        if (data.pricing) {
          document.getElementById('price').textContent =
            `$${data.pricing.totalUsd.toFixed(2)}`;
        }
      });

      // Load initial price
      const price = api.getPrice();
      if (price) {
        document.getElementById('price').textContent =
          `$${price.totalUsd.toFixed(2)}`;
      }
    });

    function switchBand(name) {
      if (!api) return;
      api.switchBand(name);
      document.querySelectorAll('.band-btn').forEach(b => {
        b.classList.toggle('active', b.textContent.toLowerCase().includes(name));
      });
    }
  </script>
</body>
</html>