Headless API
Build your own fully custom interface for the Wedding Band Builder. Set hideWbbUi: true in Mini Viewer's options to hide the built-in panel and drive everything through the API.

Getting Started
Script Tag (Direct)
let api;
let wbbPlugin;
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,
});
window.addEventListener('ijewel-viewer-ready', (e) => {
wbbPlugin = e.detail.viewer.getPluginByType('WeddingBandBuilder');
api = wbbPlugin.controller;
buildUI(api);
});iframe
Load the iframe with ?ui=false and use postMessage to control everything:
<iframe src="wbb-viewer.html?ui=false" style="width: 100%; height: 500px; border: none;"></iframe>The same API methods are available via postMessage. See iframe Integration for the protocol.
Building Controls
Each control follows the same pattern: read options from the catalog, create your UI, call an API setter on interaction.
Profile Selector
function buildProfileSelector(api) {
const profiles = api.getAvailableProfiles();
const container = document.getElementById('profiles');
profiles.forEach((profile) => {
const btn = document.createElement('button');
btn.textContent = profile.name;
if (profile.thumbnail) {
btn.style.backgroundImage = `url(${profile.thumbnail})`;
}
btn.addEventListener('click', () => api.setProfile(profile.index));
container.appendChild(btn);
});
}
Material Swatches
function buildMaterialSwatches(api) {
const materials = api.getAvailableMaterials('band');
const container = document.getElementById('metals');
materials.forEach((material) => {
const swatch = document.createElement('button');
swatch.className = 'swatch';
swatch.title = material.name;
if (material.iconUrl) {
swatch.style.backgroundImage = `url(${material.iconUrl})`;
} else if (material.swatch) {
swatch.style.backgroundColor = material.swatch;
}
swatch.addEventListener('click', async () => {
const variants = api.getAvailableVariants(material.id);
const variant = variants.find(v => v.id === material.defaultVariant)
|| variants[0];
const finishes = api.getAvailableFinishesFor(material.id)
.filter(f => !variant?.files || variant.files[f.id]);
const finish = finishes.find(f => f.id === material.defaultFinish)
|| finishes[0];
await api.setMaterialRef(1, {
base: material.id,
variant: variant?.id,
finish: finish?.id,
});
});
container.appendChild(swatch);
});
}
Outside and Inside Features
Inlays, overlays and the sleeve need a circular band on a plain vertical division. Gate the whole control group on areOutsideFeaturesAvailable(), and re-check it after any division change.
function buildOutsideControls(api) {
const panel = document.getElementById('outside');
panel.hidden = !api.areOutsideFeaturesAvailable();
// Wood and marble are inlay-only in most catalogs, so ask for the role.
const materials = api.getAvailableMaterials('inlay');
// One centred stripe. Index 0 adds the first inlay and edits it afterwards.
document.getElementById('inlay-width').addEventListener('input', async (e) => {
const bounds = api.getInlayBounds(0);
const widthMm = Math.min(Number(e.target.value), bounds.widthMax);
await api.setInlay(0, { centerZ: 0, widthMm, metal: materials[0].id });
});
// A rim overlay. rimCoverage says how far it wraps the flat side face:
// 1 covers it, 0 leaves it as base metal, 0.5 splits it.
document.getElementById('overlay-left').addEventListener('change', async (e) => {
if (!e.target.checked) return api.removeOverlay('left');
await api.setOverlay('left', {
widthMm: Math.min(1, api.getOverlayWidthMax('left')),
metal: materials[0].id,
rimCoverage: 1,
});
});
// The bore in a second material.
document.getElementById('sleeve').addEventListener('change', async (e) => {
await api.setSleeve({ enabled: e.target.checked, metal: 'yellow' });
});
}Every setter clamps its own values, so a slider cannot push two features into each other. Read getInlayBounds(index) and getOverlayWidthMax(side) to bound the controls before the user drags them, and call outsideFeaturesFit() when you apply state you did not build yourself.
Change several features at once with one rebuild. Applying the setters in sequence rebuilds the band each time, so the ring flickers through the intermediate states.
await api.setOutsideFeatures({
inlays: [{ centerZ: 0, widthMm: 1.5, metal: 'wood' }],
overlays: [{ side: 'right', widthMm: 1, metal: 'white', rimCoverage: 0.5 }],
sleeve: { enabled: true, metal: 'yellow', full: true },
});Pass sleeve: null to clear the sleeve. The inlays:changed, overlays:changed and sleeve:changed events report every result, including a clear.

Dimension Sliders
function buildDimensionSliders(api) {
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)}`;
});
}Diamond Controls
function buildDiamondControls(api) {
const types = api.getAvailableSettingTypes();
const container = document.getElementById('diamonds');
// "No diamonds" button
const noneBtn = document.createElement('button');
noneBtn.textContent = 'No Diamonds';
noneBtn.addEventListener('click', () => api.setDiamonds(null));
container.appendChild(noneBtn);
// Setting type buttons
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
function buildRingSizeControl(api) {
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(api) {
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
When state changes externally (e.g., user switches bands), update your UI to match:
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 and Presets
Apply multiple changes in one geometry rebuild instead of triggering separate rebuilds:
// Apply a "Classic Gold" preset
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 and Restore
// Save complete state, including variants and outside features
const config = api.toJSON();
localStorage.setItem('wbb-config', JSON.stringify(config));
// Restore saved configuration through the plugin
const saved = localStorage.getItem('wbb-config');
if (saved) await wbbPlugin.fromJSON(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({ state: config, summary: herConfig, price: herPrice }),
});Design Tips
Keep it simple. Start with profile, metal, and width. You don't need to expose every API option.
Use thumbnails. Catalog methods return thumbnail URLs when available. Use them for visual selectors instead of text labels.
Show loading state. Profile changes require geometry computation. Listen for build:started / build:complete to show a spinner.
Debounce sliders. For continuous sliders, debounce to avoid excessive rebuilds:
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
- API Reference for complete method and event documentation
- Pricing to configure the pricing engine
- Measurements for ring size, diamond carat, and dimension details