Voice input
Talk to Flo instead of typing. The mic dictates into the box to check before sending; holding ⌘⇧Space talks and letting go sends. Words stream into the box as they are heard.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { MicButton } from '@/ui/voice-input';<MicButton label="Dictate" />Mic button
An icon button named "Dictate". Its tooltip teaches the hold shortcut. Press it to start and press it again to stop: the words stay in the box to check.
import { useState } from 'react';
import { MicButton } from '@/ui/voice-input';
export default function VoiceInputMicButton() {
const [listening, setListening] = useState(false);
return <MicButton listening={listening} onClick={() => setListening((on) => !on)} />;
}States
Rest, listening (pressed: grey fill, red icon, named "Stop dictating", no tooltip), disabled, and the 24px size="sm" mic in the Ask bar, named "Dictate to Flo".
import { MicButton } from '@/ui/voice-input';
export default function VoiceInputStates() {
return (
<div className="flex items-center gap-300">
<MicButton />
<MicButton listening />
<MicButton disabled />
<MicButton size="sm" label="Dictate to Flo" />
</div>
);
}Listening
While the mic is on, the composer's status line shows the listening mark: a pulsing red dot and the time counting up. It is never a countdown.
import { useState } from 'react';
import { ListeningStatus, MicButton } from '@/ui/voice-input';
export default function VoiceInputListening() {
const [startedAt, setStartedAt] = useState<number | null>(null);
return (
<div className="flex w-full max-w-130 items-center gap-100">
<div className="min-w-0 flex-1">
{startedAt ? <ListeningStatus startedAt={startedAt} /> : <span className="text-body-small leading-body-small text-text-subtle">Press the mic to dictate</span>}
</div>
<MicButton listening={startedAt !== null} onClick={() => setStartedAt((at) => (at ? null : Date.now()))} />
</div>
);
}Status line, by mode
The line says how to finish for the way the session started: Enter for the mic, releasing the chord for a hold, pressing it again after a tap. "Finishing…" shows while the last words settle. A sentence that may be cut off waits in the box with cutOffMessage.
Didn't catch the end. Press Enter to send.
import { useState } from 'react';
import { cutOffMessage, ListeningStatus } from '@/ui/voice-input';
export default function VoiceInputStatusLines() {
const [startedAt] = useState(() => Date.now() - 7000);
return (
<div className="flex flex-col gap-100">
<ListeningStatus startedAt={startedAt} />
<ListeningStatus startedAt={startedAt} mode="hold" />
<ListeningStatus startedAt={startedAt} mode="handsfree" />
<ListeningStatus startedAt={startedAt} finishing />
<p role="alert" className="text-body-small leading-body-small text-text-subtle">{cutOffMessage}</p>
</div>
);
}Privacy notice
The first session names the speech processor once, with Learn more and Got it. It stays until dismissed.
Your voice is transcribed by Soniox. Neither Soniox nor Ascern keeps the audio.
Learn more (opens in a new tab)import { useState } from 'react';
import { Button } from '@/ui/button';
import { VoiceNotice } from '@/ui/voice-input';
export default function VoiceInputNotice() {
const [shown, setShown] = useState(true);
return shown ? (
<VoiceNotice className="w-full max-w-130" learnMoreHref="https://soniox.com" onDismiss={() => setShown(false)} />
) : (
<Button size="sm" onClick={() => setShown(true)}>
Show the notice again
</Button>
);
}Start in Log activity
Log activity starts dictation with a labelled button and Space rather than the chord, since it has its own box.
import { MicrophoneIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { Kbd } from '@/ui/kbd';
export default function VoiceInputLogActivity() {
return (
<Button size="sm" aria-keyshortcuts="Space">
<MicrophoneIcon aria-hidden />
Start
<Kbd>Space</Kbd>
</Button>
);
}Timezone picker
The agency's time zone, picked once in Settings from every zone with its offset today. Today, due dates, reports and the calendar all read from it.
Choice card
One choice from a short list, where each option needs a line to explain it. A radio group drawn as full-width cards; the whole card is the target.