@mymosdk/avatar
v0.2.1
Published
Lightweight animated avatar SDK for any web application
Maintainers
Readme
mymo-avatar
Lightweight animated 3D avatar SDK for any web application.
- 23 KB gzip (core, Three.js not included)
- 60 FPS WebGL rendering via Three.js
- Framework-agnostic — Vanilla, React, Vue, Next.js, Nuxt, Svelte, Electron
- AI-agnostic — works standalone or with any LLM/TTS provider
- < 5 min to integrate
How it works
The SDK renders an animated 3D avatar as a fixed floating widget that mounts itself directly to document.body. You create one Avatar instance and control everything through its chainable API — no framework bindings required unless you want them.
Under the hood, six systems work together:
| System | Responsibility |
|---|---|
| Renderer | Three.js WebGL scene with ambient + key + fill lighting, ACES Filmic tone mapping, optional shadows |
| Animation Engine | Procedural bone rotations for gestures + morph target blending for expressions |
| Audio Engine | Web Audio API wrapper — playback, amplitude analysis, gain control |
| Lip Sync | Amplitude-to-viseme mapping; drives mouth morphs in real time |
| Asset Loader | GLB/VRM loading from CDN or any URL; in-memory model cache |
| Plugin System | avatar.use(plugin) registers third-party or custom integrations |
The avatar widget is a rounded <div> containing a <canvas>. The widget can be dragged, repositioned, resized, and themed at runtime without recreating it.
Installation
# npm
npm install @mymosdk/avatar three
# pnpm
pnpm add @mymosdk/avatar three
# yarn
yarn add @mymosdk/avatar threeThree.js is a peer dependency. If you already have it in your project, you only need
@mymosdk/avatar.
Quickstart
import { Avatar } from "@mymosdk/avatar"
const avatar = new Avatar({
// Pass any GLB or VRM URL hosted on a CORS-enabled server.
// See "Model hosting" below for details.
model: "https://your-cdn.example.com/my-avatar.vrm",
position: "bottom-right",
size: 200,
theme: "dark",
framing: "bust",
})The avatar mounts itself to document.body as a fixed floating widget immediately.
Model hosting
The SDK loads models via the browser's fetch / XHR APIs, so the server that hosts your .vrm or .glb file must send CORS headers (Access-Control-Allow-Origin: *).
| Hosting option | CORS | Notes |
|---|---|---|
| Your own server / CDN | ✅ configure it | Full control |
| Cloudflare R2 (free tier) | ✅ built-in | Recommended for production |
| GitHub Pages | ✅ built-in | Free, served from your repo |
| Local public/ folder | ✅ same-origin | Use during development |
| GitHub Releases | ❌ not supported | Redirects to a domain that blocks browser CORS |
// ✅ Works — CORS-enabled CDN
avatar.load("https://your-cdn.example.com/my-avatar.vrm")
// ✅ Works — local file served by Vite / webpack dev server
avatar.load("/models/my-avatar.vrm")
// ❌ Will fail in the browser — GitHub Releases don't support CORS
avatar.load("https://github.com/user/repo/releases/download/tag/file.vrm")API
Constructor options
| Option | Type | Default | Description |
|---|---|---|---|
| model | string | — | GLB or VRM URL to load on init |
| position | AvatarPosition | "bottom-right" | Corner preset |
| size | number | 180 | Widget size in px |
| theme | AvatarTheme | "light" | Background theme |
| themeConfig | ThemeConfig | {} | Custom background/shadow per theme |
| framing | AvatarFraming | "full" | Camera framing preset |
| framingConfig | FramingSliceConfig | {} | Per-mode framing fine-tuning |
| draggable | boolean | false | Allow drag & drop repositioning |
| shadows | boolean | false | Enable shadow rendering |
| idle | boolean | true | Enable idle micro-expression animations |
| idleInterval | number | 8000 | Idle animation interval (ms) |
| blink | boolean | true | Enable auto-blink |
| blinkInterval | number | 3500 | Blink interval (ms) |
| lipSync | boolean | true | Enable audio-driven lip sync |
| followMouse | boolean | false | Auto-track mouse cursor |
| autoHide | boolean | false | Hide widget when user scrolls |
| zIndex | number | 99999 | CSS z-index of the widget |
| debug | boolean | false | Enable debug overlay + skeleton helper on startup |
| ariaLabel | string | "Interactive avatar" | Accessible label read by screen readers |
Framing
Control how much of the model is visible in the widget:
avatar.frame("full") // entire body
avatar.frame("half") // waist up
avatar.frame("bust") // chest up
avatar.frame("face") // head onlyFine-tune each mode at runtime:
avatar.setFramingConfig({
bust: { from: 0.70, to: 1.0, lookBias: 0.44 },
face: { from: 0.82, to: 1.0, lookBias: 0.34 },
})| Field | Type | Description |
|---|---|---|
| from | number | Bottom of visible slice (0–1) |
| to | number | Top of visible slice (0–1, usually 1.0) |
| lookBias | number | Where the camera aims in the slice (0.5 = center; lower = more headroom) |
Default slices:
| Mode | from | to | lookBias |
|---|---|---|---|
| full | 0.00 | 1.00 | 0.50 |
| half | 0.48 | 1.00 | 0.50 |
| bust | 0.68 | 1.00 | 0.46 |
| face | 0.80 | 1.00 | 0.36 |
Theme
Switch theme at runtime or customize colors and shadows:
avatar.setTheme("dark")
avatar.setThemeConfig({
light: {
background: "radial-gradient(circle at 60% 40%, #ffffff, #e8e8e8)",
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
},
dark: {
background: "radial-gradient(circle at 60% 40%, #2a2a2a, #1a1a1a)",
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
},
})ThemeConfig is Partial<Record<"light" | "dark", { background?: string; boxShadow?: string }>>.
Expressions
avatar.smile()
avatar.sad()
avatar.happy()
avatar.angry()
avatar.surprised()
avatar.thinking()
avatar.confused()
avatar.sleep()
avatar.idle() // reset to neutral
// With explicit intensity (0–1)
avatar.expression("smile", 0.8)Gestures
avatar.wave()
avatar.nod()
avatar.yes() // affirmative nods (larger amplitude)
avatar.no() // deliberate side-to-side shakes
avatar.shakeHead()
avatar.clap()
avatar.jump()
avatar.dance() // hip sway with counter-rotating spine
avatar.thumbsUp()Gestures support queuing: if a gesture is triggered while another is active, the current one accelerates to its exit phase and the new one starts immediately after. Only one gesture can be pending at a time.
Gestures are procedural — they animate bones directly, so no embedded animation clips are needed in the model file.
States (UI feedback)
States combine an expression + animation + a pulsing color ring:
avatar.loading() // thinking expression + blue ring
avatar.success() // happy expression + Yes animation + green ring
avatar.error() // sad expression + No animation + red ring
avatar.warning() // surprised expression + orange ring
avatar.typing() // thinking expression, no ring
avatar.listening() // smile expression + blue ring
avatar.processing() // thinking expression + blue ring
avatar.complete() // happy expression + ThumbsUp animation + green ring
avatar.setState("listening") // generic setter
avatar.clearState() // reset expression, animation, and ringSpeech & Lip Sync
// Play audio with automatic lip sync (AudioBuffer, ArrayBuffer, or URL)
await avatar.talk(audioBuffer)
// Manual control
avatar.startTalking()
avatar.stopTalking()
avatar.pauseTalking()
// Explicit phoneme/mouth control
avatar.setViseme("aa") // phoneme-level lip sync
avatar.setMouth(0.6) // direct mouth openness: 0 = closed, 1 = fully open
avatar.setVolume(0.8) // amplitude-driven mouth movementSupported visemes: "sil" "PP" "FF" "TH" "DD" "kk" "CH" "SS" "nn" "RR" "aa" "E" "ih" "oh" "ou"
Look
avatar.lookAtMouse() // follow mouse cursor
avatar.lookAt(400, 300) // look at screen coordinates (px)
avatar.lookForward() // return to center
avatar.randomLook() // random periodic lookVisibility & Layout
avatar.show()
avatar.hide()
avatar.move(100, 200) // absolute position (px from top-left)
avatar.position("top-left") // corner preset
avatar.size(240) // resize widget
avatar.scale(1.2) // relative scaleAnimations
avatar.play("Wave") // play a named clip from the GLB/VRM file
avatar.stop()Events
avatar.on("loaded", () => console.log("avatar ready"))
avatar.on("modelLoaded", (_, { model }) => console.log("model:", model))
avatar.on("click", () => avatar.wave())
avatar.on("speechStart", () => console.log("talking…"))
avatar.on("speechEnd", () => console.log("done"))
avatar.on("animationStart", (_, data) => console.log(data))
avatar.on("animationEnd", () => {})
avatar.off("click", handler) // remove a specific listenerAvailable events: "click" "loaded" "modelLoaded" "animationStart" "animationEnd" "speechStart" "speechEnd"
Load models
// By direct URL — GLB and VRM both supported
await avatar.load("https://example.com/my-avatar.glb")
await avatar.load("https://example.com/my-avatar.vrm")
// Local file
await avatar.load("/models/my-avatar.vrm")Lifecycle
avatar.destroy() // removes widget from DOM, stops all loopsAll methods except
destroy()are chainable.
Debug
Enable at construction time or toggle at runtime:
// Option A — on at startup
const avatar = new Avatar({ model: "...", debug: true })
// Option B — toggle at runtime
avatar.debug() // enable
avatar.debug(false) // disableWhen active, a fixed overlay appears in the top-left corner of the viewport showing:
| Field | Description |
|---|---|
| FPS | Frames per second (updated every second) |
| Expr | Currently active expression (smile, thinking, etc.) |
| Gest | Currently active gesture (wave, dance, etc.) |
| Morphs | Total morph target count loaded from the model |
debug: true also activates the skeleton helper automatically (same as calling debugBones()).
Additional debug helpers — each toggles on/off with repeated calls:
avatar.debugBones() // show/hide Three.js SkeletonHelper (bone lines)
avatar.debugBones(false) // hide explicitly
avatar.debugAxes() // show/hide XYZ axis helper at world origin
avatar.debugAxes(false) // hide explicitlyThe axes helper renders X (red), Y (green), and Z (blue) lines with labeled sprites at their tips so you can verify model orientation at a glance.
Plugins
Built-in TTS plugins
| Package | Provider | Install |
|---|---|---|
| @mymosdk/plugin-openai | OpenAI TTS | npm i @mymosdk/plugin-openai openai |
| @mymosdk/plugin-elevenlabs | ElevenLabs | npm i @mymosdk/plugin-elevenlabs |
| @mymosdk/plugin-anthropic | Anthropic (text streaming + optional TTS) | npm i @mymosdk/plugin-anthropic @anthropic-ai/sdk |
Each plugin calls avatar.talk() internally, so lip sync works automatically.
⚠️ Security — API keys
Never put API keys in browser code. Any key you pass to a plugin in a frontend bundle is visible to every user via DevTools → Network or by reading the JavaScript source.
The plugins support two modes:
| Mode | Option | When to use |
|---|---|---|
| Proxy (recommended) | proxyUrl: "/api/tts" | Browser / any public-facing app |
| Direct | apiKey: "sk-..." | Node.js, Electron, server-side scripts only |
If you pass apiKey in a browser environment, the plugin logs a visible console.warn pointing here. The call still works, but you are responsible for the exposure — the SDK only warns, it does not block.
Browser-safe setup (recommended):
// ✅ Key stays on your server
import { OpenAITTSPlugin } from "@mymosdk/plugin-openai"
const tts = new OpenAITTSPlugin({ proxyUrl: "/api/tts/openai" })
avatar.use(tts)
await tts.speak("Hello!")Your backend endpoint receives { text, voice, speed } and forwards it to OpenAI with the key:
// POST /api/tts/openai (Express / Next.js API Route / Edge Function)
import OpenAI from "openai"
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function POST(req: Request) {
const { text, voice = "alloy", speed = 1.0 } = await req.json()
const audio = await client.audio.speech.create({ model: "tts-1", voice, input: text, speed })
return new Response(await audio.arrayBuffer(), {
headers: { "Content-Type": "audio/mpeg" },
})
}Direct mode — Node.js / Electron only:
// ✅ Safe only when this code never reaches the browser
import { ElevenLabsTTSPlugin } from "@mymosdk/plugin-elevenlabs"
const tts = new ElevenLabsTTSPlugin({
apiKey: process.env.ELEVENLABS_API_KEY,
voiceId: "21m00Tcm4TlvDq8ikWAM",
})
avatar.use(tts)
await tts.speak("Hello!")OpenAI TTS plugin
import { OpenAITTSPlugin } from "@mymosdk/plugin-openai"
const tts = new OpenAITTSPlugin({
proxyUrl: "/api/tts/openai", // or apiKey (Node/Electron only)
voice: "nova", // alloy | echo | fable | onyx | nova | shimmer
model: "tts-1", // tts-1 | tts-1-hd
speed: 1.0,
})
avatar.use(tts)
await tts.speak("Hello from OpenAI!")
tts.stop() // stops playback immediatelyElevenLabs TTS plugin
import { ElevenLabsTTSPlugin } from "@mymosdk/plugin-elevenlabs"
const tts = new ElevenLabsTTSPlugin({
proxyUrl: "/api/tts/elevenlabs", // or apiKey (Node/Electron only)
voiceId: "21m00Tcm4TlvDq8ikWAM", // Rachel — see elevenlabs.io/voice-library
modelId: "eleven_multilingual_v2",
stability: 0.5,
similarityBoost: 0.75,
})
avatar.use(tts)
await tts.speak("Hello from ElevenLabs!")Anthropic plugin (text streaming)
Shows the avatar in typing state while Claude streams a response, then optionally speaks it with any TTS plugin.
import { AnthropicPlugin } from "@mymosdk/plugin-anthropic"
import { ElevenLabsTTSPlugin } from "@mymosdk/plugin-elevenlabs"
const tts = new ElevenLabsTTSPlugin({ proxyUrl: "/api/tts/elevenlabs" })
avatar.use(tts)
const claude = new AnthropicPlugin({
proxyUrl: "/api/chat", // or apiKey (Node/Electron only)
model: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tts, // omit if you only want typing animation, no speech
})
avatar.use(claude)
const response = await claude.chat("Tell me a joke", [
{ role: "assistant", content: "Sure! What kind of joke?" },
])
console.log(response) // full assistant textYour proxy endpoint for Anthropic should forward the SSE stream transparently so the plugin can parse content_block_delta events.
TTS integration without a plugin
You can also call any TTS service directly and pass the audio to avatar.talk() — no plugin needed:
const res = await fetch("/api/speak", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Hello!" }),
})
const audio = await res.arrayBuffer()
await avatar.talk(audio) // lip sync kicks in automaticallyBuild your own plugin
import type { AvatarPlugin } from "@mymosdk/avatar"
const myPlugin: AvatarPlugin = {
name: "my-plugin",
install(avatar) {
avatar.on("click", () => avatar.wave())
},
}
avatar.use(myPlugin)The install function receives the full AvatarApi (all public methods). Plugins are registered once and live for the lifetime of the avatar.
Framework integrations
Vanilla JS / TypeScript
No adapter needed — import and go:
import { Avatar } from "@mymosdk/avatar"
const avatar = new Avatar({ model: "https://example.com/my-avatar.vrm", framing: "bust", theme: "dark" })
document.querySelector("#wave-btn")?.addEventListener("click", () => avatar.wave())React
npm install @mymosdk/avatar threeHook
import { useAvatar } from "@mymosdk/avatar/react"
function MyComponent() {
const avatarRef = useAvatar({
model: "https://...",
position: "bottom-right",
framing: "bust",
idle: true,
blink: true,
})
return <button onClick={() => avatarRef.current?.wave()}>Wave</button>
}useAvatar creates the avatar on mount and calls destroy() on unmount. The returned RefObject<Avatar> gives you the full imperative API.
Component
import { useRef } from "react"
import { AvatarWidget } from "@mymosdk/avatar/react"
import type { Avatar } from "@mymosdk/avatar"
export function App() {
const ref = useRef<Avatar | null>(null)
return (
<>
<AvatarWidget
ref={ref}
model="https://..."
position="bottom-right"
theme="dark"
framing="bust"
idle
blink
draggable
/>
<button onClick={() => ref.current?.smile()}>Smile</button>
</>
)
}AvatarWidget renders no DOM output — it mounts the avatar directly to document.body. Control it via the forwarded ref.
Vue 3
npm install @mymosdk/avatar threeComposable
<script setup lang="ts">
import { useAvatar } from "@mymosdk/avatar/vue"
const avatar = useAvatar({
model: "https://...",
position: "bottom-right",
framing: "bust",
idle: true,
blink: true,
})
// avatar is a ShallowRef<Avatar | null>
</script>
<template>
<button @click="avatar.value?.wave()">Wave</button>
</template>useAvatar creates the avatar in onMounted and destroys it in onUnmounted.
Component
<script setup lang="ts">
import { ref } from "vue"
import { AvatarWidget } from "@mymosdk/avatar/vue"
const widgetRef = ref()
const avatar = () => widgetRef.value?.avatar.value
</script>
<template>
<AvatarWidget
ref="widgetRef"
model="https://..."
position="bottom-right"
theme="dark"
framing="bust"
:idle="true"
:blink="true"
:draggable="true"
/>
<button @click="avatar()?.wave()">Wave</button>
</template>Access the avatar instance via widgetRef.value.avatar.value (the component exposes a avatar ShallowRef).
Next.js
npm install @mymosdk/avatar threeThe avatar uses WebGL and document.body, so it must run client-side only. Always use dynamic with ssr: false.
App Router (recommended)
// app/components/AvatarDemo.tsx
"use client"
import { useAvatar } from "@mymosdk/avatar/react"
export default function AvatarDemo() {
const avatarRef = useAvatar({
model: "https://...",
position: "bottom-right",
framing: "bust",
})
return <button onClick={() => avatarRef.current?.wave()}>Wave</button>
}// app/page.tsx
import dynamic from "next/dynamic"
const AvatarDemo = dynamic(() => import("./components/AvatarDemo"), { ssr: false })
export default function Page() {
return <AvatarDemo />
}Add transpilePackages to next.config.ts:
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
transpilePackages: ["@mymosdk/avatar"],
}
export default nextConfigPages Router
// pages/index.tsx
import dynamic from "next/dynamic"
const AvatarWidget = dynamic(
() => import("@mymosdk/avatar/react").then(m => m.AvatarWidget),
{ ssr: false }
)
export default function Home() {
return (
<AvatarWidget
model="https://..."
position="bottom-right"
framing="bust"
theme="dark"
/>
)
}Nuxt 3
npm install @mymosdk/avatar threeThe avatar uses WebGL and document.body, so it must run client-side only. Wrap any component that uses the SDK with <ClientOnly>.
Plugin — create plugins/avatar.client.ts (the .client.ts suffix makes Nuxt skip it on the server):
// plugins/avatar.client.ts
export default defineNuxtPlugin(() => {
// Client-only setup. Import Avatar here for a global instance,
// or use it directly inside <ClientOnly> components.
})Page / component
<!-- pages/index.vue -->
<template>
<ClientOnly>
<AvatarDemo />
</ClientOnly>
</template><!-- components/AvatarDemo.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue"
import { Avatar } from "@mymosdk/avatar"
let avatar: Avatar
onMounted(() => {
avatar = new Avatar({ model: "https://...", position: "bottom-right", framing: "bust" })
avatar.on("click", () => avatar.wave())
})
onUnmounted(() => avatar?.destroy())
</script>The
<ClientOnly>wrapper is the only Nuxt-specific step — the rest is standard Vue 3.
Svelte
npm install @mymosdk/avatar threeComposable
<script lang="ts">
import { useAvatar } from "@mymosdk/avatar/svelte"
const avatar = useAvatar({
model: "https://...",
position: "bottom-right",
framing: "bust",
idle: true,
blink: true,
})
// avatar is a Svelte Writable<Avatar | null>
</script>
<button on:click={() => $avatar?.wave()}>Wave</button>useAvatar creates the avatar in onMount and calls destroy() in onDestroy via the returned cleanup function. Access the instance with the $ store shorthand.
Direct usage (without the composable)
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { Avatar } from "@mymosdk/avatar"
let avatar: Avatar
onMount(() => {
avatar = new Avatar({ model: "https://...", position: "bottom-right", framing: "bust" })
})
onDestroy(() => avatar?.destroy())
</script>
<button on:click={() => avatar?.smile()}>Smile</button>Electron
npm install @mymosdk/avatar threeThe avatar runs entirely in the renderer process — no main-process changes are needed beyond the standard security settings.
Main process (main.js)
const { app, BrowserWindow } = require("electron")
const path = require("path")
function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
webPreferences: {
contextIsolation: true, // required
nodeIntegration: false, // required
preload: path.join(__dirname, "preload.js"),
},
})
win.loadFile("dist/renderer/index.html")
}
app.whenReady().then(createWindow)
app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit() })Renderer process (renderer/main.ts)
import { Avatar } from "@mymosdk/avatar"
const avatar = new Avatar({
model: "/Maya.vrm", // served from the app's public/ folder
position: "bottom-right",
framing: "bust",
theme: "dark",
idle: true,
blink: true,
})
avatar.on("click", () => avatar.wave())Electron's renderer is a full browser context — all Web Audio and WebGL APIs are available. No special guards needed.
Angular
npm install @mymosdk/avatar threeImport from @mymosdk/avatar/angular. The package ships a standalone directive (AvatarDirective) and an AvatarModule for NgModule-based projects.
Standalone component
// app.component.ts
import { Component, ViewChild } from "@angular/core"
import { AvatarDirective } from "@mymosdk/avatar/angular"
@Component({
standalone: true,
imports: [AvatarDirective],
template: `
<div
mymoAvatar
model="https://..."
position="bottom-right"
framing="bust"
theme="dark"
[idle]="true"
[blink]="true"
[draggable]="true"
(avatarLoaded)="onLoaded()"
(avatarClicked)="onClicked()"
></div>
<button (click)="wave()">Wave</button>
`,
})
export class AppComponent {
@ViewChild(AvatarDirective) avatarDir!: AvatarDirective
onLoaded() { console.log("avatar ready") }
onClicked() { this.avatarDir.avatar?.wave() }
wave() { this.avatarDir.avatar?.wave() }
}NgModule-based project
// app.module.ts
import { NgModule } from "@angular/core"
import { AvatarModule } from "@mymosdk/avatar/angular"
@NgModule({ imports: [AvatarModule], ... })
export class AppModule {}Available inputs
| Input | Type |
|---|---|
| model | string |
| position | AvatarPosition |
| size | number |
| theme | AvatarTheme |
| framing | AvatarFraming |
| draggable | boolean |
| idle | boolean |
| blink | boolean |
| lipSync | boolean |
| followMouse | boolean |
| zIndex | number |
Available outputs
| Output | Payload |
|---|---|
| avatarLoaded | void |
| avatarClicked | void |
| avatarError | { message: string } |
ngOnChanges handles model, position, size, theme, and framing reactively — update the input and the avatar updates automatically. Access the full imperative API via @ViewChild(AvatarDirective).avatar.
CDN (UMD)
No build step required:
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
<script src="https://unpkg.com/@mymosdk/avatar/dist/mymo-avatar.umd.js"></script>
<script>
const { Avatar } = MymoAvatar
const avatar = new Avatar({ model: "https://example.com/my-avatar.vrm", theme: "dark", framing: "bust" })
</script>Model formats
VRM — recommended
VRM is the first-class format. All features work out of the box on any valid VRM file because the spec enforces standardized bone names and blendshapes. Gestures are procedurally animated — no embedded animation clips required.
await avatar.load("https://example.com/my-avatar.vrm")Good sources: VRoid Studio, VRoid Hub, Booth.
GLB — best-effort
GLB files are supported but feature availability depends on what the model includes. A console.warn is emitted at load time. For each feature to work, the model must ship the corresponding data:
| Feature | Requirement |
|---|---|
| Expressions | Morph targets named Smile, Sad, Happy, Angry, etc. |
| Lip sync | Morph targets: mouthOpen, jawOpen, or viseme_* |
| Blink | Morph targets: Blink or eyesClosed |
| Gestures | Animation clips: Wave, Nod, Dance, etc. |
| Head look | Bone named Head, head, or mixamorigHead |
Missing features degrade gracefully and are skipped silently after the initial load warning.
Built-in models
Coming in v1.1. Named models (
maya,robot,fox) will be published as separate npm packages (@mymosdk/model-maya, etc.) and resolved from CDN automatically. Use a direct URL in the meantime:
// Use any GLB or VRM by URL today
await avatar.load("https://example.com/my-avatar.vrm")
await avatar.load("https://example.com/my-avatar.glb")
await avatar.load("/models/local-avatar.vrm") // local fileGood free VRM sources: VRoid Hub, Booth.
TypeScript
All types are exported from @mymosdk/avatar:
import type {
AvatarOptions,
AvatarPosition, // "bottom-right" | "bottom-left" | "top-right" | "top-left"
AvatarTheme, // "light" | "dark" | "transparent"
AvatarFraming, // "full" | "half" | "bust" | "face"
AvatarEvent, // "click" | "loaded" | "modelLoaded" | "animationStart" | "animationEnd" | "speechStart" | "speechEnd"
AvatarState, // "loading" | "success" | "error" | "warning" | "typing" | "listening" | "processing" | "complete"
AvatarPlugin,
TTSPlugin,
AvatarApi,
Expression, // "smile" | "sad" | "happy" | "angry" | "surprised" | "thinking" | "confused" | "sleep" | "idle"
Gesture, // "wave" | "nod" | "yes" | "no" | "shakeHead" | "clap" | "jump" | "dance" | "thumbsUp"
Viseme,
FramingSliceConfig,
FramingModeConfig,
ThemeConfig,
} from "@mymosdk/avatar"Accessibility
The avatar widget is designed to be usable by everyone.
Screen readers — the container element carries role="img" and an aria-label. Customize the label to describe your avatar in context:
const avatar = new Avatar({
model: "https://...",
ariaLabel: "Mymo — your virtual assistant",
})Keyboard navigation — the widget is focusable (tabIndex=0). Users can Tab to it and press Enter or Space to trigger a click, which fires the "click" event just like a mouse click:
avatar.on("click", () => avatar.wave())
// ↑ triggered by mouse click AND keyboard Enter/SpaceA focus ring appears automatically via :focus-visible (no ring on mouse click).
Reduced motion — the SDK detects prefers-reduced-motion: reduce at startup and listens for OS-level changes at runtime. When active, the state-ring pulse animation is disabled. You can also read the value to conditionally reduce other animations in your app:
// After setup, check the current preference
const avatar = new Avatar({ model: "https://..." })
// The renderer exposes the value — useful if you drive animations yourself
// avatar.renderer.prefersReducedMotion ← internal; use OS media query directly
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches
if (!reduced) avatar.dance()Browser support
Chrome 90+, Firefox 88+, Safari 15+, Edge 90+
License
MIT
