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:
| Integration | How to Enable | How to Access API |
|---|---|---|
| Script Tag | showUI: false in project config | viewer.getPluginByType('WeddingBandBuilder').controller |
| iframe | ?ui=false URL parameter | postMessage 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
// 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
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
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
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`;
});
}<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
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
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
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:
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:
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
// 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:
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
- API Reference for complete method and event documentation
- Pricing Engine to configure the pricing engine
- Theming & Branding if you want to customize the built-in panel instead
- Script Tag (Direct) for built-in panel integration
- iframe Embedding for CMS and e-commerce platforms