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.
Quick start
- Sign in at three.ws.
- Open the Studio.
- Pick an avatar, pick a widget type, tweak brand colors and controls, click Generate Embed.
- 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.