Composites
Voice controls
Dictate and speak controls with a live level meter and interim transcript, as typed intent.
Starts when it scrolls into view
NORD FROST · DARK · RADIUS 6/10 · BASE 16PX · SHADOW
- Type
VoiceControls- Source
voice.rs- Story
voice- Demo height
- 90 px
- Events
- VoiceEvent
Code
Cut from the gallery story this page runs, so it stays true.
let tokens = cx.theme().semantic_tokens();
let status: SharedString = self
.last_voice_event
.clone()
.unwrap_or_else(|| "Start dictation, then stop or cancel it; Speak reads the latest reply".into());
let mut controls = VoiceControls::new("voice", self.voice_state)
.speakable(true)
.on_event(cx.listener(|this, event: &VoiceEvent, _, cx| {
match event {
VoiceEvent::DictationStarted => {
this.voice_state = VoiceState::Listening { level: 0.55 };
this.voice_transcript =
Some("compare the three suppliers for next".into());
}
VoiceEvent::DictationStopped => {
this.voice_state = VoiceState::Transcribing;
}
VoiceEvent::DictationCancelled => {
this.voice_state = VoiceState::Idle;
this.voice_transcript = None;
}
VoiceEvent::SpeakRequested => this.voice_state = VoiceState::Speaking,
VoiceEvent::SpeakStopped => this.voice_state = VoiceState::Idle,
}
this.last_voice_event = Some(format!("{event:?}").into());
cx.notify();
}));
if let Some(transcript) = self.voice_transcript.clone() {
controls = controls.transcript(transcript);
}
v_flex()
.gap(tokens.spacing.md)
.child(controls)
.when(self.voice_state == VoiceState::Transcribing, |this| {
this.child(
Button::new("voice-finish")
.outline()
.small()
.text_label("Simulate: transcription finished")
.on_click(cx.listener(|this, _, _, cx| {
this.voice_state = VoiceState::Idle;
this.voice_transcript = None;
this.last_voice_event = Some(
"Transcribed: \"compare the three suppliers for next month\"".into(),
);
cx.notify();
})),
)
})
.child(
div()
.id("voice-story-status")
.role(Role::Status)
.aria_label(status.clone())
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(status),
)
Events and ownership
- Who owns the state
- VoiceControls renders state supplied by the application; it does not own durable work.
- What it emits
- Interactive intent is reported through the typed VoiceEvent contract and stable application IDs.
- What it means
- Dictate and speak controls with a live level meter and interim transcript, as typed intent.
- Overflow and motion
- Wide content retains context in a bounded surface; reduced motion preserves a useful state.
Event types
- VoiceEvent
What the demo does and does not show
The demo above is the same Rust as the native component, compiled to WebAssembly and drawn on a WebGPU canvas — not a mock-up, a recording, or a re-implementation. What it paints is what the component paints: layout, colour from the live theme, and motion.
The live browser specimen requires WebGPU; the native component remains the authoritative runtime.
A canvas cannot stand in for the platform, so treat everything that goes through the operating system as unproven here: keyboard handling and shortcuts, screen reader semantics, text input and IME, the clipboard, file drops, and the frame rate you would get on your own GPU. Judge those in a native build. If your browser has no WebGPU, the frame says so rather than showing an empty box.