JavaScript SDK

Control Meshless embeds at runtime — switch variants, seek to a frame, and listen for viewer events.

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[] */] },
  },
  '*',
);
Eventdata shapeEffect
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

EventFired whendata shape
readyViewer has fetched project data and rendered the first frame{ modelId: string }
rotateThe active frame or row changes{ frame: number; row: number; index: number }
hotspot-clickA 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 window directly. Dynamic-hotspot commands are accepted only from that same origin.