Meshless embeds communicate with the parent page through window.postMessage. Use this API to control the viewer from your storefront JavaScript — for example, to sync the viewer with your own color swatch UI, seek to a specific frame on scroll, or respond to viewer events for analytics.
The embed runs inside a Shadow DOM, so you cannot reach viewer internals directly. All communication goes through postMessage on the shared window object.
Message shape
All messages use the same envelope structure.
Incoming messages (page → viewer):
{ source: 'meshless-viewer-config'; event: string; data: object }
Outgoing messages (viewer → page):
{ source: 'meshless-viewer'; event: string; data: object }
Incoming events (page → viewer)
setColorway — switch active variant
Switch the viewer to a ready variant by ID.
window.postMessage(
{
source: 'meshless-viewer-config',
event: 'setColorway',
data: { colorwayId: 'VARIANT_ID' },
},
'*',
);
Pass a non-matching ID (or an empty string) to return to the base frame set:
window.postMessage(
{
source: 'meshless-viewer-config',
event: 'setColorway',
data: { colorwayId: '' },
},
'*',
);
The viewer ignores variants that are not in the Ready state.
setFrame — seek to a specific frame
Jump the viewer to a zero-based frame index.
window.postMessage(
{
source: 'meshless-viewer-config',
event: 'setFrame',
data: { frame: 18 },
},
'*',
);
Frame indexes wrap around — you can send any integer and the viewer will normalise it to the valid range.
Dynamic hotspots — overlay callouts at runtime
Add, replace, or remove hotspots from your page without saving them to the project. Dynamic hotspots are layered on top of the project's saved hotspots and are never persisted; they don't count against your plan limit.
// Replace the entire dynamic set
window.postMessage(
{
source: 'meshless-viewer-config',
event: 'setHotspots',
data: { hotspots: [/* Hotspot[] */] },
},
'*',
);
| Event | data shape | Effect |
|---|---|---|
setHotspots | { hotspots: Hotspot[] } | Replace the dynamic set |
addHotspot | { hotspot: Hotspot } | Add, or replace one with the same id |
updateHotspot | { hotspot: Hotspot } | Merge fields into an existing one by id |
removeHotspot | { hotspotId: string } | Remove one by id |
Each event also accepts an optional modelId to target a single viewer when several are mounted. The Hotspot shape matches the API Reference; see Hotspots for the saved-vs-dynamic model. You can also provide a starting set declaratively with the data-hotspots (inline JSON) or data-hotspots-url (fetched) attributes on the embed element.
Dynamic-hotspot messages are accepted only from the same origin as the page that loaded the embed, so a third-party iframe on the page cannot inject callouts.
Outgoing events (viewer → page)
Listen for outgoing events with a message event handler. Always check event.data.source before processing.
window.addEventListener('message', (e) => {
if (!e.data || e.data.source !== 'meshless-viewer') return;
const { event, data } = e.data;
if (event === 'ready') {
console.log('Viewer ready for project', data.modelId);
}
if (event === 'rotate') {
console.log('Frame', data.frame, 'Row', data.row);
}
if (event === 'hotspot-click') {
console.log('Hotspot', data.hotspot.id, 'clicked on', data.modelId);
}
});
Event reference
| Event | Fired when | data shape |
|---|---|---|
ready | Viewer has fetched project data and rendered the first frame | { modelId: string } |
rotate | The active frame or row changes | { frame: number; row: number; index: number } |
hotspot-click | A viewer hotspot is clicked | { modelId: string; hotspot: Hotspot } |
index in the rotate event is the flat frame index across all rows: row × frameCount + frame.
Full TypeScript types
type MeshlessIncomingEvent =
| { source: 'meshless-viewer-config'; event: 'setColorway'; data: { colorwayId: string } }
| { source: 'meshless-viewer-config'; event: 'setFrame'; data: { frame: number } }
| { source: 'meshless-viewer-config'; event: 'setHotspots'; data: { hotspots: Hotspot[]; modelId?: string } }
| { source: 'meshless-viewer-config'; event: 'addHotspot'; data: { hotspot: Hotspot; modelId?: string } }
| { source: 'meshless-viewer-config'; event: 'updateHotspot'; data: { hotspot: Hotspot; modelId?: string } }
| { source: 'meshless-viewer-config'; event: 'removeHotspot'; data: { hotspotId: string; modelId?: string } };
type MeshlessOutgoingEvent =
| { source: 'meshless-viewer'; event: 'ready'; data: { modelId: string } }
| { source: 'meshless-viewer'; event: 'rotate'; data: { frame: number; row: number; index: number } }
| { source: 'meshless-viewer'; event: 'hotspot-click'; data: { modelId: string; hotspot: Hotspot } };
Example — sync a color swatch UI
document.querySelectorAll('[data-variant-id]').forEach((swatch) => {
swatch.addEventListener('click', () => {
window.postMessage(
{
source: 'meshless-viewer-config',
event: 'setColorway',
data: { colorwayId: swatch.dataset.variantId },
},
'*',
);
});
});
Example — seek on scroll
Show a specific product angle as the user scrolls into the section:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
window.postMessage(
{ source: 'meshless-viewer-config', event: 'setFrame', data: { frame: 0 } },
'*',
);
}
});
});
observer.observe(document.querySelector('.meshless-viewer'));
Multiple viewers on one page
When the same page has more than one viewer, every outgoing event includes the modelId of the viewer that fired it. Use it to track which viewer an event came from. Incoming setColorway and setFrame are broadcast to all mounted viewers — if you need to target one specifically, control it via its own scoped wrapper rather than global postMessage. The dynamic-hotspot events can target a single viewer directly with the optional modelId field.
Security notes
- Only process messages where
event.data.source === 'meshless-viewer'. - Treat identifiers from outgoing events (e.g.
hotspot.id) as opaque — do not reflect them into URL parameters or server requests without validation. - The embed runs in a Shadow DOM on the same origin as the parent page, not in an iframe. There is no cross-origin boundary; messages travel on
windowdirectly. Dynamic-hotspot commands are accepted only from that same origin.