Widgets

Embeddable 3D — a configured, shareable view of an avatar that any site can drop in with one line of HTML.

A widget is a saved bundle of: an avatar (GLB stored in R2), a widget type (turntable, animation gallery, talking agent, ERC-8004 passport, hotspot tour), and brand config (background, accent, caption, controls, camera). Widgets are created in the Widget Studio and shared via short URLs.

On this page:

Quick start

  1. Sign in at three.ws.
  2. Open the Studio.
  3. Pick an avatar, pick a widget type, tweak brand colors and controls, click Generate Embed.
  4. Copy the iframe snippet into any HTML page, blog post, Notion doc, or CMS.

Widget types

Type Best for Default size
turntable Hero banners, product showcases. Auto-rotate, no UI. 600 × 600
animation-gallery Showcasing a rigged avatar's animation library. 720 × 720
talking-agent Embodied chat — your agent on your site. 420 × 600
passport On-chain identity card backed by ERC-8004. 480 × 560
hotspot-tour Annotated 3D scenes with clickable POIs. 800 × 600
kol-trades Live buy and sell activity from tracked KOL and whale wallets for one token. 600 × 600
walking-avatar A roaming 3D avatar visitors steer around your page with joystick or keyboard. 600 × 600

Only the first five types carry a per-type default size (see TYPE_SIZES in /embed.js); every other type falls back to 600 × 600 unless you pass data-width / data-height. The Studio picker is the authoritative list and also offers the live-market feed widgets, which are configured there rather than documented here.

URLs

Every widget has three canonical URLs.

URL Use it for
/w/<id> Sharing in Slack/Discord/X — server-rendered with rich OG card.
/widget#widget=<id> Slim viewer shell — no nav/footer chrome. The canonical embed URL.
/app#widget=<id> Legacy SPA viewer — still works, but includes the full main-app chrome.
/api/widgets/<id>/og The 1200×630 preview image.

Hash parameters

Param Type Description
widget string Loads a saved widget by id.
model string Loads a raw GLB by URL (mutually exclusive with widget).
kiosk boolean Hides the header chrome for clean iframe embeds.
cameraPosition x,y,z Comma-separated camera position override.
preset string HDR environment preset name.

Embedding

iframe (recommended)

<iframe
  src="https://three.ws/widget#widget=wdgt_abc123def456&kiosk=true"
  width="600"
  height="600"
  style="border:0;border-radius:12px;max-width:100%"
  allow="autoplay; xr-spatial-tracking; clipboard-write"
  loading="lazy"></iframe>

Script embed

<script async src="https://three.ws/embed.js" data-widget="wdgt_abc123def456"></script>

oEmbed (WordPress, Ghost, Notion)

Most CMSes auto-detect oEmbed. Paste a /w/<id> URL on its own line and the editor should turn it into a live embed automatically.

postMessage API

Widgets communicate with the parent page via window.postMessage. Always check event.origin before trusting any message.

Every message carries its event name in data.type; the payload columns below list the other fields on the same object.

iframe → parent

Event Payload When
widget:ready { id, widgetType } Widget runtime has booted.
widget:load:error { id, url, error } GLB or config failed to load.
widget:chat:message { id, role, content } Talking-agent only.
widget:hotspot:open { id, label, widgetId } Hotspot-tour only. Here id is the hotspot, widgetId the widget.
window.addEventListener('message', (e) => {
  // event.origin never carries a trailing slash. Compare against the bare origin.
  if (e.origin !== 'https://three.ws') return;
  if (e.data?.type === 'widget:ready') {
    console.log('Widget mounted:', e.data.id, e.data.widgetType);
  }
});

parent → iframe

Event Payload Effect
widget:config { background, accent, autoRotate, … } Live-update brand config.
widget:command { command, args } Trigger a runtime action.

OG / oEmbed

Every widget URL serves rich-preview metadata. /w/<id> is server-rendered with Open Graph + Twitter card tags and a <link rel="alternate" type="application/json+oembed"> pointer.

GET /api/widgets/oembed?url=<widget-url>&format=json

Returns standard oEmbed v1.0 JSON, type rich. Accepts /w/<id>, /widget#widget=<id>, and legacy /app#widget=<id> forms.

CSP / CORS

Widgets run in a sandboxed iframe with these allow flags: autoplay, xr-spatial-tracking, clipboard-write. If your site uses strict CSP, allow:

frame-src https://three.ws/;
img-src https://three.ws/ data:;

Privacy & analytics

We log a minimal first-party event per widget load, four columns wide: widget_id, country (a two-letter code, only when the edge supplies one, never derived from a raw IP), referer_host (hostname only, no path and no query), and created_at. No IPs. No cookies. No user agents. No chat content. No cross-widget tracking.

FAQ

Can I self-host? The runtime is open source. The Studio + saved-widgets API depends on Neon + R2; you'd need to wire those yourself. See deployment guide.

Why no WordPress plugin? The oEmbed endpoint already does what a plugin would.

How big is the bundle? Cold load ~400KB JS + the GLB itself.

Mobile? Yes — every widget type works on iOS Safari and Android Chrome.


← Back to gallery · Open the Studio →