Skip to content
Sign in
ConversationRead aloudMenu

Read aloud

Read a message aloud. An icon button in the message actions turns into a compact player with play and pause, progress, speed and stop, through idle, preparing, playing, paused and failed states. Plays a URL, your own text to speech, or the browser's Web Speech API.

The browser's own speech engine, with the sentence being read marked as it goes.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/read-aloud.json

Usage

tsx
import { ReadAloud } from "@/components/ui/chatcn/read-aloud"

<MessageActions>
  <MessageActionCopy value={text} />
  <ReadAloud text={text} />
</MessageActions>

With only text, it speaks through the Web Speech API, one sentence at a time. That makes pause and speed changes reliable across browsers, and the index of the sentence being read comes for free. Where there is no speech engine, the button stays visible but inert, with a tooltip that says why.

For your own voices, pass src, or onSynthesize, which is called once on first play and may return a URL or a URL with sentence timings. The result is reused for replays, and the call is aborted if the user stops first.

tsx
<ReadAloud
  text={text}
  onSynthesize={async (text, { signal }) => {
    const res = await fetch("/api/speech", { method: "POST", body: text, signal })
    return URL.createObjectURL(await res.blob())
  }}
/>

To mark the sentence being read, keep the index from onSentenceChange and render plain text with ReadAloudText, which splits sentences exactly as the reader does.

tsx
const [sentence, setSentence] = React.useState(-1)

<MessageContent><ReadAloudText text={text} active={sentence} /></MessageContent>
<ReadAloud text={text} onSentenceChange={setSentence} />

Examples

Your own speech

onSynthesize returns a URL and sentence timings. This stand-in returns silent audio after 1.2 s.

Player states

ReadAloudPlayer is the presentational player on its own, for driving from another engine or for showing states.

Idle, preparing audio, playing, paused, failed and no speech engine.

Props

PropTypeNotes
textstringRequired. Spoken by speech, and split into sentences
srcstringA ready audio URL. Wins over the rest
timings{ start, end }[]Sentence timings in seconds for src
onSynthesize(text, { signal }) => Promise<string | { url, timings? }>Your text to speech
speechbooleanFall back to Web Speech. Defaults to true
lang, voicestringSpeech language and preferred voice name
rate, defaultRate, onRateChangenumberControlled or uncontrolled speed
ratesnumber[]Speeds the button cycles. Defaults to 1, 1.25, 1.5, 2
onSentenceChange(index: number) => void-1 when nothing is being read
onEnd, onError() => void, (error) => void
labelstringTooltip and name of the button. Defaults to "Read aloud"

useChatcnReadAloud exposes the same engine as a hook: status, progress, elapsed, duration, rate, sentence, and play, pause, toggle, stop, retry.

Keyboard

KeyAction
Enter, SpaceOn the button, start reading and move focus to the player's play button
TabMove between play, speed and stop
Enter on stopStop and return focus to the read aloud button

Accessibility

  • The player is a group named Read aloud. Progress is a progressbar with a percentage, and time is announced as 0:12 of 1:04.
  • Preparing audio and Paused are announced politely. Errors use role="alert" and say how to fix the problem.
  • The speed button names both the current and the next speed. Every button has a 44 px hit area on touch screens.

Motion

  • Opened by pointer, the player grows from the button's edge: opacity and scale from 0.96, 150 ms. Opened from the keyboard it appears at once. Under reduced motion it only fades.
  • Play, pause and the spinner swap with the kit's 150 ms blur crossfade. Progress moves by transform only.
  • The sentence mark changes instantly: it follows speech, not the eye.