Dictation
ProA microphone button for the composer's tools slot: press it, speak, press it again. Words land in the box as they're recognised and stay editable; nothing sends until the person sends it. Its state is shown beside it in words (starting, listening, transcribing) and announced to screen readers, and a problem (a blocked microphone, nothing heard) says what happened.
Opt-in, and private by default: without transcribe it uses the browser's speech recognition only where that runs on the device, unless you set allowBrowserService. Where dictation can't run, the button is hidden, or says why with whenUnsupported="explain".
<Composer {...composer} tools={<DictationButton value={composer.value} onChange={composer.onChange} />} />
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/dictation
Brings in: locale, locked, tooltip, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { Composer } from "@/components/composer"
import { DictationButton } from "@/components/dictation"
export default function Example() {
const [value, setValue] = React.useState("")
return (
<Composer
className="max-w-2xl"
value={value}
onChange={setValue}
onSubmit={() => setValue("")}
tools={
// On-device speech recognition where the browser has it; where it doesn't, the button says why. To use your own
// speech-to-text instead, pass transcribe={async (audio, { signal }) =>
// (await fetch("/api/transcribe", { method: "POST", body: audio, signal })).text()}
<DictationButton value={value} onChange={setValue} whenUnsupported="explain" />
}
/>
)
}<DictationButton>
| Prop | Type | Default | Description |
|---|---|---|---|
| valuerequired | string | — | The text the words go into, usually the composer's `value`. |
| onChangerequired | (value: string) => void | — | Called as words arrive, usually the composer's `onChange`. |
| lang | string | undefined | — | A BCP 47 language such as "en-GB". Left out, LocaleProvider's locale, else the page's `lang`, else the browser's. |
| transcribe | Transcribe | undefined | — | Your own speech-to-text. With it, the browser's speech recognition isn't used; the recording goes only to this. |
| allowBrowserService | boolean | undefined | — | Let the browser's own speech service hear the audio. Some browsers (Chrome among them) send it to the browser maker's servers. Off by default: without `transcribe`, dictation runs only where the browser recognises speech on the device, and is unavailable elsewhere. |
| maxSeconds | number | undefined | — | Stops listening after this many seconds of listening (not counting the permission prompt); 120 by default. |
| whenUnsupported | "hide" | "explain" | undefined | "hide" | Where dictation can't run: "hide" the button (the default), or "explain" with a locked button that says why. |
| disabled | boolean | undefined | false | Off while the composer can't take input, e.g. when it's locked or a reply is streaming. |
| className | string | undefined | — | Extra classes for the outer element. |
Functions
useDictation({ value, onChange, lang: langProp, transcribe, allowBrowserService = false, maxSeconds = 120 }: DictationOptions)Speech to text into a field you own. Words appear in the field as they're recognised (interim words are replaced as the recogniser settles on them) and stay plain, editable text: nothing is sent. Typing in the field while it listens keeps your edit and carries on after it. Emptying the field (sending the message) stops it and drops whatever it hadn't written yet, so nothing lands in the next message.
Audio goes only where you choose: to your transcribe, or to on-device recognition, or, with allowBrowserService, to whatever service the browser uses.
Strings
The fixed words, in English by default. Change any of them under dictation in <LocaleProvider strings={{ … }}>; the table is exported as dictationStrings.
| Key | English |
|---|---|
| dictate | Dictate |
| stop | Stop dictating |
| starting | Starting the microphone… |
| listening | Listening. Stop when you’re done, then check the text before you send. |
| transcribing | Turning your recording into text… |
| stopped | Stopped dictating. You can edit the text before you send it. |
| unsupported | Dictation isn’t available in this browser. |
| notOnDevice | This browser can’t turn speech into text on this device, so dictation is off. |
| denied | The microphone is blocked. Allow it in the browser’s site settings to dictate. |
| noMicrophone | No microphone was found. |
| noSpeech | Nothing was heard. Try again, closer to the microphone. |
| network | Speech recognition couldn’t reach its service. Try again. |
| language | Dictation doesn’t support this language here. |
| failed | Dictation stopped because of an error. Try again. |
| transcribeFailed | Your recording couldn’t be turned into text. Try again. |
Types
- Transcribe
- Your own speech-to-text: given the recording, return its text. Dictation then records with the browser's MediaRecorder and hands you the audio; where it goes from there is your choice.
(audio: Blob, options: { lang?: string | undefined; signal: AbortSignal }) => Promise<string>- DictationStatus
| "checking" | "unsupported" | "idle" /** Asking for the microphone, or readying on-device recognition. */ | "starting" | "listening" /** `transcribe` is working on the recording. */ | "transcribing"- DictationProblem
- Why dictation stopped or can't run; each is a key in `dictationStrings`.
"unsupported" | "notOnDevice" | "denied" | "noMicrophone" | "noSpeech" | "network" | "language" | "failed" | "transcribeFailed"- DictationOptions
{ /** The text the words go into, usually the composer's `value`. */ value: string /** Called as words arrive, usually the composer's `onChange`. */ onChange: (value: string) => void /** A BCP 47 language such as "en-GB". Left out, LocaleProvider's locale, else the page's `lang`, else the browser's. */ lang?: string | undefined /** Your own speech-to-text. With it, the browser's speech recognition- Dictation
{ status: DictationStatus /** Why it stopped, or why it can't run (with status "unsupported"). Cleared when it starts again. */ problem?: DictationProblem | undefined /** It stopped without a problem, so the words are in the field to check. Cleared when it starts again. */ done: boolean start: () => void stop: () => void toggle: () => void }