Voice Roster
Who is on a voice channel and who is on air right now. The stack of members a game, dispatch or call overlay lights up as people talk, the companion of Push to Talk: a green spine, a wash and a running level meter on whoever has the channel.
Installation#
Usage#
import {
VoiceRoster,
VoiceRosterContent,
VoiceRosterDetail,
VoiceRosterHeader,
VoiceRosterItem,
VoiceRosterLevel,
VoiceRosterList,
VoiceRosterName,
} from "@/components/ui/voice-roster"
export function RadioOverlay({ channel, members }) {
if (members.length === 0) return null
return (
<VoiceRoster side="right" className="pointer-events-none fixed top-6 right-6">
<VoiceRosterHeader frequency={channel.frequency}>{channel.name}</VoiceRosterHeader>
<VoiceRosterList max={6}>
{members.map((m) => (
<VoiceRosterItem key={m.id} talking={m.talking} leaving={m.leaving}>
<VoiceRosterContent>
<VoiceRosterName>{m.name}</VoiceRosterName>
<VoiceRosterDetail>{m.rank}</VoiceRosterDetail>
</VoiceRosterContent>
<VoiceRosterLevel />
</VoiceRosterItem>
))}
</VoiceRosterList>
</VoiceRoster>
)
}Anatomy#
import {
VoiceRoster,
VoiceRosterAvatar,
VoiceRosterContent,
VoiceRosterDetail,
VoiceRosterHeader,
VoiceRosterItem,
VoiceRosterLevel,
VoiceRosterList,
VoiceRosterMark,
VoiceRosterMore,
VoiceRosterName,
VoiceRosterOrigin,
VoiceRosterSupervisor,
} from "@/components/ui/voice-roster"
<VoiceRoster>
<VoiceRosterHeader />
<VoiceRosterList>
<VoiceRosterItem>
<VoiceRosterAvatar />
<VoiceRosterMark />
<VoiceRosterSupervisor />
<VoiceRosterContent>
<VoiceRosterName />
<VoiceRosterDetail />
<VoiceRosterOrigin />
</VoiceRosterContent>
<VoiceRosterLevel />
</VoiceRosterItem>
</VoiceRosterList>
<VoiceRosterMore />
</VoiceRoster>Talking#
An item has two states. Idle is a member who is on the channel and quiet: a neutral capsule, a short grey spine and four low, still bars, its slot reserved so nothing moves when it lights. talking lights all of it at once: the spine fills the edge in green, a wash bleeds in from it, the ring, the avatar and the secondary line turn green, and the bars run a voice waveform. Green, not Push to Talk's red: red says you are on air, and it already means a busy channel here.
Sides#
side is the edge the roster hangs from, left (the default) or right. Everything aligns to it, items enter from it and leave toward it, so the edge facing the screen is always straight. Each capsule is as wide as its content above a shared minimum, so a voice that comes and goes never resizes the others; for one even column, give the list items-stretch. Where the roster is pinned is yours: a fixed corner, an absolute one.
In a panel#
The default variant="overlay" floats over a game: translucent capsules, each as wide as its content up to a cap, with a lift. To set the roster inside a page instead (a column of a dispatch screen, a card, a sidebar), use variant="panel": no width cap and no shadow, an opaque neutral fill on the panel's ground, and every row the column's full width. The on-air state, the meter, the busy header and the overflow line behave the same; side only sets which edge rows enter from and leave toward.
Density#
comfortable stacks the name over the secondary line and sizes an avatar at 32px; compact sets them on one line, for a crowded channel where every row of height counts, and drops the avatar to 24px. Leave the avatar out entirely for the tightest stack. With no density prop the roster follows the nearest DensityProvider, compact by default.
Channel header#
VoiceRosterHeader names the channel the members are on: the name as its children, in the meta ink, and the frequency in the ink, because the figure is what people look for. It also names the list for a screen reader. It is optional; without it the stack starts at the first member.
busy marks the whole channel busy: a red wash from the edge, the name in red and a prohibited mark named by labels.busy. It sits on the header because the flag belongs to the channel, not to anyone in it, and it holds still: a standing state, not an alert.
Groups and supervisors#
On a channel shared by several teams, VoiceRosterMark tells them apart: a small square in the group's own color (any CSS colour, the one your app already uses for that team) named by label for screen readers and the tooltip. VoiceRosterSupervisor puts a grey star before the name of whoever leads the channel. Both stay grey and small on air: they say who someone is, and colour here means talking.
Voices from another channel#
Someone heard from outside the channel (another unit, a second radio) takes VoiceRosterOrigin in the secondary line's place: the channel they come in on and its frequency. With no frequency the name alone is the answer and takes the ink. Such a voice is usually only shown while it talks: mount it talking, set leaving when it stops, and unmount it once the exit is done. The exit takes --duration-base and is a transition, so a voice that comes back mid-exit slides straight back in.
Overflow#
A full channel would cover the screen. max on the list shows that many members and sums up the rest in one line, labels.more. The roster keeps the order it is given, so who makes the cut is yours: put the latest speaker first and a voice from below the cut still gets a row, and keeps it until others have spoken after them.
Labels#
Every word the roster says on its own is in labels: talking (read after a member on air), busy, supervisor and more, where {count} becomes the number left out. Pass only the ones you change.
Accessibility#
The members are a list, named by the header: a screen reader hears "Adam-12 202.0, list, 3 items". Who is talking is in the text (labels.talking after their name), not only in the colour. The roster is deliberately not a live region: on a channel where people talk over each other it would be read out without pause. A leaving item is hidden from assistive tech straight away. Marks and stars are images named by their label; the level meter is decoration. Under reduced motion nothing slides and the bars hold a still, uneven level instead of the wave.
API reference#
VoiceRoster
The stack, a <div>. Display only: nothing in it takes focus or a click. Forwards every div prop; className is merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "left" | "right" | "left" | The edge the roster hangs from: everything aligns to it, enters from it and leaves toward it. |
| variant | "overlay" | "panel" | "overlay" | overlay floats over a game or a page. panel is for a page column: no width cap, no shadow, opaque full-width rows. |
| density | "comfortable" | "compact" | - | comfortable stacks name and secondary line; compact sets them on one line. Defaults to the nearest DensityProvider, else compact. |
| labels | Partial<VoiceRosterLabels> | - | Override the words the roster says itself: talking ("Talking"), busy ("Busy"), supervisor ("Supervisor"), more ("+{count} more on the channel"). |
| Data attribute | Description |
|---|---|
| data-slot="voice-roster" | On the root. |
| data-side | left or right. |
| data-variant | overlay or panel. |
| data-density | comfortable or compact. |
VoiceRosterHeader
The channel, a <div>: its name as children. Its id names the list. Forwards every div prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| frequency | React.ReactNode | - | The channel's frequency (or number), shown after its name. |
| busy | boolean | false | The whole channel is busy: a red wash and the busy mark, named by labels.busy. |
| Data attribute | Description |
|---|---|
| data-busy | Present while busy. |
VoiceRosterList
The members, a <ul> of items named by the header. Not a live region. Forwards every ul prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| max | number | - | Shows at most this many members and renders VoiceRosterMore after them with the rest. Omit it, or pass 0, for no cap. The order is kept, so put the latest speaker first. |
VoiceRosterItem
A member, an <li>. Any subset of the parts below goes inside, in this order. Forwards every li prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| talking | boolean | false | On air right now: lights the item and adds labels.talking for screen readers. |
| leaving | boolean | false | Plays the exit (fades and slides toward the edge) and hides it from assistive tech. Unmount it once the exit is done (--duration-base). |
| Data attribute | Description |
|---|---|
| data-state | talking or idle. |
| data-leaving | Present while leaving. |
| data-entered | Set once the entrance has played, so it never replays. |
VoiceRosterAvatar
A Koala Avatar (AvatarImage and AvatarFallback inside). Rings green on air. Takes every Avatar prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "xl" | - | Defaults to sm comfortable and xs compact. |
VoiceRosterMark
A small square in the colour of the member's group or department. Forwards every span prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| color* | string | - | Any CSS colour (#3b82f6, var(--info)). |
| label* | string | - | The group's name: its accessible name and tooltip. |
| CSS variable | Description |
|---|---|
| --voice-roster-mark | Set from color. |
VoiceRosterSupervisor
The star before the name: this member leads the channel. Forwards every span prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | labels.supervisor | Its accessible name and tooltip. |
| icon | React.ReactNode | - | Replaces the star. |
VoiceRosterContent
The text, a <div>: stacks the name over the detail (comfortable) or sets them on one line (compact).
VoiceRosterName
The member's name, a <span>. Ends in an ellipsis.
VoiceRosterDetail
The secondary line, a <span>: a rank, a role, a callsign. Ends in an ellipsis and turns green on air.
VoiceRosterOrigin
Where a voice from outside the channel comes from, in the detail's place: the other channel as children.
| Prop | Type | Default | Description |
|---|---|---|---|
| frequency | React.ReactNode | - | The frequency the voice comes in on. Without it, the name alone is the answer and takes the ink. |
| Data attribute | Description |
|---|---|
| data-solo | Present when there is no frequency. |
VoiceRosterLevel
The four-bar meter, hidden from assistive tech. Takes no children.
| CSS variable | Description |
|---|---|
| --animate-voice-level-1…4 | The waveform each bar runs on air. Still at rest and under reduced motion. |
VoiceRosterMore
The overflow line, a <p>. Rendered by VoiceRosterList when max cuts it; render it yourself when you cut the list first.
| Prop | Type | Default | Description |
|---|---|---|---|
| count* | number | - | How many members are left out; fills labels.more. |
| children | React.ReactNode | - | Replaces the words. |
voiceRosterVariants
The tv recipe behind every part, for styling a part of your own to match.