The face of a voice agent: rings that swell with the voice, circle while thinking and ripple while speaking.
Listening
'use client';
import { useEffect, useState } from 'react';
import { DictationButton, MagnetTabs, VoiceOrb } from '@brand-studio/ui';
import type { VoiceState } from '@brand-studio/ui';
const states: VoiceState[] = ['idle', 'listening', 'thinking', 'speaking'];
export default function VoiceOrbDemo() {
const [state, setState] = useState<VoiceState>('listening');
const [mic, setMic] = useState(false);
const [level, setLevel] = useState(0);
// Without the microphone, a made-up voice drives the level.
useEffect(() => {
if (mic || state !== 'listening') { if (!mic) setLevel(0); return; }
let frame = 0;
const tick = (time: number) => { setLevel(Math.max(0, Math.sin(time / 180) * Math.sin(time / 530)) * 0.9); frame = requestAnimationFrame(tick); };
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [mic, state]);
return (
<div style={{ width: '100%', display: 'grid', gap: 28, justifyItems: 'center' }}>
<VoiceOrb state={state} level={level} size={140} />
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'center' }}>
<div style={{ maxWidth: '100%', overflowX: 'auto' }}>
<MagnetTabs items={states.map(value => ({ value, label: value[0].toUpperCase() + value.slice(1) }))} value={state} onValueChange={value => setState(value as VoiceState)} aria-label="Orb state" />
</div>
<DictationButton label="Use my microphone" listening={mic} onListeningChange={on => { setMic(on); if (on) setState('listening'); }} onLevel={setLevel} />
</div>
</div>
);
}Installation
pnpm add @brand-studio/uinpm install @brand-studio/uiyarn add @brand-studio/uibun add @brand-studio/uiInstall the peer dependencies
npm install react react-domImport the stylesheet once
import '@brand-studio/ui/styles.css';Wrap your app in a brand theme
import { BrandTheme, VoiceOrb } from '@brand-studio/ui'; import brand from './brand.json'; <BrandTheme palette={brand.tokens} mode="system"> {/* your app */} </BrandTheme>
Usage
import { VoiceOrb } from '@brand-studio/ui';<VoiceOrb state="listening" level={level} size={140} />level runs from 0 to 1. Feed it the microphone's loudness while listening (DictationButton's onLevel gives you this) or the agent's voice while speaking. Idle breathes slowly, thinking circles and speaking ripples outward.
Accessibility
- The orb is a status that says its state in words: Ready, Listening, Thinking or Speaking.
labelshows your own words instead. - The drawing is hidden from screen readers.
- With reduced motion the rings still follow the level but nothing loops.
API Reference
| Prop | Type | Default |
|---|---|---|
state? | "idle" | "listening" | "speaking" | "thinking" | 'idle' |
level? | number | 0 |
size? | number | 120 |
label? | string | – |
className? | string | '' |