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.

TAC-2 401.5
  • A
    Ana RomeroYou
  • L
    Liam DixonOfficer
    Talking
  • E
    Evelyn MolinaSergeant
  • M
    Marcus ReyesDetective

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add voice-roster

Usage#

usage.tsxTSX
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.

  • L
    Liam DixonOfficer
  • L
    Liam DixonOfficer
    Talking

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.

Adam-12 202.0
  • Tariq OkaforSergeant
  • Elena VásquezOfficer
    Talking
  • Marcus DixonTrooper
Lincoln-4 202.0
  • Tariq OkaforSergeant
    Talking
  • Elena VásquezOfficer
  • Marcus DixonTrooper

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.

Channel
Who is on TAC-2, and who has it now.
TAC-2 401.5
  • E
    Evelyn MolinaSergeant
  • L
    Liam DixonOfficer
    Talking
  • T
    Tariq OkaforTrooper
  • M
    Marcus ReyesDetective

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.

  • T
    Tariq OkaforSergeant
    Talking
  • E
    Elena VásquezOfficer
  • M
    Marcus DixonTrooper
  • Tariq OkaforSergeant
    Talking
  • Elena VásquezOfficer
  • Marcus DixonTrooper

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.

Adam-12 202.0
TAC-2 401.5 MHz
Dispatch

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.

Adam-12 202.0
  • Tariq OkaforSergeant
  • Elena VásquezOfficer
    Talking

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.

TAC-2 401.0
  • Evelyn MolinaSergeant
  • Ovidiu DragomirCaptain
    Talking
  • James VillanuevaCommander
  • Corinne VanceDeputy
  • Ana RomeroSpecial Agent

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.

Adam-12 202.0
  • Tariq OkaforSergeant
  • Elena VásquezOfficer
  • Rosa DelgadoAdam-14
    Talking

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.

SWAT-1 455.0
  • Carter Harrison
  • Derek McGarrett
  • Eric Kristensen
  • David Doakes
  • Lauren Bennet
  • Vega Chambers

+14 more on the channel

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.

Adam-12 202.0
  • Tariq OkaforSargento
  • Elena VásquezAgente
    Hablando

+2 más en frecuencia

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.

PropTypeDefaultDescription
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.
labelsPartial<VoiceRosterLabels>-Override the words the roster says itself: talking ("Talking"), busy ("Busy"), supervisor ("Supervisor"), more ("+{count} more on the channel").
Data attributeDescription
data-slot="voice-roster"On the root.
data-sideleft or right.
data-variantoverlay or panel.
data-densitycomfortable or compact.

VoiceRosterHeader

The channel, a <div>: its name as children. Its id names the list. Forwards every div prop.

PropTypeDefaultDescription
frequencyReact.ReactNode-The channel's frequency (or number), shown after its name.
busybooleanfalseThe whole channel is busy: a red wash and the busy mark, named by labels.busy.
Data attributeDescription
data-busyPresent while busy.

VoiceRosterList

The members, a <ul> of items named by the header. Not a live region. Forwards every ul prop.

PropTypeDefaultDescription
maxnumber-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.

PropTypeDefaultDescription
talkingbooleanfalseOn air right now: lights the item and adds labels.talking for screen readers.
leavingbooleanfalsePlays the exit (fades and slides toward the edge) and hides it from assistive tech. Unmount it once the exit is done (--duration-base).
Data attributeDescription
data-statetalking or idle.
data-leavingPresent while leaving.
data-enteredSet 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.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
color*string-Any CSS colour (#3b82f6, var(--info)).
label*string-The group's name: its accessible name and tooltip.
CSS variableDescription
--voice-roster-markSet from color.

VoiceRosterSupervisor

The star before the name: this member leads the channel. Forwards every span prop.

PropTypeDefaultDescription
labelstringlabels.supervisorIts accessible name and tooltip.
iconReact.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.

PropTypeDefaultDescription
frequencyReact.ReactNode-The frequency the voice comes in on. Without it, the name alone is the answer and takes the ink.
Data attributeDescription
data-soloPresent when there is no frequency.

VoiceRosterLevel

The four-bar meter, hidden from assistive tech. Takes no children.

CSS variableDescription
--animate-voice-level-1…4The 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.

PropTypeDefaultDescription
count*number-How many members are left out; fills labels.more.
childrenReact.ReactNode-Replaces the words.

voiceRosterVariants

The tv recipe behind every part, for styling a part of your own to match.

FAQ#

Both, side by side. Push to Talk is your transmit key; the roster shows who else is on the channel and who has it now, your own row included.