Voice Control Bar

The capstone of the voice suite — a floating call pill that holds the media controls and the hang-up key, and morphs open into a chat composer. It's where Media Toggle, Device Selector, and Composer come together into one control.

A split mic control, camera/screen keys, a chat toggle, and the hang-up key.

tsx
"use client"

import * as React from "react"
import { MessageSquare } from "lucide-react"

import {
  VoiceControlBar,
  VoiceControlBarActions,
  VoiceControlBarTrigger,
  VoiceControlBarEnd,
} from "@/registry/seam/ui/voice-control-bar"
import {
  DeviceSelector,
  DeviceSelectorContent,
  DeviceSelectorTrigger,
} from "@/registry/seam/ui/device-selector"
import { MediaToggle } from "@/registry/seam/ui/media-toggle"

const MICS = [
  { deviceId: "default", label: "MacBook Pro Microphone" },
  { deviceId: "airpods", label: "AirPods Pro" },
]

// The resting call pill: a split mic control, camera and screen keys, a chat
// toggle, and the hang-up key — all riding in one raised, rounded pill.
export default function VoiceControlBarDemo() {
  const [mic, setMic] = React.useState("default")

  return (
    <VoiceControlBar>
      <VoiceControlBarActions>
        <div className="bg-muted flex items-center gap-1 rounded-full p-1 shadow-well">
          <MediaToggle kind="mic" defaultPressed className="size-8" />
          <DeviceSelector devices={MICS} value={mic} onValueChange={setMic}>
            <DeviceSelectorTrigger />
            <DeviceSelectorContent />
          </DeviceSelector>
        </div>
        <MediaToggle kind="camera" />
        <MediaToggle kind="screen-share" />
        <VoiceControlBarTrigger>
          <MessageSquare className="size-4" />
        </VoiceControlBarTrigger>
        <VoiceControlBarEnd />
      </VoiceControlBarActions>
    </VoiceControlBar>
  )
}
bunx --bun @seamui/cli@latest add voice-control-bar

API

PropTypeDefaultDescription
expandedbooleanControlled expanded state of the panel.
defaultExpandedbooleanfalseUncontrolled initial state; the Trigger flips it.
onExpandedChange(expanded: boolean) => voidCalled when the panel opens or closes.

Props on the root <VoiceControlBar>; Trigger and End accept all Button props.

Notes

  • The bar owns no transport — drop in whatever call controls you like and wire VoiceControlBarEnd to your disconnect. It defaults to an “End call” label when it renders icon-only.
  • The morph is the one sanctioned duration case: the panel opens with the grid-rows 0fr → 1fr height trick while the container's radius and padding transition together — pill (rounded-full) to card (rounded-3xl squircle). Under reduced motion it snaps (motion-reduce:transition-none) rather than freezing mid-morph, while every control inside keeps its own press feedback.
  • The trigger drives aria-expanded (and aria-pressed) with a label that flips with the panel, and the panel is aria-hidden while collapsed so its contents stay out of the tab order.

Press feedback, reduced motion, and haptics follow the global policy — see Motion and Haptics.