Xzero UI
Pro library

Dictation buttonPro

A chat composer with voice input, where the mic springs into a recording pill with a smooth live waveform, a timer, and cancel and insert buttons. Words stream into the field as you speak, muted until the recognizer settles them.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/dictation-button
Live · keyboard ready

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/dictation-button

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { DictationButton, createWebSpeechRecognizer } from "@/components/xzero/dictation-button/dictation-button";

const recognizer = createWebSpeechRecognizer({ lang: "en-US" });

export function Composer({ onSubmit }: { onSubmit: (text: string) => Promise<void> }) {
  return <DictationButton
    recognizer={recognizer}
    placeholder="Message Claude"
    onSend={onSubmit}
    onStatusChange={(status, error) => { if (status === "denied") console.info("Microphone blocked", error); }}
  />;
}

Examples

Only the control, in your own composer

import { useState } from "react";
import { DictationControl } from "@/components/xzero/dictation-button/dictation-button";

export function Field() {
  const [text, setText] = useState("");
  const [live, setLive] = useState({ final: "", interim: "" });
  return <div className="composer">
    <p>{text} {live.final} <span className="muted">{live.interim}</span></p>
    <DictationControl
      onTranscript={setLive}
      onCommit={final => { setText(t => (t ? t + " " : "") + final); setLive({ final: "", interim: "" }); }}
      onCancel={() => setLive({ final: "", interim: "" })}
    />
  </div>;
}

Your own speech service

import { DictationError, type DictationRecognizer } from "@/components/xzero/dictation-button/dictation-button";

export const deepgram: DictationRecognizer = async (handlers, signal) => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true }).catch(() => { throw new DictationError("not-allowed"); });
  const socket = new WebSocket("wss://your-proxy.example.com/listen");
  socket.onmessage = event => {
    const { final, interim, level } = JSON.parse(event.data);
    handlers.onTranscript({ final, interim });
    handlers.onLevel(level);
  };
  signal.addEventListener("abort", () => { socket.close(); stream.getTracks().forEach(t => t.stop()); });
  // ...pipe stream audio into the socket...
  return { stop: () => new Promise(resolve => { socket.onclose = () => resolve(lastFinal); socket.send("finalize"); }) };
};

API reference

DictationButton

A composer with voice input: the field, a model label, the dictation pill and send. Words stream into the field while the person speaks, interim guesses muted until they settle.

PropTypeDefaultDescription
valuestring–Field text, controlled.
defaultValuestring""Initial field text when uncontrolled.
onValueChange(value: string) => void–Called on typing, after a dictation is inserted, and when the field clears on send.
placeholderstring"Message Claude"Field placeholder.
recognizerDictationRecognizersimulatedSpeech source. Defaults to a simulated recognizer that never touches the microphone. Pass createWebSpeechRecognizer() or your own adapter.
onTranscript(transcript: { final: string; interim: string }) => void–Live, cumulative transcript of the current session.
onCommit(text: string) => void–Final text of a session, after it has been appended to the field.
onSend(text: string) => void | Promise<void>–Send the message. The send button shows a spinner while the promise is pending and the field clears when it resolves. Without it, sending is simulated.
onStatusChange(status: "idle" | "starting" | "listening" | "processing" | "denied" | "error", error?: DictationErrorCode) => void–Every state change, with the error code for denied and error.
autoPlaybooleantrue with the simulated recognizerLoop a scripted dictation (listen, insert, send) until the first pointer, key or focus inside the card. Pauses off screen.
maxDurationnumber120Seconds before listening stops and inserts on its own. 0 turns the limit off.
model{ name: string; mark?: ReactNode } | nullClaude Sonnet 5.5Model label at the start of the toolbar. null hides it.
disabledbooleanfalseDisables the field, mic and send.
aria-labelstring"Message"Accessible name of the field.
classNamestring–Class on the root.

DictationControl

Just the mic and recording pill, for a composer you already have. Same state machine, no field.

PropTypeDefaultDescription
recognizerDictationRecognizersimulatedSpeech source.
onTranscript(transcript: DictationTranscript) => void–Live text to render in your own field (style interim as muted).
onCommit(text: string) => void–Final text to append to your field.
onCancel() => void–The person cancelled; drop any live text you rendered.
onSpeechEnd() => void–The recognizer detected the end of an utterance. Call nothing, or confirm hands-free.
onStatusChange(status, error?) => void–Every state change.
maxDurationnumber120Seconds before listening inserts on its own.
labelstring"Dictate"Accessible name and tooltip of the mic.
disabledbooleanfalseDisables the mic.
classNamestring–Class on the wrapper.

useDictation

The headless state machine behind both parts: idle, starting, listening, processing, then idle, or denied and error. Returns status, transcript, error, startedAt, levelRef (0 to 1, read per frame), start, confirm, cancel and dismiss.

PropTypeDefaultDescription
optionsUseDictationOptions–recognizer, maxDuration, onTranscript, onCommit, onCancel, onSpeechEnd, onStatusChange.

DictationRecognizer

The adapter contract: (handlers, signal) => Promise<{ stop(): Promise<string> }>. Resolve once audio flows, reject with new DictationError("not-allowed") on a blocked microphone, call handlers.onTranscript({ final, interim }), handlers.onLevel(0..1), handlers.onSpeechEnd() and handlers.onError(code). Abort the signal to cancel.

PropTypeDefaultDescription
createSimulatedRecognizer(options?: { scripts?: DictationPhrase[][]; deny?: boolean; startDelay?: number; finalizeDelay?: number; wordInterval?: number }) => DictationRecognizer–Scripted speech with interim corrections and speech-like levels. deny: true rejects every start, like a blocked microphone.
createWebSpeechRecognizer(options?: { lang?: string; levels?: boolean }) => DictationRecognizer–Browser SpeechRecognition (Chrome, Edge, Safari) with levels from getUserMedia and an AnalyserNode. Rejects with "unsupported" where it is missing.

Keyboard interactions

KeysAction
Enter / SpaceOn the mic: start dictating. Focus moves to Insert.
EnterWhile listening: stop and insert the transcript (also from anywhere inside the card).
EscapeWhile listening: cancel and discard. On Try again: dismiss the error.
EnterIn the field: send. Shift+Enter adds a line.
TabMoves through cancel, insert, then send. Hidden faces of the pill are inert.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Also in ai

Guidance for AI tools

Dictation button: A chat composer with voice input, where the mic springs into a recording pill with a smooth live waveform, a timer, and cancel and insert buttons. Words stream into the field as you speak, muted until the recognizer settles them. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components