A voice-note recorder: press to record, watch the input level move while it does, stop, play it back, and get a Blob you can upload — and, the part every hand-rolled version gets wrong, the microphone is handed back the moment it is finished with. Reach for it wherever a screen is better off taking someone's voice than their typing: an audio attachment on a support ticket or bug report, a feedback or NPS widget that offers "say it instead", voice comments on a design or document review, pronunciation and language practice, a singing or instrument lesson, interview and research recordings, a meeting or consultation memo, a field-service or inspection report dictated on site, a delivery or warehouse note, a clinical or veterinary note, voice messages in a chat or community app, a voice journal or diary, call-centre after-call notes, and anywhere speaking is the accessible alternative for someone who cannot comfortably type. Common asks it answers: "react audio recorder", "record audio in browser react", "MediaRecorder react hook", "useMediaRecorder", "voice note component", "voice memo recorder react", "audio message component", "shadcn audio recorder", "shadcn voice recorder", "react-media-recorder alternative", "recordrtc alternative", "react-mic alternative", "getUserMedia audio react", "microphone input level meter react", "audio level meter react", "show mic volume while recording", "waveform while recording", "AnalyserNode rms react", "pause and resume MediaRecorder", "MediaRecorder mimeType safari", "audio/webm is not supported in safari", "NotSupportedError MediaRecorder constructor", "MediaRecorder.isTypeSupported", "record audio that works in safari and chrome", "audio duration Infinity MediaRecorder", "webm blob duration infinity", "how to get the length of a MediaRecorder recording", "audio element duration NaN blob", "microphone still on after stop", "recording indicator stays on react", "stop microphone stream react", "AudioContext was not allowed to start", "too many AudioContexts", "ondataavailable only fires once", "MediaRecorder empty blob", "recorded audio is silent", "detect muted microphone browser", "microphone permission denied react", "allow microphone in an iframe", "microphone blocked on http", "録音 react コンポーネント", "ボイスメモ 入力", "マイク 入力レベル 表示", "録音したのに無音". Official shadcn/ui has nothing for this and no combination of its parts reaches it. The measurement, taken today over all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source: MediaRecorder, getUserMedia, mediaDevices, AudioContext, AnalyserNode, analyser, ondataavailable, isTypeSupported, webm, microphone, waveform, getTracks, NotAllowedError and even the bare word audio are every one of them a zero hit. There is no component in official shadcn that touches sound at all, so an agent asked for a voice-note field builds it from a bare MediaRecorder and gets the same six things wrong each time. Within this registry it is the first component that records: camera-capture tells the same permission story for video and is the only other thing here that opens a device, and speech-input uses the Web Speech API to return text and deliberately keeps no audio — which is the line to draw when choosing between them. Use speech-input to fill a field with words; use this when the sound itself is the thing being kept, or when the recording has to work offline, or must not be sent to a remote transcription service. Pipe the blob straight into a FormData with form.append("audio", recording.blob, `note${audioFileExtension(recording.type)}`), and upload-list will show it going up. Six things separate it from the thirty-line version. First, and the reason the meter is not decoration: a muted microphone does not fail. Mute it in the operating system, at a switch on the headset, or in a mixer app, and getUserMedia still resolves, the track is still live and unmuted as far as the browser is concerned, MediaRecorder still runs, and what comes out is a perfectly valid file of digital silence. Nothing in the platform reports this; the person finds out when someone tells them the voice note is empty, long after they sent it. An AnalyserNode watches the signal, the level is on screen the whole time, and after a few seconds with nothing arriving the component says so in words. Second, there is no audio format every browser records. Chromium and Firefox write WebM, Safari writes MP4, and `new MediaRecorder(stream, { mimeType })` with a type the browser cannot write throws NotSupportedError out of the constructor immediately — so a recorder hard-coded to audio/webm has never once worked in Safari. A descent through isTypeSupported picks the best type this browser admits to, a constructor that rejects it anyway is retried with the browser's own default, and the type reported back is read off recorder.mimeType rather than from the request, because naming an upload from what you asked for hands the server an MP4 called WebM. Third, the file has no duration. A MediaRecorder writes its container header before it knows how long the recording will be and never goes back to finalise it, so handing the blob to an <audio> element gives duration === Infinity — the broken scrubber behind every half-finished voice-note widget, usually worked around by seeking to a huge offset and waiting to see where it lands. The duration here is measured while recording, from timestamps rather than by counting ticks, so a tab left in the background (where timers are throttled to about once a second) still reports its true length, and paused stretches are subtracted. Fourth, stopping. recorder.stop() ends the recording and nothing else: track.stop() is the only thing that ends the capture, and without it the browser's tab indicator — and on a phone the operating system's own — stays lit over a page that has finished. The AudioContext has to be closed too, or it holds the audio hardware and eats into the small number of contexts a page is allowed, so a recorder mounted a few times stops being able to meter at all. Both happen on stop, on discard, and on unmount, which is the moment a single-page app would otherwise leak them with no control left that could reach them. The analyser is also connected to the analyser and to nothing else, because connecting on to context.destination is the obvious next line and it routes the microphone into the speakers: an immediate feedback howl on any device without headphones, recorded. Fifth, the chunks. The specification does not promise a single dataavailable at the end, so event.data saved over a variable yields a file holding whichever piece arrived last; they are accumulated, and a stop that produced no bytes at all is reported as a failure rather than handed back as a zero-byte clip to upload. A microphone that goes away mid-sentence — a headset unplugged, the device seized by another app, the permission revoked from the browser's own UI — ends the recording and keeps what was captured, because discarding four minutes of an interview over the last second of it is the one outcome nobody forgives. Sixth, permissions. NotAllowedError means four unrelated things — an http origin, an iframe without allow="microphone", a stored block, and a prompt closed without an answer — and only the stored block is worth sending somebody to their site settings for, so the other three say something true instead and are not offered a retry that cannot work. A permission fixed in those settings is picked up live through the Permissions API change event rather than staying dead until a reload, and because MediaDevices is [SecureContext] the whole object is missing on http — where the obvious feature detect fires and produces the one wrong answer, "this browser has no microphone support". The two halves of support are detected separately for the same reason: a browser that can reach a microphone but cannot record one needs different words, and Safari was that browser for four releases. Video is explicitly refused so the camera indicator stays dark and only one permission is asked for, and there is deliberately no autoStart — a component that opens the microphone on mount records the room before anybody agreed to it, and does so without a prompt on the second visit. Ships as a hook (useAudioRecorder) plus a component, with echoCancellation, noiseSuppression and autoGainControl exposed because the platform defaults are right for speech and ruinous for music, plus maxDuration so a forgotten recording cannot grow until the tab dies. pickAudioMimeType, audioFileExtension and formatDuration are exported as pure functions. The level meter carries role=meter and is deliberately not a live region, so it is read when asked for rather than narrating every syllable, while the silence warning goes through a permanently mounted polite live region; controls are aria-disabled rather than disabled so a refusal stays reachable and can explain itself. Styled entirely with shadcn tokens (input, accent, ring, primary, muted, destructive, muted-foreground), so it follows light and dark mode, and its only dependency is lucide-react for the icons.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/audio-recorder.json"