Skip to content

Custom UI (Headless)

View the tested Custom UI template or browse its source.

The source is a multi-file template. The Drive-backed page uses script-drive.js; the self-hosted page uses script-self-hosted.js and wedding_band_project.json. Copy the matching HTML, script, stylesheet, and project JSON instead of mixing files from the two variants.

Build your own fully custom interface for the Wedding Band Builder. Hide the built-in panel, use the API's catalog methods to populate your controls, and wire them to the write methods. Your team or agency owns the UI entirely. No dependency on a third party for branding changes, layout updates, or feature additions.

Overview

Set hideWbbUi: true in Mini Viewer's options to run in headless mode. The 3D viewer renders with no built-in panel. This works with both integration approaches:

IntegrationHow to EnableHow to Access API
Script TaghideWbbUi: true in viewer optionsviewer.getPluginByType('WeddingBandBuilder').controller
iframe?ui=false URL parameterpostMessage from the host page

This page covers the Script Tag approach. For iframe-based custom UI, the same concepts apply but you wrap each API call in postMessage. See iframe headless mode.

Getting Started

javascript
// Initialize 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',
    },
  },
}, {
  showCard: false, showSwitchNode: false, showUiButtons: false,
  showConfigurator: false, showZoomButtons: false, enableZoom: true,
  hideWbbUi: true,
});

// Wait for API, then build your UI
window.addEventListener('ijewel-viewer-ready', (e) => {
  const api = e.detail.viewer.getPluginByType('WeddingBandBuilder').controller;

  // Read the catalog to see what's available in your manifest
  const profiles = api.getAvailableProfiles();
  const metals   = api.getAvailableMetals();
  const finishes = api.getAvailableFinishes();

  // Wire your controls to the API
  buildProfileSelector(profiles);
  buildMetalSwatches(metals);
  buildWidthSlider();
  buildPriceDisplay();
});

Building Controls

Each control follows the same pattern: read options from the catalog, create your UI, and call an API setter on interaction.

Profile Selector
javascript
function buildProfileSelector(profiles) {
  const container = document.getElementById('profile-selector');

  profiles.forEach((profile) => {
    const btn = document.createElement('button');
    btn.textContent = profile.name;

    // Use thumbnail from manifest if available
    if (profile.thumbnail) {
      btn.style.backgroundImage = `url(${profile.thumbnail})`;
    }

    btn.addEventListener('click', async () => {
      await api.setProfile(profile.index);
      container.querySelectorAll('button').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
    });

    container.appendChild(btn);
  });
}
Material Swatches
javascript
function buildMetalSwatches(metals) {
  const container = document.getElementById('metal-selector');
  const finishes = api.getAvailableFinishes();

  metals.forEach((metal) => {
    const swatch = document.createElement('button');
    swatch.className = 'swatch';
    swatch.title = metal.name;

    if (metal.thumbnail) {
      swatch.style.backgroundImage = `url(${metal.thumbnail})`;
      swatch.style.backgroundSize = 'cover';
    }

    swatch.addEventListener('click', () => {
      api.setMaterial(1, metal.id, finishes[0]?.id || 'Polished');
    });

    container.appendChild(swatch);
  });
}
Dimension Sliders
javascript
function buildDimensionSliders() {
  const widthSlider = document.getElementById('width-slider');
  const widthLabel = document.getElementById('width-value');

  const widthLimits = api.getLimits().width;
  widthSlider.min = widthLimits.min;
  widthSlider.max = widthLimits.max;
  widthSlider.step = widthLimits.step || 0.05;

  const dims = api.getDimensions();
  widthSlider.value = dims.widthMm;
  widthLabel.textContent = `×${dims.widthMm.toFixed(2)}`;

  widthSlider.addEventListener('input', (e) => {
    const multiplier = parseFloat(e.target.value);
    api.setWidthMultiplier(multiplier);
    widthLabel.textContent = `×${multiplier.toFixed(2)}`;
  });
}
html
<input id="width-slider" type="range" min="0.5" max="2" step="0.05" />
<span id="width-value">×1.00</span>
Diamond Controls
javascript
function buildDiamondControls() {
  const container = document.getElementById('diamond-controls');
  const types = api.getAvailableSettingTypes();

  // "None" option
  const noneBtn = document.createElement('button');
  noneBtn.textContent = 'No Diamonds';
  noneBtn.addEventListener('click', () => api.setDiamonds(null));
  container.appendChild(noneBtn);

  types.filter(type => type.id !== 'none').forEach((type) => {
    const btn = document.createElement('button');
    btn.textContent = type.name;
    btn.addEventListener('click', () => api.setDiamonds({ settingType: type.id }));
    container.appendChild(btn);
  });
}
Ring Size
javascript
function buildRingSizeControl() {
  const slider = document.getElementById('ring-size');
  const label = document.getElementById('ring-size-value');

  // Ring size: inner diameter in mm, snapping to standard half-sizes
  slider.min = 14.04;  // US 3
  slider.max = 22.32;  // US 13
  slider.step = 0.4;   // ~half-size increments

  slider.addEventListener('input', (e) => {
    const diamMm = parseFloat(e.target.value);
    api.setRingSize(diamMm / 2);  // API takes radius
    label.textContent = `${diamMm.toFixed(1)} mm`;
  });
}
Live Price Display
javascript
function buildPriceDisplay() {
  const el = document.getElementById('price');

  api.events.on('price:updated', (data) => {
    const p = data.pricing;
    let text = `$${p.totalUsd.toFixed(2)}`;
    if (p.diamonds) {
      text += ` (${p.diamonds.count} diamonds, ${p.diamonds.totalCarats.toFixed(2)}ct)`;
    }
    el.textContent = text;
  });

  // Show initial price
  const price = api.getPrice();
  if (price) el.textContent = `$${price.totalUsd.toFixed(2)}`;
}

Staying in Sync

Listen for events to keep your UI in sync when the ring state changes:

javascript
api.events.on('band:switched', (data) => {
  const snapshot = api.getSnapshot(data.to);
  updateProfileHighlight(snapshot.profile.index);
  updateMetalHighlight(snapshot.materials.slots[0].metal);
  updateWidthSlider(snapshot.dimensions.widthMm);
});

api.events.on('build:started', () => showSpinner());
api.events.on('build:complete', () => hideSpinner());

TIP

When the user switches bands (her / his), all your controls should update to reflect the new band's state. Call getSnapshot() to get the full configuration.

Batch Updates & Presets

Use batch() to apply multiple changes in one geometry rebuild:

javascript
await api.batch({
  profile: { name: 'D-Shape' },
  dimensions: { widthMm: 1.0, heightMm: 1.0 },
  materials: {
    partition: 1,
    slots: [{ slot: 1, metal: 'Yellow', finish: 'Polished' }],
  },
  diamonds: null,
  edge: { type: 'None' },
});

Save & Restore Configurations

javascript
// Save to localStorage
const config = api.exportConfig();
localStorage.setItem('wbb-config', JSON.stringify(config));

// Restore
const saved = localStorage.getItem('wbb-config');
if (saved) await api.importConfig(JSON.parse(saved));

// Send to backend for order processing
const herConfig = api.getSnapshot('her');
const herPrice = api.getPrice('her');
await fetch('/api/cart/add', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ config: herConfig, price: herPrice }),
});

Complete Example

A full working page with profile buttons, material swatches, width slider, diamond options, band toggle, and live price:

View complete custom UI example

Design Tips

Keep it simple. You don't need to expose every API option. Start with profile, metal, and width.

Use thumbnails. Catalog methods return thumbnail URLs when available. Use them for visual selectors.

Show loading state. Profile changes require geometry loading. Listen for build:started / build:complete.

Debounce sliders. For continuous sliders, debounce to avoid excessive rebuilds:

TIP

Debounce slider inputs. The input event fires on every pixel of drag, which can trigger 60+ geometry rebuilds per second. A 50ms debounce keeps the UI responsive:

javascript
let timer;
slider.addEventListener('input', (e) => {
  clearTimeout(timer);
  timer = setTimeout(() => api.setWidthMultiplier(parseFloat(e.target.value)), 50);
});

Test on mobile. Make sure your custom controls work on touch devices. The 3D viewer handles touch/pinch natively.

Next Steps