Custom UI - "Headless UI"
Introduction
The Epigraph Configurator ships with a default UI that covers most use cases out of the box. However, if you need full control over the look and feel of your product configurator, you can run the component in headless mode. This disables all built-in UI elements and exposes the Core API, letting you build your own interface from scratch while the configurator handles 3D rendering, scene management, and AR.
Headless mode is ideal when you need to:
- Match a specific brand or design system
- Build a non-standard layout (e.g. sidebar panels, floating menus)
- Integrate the configurator into an existing application shell
Prerequisites
- A working Epigraph Configurator setup on your page. If you haven't done this yet, follow the Adding Configurator to Your Page guide first.
- A
experience-idprovided by Epigraph. This key is domain-locked, so request a separate key if you need to whitelist additional domains (e.g. staging). - Familiarity with the Configurator Events system.
Step 1: Setting Up Headless Mode
Add the disable-ui attribute to the <epigraph-configurator> element. This tells the component to render only the 3D canvas, with no built-in buttons, menus, or overlays.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Configurator UI</title>
<script src="https://cdn.jsdelivr.net/npm/@epigraph/configurator"></script>
</head>
<body>
<div class="epg-configurator-container">
<epigraph-configurator
id="wcEpigraphConfigurator"
experience-id="experience-id-provided-by-epigraph"
disable-ui>
</epigraph-configurator>
</div>
</body>
</html>Note: Replace
YOUR_ACCESS_KEYwith the key provided by Epigraph.
Step 2: Accessing the Core API
The Core API is your main interface for interacting with the configurator programmatically. It becomes available asynchronously, so you must listen for the coreApi:ready event before calling any methods.
const configurator = document.getElementById("wcEpigraphConfigurator");
configurator.addEventListener("coreApi:ready", function (event) {
console.log("Core API ready.");
const coreApi = configurator.api.core;
// You can now call coreApi methods
});
configurator.addEventListener("coreApi:failed", function (event) {
console.error("Core API failed to initialize.", event);
// Show a user-friendly error message.
// This can happen due to network issues, invalid access keys,
// or unsupported hardware.
});The API is accessed via configurator.api.core. Store a reference to it for use throughout your application.
Step 3: Loading a Pre-Configured Scene
If you store configurations with Epigraph, each one receives a unique ID. Pass that ID to loadSceneFromConfigurationID() to load it into the scene:
const configId = "mWVXMSG5o1TwaqTTNd16";
coreApi.loadSceneFromConfigurationID(configId);This fetches the configuration from Epigraph's database and loads it into the scene. You can wire this up to a button:
<button data-config-id="mWVXMSG5o1TwaqTTNd16" id="loadConfig1">
Load Configuration 1
</button>const loadButton = document.getElementById("loadConfig1");
loadButton.addEventListener("click", () => {
const configId = loadButton.dataset.configId;
coreApi.loadSceneFromConfigurationID(configId);
});Step 4: Switching Global Variants
Global variants let users change the material or finish of an entire category at once (e.g. all "wood" items switch to "walnut").
Call switchGlobalVariant(category, variant) with the category name and the desired variant:
coreApi.switchGlobalVariant("wood", "birch");Here's how to wire up a set of variant buttons:
<button data-look-category="wood" data-look-variant="birch">Birch</button>
<button data-look-category="wood" data-look-variant="oak">Oak</button>
<button data-look-category="wood" data-look-variant="walnut">Walnut</button>
<button data-look-category="hardware" data-look-variant="black">Black Hardware</button>
<button data-look-category="hardware" data-look-variant="white">White Hardware</button>const variantButtons = document.querySelectorAll("[data-look-category]");
variantButtons.forEach((button) => {
button.addEventListener("click", () => {
const category = button.dataset.lookCategory;
const variant = button.dataset.lookVariant;
coreApi.switchGlobalVariant(category, variant);
});
});Step 5: View In Your Space (AR)
The configurator can launch an AR experience, allowing users to place the configured product in their physical environment.
const arButton = document.getElementById("viewInYourSpace");
arButton.addEventListener("click", () => {
coreApi.viewInYourSpace();
});Production tip: Use
coreApi.canLaunchAR()to check device/browser support before showing the AR button. This avoids presenting a non-functional button on unsupported devices.
Step 6: Styling the Configurator
Hiding the Configurator Until Ready
When the page first loads, the web component has not yet been defined and the 3D canvas is not ready. Use the :defined pseudo-class to transition from a loading state to the rendered configurator:
/* Hidden by default */
#wcEpigraphConfigurator {
width: 100%;
height: 100%;
position: relative;
opacity: 0;
}
/* Visible once the web component is registered */
epigraph-configurator:defined {
opacity: 1 !important;
}Customizing the Loading Spinner
The built-in loading spinner is exposed as a CSS shadow part. Override its styles with the ::part() pseudo-element:
epigraph-configurator::part(circular-loader) {
border: 8px solid #0000001a;
border-top: 8px solid #000000;
border-radius: 50%;
width: 140px;
height: 140px;
}Step 7: Custom Context Menu
When a user clicks an item in the scene, the configurator fires a context menu event. You can intercept this to show your own menu with variant options for the selected item.
Note: Ask the Epigraph team to disable the default context menu for your project to avoid showing two menus.
HTML Structure
Position the context menu container inside the configurator wrapper so it overlays the canvas:
<div class="epg-configurator-container">
<!-- Context menu overlays the canvas -->
<div id="contextMenuContainer" class="context-menu-container">
<div class="context-menu-info-container">
<div id="contextMenuTargetItem">ITEM NAME</div>
<button id="contextMenuCloseButton">X</button>
</div>
<div id="contextMenuThumbnailsContainer" class="context-menu-thumbnails-container">
<!-- Variant thumbnails are populated dynamically -->
</div>
</div>
<epigraph-configurator
id="wcEpigraphConfigurator"
experience-id="YOUR_EXPERIENCE_ID"
disable-ui>
</epigraph-configurator>
</div>Subscribing to Context Menu Events
Use the type-safe event accessors on the Core API's EVENTS object:
function setupCustomContextMenu() {
const contextMenu = document.getElementById("contextMenuContainer");
configurator.addEventListener(
coreApi.EVENTS.UI.ContextMenu_Show,
(event) => {
clearContextMenuThumbnails(contextMenu);
populateContextMenu(contextMenu, event);
contextMenu.classList.add("show-context-menu");
}
);
configurator.addEventListener(
coreApi.EVENTS.UI.ContextMenu_Hide,
() => {
contextMenu.classList.remove("show-context-menu");
}
);
const closeButton = contextMenu.querySelector("#contextMenuCloseButton");
closeButton.addEventListener("click", () => {
coreApi.closeContextMenu();
});
}Populating Variant Options
The ContextMenu_Show event carries data about the clicked item, including its available looks and GUID. Use overrideItemMaterialByGuid(guid, variantName) to apply a variant.
function clearContextMenuThumbnails(contextMenu) {
contextMenu.querySelectorAll(".context-menu-thumbnail").forEach((el) => el.remove());
}
function populateContextMenu(contextMenu, event) {
const details = event.data.primary;
const looks = [...details.data.itemData.looks].sort();
const container = contextMenu.querySelector("#contextMenuThumbnailsContainer");
contextMenu.querySelector("#contextMenuTargetItem").textContent = details.name;
looks.forEach((variantName) => {
const thumbnail = document.createElement("div");
thumbnail.classList.add("context-menu-thumbnail");
thumbnail.textContent = variantName;
thumbnail.addEventListener("click", () => {
coreApi.overrideItemMaterialByGuid(details.guid, variantName);
});
container.appendChild(thumbnail);
});
}Event Data Shape
The ContextMenu_Show event provides:
| Path | Type | Description |
|---|---|---|
event.data.primary | object | The primary item that was clicked |
.name | string | Display name of the item |
.guid | string | Unique identifier for this item instance |
.data.itemData.looks | string[] | Available variant/look names for the item |
Context Menu Styling
The context menu uses a CSS transition to slide in:
.context-menu-container {
max-width: 400px;
height: 150px;
background-color: gray;
position: absolute;
z-index: 3;
opacity: 0;
transition: 0.5s;
display: flex;
gap: 10px;
flex-direction: column;
overflow: hidden;
padding: 10px;
box-sizing: border-box;
transform: translateX(-50%);
}
.context-menu-container.show-context-menu {
opacity: 1;
transform: translateX(0%);
}
.context-menu-info-container {
width: 100%;
height: 20%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.context-menu-thumbnails-container {
width: 100%;
height: 80%;
display: flex;
justify-content: flex-start;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
}
.context-menu-thumbnail {
min-width: 100px;
min-height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 5px;
box-sizing: border-box;
}
.context-menu-thumbnail:hover {
background-color: rgba(0, 0, 0, 0.4);
}Complete Example
A full, self-contained page that demonstrates all the concepts above:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Configurator UI</title>
<script src="https://cdn.jsdelivr.net/npm/@epigraph/configurator"></script>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.main-container {
width: 90vw;
height: 90vh;
display: flex;
flex-direction: row;
}
.epg-configurator-container {
width: 100%;
height: 100%;
position: relative;
}
#wcEpigraphConfigurator {
width: 100%;
height: 100%;
position: relative;
opacity: 0;
}
epigraph-configurator:defined {
opacity: 1 !important;
}
epigraph-configurator::part(circular-loader) {
border: 8px solid #0000001a;
border-top: 8px solid #000000;
border-radius: 50%;
width: 140px;
height: 140px;
}
.sidebar {
display: flex;
flex-direction: column;
min-width: 200px;
background-color: #c5c5c5;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
font-family: sans-serif;
}
.category-title {
font-size: 1.25rem;
font-weight: 700;
margin-top: 1rem;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 8px;
border: 1px solid #999;
background: #fff;
transition: background-color 0.3s, color 0.3s;
}
.btn:hover {
background-color: #000;
color: #fff;
}
/* Context Menu */
.context-menu-container {
max-width: 400px;
height: 150px;
background-color: gray;
position: absolute;
z-index: 3;
opacity: 0;
transition: 0.5s;
display: flex;
gap: 10px;
flex-direction: column;
overflow: hidden;
padding: 10px;
box-sizing: border-box;
transform: translateX(-50%);
}
.context-menu-container.show-context-menu {
opacity: 1;
transform: translateX(0%);
}
.context-menu-info-container {
width: 100%;
height: 20%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.context-menu-thumbnails-container {
width: 100%;
height: 80%;
display: flex;
justify-content: flex-start;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
}
.context-menu-thumbnail {
min-width: 100px;
min-height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 5px;
box-sizing: border-box;
color: #fff;
}
.context-menu-thumbnail:hover {
background-color: rgba(0, 0, 0, 0.4);
}
</style>
</head>
<body>
<div class="main-container">
<div class="epg-configurator-container">
<!-- Custom Context Menu -->
<div id="contextMenuContainer" class="context-menu-container">
<div class="context-menu-info-container">
<div id="contextMenuTargetItem">ITEM NAME</div>
<button id="contextMenuCloseButton">X</button>
</div>
<div id="contextMenuThumbnailsContainer" class="context-menu-thumbnails-container">
</div>
</div>
<epigraph-configurator
id="wcEpigraphConfigurator"
experience-id="experience-id-provided-by-epigraph"
disable-ui>
</epigraph-configurator>
</div>
<div class="sidebar">
<div class="category-title">Saved Configurations</div>
<div class="button-group">
<button class="btn" data-config-id="mWVXMSG5o1TwaqTTNd16" id="loadConfig1">
Load Configuration
</button>
</div>
<div class="category-title">Wood Finish</div>
<div class="button-group">
<button class="btn" data-look-category="wood" data-look-variant="birch">Birch</button>
<button class="btn" data-look-category="wood" data-look-variant="oak">Oak</button>
<button class="btn" data-look-category="wood" data-look-variant="walnut">Walnut</button>
</div>
<div class="category-title">Hardware Finish</div>
<div class="button-group">
<button class="btn" data-look-category="hardware" data-look-variant="black">Black</button>
<button class="btn" data-look-category="hardware" data-look-variant="white">White</button>
</div>
<div class="category-title">Additional Features</div>
<div class="button-group">
<button class="btn" id="viewInYourSpace">View In Your Space</button>
</div>
</div>
</div>
<script>
(function main() {
const configurator = document.getElementById("wcEpigraphConfigurator");
let coreApi;
configurator.addEventListener("coreApi:ready", function () {
console.log("Core API ready.");
coreApi = configurator.api.core;
// --- Saved Configuration ---
const loadConfig1 = document.getElementById("loadConfig1");
loadConfig1.addEventListener("click", () => {
coreApi.loadSceneFromConfigurationID(loadConfig1.dataset.configId);
});
// --- Global Variant Buttons ---
const variantButtons = document.querySelectorAll("[data-look-category]");
variantButtons.forEach((button) => {
button.addEventListener("click", () => {
coreApi.switchGlobalVariant(
button.dataset.lookCategory,
button.dataset.lookVariant
);
});
});
// --- View In Your Space ---
const arButton = document.getElementById("viewInYourSpace");
arButton.addEventListener("click", () => {
coreApi.viewInYourSpace();
});
// --- Custom Context Menu ---
setupCustomContextMenu();
});
configurator.addEventListener("coreApi:failed", function (event) {
console.error("Core API failed to initialize.", event);
});
function setupCustomContextMenu() {
const contextMenu = document.getElementById("contextMenuContainer");
configurator.addEventListener(
coreApi.EVENTS.UI.ContextMenu_Show,
(event) => {
contextMenu.querySelectorAll(".context-menu-thumbnail").forEach((el) => el.remove());
populateContextMenu(contextMenu, event);
contextMenu.classList.add("show-context-menu");
}
);
configurator.addEventListener(
coreApi.EVENTS.UI.ContextMenu_Hide,
() => {
contextMenu.classList.remove("show-context-menu");
}
);
contextMenu.querySelector("#contextMenuCloseButton").addEventListener("click", () => {
coreApi.closeContextMenu();
});
}
function populateContextMenu(contextMenu, event) {
const details = event.data.primary;
const looks = [...details.data.itemData.looks].sort();
const container = contextMenu.querySelector("#contextMenuThumbnailsContainer");
contextMenu.querySelector("#contextMenuTargetItem").textContent = details.name;
looks.forEach((variantName) => {
const thumbnail = document.createElement("div");
thumbnail.classList.add("context-menu-thumbnail");
thumbnail.textContent = variantName;
thumbnail.addEventListener("click", () => {
coreApi.overrideItemMaterialByGuid(details.guid, variantName);
});
container.appendChild(thumbnail);
});
}
})();
</script>
</body>
</html>API Reference Summary
| Method / Property | Description |
|---|---|
configurator.api.core | Reference to the Core API singleton |
loadSceneFromConfigurationID(id) | Loads a saved configuration by its Epigraph-hosted ID |
switchGlobalVariant(category, variant) | Changes the active variant for an entire material category |
viewInYourSpace() | Launches the AR experience on supported devices |
canLaunchAR() | Returns whether the current device supports AR |
overrideItemMaterialByGuid(guid, variant) | Changes the material/look of a specific item instance |
closeContextMenu() | Programmatically closes the context menu |
| Event | Description |
|---|---|
coreApi:ready | Fired when the Core API has initialized successfully |
coreApi:failed | Fired when the Core API fails to initialize |
EVENTS.UI.ContextMenu_Show | An item was clicked in the scene; includes item data and available looks |
EVENTS.UI.ContextMenu_Hide | The context menu should be dismissed |
| CSS Selector | Description |
|---|---|
epigraph-configurator:defined | Matches once the web component is registered |
epigraph-configurator::part(circular-loader) | Targets the built-in loading spinner |
What's Next
Now that you have a custom UI controlling the configurator, see Implementing Drag and Drop to let users drag products from a sidebar into the 3D scene.
For more API methods and events, refer to the full API reference and Core Events documentation.
Updated 7 days ago
