ComponentsInputs and forms

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.

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

Press the mic to dictate
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.

0:00Listening · Enter to send, Esc to cancel
0:00Listening · release ⌘⇧Space to send, Esc to cancel
0:00Listening · press ⌘⇧Space to send, Esc to cancel
Finishing…
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>
  );
}

On this page