Skip to content

Custom UI (Headless)

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.

Image

Screenshot showing a Wedding Band Builder with a completely custom-designed interface: the 3D viewer takes the full width, with a sleek, brand-styled control bar at the bottom featuring profile thumbnails, material swatches, and a width slider.

Overview

Set showUI: false 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 TagshowUI: false in project configviewer.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',
      showUI: false,
    },
  },
}, {
  showCard: false, showSwitchNode: false, showUiButtons: false,
  showConfigurator: false, showZoomButtons: false, enableZoom: 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();
});

Image

Screenshot showing just the 3D viewer rendering full width with no UI overlays, a blank canvas ready for custom controls.

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);
  });
}

Image

Screenshot showing a row of profile buttons with ring cross section silhouettes: D-Shape, Flat, Court, Beveled, Knife Edge, Round.

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);
  });
}

Image

Screenshot showing circular material swatches: White Gold, Yellow Gold, Rose Gold, with a subtle ring highlight on the selected swatch.

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

  // Ring width: 0.5mm increments
  widthSlider.min = 2;
  widthSlider.max = 10;
  widthSlider.step = 0.5;

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

  widthSlider.addEventListener('input', (e) => {
    const mm = parseFloat(e.target.value);
    api.setWidth(mm);
    widthLabel.textContent = `${mm.toFixed(1)} mm`;
  });
}
html
<input id="width-slider" type="range" min="2" max="10" step="0.1" />
<span id="width-value">4.0 mm</span>

Video

Short video showing a width slider being dragged with the 3D ring updating in real time as the slider moves.

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(t => t !== 'none').forEach((type) => {
    const btn = document.createElement('button');
    btn.textContent = type;
    btn.addEventListener('click', () => api.setDiamonds({ settingType: type }));
    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: 4.0, heightMm: 1.8 },
  materials: {
    partition: 1,
    slots: [{ slot: 1, metal: 'Yellow', finish: 'Polished' }],
  },
  diamonds: null,
  edge: { type: 'None' },
});

Image

Screenshot showing a row of styled preset cards: "Classic Gold", "Diamond Elegance", "Two-Tone Modern", each with a small preview image.

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

Video

Video walkthrough of the custom UI: selecting profiles, changing metals via swatches, dragging the width slider with real time 3D updates, toggling diamonds, switching between her and his rings, and seeing the price update live.

Image

Screenshot of the custom UI on a mobile device: viewer on top, compact single-row controls below, price bar at the bottom.

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.setWidth(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