Pro

Voice input

Pro component

Mic button whose circle opens into a recording bar with a scrolling waveform, a timer and Stop. A blocked mic gets a plain explanation.

Voice message

SimulatedBlockedYour mic
Record a note for the book club: what to bring Friday
Waiting for microphone permission…

Recorded

No recording yet.

Installation

This is a Pro component. UI Polish Pro costs $149 one time and includes every Pro component, block and template, now and later. Add your license key to your MCP connection or components.json, then install it either way below.

With UI Polish connected, ask your agent:

Add the UI Polish voice input to this project.

Or with the shadcn CLI:

npx shadcn@latest add @uipolish/voice-input
Brings inIcons, Text morph

A mic button that records a voice message. Pressing it opens the button's own circle out into a recording bar that fills the row: Cancel, a waveform that scrolls smoothly as you speak, the elapsed time, and the mic turned into Stop in place. It asks for permission without flashing anything when the answer is quick, and says plainly what to do when the microphone is blocked. Native APIs only: getUserMedia, an AnalyserNode for the level and MediaRecorder for the audio.

TSX
<VoiceInput onComplete={({ blob, duration }) => transcribe(blob)} onDiscard={clearClip}>  <span style={{ paddingInlineStart: "var(--voice-inset)" }}>Record a note for the book club</span></VoiceInput>

When to use it

  • Dictating a message to an assistant, in a composer toolbar or its own row.
  • Short voice notes the app sends or transcribes.

When not to

  • Live, continuous conversation: that needs streaming audio and turn-taking, not a record-then-send control.
  • Long recordings with pause and resume, or editing: use a dedicated recorder.

States

Every state fits the same 32px row. The states share one grid cell and crossfade where they are; the mic button never moves. Text in the row starts at --voice-inset (10px: a 16px gutter when the row sits 6px inside its container); give your idle children the same inset.

StateWhat you seeHow
IdleYour children and a mic button
Waiting for permissionAfter 300ms, "Waiting for microphone permission…" with a light band passing across it; the mic shows a fill and ignores presses. If permission comes back sooner, nothing flashesaria-busy
RecordingA bar the full width of the row, opened out of the mic's circle: Cancel, the waveform, a red dot breathing slowly before m:ss (live, at a glance); the mic becomes a filled Stop button at the bar's enddata-status="recording"
DoneThe same bar, waveform held still at 40%, the red dot gone (its space kept) and the time muted; Cancel becomes Discard (the cross turns into a bin) and Stop becomes the mic again (record again)onComplete
Blocked"Microphone blocked. Allow it in site settings, then try again." The mic tries againNotAllowedError
No microphone"No microphone found. Connect one, then try again."NotFoundError
Not secure"Voice input needs a secure (https) page."no isSecureContext

Recording and done are one layer, so Cancel, the waveform and the time stay exactly where they were when recording stops; only the waveform's tone, the time's color and the buttons change.

The bar. One pill behind the row, clipped to the mic's 34px circle at rest. Recording opens the clip to the whole row; its ends are the 32px buttons' circles, so they're concentric (and 6px padding in a 22px container keeps the container concentric too). The bar's contents are clipped from the same side on the same curve, so nothing shows outside the bar while it grows. clip-path only: nothing lays out.

The waveform. A canvas, so it never touches layout. Samples land on a fixed 60ms grid and every bar is drawn at its age, so the row slides left continuously instead of stepping a bar at a time, snapped to device pixels so bars stay crisp. Each new bar grows from a dot to its level over 140ms. Bars are 2px on a 4px pitch. Where there's no sound yet, faint dots hold the line; a voice comes up in full ink. The far end fades out with a mask. The level is the RMS of the input, mapped over the range a voice uses (−55 to −10 dBFS) on a curve, so quiet syllables draw short bars and loud ones tall: the waveform has the rhythm of speech, not a wall of full bars. It rises fast and falls quickly enough that the dips between syllables show.

The time. m:ss in tabular figures, as wide as the longest time maxDuration allows, so nothing gives up room as minutes grow. It's a TextMorph, so only the figure that changes moves.

Simulated microphone. simulate="allow" skips the permission prompt and generates a speech-like level; simulate="deny" behaves as if the user blocked the microphone. Use them for demos, tests and screenshots.

maxDuration (seconds, default 300) stops the recording on its own.

Keyboard

KeyDoes
Enter, Space on the micStart, stop, record again, or try again
Escape while recording or waitingCancel; focus moves to the mic

Motion

WhatDurationEasing
Bar opens out of the mic's circle (and its contents are uncovered)--ui-dur-panel--ui-ease-expand
Bar closes back into the button, fading as it lands--ui-dur-base--ui-ease-collapse
A figure of the time changesfrom TextMorph
Mic strokes retract, stop block sweeps in (and back)out --ui-dur-fast, in --ui-dur-base after 60ms--ui-ease-draw
Stop button fill--ui-dur-instant--ui-ease-color
States crossfadein --ui-dur-fast, out --ui-dur-instant--ui-ease-enter / --ui-ease-exit
Waiting line appearsafter 300ms, --ui-dur-fast--ui-ease-enter
Light band on the waiting line--ui-dur-sweep per passlinear
Press0ms in, --ui-dur-snap back; only the fill shrinks--ui-ease-snap

The waveform moves because the voice does; nothing else loops. Under prefers-reduced-motion: reduce states switch at once, the bar appears without opening, the waveform steps instead of sliding and bars appear at full height, the band holds still, and the mic and stop swap without drawing.

Accessibility

  • The one button's name follows the state: "Record a voice message", "Stop recording", "Record again", "Try the microphone again".
  • Starting, stopping (with the length: "Recording stopped. 7 seconds."), canceling and every error are announced in a role="status" region. The ticking time isn't announced each second.
  • Hidden states are inert, so their buttons leave the tab order.
  • 40px hit areas on coarse pointers.

Before / after

Before: a mic button that toggles a red dot. A blocked microphone does nothing visible, and a spinner flashes even when permission was already granted.

TSX
<button onClick={toggle}>{recording ? <RedDot /> : <Mic />}</button>{asking && <Spinner />}

After:

TSX
<VoiceInput onComplete={send} />