Embed API Reference
The
<open-human>custom element is the fastest way to embed a photorealistic digital human into any webpage - no build step, no framework, no WebGL boilerplate. Drop in one script tag and one HTML element and the engine handles everything: asset loading, rendering, animation, and streaming.
Two Embedding Modes
OpenHuman ships two integration paths. Choose based on how much control you need:
| Web Component | JavaScript SDK | |
|---|---|---|
| Setup | One <script> + one HTML tag | npm install @openhuman/sdk |
| Control | HTML attributes + postMessage | Full programmatic API |
| Framework | Framework-agnostic | ES module, tree-shakeable |
| Isolation | Shadow DOM - no CSS bleed | Shares your page's DOM |
| Best for | CMS embeds, no-code tools, iframes | React / Vue / Svelte apps, custom pipelines |
Quick Embed
The absolute minimum to render a character on your page:
<!-- 1. Load the embed script (once, anywhere in <head> or <body>) -->
<script type="module" src="https://cdn.openhuman.io/sdk/latest/embed.js"></script>
<!-- 2. Place the component wherever you need it -->
<open-human src="https://assets.openhuman.io/characters/aria.ohb" style="width: 480px; height: 640px; display: block;"></open-human>That's it. The component bootstraps a WebGL 2.0 context inside a Shadow DOM, streams the .ohb bundle, and starts playing the default idle animation.
Attributes
All configuration is done via HTML attributes on the <open-human> element. Attributes can be changed at runtime with element.setAttribute() and the engine responds immediately.
Asset & Identity
| Attribute | Type | Default | Description |
|---|---|---|---|
src | string | - | URL to a .ohb character bundle (required) |
name | string | '' | Accessible name for the element (aria-label fallback) |
<open-human src="https://assets.example.com/characters/aria.ohb" name="Aria, your AI assistant"></open-human>Playback & Animation
| Attribute | Type | Default | Description |
|---|---|---|---|
animation | string | 'idle' | Name of the animation state to play on load |
autoplay | boolean | true | Start animating as soon as the bundle is ready |
loop | boolean | true | Loop the current animation clip |
speed | number | 1.0 | Playback speed multiplier |
<open-human src="./characters/aria.ohb" animation="idle" autoplay loop speed="1.0"></open-human>Rendering Quality
| Attribute | Type | Default | Description |
|---|---|---|---|
quality | 'high' | 'medium' | 'low' | 'high' | Rendering quality preset |
fps | number | 60 | Target frame rate (use 30 for mobile) |
shadows | boolean | true | Enable PCF soft shadow map |
post-process | boolean | true | Enable bloom, DoF, ACES tonemap, FXAA |
sss | boolean | true | Enable screen-space subsurface scattering |
environment | string | 'studio_warm' | IBL lighting preset (see Environments) |
exposure | number | 1.0 | Renderer exposure value |
<!-- High quality desktop embed -->
<open-human src="./aria.ohb" quality="high" fps="60" shadows post-process sss environment="studio_cool" exposure="1.1"></open-human>
<!-- Mobile-optimised embed -->
<open-human src="./aria.ohb" quality="low" fps="30" no-shadows no-post-process></open-human>Boolean attributes follow HTML conventions: presence =
true, absence =false. To disable a boolean feature, either omit the attribute or prefix it withno-:no-shadows,no-post-process,no-sss.
Camera
| Attribute | Type | Default | Description |
|---|---|---|---|
fov | number | 35 | Vertical field of view in degrees |
camera-position | string | '0,1.65,0.8' | Camera position as x,y,z in world space (metres) |
camera-target | string | '0,1.55,0' | Look-at target as x,y,z |
orbit | boolean | false | Enable mouse/touch orbit control for the viewer |
orbit-min-distance | number | 0.4 | Minimum orbit zoom distance (metres) |
orbit-max-distance | number | 3.0 | Maximum orbit zoom distance |
<!-- Portrait bust shot with orbit enabled -->
<open-human src="./aria.ohb" fov="35" camera-position="0,1.65,0.8" camera-target="0,1.55,0" orbit orbit-min-distance="0.5" orbit-max-distance="2.0"></open-human>
<!-- Full-body shot -->
<open-human src="./aria.ohb" fov="45" camera-position="0,0.9,2.2" camera-target="0,0.9,0"></open-human>Streaming
| Attribute | Type | Default | Description |
|---|---|---|---|
streaming-url | string | - | WebSocket (wss://) or HTTP (https://) stream URL |
streaming-mode | 'full' | 'facs' | 'full' | 'facs' for lip-sync-only streams |
streaming-smoothing | number | 0.7 | FACS exponential moving average α |
streaming-buffer | number | 80 | Jitter buffer depth in ms |
<open-human src="./aria.ohb" streaming-url="wss://tts-backend.example.com/lipsync" streaming-mode="facs" streaming-smoothing="0.7" streaming-buffer="80"></open-human>Loading & Fallback
| Attribute | Type | Default | Description |
|---|---|---|---|
poster | string | - | URL of an image shown while the bundle loads |
loading | 'eager' | 'lazy' | 'eager' | 'lazy' defers loading until the element enters the viewport |
background | string | 'transparent' | CSS colour behind the canvas (e.g. '#1a1a1a', 'transparent') |
<open-human src="./aria.ohb" poster="./aria-poster.jpg" loading="lazy" background="#111111"></open-human>Environments
The environment attribute sets the IBL (image-based lighting) preset used for all lighting and reflections.
| Preset name | Description |
|---|---|
studio_warm | Soft warm key light - default, flattering for skin |
studio_cool | Cool daylight fill - clean, neutral look |
studio_rim | Dramatic rim lighting - high contrast |
outdoor_overcast | Soft diffuse overcast sky |
outdoor_golden | Warm golden hour directional sun |
dark_cinematic | Low-key dark background with a single spotlight |
<open-human src="./aria.ohb" environment="dark_cinematic"></open-human>JavaScript API (on the element)
Once the component is defined and ready, you can call methods directly on the DOM element to control it programmatically.
Getting a reference
const el = document.querySelector("open-human")
// Wait for the engine to be fully initialised
await el.ready // Promise<void> - resolves when character is loaded and first frame is renderedPlayback
// Play an animation state (immediate, no crossfade)
el.play("talk_neutral")
// Crossfade to a new state
el.crossFadeTo("idle", 0.4) // 400 ms crossfade
// Pause / resume the render loop
el.pause()
el.resume()Morph targets
// Set individual FACS weights
el.setMorph("mouthSmileLeft", 0.8)
el.setMorph("mouthSmileRight", 0.8)
// Set multiple at once
el.setMorphs({ jawOpen: 0.4, browInnerUp: 0.3 })
// Apply a named expression preset
el.applyPreset("happy", 0.7)
// Animate to target weights
await el.animateMorphsTo({ mouthSmileLeft: 0, mouthSmileRight: 0 }, { duration: 0.4 })
// Reset all morphs
el.resetMorphs()Camera
// Set camera position and target
el.setCameraPosition([0, 1.65, 0.8])
el.setCameraTarget([0, 1.55, 0])
el.setFOV(35)
// Enable orbit control at runtime
el.enableOrbit(true)Renderer
el.setExposure(1.2)
el.setEnvironment("studio_cool")
el.setSSS(true)
el.setPostProcess(true)
el.setDOF({ focalDistance: 1.4, aperture: 0.04 })Streaming
// Connect a stream
await el.connectStream("wss://tts-backend.example.com/lipsync", { mode: "facs" })
// Disconnect
el.disconnectStream()
// Stream stats
const stats = el.getStreamStats()
console.log(stats.bufferDepth, stats.estimatedLatency, stats.fps)Lifecycle
// Tear down the engine and release all GPU resources
el.destroy()Events
The <open-human> element dispatches standard CustomEvent instances on itself. Listen with addEventListener:
const el = document.querySelector("open-human")
// Engine lifecycle
el.addEventListener("oh:ready", (e) => console.log("Character ready"))
el.addEventListener("oh:error", (e) => console.error("Error:", e.detail.message))
el.addEventListener("oh:loadprogress", (e) => console.log(`${e.detail.stage}: ${e.detail.percent}%`))
// Animation
el.addEventListener("oh:animationstart", (e) => console.log("Started:", e.detail.state))
el.addEventListener("oh:animationend", (e) => console.log("Ended:", e.detail.clip))
el.addEventListener("oh:transition", (e) => console.log(`${e.detail.from} → ${e.detail.to}`))
// Streaming
el.addEventListener("oh:streamconnected", () => console.log("Stream connected"))
el.addEventListener("oh:streamdisconnected", (e) => console.warn("Stream dropped:", e.detail.reason))
el.addEventListener("oh:streamframe", (e) => {
// e.detail: { timestamp, joints?, facs? }
})
// User interaction (requires orbit="true")
el.addEventListener("oh:orbitstart", () => console.log("User started rotating"))
el.addEventListener("oh:orbitend", () => console.log("User stopped rotating"))Event detail shapes
| Event | e.detail fields |
|---|---|
oh:ready | { webglVersion: number, character: string } |
oh:error | { code: string, message: string } |
oh:loadprogress | { stage: string, percent: number } |
oh:animationstart | { state: string, clip: string } |
oh:animationend | { clip: string, loop: boolean } |
oh:transition | { from: string, to: string, duration: number } |
oh:streamconnected | { url: string, transport: string } |
oh:streamdisconnected | { url: string, code: number, reason: string } |
oh:streamframe | { timestamp: number, joints?: Float32Array, facs?: Float32Array } |
postMessage API (iframe embed)
For maximum isolation - or when embedding inside a no-script CMS - serve the component inside an <iframe> and communicate via postMessage. The embed page at https://cdn.openhuman.io/embed/ accepts all the same commands.
Basic iframe setup
<iframe
id="oh-frame"
src="https://cdn.openhuman.io/embed/?src=https://assets.example.com/aria.ohb&quality=high&animation=idle"
width="480"
height="640"
frameborder="0"
allow="accelerometer; fullscreen"
></iframe>Sending commands
const frame = document.getElementById("oh-frame").contentWindow
// Play an animation
frame.postMessage({ type: "oh:play", state: "talk_neutral" }, "*")
// Crossfade
frame.postMessage({ type: "oh:crossFadeTo", state: "idle", duration: 0.4 }, "*")
// Set morph targets
frame.postMessage({ type: "oh:setMorphs", weights: { jawOpen: 0.5, mouthFunnel: 0.3 } }, "*")
// Apply preset
frame.postMessage({ type: "oh:applyPreset", name: "happy", intensity: 0.8 }, "*")
// Connect a stream
frame.postMessage({ type: "oh:connectStream", url: "wss://...", mode: "facs" }, "*")
// Update camera
frame.postMessage({ type: "oh:setCamera", position: [0, 1.65, 0.8], target: [0, 1.55, 0] }, "*")Receiving events from the iframe
window.addEventListener("message", (e) => {
if (e.origin !== "https://cdn.openhuman.io") return // always check origin
const { type, detail } = e.data
switch (type) {
case "oh:ready":
console.log("Character loaded:", detail.character)
break
case "oh:animationend":
console.log("Clip finished:", detail.clip)
break
case "oh:error":
console.error("Engine error:", detail.message)
break
}
})Supported postMessage commands
type | Payload fields | Description |
|---|---|---|
oh:play | state: string | Play a state immediately |
oh:crossFadeTo | state: string, duration: number | Crossfade to a state |
oh:pause | - | Pause the render loop |
oh:resume | - | Resume the render loop |
oh:setMorph | target: string, weight: number | Set a single morph weight |
oh:setMorphs | weights: Record<string, number> | Set multiple morph weights |
oh:applyPreset | name: string, intensity?: number | Apply an expression preset |
oh:resetMorphs | - | Reset all morphs to neutral |
oh:connectStream | url: string, mode?: string | Connect an animation stream |
oh:disconnectStream | - | Disconnect the stream |
oh:setCamera | position: number[], target: number[], fov?: number | Update camera |
oh:setEnvironment | name: string | Change IBL lighting preset |
oh:setExposure | value: number | Set renderer exposure |
oh:setQuality | quality: string | Switch quality preset at runtime |
oh:destroy | - | Tear down the engine |
React Integration
import { useEffect, useRef } from "react"
// Load the web component definition once at app boot
import "@openhuman/sdk/embed"
interface AvatarProps {
src: string
animation?: string
streamUrl?: string
quality?: "high" | "medium" | "low"
onReady?: () => void
onError?: (code: string, message: string) => void
}
export function Avatar({ src, animation = "idle", streamUrl, quality = "high", onReady, onError }: AvatarProps) {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const handleReady = () => onReady?.()
const handleError = (e: Event) => {
const { code, message } = (e as CustomEvent).detail
onError?.(code, message)
}
el.addEventListener("oh:ready", handleReady)
el.addEventListener("oh:error", handleError)
return () => {
el.removeEventListener("oh:ready", handleReady)
el.removeEventListener("oh:error", handleError)
}
}, [onReady, onError])
return (
<open-human
ref={ref}
src={src}
animation={animation}
quality={quality}
streaming-url={streamUrl}
streaming-mode="facs"
style={{ width: "100%", height: "100%", display: "block" }}
/>
)
}TypeScript users should add a declaration for the custom element to avoid
JSX.IntrinsicElementstype errors:open-human.d.tsdeclare namespace JSX { interface IntrinsicElements { "open-human": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { src?: string animation?: string quality?: "high" | "medium" | "low" autoplay?: boolean loop?: boolean orbit?: boolean environment?: string exposure?: number fov?: number "camera-position"?: string "camera-target"?: string "streaming-url"?: string "streaming-mode"?: "full" | "facs" "streaming-smoothing"?: number "streaming-buffer"?: number poster?: string loading?: "eager" | "lazy" background?: string shadows?: boolean "post-process"?: boolean sss?: boolean fps?: number } } }
Vue Integration
<template>
<open-human
ref="el"
:src="src"
:animation="animation"
:quality="quality"
:streaming-url="streamUrl"
streaming-mode="facs"
style="width: 100%; height: 100%; display: block"
@oh:ready="emit('ready')"
@oh:error="onError"
/>
</template>
<script setup lang="ts">
import { ref } from "vue"
import "@openhuman/sdk/embed"
const props = defineProps<{ src: string; animation?: string; quality?: string; streamUrl?: string }>()
const emit = defineEmits<{ ready: []; error: [code: string, message: string] }>()
const el = ref<HTMLElement>()
const onError = (e: CustomEvent) => emit("error", e.detail.code, e.detail.message)
// Expose element methods to parent via template ref
defineExpose({
play: (state: string) => el.value?.play(state),
setMorphs: (w: Record<string, number>) => el.value?.setMorphs(w),
applyPreset: (name: string, intensity = 1) => el.value?.applyPreset(name, intensity),
connectStream: (url: string) => el.value?.connectStream(url),
})
</script>Content Security Policy
If your site uses a CSP header, add the following directives to allow the engine to run:
Content-Security-Policy:
script-src 'self' https://cdn.openhuman.io;
connect-src 'self' https://assets.openhuman.io wss: https:;
worker-src blob:;
img-src 'self' data: blob:;The engine uses
OffscreenCanvasandSharedArrayBufferinternally where available. These require theCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpheaders on pages that host the component directly (not required for the iframe embed path).
Attribute Quick Reference
A flat reference of every supported attribute on <open-human>:
| Attribute | Type | Default |
|---|---|---|
src | string | - (required) |
name | string | '' |
animation | string | 'idle' |
autoplay | boolean | true |
loop | boolean | true |
speed | number | 1.0 |
quality | 'high' | 'medium' | 'low' | 'high' |
fps | number | 60 |
shadows | boolean | true |
post-process | boolean | true |
sss | boolean | true |
environment | string | 'studio_warm' |
exposure | number | 1.0 |
fov | number | 35 |
camera-position | string | '0,1.65,0.8' |
camera-target | string | '0,1.55,0' |
orbit | boolean | false |
orbit-min-distance | number | 0.4 |
orbit-max-distance | number | 3.0 |
streaming-url | string | - |
streaming-mode | 'full' | 'facs' | 'full' |
streaming-smoothing | number | 0.7 |
streaming-buffer | number | 80 |
poster | string | - |
loading | 'eager' | 'lazy' | 'eager' |
background | string | 'transparent' |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank canvas, no error | WebGL 2.0 not supported | Add a poster fallback image; check browser compatibility |
oh:error with code BUNDLE_FETCH_FAILED | CORS on .ohb URL | Add Access-Control-Allow-Origin: * to your asset server |
oh:error with code WEBGL2_NOT_SUPPORTED | Old browser or GPU blacklisted | Set quality="low" to attempt WebGL 1.0 fallback |
| Component renders but no animation plays | animation attribute names a clip not in the bundle | Check animations/index.json inside the .ohb for valid clip names |
| Stream connects but mouth does not move | streaming-mode is 'full' but server sends FACS-only frames | Set streaming-mode="facs" |
| High memory on mobile | Quality too high for device GPU | Set quality="low" and fps="30" |
| CSP blocks script | cdn.openhuman.io not in script-src | Add it to your CSP (see Content Security Policy) |
Next Steps
- Get Started - set up the SDK from scratch with
npm install - Loading Custom Characters - build your own
.ohbbundles - Animation Graph - configure a full state machine for body animation
- Facial Blendshapes - drive the 52 FACS morph targets
- Streaming Protocol - integrate WebSocket or HTTP animation streams