Voice Button
A press-to-record / release-to-transcribe push-to-talk button with an explicit state machine — idle → recording (pulse + live waveform) → processing → success/error.
Voice Button
Voice Button is a press-to-record / release-to-transcribe push-to-talk control with an explicit visual state machine: idle → recording (animated pulse rings + live waveform) → processing (loader) → success / error. Recording is the app's job (useVoiceRecorder → getUserMedia / MediaRecorder); transcription routes to local Whisper (useTranscribe). This component renders the state and emits press/release events.
When to use it: a single hands-on-keyboard (or touch) affordance to capture and transcribe a short spoken phrase — search-by-voice, quick notes, command input.
Preview
Installation
pnpm dlx shadcn@latest add @localmode/ui/audio/voice-buttonnpx shadcn@latest add @localmode/ui/audio/voice-buttonyarn dlx shadcn@latest add @localmode/ui/audio/voice-buttonbunx --bun shadcn@latest add @localmode/ui/audio/voice-buttonData source & dependencies
Data source: renders the recording state you pass and emits press/release events — works with any backend. Recommended producer: useVoiceRecorder (recording, deviceId selection, live getVolume()) + useTranscribe (with the Whisper STT model from @localmode/transformers) from @localmode/react (on-device, optional).
clsx+tailwind-merge— via the sharedcn()util
Files installed
voice-button.tsx— the componentwaveform-activity-bars.tsx— the in-button recording waveform (registry dependency)lib/utils.ts— thecn()helper (if not already present)
Props
VoiceButton
Prop
Type
Examples
Push-to-talk with local Whisper
import { VoiceButton, type VoiceButtonState } from '@/components/voice-button';
import { useVoiceRecorder, useTranscribe } from '@localmode/react';
import { transformers } from '@localmode/transformers';
export function VoiceSearch({ onResult }: { onResult: (text: string) => void }) {
const [state, setState] = useState<VoiceButtonState>('idle');
const [volume, setVolume] = useState(0);
const recorder = useVoiceRecorder();
const stt = useTranscribe({ model: transformers.speechToText('onnx-community/whisper-base') });
// Sample the live mic level while recording to drive the in-button waveform.
useEffect(() => {
if (!recorder.isRecording) return;
let raf = requestAnimationFrame(function tick() {
setVolume(recorder.getVolume());
raf = requestAnimationFrame(tick);
});
return () => cancelAnimationFrame(raf);
}, [recorder.isRecording]);
return (
<VoiceButton
state={state}
volume={volume}
onStart={() => { setState('recording'); recorder.startRecording(); }}
onStop={async () => {
setState('processing');
const blob = await recorder.stopRecording();
const res = blob ? await stt.execute(blob) : null;
if (res) { onResult(res.text); setState('success'); } else { setState('error'); }
setTimeout(() => setState('idle'), 1500);
}}
/>
);
}Customization
The button is fully controlled — you own the state and decide when to advance the machine. The recording state shows the in-button WaveformActivityBars; pass volume (0..1) from useVoiceRecorder().getVolume() (sampled in a requestAnimationFrame loop, as above) to make it react to real mic loudness — no hand-rolled AnalyserNode needed. State colors use bg-primary / bg-destructive / bg-emerald-600 — swap in the copied file to match your tokens.
Voice Orb
An animated canvas voice-agent orb that reflects discrete agent states and pulses with input/output volume via callbacks — visual state decoupled from the audio source.
Mic Selector
A microphone input-device picker with permission handling and live device enumeration — fully offline, browser device APIs only.