Get a quote

The <splat-viewer> element

<script type="module" src="/splat-viewer-0.1.0/splat-viewer.0.1.0.js"></script>

<splat-viewer src="/products/chair.sog" view="/products/chair.view.json" alt="Oak chair">
  <img src="/products/chair.webp" alt="Oak chair" width="2048" height="2048">
</splat-viewer>

The <img> inside is the poster: it shows at once, and the 3D view replaces it when ready. If anything fails, the poster stays.

Attributes

Attribute Meaning Default
src The product file (.sog or .ply) Required
poster Poster image, only when there is no <img> inside None
alt Text for screen readers None
view A .view.json file with the start view, limits, and optionally a background colour behind the product ("background": "#000000"), such as black for night scenes Automatic view, see-through background
up The file’s up axis: +x -x +y -y +z -z +y
loading lazy waits until the page has loaded and the viewer is near the screen; eager starts at once lazy
auto-rotate The product turns slowly by itself until the shopper touches it. Never when the device asks for reduced motion Off
gallery For a swipe gallery: sideways swipes move the gallery until the shopper taps the viewer; after that they turn the product, until focus moves elsewhere. Use the poster image as the gallery thumbnail Off
interaction-prompt none hides the “Drag to turn” hint shown after 3 seconds without interaction Shown

Attributes are read once, when the viewer starts.

Events

Event When event.detail
load The 3D view has replaced the poster { ms, splats, rebuilt }: milliseconds from the start of loading, the product’s splat count, and true when the view came back after the graphics card reset
error Something failed; the poster stays { code, message }
first-interaction The shopper’s first drag, tap, wheel, key or preset-view click, once { ms }: milliseconds after the 3D view showed
view-time When the page is hidden or left, and when the element is removed { ms }: how long the 3D view was at least half on screen since the last report

The usage events (load, first-interaction, view-time) are for your own analytics. They go only to your page, carry no personal data (only times, a count and error codes) and nothing is sent to any server. first-interaction and view-time bubble, so one listener on document hears every viewer. The one exception: the view-time sent when an element is removed is no longer in the page, so only a listener on the element itself hears it. For example:

document.addEventListener('first-interaction', (e) => analytics.track('3d_interaction', { ms: e.detail.ms }));

Preset views

Add your own buttons inside the element; a click turns the camera to that view:

<splat-viewer src="/products/chair.sog" auto-rotate>
  <img src="/products/chair.webp" alt="Oak chair">
  <button slot="view" data-camera-orbit="0deg 75deg 100%">Front</button>
  <button slot="view" data-camera-orbit="90deg 75deg 100%">Side</button>
  <button slot="view" data-camera-orbit="0deg 20deg 80%">Top</button>
</splat-viewer>

data-camera-orbit is the angle around the product from the front, the angle down from straight above, and the distance as a share of the start view’s (optional). The view file’s limits still apply. The buttons are yours: style and word them as you like.

Hotspots

Pin your own labels to points on the product:

<splat-viewer src="/products/drill.sog">
  <img src="/products/drill.webp" alt="Hammer drill">
  <button slot="hotspot" data-position="0.02 0.11 0.05" data-normal="0 0 1">Chuck</button>
</splat-viewer>

React, Vue and other frameworks

The element works as it is, with no wrapper:

Shoppers

Styling

Size the element with your own CSS, such as width: 100%; aspect-ratio: 1. It is 300 × 300 pixels if you set nothing, and stays square if you set only a width. For a wider box use aspect-ratio: 4/3 or aspect-ratio: 16/9: the 3D view keeps the same framing from top to bottom and shows more at the sides. Make the poster in the same shape (the view editor’s Shape and Download poster), or it shows smaller until the 3D view replaces it.

Custom properties, set on the element or any parent:

Property Styles Default
--splat-viewer-background Behind the poster and the 3D view transparent
--splat-viewer-focus-color The keyboard focus ring The browser’s highlight colour
--splat-viewer-accent-color The line under the loading sign #fff
--splat-viewer-text-color Text on the loading sign and the hint #fff
--splat-viewer-panel-color The background of the loading sign and the hint rgba(0, 0, 0, 0.6)
--splat-viewer-radius The loading sign’s corners 999px
--splat-viewer-font The loading sign’s font, as the CSS font shorthand 500 14px/1.2 system-ui, sans-serif

Parts, for any other CSS through ::part():

Part What it is
loading The loading sign, shown after one second of loading
canvas The 3D view
prompt The “Drag to turn” hint
views The row holding your preset-view buttons, at the top right
hotspots The layer holding your hotspot labels

Your own loading text and hint, for example in another language:

<splat-viewer src="/products/chair.sog">
  <img src="/products/chair.webp" alt="Stolica od hrasta">
  <span slot="loading">Učitavanje 3D prikaza…</span>
  <span slot="prompt">Prevucite da okrenete</span>
</splat-viewer>

Example:

splat-viewer {
  --splat-viewer-accent-color: #e4572e;
  --splat-viewer-panel-color: #1d3557;
}
splat-viewer::part(loading) { text-transform: uppercase; }