Voice

Realtime voice session controls with connect, mute, and status indicator.

A control bar for realtime bidirectional voice sessions with an animated orb indicator. Works with any RealtimeVoiceAdapter (LiveKit, ElevenLabs, etc.).

idle

Getting Started

Add the component

npx shadcn@latest add https://r.assistant-ui.com/voice.json

This adds /components/assistant-ui/voice.tsx to your project, which you can adjust as needed.

Configure a voice adapter

Pass a RealtimeVoiceAdapter to your runtime. See the Realtime Voice guide for details.

const runtime = useChatRuntime({
  adapters: {
    voice: myVoiceAdapter,
  },
});

Use in your application

app/page.tsx
import { Thread } from "@/components/assistant-ui/thread";
import { VoiceControl } from "@/components/assistant-ui/voice";
import { AuiIf } from "@assistant-ui/react";

export default function Chat() {
  return (
    <div className="flex h-full flex-col">
      <AuiIf condition={(s) => s.thread.capabilities.voice}>
        <VoiceControl />
      </AuiIf>
      <div className="min-h-0 flex-1">
        <Thread />
      </div>
    </div>
  );
}

Anatomy

The VoiceControl component is built with the following hooks and conditionals:

import { AuiIf, useVoiceState, useVoiceControls } from "@assistant-ui/react";

<div className="aui-voice-control">
  <VoiceIndicator />

  <AuiIf condition={(s) => s.thread.voice == null}>
    <VoiceConnectButton />
  </AuiIf>

  <AuiIf condition={(s) => s.thread.voice?.status.type === "running"}>
    <VoiceMuteButton />
    <VoiceDisconnectButton />
  </AuiIf>
</div>

Examples

Conditionally show voice controls

Only render when a voice adapter is configured:

<AuiIf condition={(s) => s.thread.capabilities.voice}>
  <VoiceControl />
</AuiIf>

Voice toggle in composer

Add a compact voice toggle button inside the composer action area:

function ComposerVoiceToggle() {
  const voiceState = useVoiceState();
  const { connect, disconnect } = useVoiceControls();
  const isActive =
    voiceState?.status.type === "running" ||
    voiceState?.status.type === "starting";

  return (
    <AuiIf condition={(s) => s.thread.capabilities.voice}>
      <button
        type="button"
        onClick={() => (isActive ? disconnect() : connect())}
        aria-label={isActive ? "End voice" : "Start voice"}
      >
        {isActive ? <PhoneOffIcon /> : <PhoneIcon />}
      </button>
    </AuiIf>
  );
}

Custom indicator colors

Override the indicator styles by targeting the aui-voice-indicator class:

.aui-voice-indicator {
  /* Override active color */
  &.bg-green-500 {
    background: theme("colors.blue.500");
  }
}

States

The VoiceOrb responds to five voice session states with distinct animations:

idle
connecting
listening
speaking
muted

Variants

Four built-in color palettes. Size is controlled via className.

default
blue
violet
emerald

Sub-components

ComponentDescription
VoiceOrbAnimated orb visual with gradient, glow, and ripple effects. Accepts state and variant props.
VoiceControlControl bar with status dot, connect/disconnect, and mute/unmute buttons.
VoiceConnectButtonCalls connect(). Shown when no session is active.
VoiceMuteButtonToggles mute()/unmute(). Shown when session is running.
VoiceDisconnectButtonCalls disconnect(). Shown when session is active.

All sub-components are exported and can be used independently for custom layouts.

State Selectors

Use these with AuiIf or useAuiState to build custom voice UI:

SelectorTypeDescription
s.thread.capabilities.voicebooleanWhether a voice adapter is configured
s.thread.voiceVoiceSessionState | undefinedundefined when no session
s.thread.voice?.status.type"starting" | "running" | "ended"Session phase
s.thread.voice?.isMutedbooleanMicrophone muted state
s.thread.voice?.mode"listening" | "speaking"Who is currently active (user or agent)
useVoiceVolume()numberReal-time audio level (0–1), separate from main state to avoid 20Hz re-renders