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

URLs

Every widget has three canonical URLs.

URL Use it for
/w/<id> Sharing in Slack/Discord/X — server-rendered with rich OG card.
/app#widget=<id> The SPA viewer with the widget config applied.
/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/app#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.

iframe → parent

Event Payload When
widget:ready { id, type } Widget runtime has booted.
widget:load { id, model_url } Underlying GLB loaded successfully.
widget:load:error { id, error } GLB or config failed to load.
widget:chat:message { role, content } Talking-agent only.
widget:hotspot:open { id, label } Hotspot-tour only.
widget:resize { width, height } Widget reports preferred size.
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://three.ws/') return;
  if (e.data?.type === 'widget:ready') {
    console.log('Widget mounted:', e.data.id);
  }
});

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 both /w/<id> and /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: widget_id, type, country (Vercel edge header), referer_host (hostname only), 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 →