> ## Documentation Index
> Fetch the complete documentation index at: https://orb-ui.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Vapi Voice UI for React

> Add a Vapi voice UI to React with orb-ui's adapter, animated orb visuals, and state-aware voice agent visuals.

Vapi handles the voice agent platform layer. orb-ui handles the visible React UI layer: an animated voice orb, audio-reactive feedback, and predictable states that make a Vapi assistant feel present in your app.

## Install

Install both orb-ui and the Vapi web SDK used by the adapter:

```bash theme={null}
npm install orb-ui @vapi-ai/web
```

## Use the adapter

```tsx theme={null}
import Vapi from '@vapi-ai/web'
import { Orb } from 'orb-ui'
import { createVapiAdapter } from 'orb-ui/adapters'

const vapi = new Vapi('your-public-key')
const adapter = createVapiAdapter(vapi, {
  assistantId: 'your-assistant-id',
})

export function VapiVoiceUI() {
  return <Orb adapter={adapter} theme="circle" aria-label="Start Vapi assistant" />
}
```

Create the Vapi client and adapter outside the render path, or memoize them when configuration is
dynamic. Recreating either object on every render can replace event subscriptions while a call is
active.

## Session ownership

The adapter can start and stop the configured assistant, and it also observes calls started
elsewhere through the same Vapi client. Activating an interactive orb calls `vapi.start()` with the
configured `assistantId`; activating it again calls `vapi.stop()`.

If the product already has labeled call controls, render a passive orb instead:

```tsx theme={null}
export function VapiCallSurface() {
  return (
    <section aria-label="Vapi conversation">
      <Orb adapter={adapter} theme="cloud" interactive={false} />
      <button onClick={() => void adapter.start?.()}>Start conversation</button>
      <button onClick={() => adapter.stop?.()}>End conversation</button>
    </section>
  )
}
```

## State mapping

The adapter maps Vapi conversation lifecycle events into orb-ui state changes. The UI can then communicate:

* connection startup
* listening state
* assistant speech
* errors

Vapi assistant audio levels are emitted as `outputVolume` while the assistant is speaking.
The Vapi web SDK does not currently expose local microphone input levels through the
adapter event stream, so this adapter does not emit `inputVolume` while listening. If your
app already meters the microphone separately, pass a controlled `signal` to `Orb`.

The core mapping is:

| Vapi event     | Orb state    |
| -------------- | ------------ |
| `vapi.start()` | `connecting` |
| `call-start`   | `listening`  |
| `speech-start` | `speaking`   |
| `speech-end`   | `listening`  |
| `call-end`     | `idle`       |
| `error`        | `error`      |

The adapter smooths Vapi's assistant volume events before emitting `outputVolume`. It also
debounces short speaking-to-listening changes so normal turn boundaries do not create distracting
visual flicker.

## Accessibility

When an adapter or `onStart`/`onStop` handler is provided, clickable `circle` and `bars` themes render as `<button type="button">` controls. Pass an `aria-label` when no visible text labels the orb.

## When to use controlled mode instead

Use controlled mode if your app wraps Vapi in its own session layer or combines Vapi with custom state. The Orb component does not need to own the provider SDK if your app already knows the voice signal.

```tsx theme={null}
<Orb signal={{ state: voiceState, outputVolume }} theme="circle" />
```

Controlled mode is the better fit when you combine Vapi with a separate microphone meter, tool
status, human handoff, or application-owned call state. Keep the Vapi client lifecycle in your
session layer and pass one normalized `OrbSignal` to the UI.

## Troubleshooting

**The orb never leaves idle.** Make sure the adapter and the code starting the call share the same
Vapi client instance. The adapter listens for lifecycle events on that object.

**Listening has little or no audio motion.** The current Vapi event stream used by this adapter
provides assistant output levels, not the local microphone level. This is expected. Supply a custom
`inputVolume` through controlled mode if microphone-reactive listening is important to the design.

**The interface starts multiple calls.** Use either the interactive orb or your own start control
as the primary action. When the product owns the controls, set `interactive={false}`.

**The orb stays in error after retrying.** Confirm that the underlying Vapi call can start again and
that the same subscribed client receives the new `call-start` event. Surface the provider error in
adjacent recovery copy rather than relying on the visual alone.

## Production checklist

* The Vapi public key and assistant configuration are appropriate for browser use.
* One stable client and adapter instance owns the current call.
* The missing local input meter is acceptable or supplied through controlled mode.
* Permission and provider errors have labeled retry actions.
* Interactive behavior is disabled when separate call controls are present.
* Start, stop, interruption, failure, and restart are tested in the real browser.

## Positioning

orb-ui is not a Vapi replacement. It is a frontend UI layer for teams already building with Vapi or comparing voice agent stacks.

## Related

* [Adapter overview](/docs/adapters/overview)
* [Voice orb UI example](/docs/examples/voice-orb-ui)
* [Custom integrations](/docs/adapters/custom)
* [Designing a React voice agent UI](/docs/guides/voice-agent-ui)
