Custom UI - Drag and Drop
Introduction
Drag and drop lets users build product configurations by dragging thumbnails from a sidebar and dropping them onto the 3D canvas. When an item enters the canvas, the configurator takes over and shows valid attachment points. When the user releases, the item snaps into place.
This guide covers:
- Populating a product sidebar from the API's product data
- Implementing mouse and touch drag with a floating preview image
- Handing off to the configurator when the dragged item enters the canvas
Prerequisites
- A working headless configurator setup. If you haven't done this yet, follow the Building a Custom UI guide first.
- Familiarity with the Core Events system, in particular the type-safe
EVENTS.UI.*andEVENTS.SCENE.*accessors.
Step 1: Populating Product Thumbnails
Once the Core API is ready, access the productData property to get all available products. Each product contains look variants with thumbnail URLs.
const configurator = document.getElementById("wcEpigraphConfigurator");
let coreApi;
configurator.addEventListener("coreApi:ready", function () {
coreApi = configurator.api.core;
populateProductThumbnails(coreApi.productData);
});The productData object is keyed by SKU ID. Each entry includes a defaultVariant name and a lookDetails map containing thumbnail paths per variant.
Important:
productDataincludes a synthetic"Root"entry used internally. Remove it before iterating.
function populateProductThumbnails(productData) {
delete productData["Root"];
const container = document.getElementById("productButtonsContainer");
for (const skuId of Object.keys(productData)) {
const product = productData[skuId];
const defaultVariant = product.defaultVariant;
const thumbnailPath = product.lookDetails[defaultVariant]?.thumbnails.low;
const img = document.createElement("img");
img.classList.add("draggable-thumbnail");
img.id = skuId;
img.src = coreApi.getResolvedAssetUrl(thumbnailPath);
img.draggable = false;
container.appendChild(img);
}
}Use getResolvedAssetUrl() to resolve all asset paths returned by the API to their full URLs. Thumbnail paths come in .low and .high resolutions.
Product Data Shape
| Path | Type | Description |
|---|---|---|
productData[skuId] | object | Product entry keyed by SKU |
.defaultVariant | string | Name of the default look variant |
.lookDetails[variant] | object | Appearance details for each variant |
.lookDetails[variant].thumbnails.low | string | Low-resolution thumbnail path |
.lookDetails[variant].thumbnails.high | string | High-resolution thumbnail path (if available) |
Step 2: Implementing Drag
The drag interaction has three phases: start, track, and end. A floating image follows the cursor to provide visual feedback.
Floating Drag Image
Add an <img> element that will follow the cursor during a drag. It starts hidden and is shown when a drag begins.
<img id="currentlyDragging" class="currently-dragging" draggable="false" src="" alt="" />.currently-dragging {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.5s;
width: 100px;
height: 100px;
pointer-events: none;
z-index: 3;
}
.currently-dragging.show {
opacity: 1;
}The pointer-events: none rule ensures the floating image doesn't intercept mouse events, which would interfere with canvas detection.
Starting a Drag
Listen for mousedown and touchstart on each thumbnail. Copy the thumbnail's src to the floating image, record which product is being dragged, and begin tracking.
let draggingImg;
let draggingImgRect;
let currentProductId;
let isDragging = false;
function setupDraggableThumbnails() {
draggingImg = document.getElementById("currentlyDragging");
draggingImgRect = draggingImg.getBoundingClientRect();
const thumbnails = document.querySelectorAll(".draggable-thumbnail");
thumbnails.forEach((thumbnail) => {
thumbnail.addEventListener("mousedown", startDragging);
thumbnail.addEventListener("touchstart", startDragging);
});
document.addEventListener("mouseup", endDragging);
document.addEventListener("touchend", endDragging);
}
function startDragging(rawEvent) {
rawEvent.preventDefault();
const e = rawEvent.touches ? rawEvent.touches[0] : rawEvent;
draggingImg.src = e.target.src;
currentProductId = e.target.id;
// Position the floating image before showing it
trackDrag(rawEvent);
document.addEventListener("mousemove", trackDrag);
document.addEventListener("touchmove", trackDrag);
draggingImg.classList.add("show");
}Tracking Position and Detecting Canvas Entry
On every mousemove/touchmove, reposition the floating image and check whether the cursor has entered the configurator canvas. When it enters, call itemDragStart() to notify the configurator. When it leaves, call itemDragEnd().
function trackDrag(rawEvent) {
rawEvent.preventDefault();
const e = rawEvent.touches ? rawEvent.touches[0] : rawEvent;
// Position the floating image offset by its own dimensions
draggingImg.style.top = `${e.clientY - draggingImgRect.height}px`;
draggingImg.style.left = `${e.clientX - draggingImgRect.width}px`;
const overCanvas = hasEnteredCanvas(e);
if (overCanvas && !isDragging) {
isDragging = true;
coreApi.itemDragStart(currentProductId);
} else if (!overCanvas && isDragging) {
coreApi.itemDragEnd();
isDragging = false;
}
}
function hasEnteredCanvas(e) {
const target = document.elementFromPoint(e.clientX, e.clientY);
return target && target.id === "wcEpigraphConfigurator";
}itemDragStart(skuId) puts the configurator into placement mode, showing valid snap points. The configurator tracks the pointer internally from this point.
Ending the Drag
When the user releases the mouse or lifts their finger, clean up event listeners and reset state. If the item was over the canvas, the configurator handles placement automatically.
function endDragging() {
if (currentProductId === undefined) return;
coreApi.itemDragEnd();
currentProductId = undefined;
isDragging = false;
draggingImg.classList.remove("show");
document.removeEventListener("mousemove", trackDrag);
document.removeEventListener("touchmove", trackDrag);
}Complete Example
A full, self-contained drag-and-drop page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drag and Drop Configurator</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%;
}
#wcEpigraphConfigurator {
width: 100%;
height: 100%;
position: relative;
opacity: 0;
z-index: 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;
max-width: 30%;
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;
}
.product-buttons-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.draggable-thumbnail {
width: 100px;
height: 100px;
cursor: pointer;
border-radius: 10px;
}
.currently-dragging {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.5s;
width: 100px;
height: 100px;
pointer-events: none;
z-index: 3;
}
.currently-dragging.show {
opacity: 1;
}
</style>
</head>
<body>
<div class="main-container">
<div class="epg-configurator-container">
<epigraph-configurator
id="wcEpigraphConfigurator"
experience-id="experience-id-provided-by-epigraph"
disable-ui>
</epigraph-configurator>
</div>
<div class="sidebar">
<div class="category-title">Products</div>
<p>Drag an item onto the 3D canvas to add it to the scene.</p>
<div class="product-buttons-container" id="productButtonsContainer">
<!-- Thumbnails populated from JS -->
</div>
</div>
<img id="currentlyDragging" class="currently-dragging" draggable="false" src="" alt="" />
</div>
<script>
const CONFIGURATOR_ID = "wcEpigraphConfigurator";
const configurator = document.getElementById(CONFIGURATOR_ID);
let coreApi;
// --- Drag State ---
let draggingImg;
let draggingImgRect;
let currentProductId;
let isDragging = false;
// --- Product Thumbnails ---
function populateProductThumbnails(productData) {
delete productData["Root"];
const container = document.getElementById("productButtonsContainer");
for (const skuId of Object.keys(productData)) {
const product = productData[skuId];
const defaultVariant = product.defaultVariant;
const thumbnailPath = product.lookDetails[defaultVariant]?.thumbnails.low;
const img = document.createElement("img");
img.classList.add("draggable-thumbnail");
img.id = skuId;
img.src = coreApi.getResolvedAssetUrl(thumbnailPath);
img.draggable = false;
container.appendChild(img);
}
}
// --- Drag Implementation ---
function setupDraggableThumbnails() {
draggingImg = document.getElementById("currentlyDragging");
draggingImgRect = draggingImg.getBoundingClientRect();
const thumbnails = document.querySelectorAll(".draggable-thumbnail");
thumbnails.forEach((thumb) => {
thumb.addEventListener("mousedown", startDragging);
thumb.addEventListener("touchstart", startDragging);
});
document.addEventListener("mouseup", endDragging);
document.addEventListener("touchend", endDragging);
}
function startDragging(rawEvent) {
rawEvent.preventDefault();
const e = rawEvent.touches ? rawEvent.touches[0] : rawEvent;
draggingImg.src = e.target.src;
currentProductId = e.target.id;
trackDrag(rawEvent);
document.addEventListener("mousemove", trackDrag);
document.addEventListener("touchmove", trackDrag);
draggingImg.classList.add("show");
}
function trackDrag(rawEvent) {
rawEvent.preventDefault();
const e = rawEvent.touches ? rawEvent.touches[0] : rawEvent;
draggingImg.style.top = `${e.clientY - draggingImgRect.height}px`;
draggingImg.style.left = `${e.clientX - draggingImgRect.width}px`;
const overCanvas = hasEnteredCanvas(e);
if (overCanvas && !isDragging) {
isDragging = true;
coreApi.itemDragStart(currentProductId);
} else if (!overCanvas && isDragging) {
coreApi.itemDragEnd();
isDragging = false;
}
}
function hasEnteredCanvas(e) {
const target = document.elementFromPoint(e.clientX, e.clientY);
return target && target.id === CONFIGURATOR_ID;
}
function endDragging() {
if (currentProductId === undefined) return;
coreApi.itemDragEnd();
currentProductId = undefined;
isDragging = false;
draggingImg.classList.remove("show");
document.removeEventListener("mousemove", trackDrag);
document.removeEventListener("touchmove", trackDrag);
}
// --- Initialize ---
configurator.addEventListener("coreApi:ready", function () {
console.log("Core API ready.");
coreApi = configurator.api.core;
populateProductThumbnails(coreApi.productData);
setupDraggableThumbnails();
});
configurator.addEventListener("coreApi:failed", function (event) {
console.error("Core API failed to initialize.", event);
});
</script>
</body>
</html>API Reference Summary
Methods
| Method | Description |
|---|---|
getResolvedAssetUrl(path) | Resolves a relative asset path from the API to its full URL |
itemDragStart(skuId) | Starts a drag operation, putting the configurator into placement mode |
itemDragEnd() | Ends the current drag operation |
Properties
| Property | Description |
|---|---|
productData | Object containing all available products, keyed by SKU ID |
Events
| Event | Description |
|---|---|
coreApi:ready | Core API has initialized successfully |
coreApi:failed | Core API failed to initialize |
What's Next
- Updating Pricing and Inventory -- Sync product prices and stock levels with your store.
- Setting the Cart Message -- Display lead times or promotional messages in the cart.
- Full API Reference -- Complete list of all Core API methods, properties, and events.
Updated 7 days ago
