webdevpuneet.com

Voice Memo Recorder — Waveform UI HTML CSS JS

Voice Memo Recorder · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="vmr-card">
  <div class="vmr-head">
    <h3>Voice memo</h3>
    <span class="vmr-time" id="vmrTime">0:00</span>
  </div>

  <div class="vmr-bars" id="vmrBars"></div>

  <div class="vmr-controls">
    <button type="button" class="vmr-record" id="vmrRecord" aria-label="Record">
      <span class="vmr-record-dot"></span>
    </button>
    <button type="button" class="vmr-play" id="vmrPlay" hidden aria-label="Play">
      <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="6 4 20 12 6 20"/></svg>
    </button>
    <button type="button" class="vmr-discard" id="vmrDiscard" hidden aria-label="Discard">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
    </button>
  </div>
  <p class="vmr-status" id="vmrStatus">Tap to start recording</p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Morphing record button
The inner dot shifts from circle to rounded square on record/stop, the universal "tap to stop" visual cue.
Two-phase waveform rendering
Bars animate live with staggered per-index timing while recording, then freeze into a fixed shape from the captured levels on stop.
Pulsing record-state ring
A looping box-shadow animation reinforces the active recording state independent of the button's own shape change.
Playback bar-fill scrubbing
Play highlights a proportional number of bars as time elapses, visually scrubbing through the fixed waveform.
Explicit discard action
Discarding is a separate, deliberate control from re-recording, avoiding ambiguity about what a tap will do.
Live elapsed-time counter
A formatted m:ss timer counts up while recording and reflects playback position while playing.
Status text for every state
"Tap to start recording," "Recording…," "X:XX recorded," and "Playing…" each give an explicit text status alongside the visuals.
Pure CSS/JS — ready for the real MediaRecorder API
The interaction layer is fully built; swapping in real microphone capture only touches the level-generation logic.

About this UI Snippet

Voice Memo Recorder — Live Waveform, Pulsing Record Button & Playback Scrub

Screenshot of the Voice Memo Recorder snippet rendered live

Voice messages have become a default communication mode in chat apps, support tools, and async-first workplaces, and the UI that sells the feature is the live waveform — bars jumping in time with your voice, then settling into a fixed shape you can play back. This snippet builds the complete front-end interaction (record, waveform, play, discard) in plain HTML, CSS, and vanilla JavaScript, ready to wire to the real MediaRecorder API.

A round-to-square morphing record button

The record button is a circle with a smaller white circle inside; clicking it toggles a .recording class that morphs the inner dot from a circle to a rounded square via border-radius transition — the same visual shorthand iOS and most voice-recording apps use for "tap again to stop." A pulsing box-shadow ring animates outward continuously while recording, reinforcing the active state independent of the dot's own shape change.

Two waveform phases, one set of bar elements

The same 40 bar <div>s serve two purposes. While recording, animateLiveBars() staggers each bar's height update by a small per-index delay and re-runs on a loop, producing an organic, left-to-right "wave" of motion rather than every bar jumping in perfect unison — a more convincing fake live-audio look. The instant recording stops, every bar's height is rewritten *once* from the recordedLevels captured during recording (cycled to fill all 40 bars), turning the live animation into a fixed, static waveform that represents "this take" — exactly like a real recorded memo's waveform freezing in place.

Playback that highlights bars as it goes

Pressing play starts a one-second interval that computes how many bars *should* be lit based on elapsed time over total duration (Math.round((elapsed / seconds) * BAR_COUNT)) and toggles the .live class on that many bars from the left — visually "filling in" the waveform as playback progresses, the same scrubbing-position feedback real voice-memo players give.

Discard resets everything explicitly

The discard button is a deliberate, separate action from re-recording: it stops any playback, clears the captured levels and timer, and resets every bar back to its resting height — so there's no ambiguity between "I want to record over this" (tap record again) and "I want to delete this and start fresh" (tap discard).

From simulated levels to a real microphone

Everything above works identically once the random randomLevel() calls are swapped for real input: navigator.mediaDevices.getUserMedia({ audio: true }) grants microphone access, a MediaRecorder captures the resulting stream into a blob for upload or playback, and a Web Audio AnalyserNode reads live frequency or time-domain data each animation frame to drive the exact same bar-height updates this demo already wires up. The UI layer — record button, waveform, play, discard — doesn't need to change at all.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA voice memo card renders with a flat row of bars at rest and a red record button.
  2. 2
    Tap recordThe button morphs and pulses, the timer counts up, and bars animate in a left-to-right wave to simulate live audio.
  3. 3
    Tap record again to stopThe waveform freezes into a fixed shape from the captured levels, and Play and Discard buttons appear.
  4. 4
    Tap playThe recording "plays" for its recorded duration, with bars lighting up from left to right as time elapses.
  5. 5
    Tap discardEverything resets to the initial flat-bar, "Tap to start recording" state.
  6. 6
    Wire up real audio captureReplace the simulated timer/levels with the MediaRecorder API and Web Audio AnalyserNode for real microphone levels, feeding the same bar-height update calls.

Real-world uses

Common Use Cases

Chat and messaging apps
The voice-note recording UI for a messaging product, pairing with a chat UI message list.
Customer support and feedback tools
Let users record a quick voice description of an issue instead of typing a long text explanation.
Voice journaling and note-taking apps
A focused single-recording interface for quick audio notes.
Async standups and team updates
Replace a typed status update with a 30-second voice memo in a remote-work tool.
Language learning and pronunciation apps
Record and immediately play back a learner's pronunciation attempt using the same record/play/discard cycle.
Learning waveform UI technique
A clear reference for staggered bar animation and freeze-on-stop waveform rendering — compare with an audio waveform visualizer for a playback-only variant.

Got questions?

Frequently Asked Questions

Call navigator.mediaDevices.getUserMedia({ audio: true }) on record-start, pipe the stream into a MediaRecorder to capture the audio blob, and use a Web Audio AnalyserNode's getByteFrequencyData() on an animation frame to drive the live bar heights instead of the random randomLevel() values — the rest of the UI logic stays the same.

Store the MediaRecorder's resulting Blob, create an <audio> element (or Audio() instance) with its object URL, and call .play()/.pause() from the existing play button handler, syncing the bar-highlight progress to the audio element's timeupdate event instead of a fixed setInterval.

Once stopped, send the captured Blob via fetch with FormData to your upload endpoint, showing an upload-progress state (see the upload progress snippet) before revealing the play/discard controls.

In the recording setInterval, check seconds against a MAX_SECONDS constant and call stopRecording() automatically once reached, optionally showing a brief "Maximum length reached" status message.

In React, keep recording/playing/seconds/levels in useState and run the capture/playback loops in useEffect with cleanup on unmount; in Vue, use ref()/onUnmounted for the same timer cleanup; in Angular, use component fields with ngOnDestroy. The MediaRecorder and AnalyserNode integration is identical across frameworks since it's a browser API, not a UI-layer concern.