Voice
tier 2 — classes
A spoken conversation with an agent. The app owns the microphone, the audio and the recogniser; the
library paints what they report. One number connects them: the app writes
--level, from 0 to 1, and the avatar, the orb,
Mochi, the bars and the mic button all move with it.
AnalyserNode on each animation frame, smooth it, and write it
with element.style.setProperty('--level', value) on the session — once, on the
element above everything that follows it. The examples here hold a fixed level so the page stays
still. Under prefers-reduced-motion the bars and the button stand at the level
instead of easing to it, and the orb holds a still frame.
A voice session
.voice-session centres the call: .voice-stage holds the one thing to look
at — an .agent-avatar--hero, an
.orb--hero or Mochi — then .voice-who, the
.voice-caption, the bars and .voice-controls. The caption is
aria-live="polite", so what the agent says is also read out to a reader who cannot
hear it.
.voice-caption-interim is the words the recogniser may still change: muted until they
settle, so a correction does not look like the agent changing its mind.
Done. The vacuum now starts at 03:30, after the sync window
<div class="card" style="max-width: 460px">
<div class="voice-session" style="--level: 0.45">
<div class="voice-stage">
<span class="agent-avatar agent-avatar--hero" data-state="speaking" role="img" aria-label="Ops agent, speaking"></span>
</div>
<div class="sedna-col sedna-center sedna-gap-2">
<span class="voice-who">Ops agent</span>
<p class="voice-caption" aria-live="polite">Done. The vacuum now starts at 03:30, <span class="voice-caption-interim">after the sync window</span></p>
</div>
<span class="voice-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
<div class="voice-controls">
<button class="btn btn-mic" type="button" aria-label="Mute"><i class="ri-mic-line"></i></button>
<button class="btn" type="button" aria-label="Captions" aria-pressed="true"><i class="ri-closed-captioning-line"></i></button>
<button class="btn btn--danger" type="button" aria-label="End the call"><i class="ri-phone-line" style="rotate: 135deg"></i></button>
</div>
</div>
</div>
Listening
While the reader speaks, the stage shows the agent listening and the bars are
.voice-bars--mine, in the brand colour, so it is clear whose voice is moving them.
Move the vacuum to half past three
<div class="card" style="max-width: 460px">
<div class="voice-session" style="--level: 0.6">
<div class="voice-stage">
<span class="orb orb--hero" data-orb="listening" role="img" aria-label="Listening"></span>
</div>
<div class="sedna-col sedna-center sedna-gap-2">
<span class="voice-who">Listening</span>
<p class="voice-caption" aria-live="polite">Move the vacuum to <span class="voice-caption-interim">half past three</span></p>
</div>
<span class="voice-bars voice-bars--mine" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
<div class="voice-controls">
<button class="btn btn-mic" type="button" data-state="listening" aria-label="Stop listening"><i class="ri-mic-fill"></i></button>
<button class="btn btn--danger" type="button" aria-label="End the call"><i class="ri-phone-line" style="rotate: 135deg"></i></button>
</div>
</div>
</div>
The mic button
.btn-mic on any .btn, round. data-state="listening" fills it
and rings it out with --level; "muted" paints it in danger. With no
microphone, disable it and say why nearby. Change the aria-label with the state —
it names what pressing does next.
idlelisteningmutedno microphone<div class="sedna-row-wrap sedna-gap-3">
<div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" aria-label="Talk"><i class="ri-mic-line"></i></button><code>idle</code></div>
<div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" data-state="listening" style="--level: 0.5" aria-label="Stop listening"><i class="ri-mic-fill"></i></button><code>listening</code></div>
<div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" data-state="muted" aria-label="Unmute"><i class="ri-mic-off-line"></i></button><code>muted</code></div>
<div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" disabled aria-label="Talk"><i class="ri-mic-line"></i></button><code>no microphone</code></div>
</div>
Live bars
.voice-bars is a live voice, as many <i> as fit; each bar has its
own share of the level, so the row reads as a voice rather than a meter. It is decoration:
aria-hidden. A recorded clip is .audio-wave, which
draws a stored waveform rather than a live level.
.voice-bars.voice-bars--mine<div class="sedna-row-wrap sedna-gap-3" style="--level: 0.5">
<div class="sedna-col sedna-center sedna-gap-1"><span class="voice-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><code>.voice-bars</code></div>
<div class="sedna-col sedna-center sedna-gap-1"><span class="voice-bars voice-bars--mine" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><code>.voice-bars--mine</code></div>
</div>
The transcript
A .chat--flat beside the call, with the time into the call in
.chat-time. The line still being recognised ends in
.voice-caption-interim.
<div class="chat chat--flat" role="log" aria-label="Call transcript" style="max-width: 460px">
<div class="chat-message">
<span class="avatar avatar--sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time">0:04</time></div>
<div class="chat-bubble">Move the vacuum to half past three.</div>
</div>
</div>
<div class="chat-message">
<span class="agent-avatar agent-avatar--sm" role="img" aria-label="Ops agent"></span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Ops agent</span><time class="chat-time">0:07</time></div>
<div class="chat-bubble">Done. It now starts at 03:30, <span class="voice-caption-interim">after the sync window</span></div>
</div>
</div>
</div>