Waveform
Aira's voice, drawn — a bar chart of amplitude that can sit still or move.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/waveform.jsonUsage
import { Waveform } from '@/components/ui/waveform'<Waveform state="listening" label="Aira is listening" />Figma ships this as two frames — a regular motif and an irregular one — and a frame is not a component. Measured, it is 28 bars at a uniform 5.92px pitch whose heights are exact eighths of the container. So the thing to build is a bar chart of amplitude, and the two frames become two things you can do with it: sit still, or move.
States
idle — the drawn motif, unmovinglistening — taking audio inspeaking — slower, full travelidle is the drawn pattern, unmoving — the right thing before a session starts, or when the
microphone is off but the affordance should stay put rather than disappear.
listening and speaking are the same motion at different speeds. The distinction is worth
keeping: a person needs to know whether they are being heard or being talked to, and those are
opposite instructions about whether to speak.
The bars animate with scaleY rather than height, so 28 of them do not force a relayout on
every frame.
From real data
values takes amplitudes from 0 to 1, and its length decides the bar count. The shape is the one
an AnalyserNode gives you:
const data = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteFrequencyData(data)
setValues([...data].map(v => v / 255))Values are clamped rather than trusted. A live analyser overshoots, and an unclamped bar taller than its box is exactly how two of the bars in the Figma frame ended up clipped.
Size
sm, 14 barsmd, 28 bars — the Figma sizemd is the Figma size, 23px tall with 28 bars. sm is for a toolbar or an inline chip. bars
sets the count independently — fewer, wider-spaced bars read better in a narrow slot than 28
crushed together.
In place
A dictation bar on a report. Two things in that example are not decoration:
The waveform carries a label, because here it is the only thing on screen saying the
microphone is live. And there is an elapsed time beside it, because a waveform says "sound is
happening" and nothing whatever about how long for — someone who walked away needs the number,
not the animation.
Accessibility
By default the waveform is aria-hidden. It is a picture of a sound, and a screen reader
announcing it adds nothing when the state is already written next to it.
Pass label and it becomes role="img" with that name instead. Do that whenever the waveform is
the only indication of a live microphone — which is a thing a person must be able to find out
without seeing it.
Never let the animation be the sole signal that recording is happening. Under
prefers-reduced-motion the bars hold still, and someone who set that preference would have no
way to tell a live microphone from a decorative one. Keep a word, an icon or a timer alongside.
Colour
The peach is --color-aira (#ff7869), and it is deliberately not a design system token.
Aira has its own identity colour; qtrack defines it in the app layer rather than the SDS for the
same reason. It is named in theme-qure.css so every Aira surface agrees on it instead of each
one carrying the hex.
Recolour by setting color — the bars are currentColor:
<Waveform className="text-primary" state="listening" />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
state | "idle" | "listening" | "speaking" | "idle" | Still, or moving at one of two speeds. |
size | "sm" | "md" | "md" | 16px and 23px tall. |
bars | number | 28 | How many, when values is not given. |
values | number[] | — | Amplitudes 0–1. Length wins over bars. |
label | string | — | Makes it a role="img" with this name. Omitted, it is aria-hidden. |
IDLE_PATTERN exports the seven fractions the drawing uses, if you want to seed an animation from
the same shape.