npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xoboid/avatar

v0.1.0

Published

Animated SVG avatar component with multi-variant face system and audio reactivity

Downloads

20

Readme


Quick Start

"use client";

import { Avatar } from "@xoboid/avatar";
import "@xoboid/avatar/avatar.css";

export default function App() {
  return (
    <Avatar
      emotion={{ joy: 0.8, sadness: 0, surprise: 0.2, anger: 0, curiosity: 0.3 }}
      variant="minimal"
      color="#FF6B6B"
      size={280}
      interactive
    />
  );
}

That's it. A living face with blinking, breathing, idle glances, pointer tracking, and boop reactions — zero config.


Variants

Three visual archetypes, each with distinct geometry and animation feel:

| Variant | Style | Default Color | Eyes | Animation | |---|---|---|---|---| | minimal | Clean, soft | #FF6B6B Coral | Solid ellipses | Smooth sinusoidal | | tron | Digital, sharp | #06B6D4 Cyan | Rounded rects | Stepped (retro CRT) | | analogue | Hand-drawn | #FBBF24 Amber | Stroked ellipses + pencil filter | Organic wobble |

<Avatar variant="tron" color="#06B6D4" />

Emotion Model

A 5-axis float system. Each value is 0 to 1.

interface EmotionState {
  joy: number;        // smile width, eye roundness
  sadness: number;    // drooped eyes, inverted mouth
  surprise: number;   // wide eyes, open mouth
  anger: number;      // narrowed eyes, tight mouth
  curiosity: number;  // head tilt, asymmetric eyes
}
// Combine axes for complex expressions
<Avatar emotion={{ joy: 0.6, curiosity: 0.8, surprise: 0.2, sadness: 0, anger: 0 }} />

Audio Reactivity

Connect a microphone, audio element, or file — the face reacts in real-time.

import { Avatar, useAudioAnalysis } from "@xoboid/avatar";

function LiveAvatar() {
  const { levels, isAnalyzing, connectMicrophone } = useAudioAnalysis();

  return (
    <>
      <button onClick={connectMicrophone}>Enable Mic</button>
      <Avatar
        audioLevels={isAnalyzing ? levels : undefined}
        speaking={isAnalyzing}
      />
    </>
  );
}

Audio Sources

const { connectMicrophone } = useAudioAnalysis();  // mic input
const { connectElement } = useAudioAnalysis();      // <audio> or <video> element
const { connectFile } = useAudioAnalysis();          // File / Blob
const { connectExternalAnalyser } = useAudioAnalysis(); // existing AnalyserNode

Frequency Bands → Face Mapping

| Band | Range | Drives | |---|---|---| | bass | 60–250 Hz | Container breathing / scale pulse | | lowMid | 250–500 Hz | Mouth opening (primary) | | mid | 500–2000 Hz | Mouth width variation | | highMid | 2000–4000 Hz | Eye micro-pulse | | presence | 4000–8000 Hz | Random glances |


Voice Synthesis

Pair with a TTS endpoint for speaking avatars:

import { Avatar, useVoiceSynthesis, useAudioAnalysis } from "@xoboid/avatar";

function SpeakingAvatar() {
  const { speak, isSpeaking, analyserRef } = useVoiceSynthesis({
    ttsEndpoint: "/api/tts",
  });
  const { levels, connectExternalAnalyser } = useAudioAnalysis();

  const say = async (text: string) => {
    const analyser = await speak(text);
    if (analyser) connectExternalAnalyser(analyser);
  };

  return (
    <>
      <button onClick={() => say("Hello world")}>Speak</button>
      <Avatar speaking={isSpeaking} audioLevels={levels} />
    </>
  );
}

Imperative API

For programmatic control via ref:

import { useRef } from "react";
import { Avatar, type AvatarHandle } from "@xoboid/avatar";

function App() {
  const ref = useRef<AvatarHandle>(null);

  return (
    <>
      <Avatar ref={ref} />
      <button onClick={() => ref.current?.wink("left")}>Wink</button>
      <button onClick={() => ref.current?.surprise()}>Surprise</button>
      <button onClick={() => ref.current?.boop()}>Boop</button>
      <button onClick={() => ref.current?.setEmotion({
        joy: 1, sadness: 0, surprise: 0, anger: 0, curiosity: 0
      })}>
        Happy
      </button>
    </>
  );
}

| Method | Effect | |---|---| | setEmotion(state) | Animate to an emotion state | | wink(eye) | "left" or "right" wink | | surprise() | Surprised expression with scale bounce | | boop() | Squish bounce (like poking the face) | | feedAudio(levels) | Inject a single AudioLevels frame manually |


Interaction Callbacks

<Avatar
  onEyeClick={(eye) => console.log(eye)}   // "left" | "right"
  onMouthClick={() => {}}                    // tap on the mouth
  onLongPress={() => {}}                     // 500ms hold → deep emotion mode
  onStateChange={(state) => {}}              // { emotion, isSpeaking, variant }
/>

Data-Driven Emotion

Feed arbitrary data into the avatar via the adapter pattern:

import type { InteractionFeed, AdapterFn } from "@xoboid/avatar";

// Adapter: convert chat sentiment to emotion
const chatAdapter: AdapterFn<{ sentiment: number }> = (data) => [{
  emotion: {
    joy: Math.max(0, data.sentiment),
    sadness: Math.max(0, -data.sentiment),
  },
  timestamp: Date.now(),
}];

// Feed into Avatar
<Avatar interactions={chatAdapter({ sentiment: 0.7 })} />

Theming

CSS Variables

import "@xoboid/avatar/avatar.css";

Override in your stylesheet:

:root {
  --face-color: #ff6b6b;
  --face-glow: rgba(255, 107, 107, 0.6);
  --face-bg-tint: rgba(255, 107, 107, 0.04);
  --face-transition-fast: 0.04s;
  --face-transition-base: 0.3s;
  --face-transition-slow: 0.8s;
}

Runtime

import { applyAgentTheme } from "@dot/avatar";

// Apply to document root
applyAgentTheme("tron");

// Apply with custom color
applyAgentTheme("minimal", "#8B5CF6");

// Apply to a specific element
applyAgentTheme("analogue", "#FBBF24", myContainerRef.current);

Props Reference

| Prop | Type | Default | Description | |---|---|---|---| | emotion | EmotionState | { joy: 0.3, ... } | 5-axis emotion state | | variant | FaceVariant | "minimal" | Visual style | | color | string | variant default | Custom accent color | | size | number | 260 | Height in px | | speaking | boolean | false | Voice animation mode | | audioLevels | AudioLevels | — | Multi-band audio data | | voiceLevel | number | 0 | Simple 0–1 level (deprecated) | | interactive | boolean | true | Pointer interactions | | className | string | "" | Wrapper class | | onEyeClick | (eye) => void | — | Eye tap callback | | onMouthClick | () => void | — | Mouth tap callback | | onLongPress | () => void | — | Long-press callback | | onStateChange | (state) => void | — | State change callback | | interactions | InteractionFeed[] | — | Data-driven emotion feed | | ref | AvatarHandle | — | Imperative handle |


Built-in Behaviors

These run automatically — no configuration needed:

  • Breathing — subtle scale oscillation (variant-specific: smooth, stepped, or organic)
  • Blinking — random interval (2.5–6s), variant-styled close/open
  • Glancing — random micro eye movements (4–12s intervals)
  • Idle mouth — gentle wave on the mouth curve
  • Pointer repulsion — face subtly leans away when cursor gets close
  • Cursor tracking — eyes and mouth follow the pointer
  • Haptic feedback — vibration on wink, boop, and surprise (mobile)
  • Pencil boil — animated turbulence filter on analogue variant

Requirements

| Dependency | Version | Note | |---|---|---| | react | ≥ 18 | Peer dependency | | gsap | ≥ 3.12 | Peer dependency — GSAP license applies |


License

MIT