Volume Control — Free Custom Slider HTML CSS JS Snippet
Volume Control · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Volume Control — Custom Draggable Volume Slider

A volume control is the speaker-plus-slider you find in every media player, video app, and sound settings panel. This snippet builds a polished, fully custom one — not a styled <input type=range> — in HTML, CSS, and vanilla JavaScript, with pointer dragging, a mute button that remembers the last level, full keyboard support, and a speaker icon that changes with the volume. No library, no audio dependency.
A custom track, not a native range
The track is a plain div with an absolutely-positioned fill and a round knob whose left is a percentage. Building it by hand (instead of restyling <input type=range>, which is notoriously inconsistent across browsers) gives total control over the knob size, the focus ring, and the active scale animation. touch-action:none on the track stops the browser from scrolling the page while you drag on touch devices.
Pointer Events for unified drag
A single set of Pointer Events handles mouse, touch, and pen identically. On pointerdown the track calls setPointerCapture, so dragging keeps tracking even when the cursor leaves the element, and fromPointer() converts clientX into a 0–100 value using getBoundingClientRect(). That one geometry function powers both the initial click-to-seek and continuous dragging.
Mute that remembers
The speaker button toggles muted, which renders the fill at zero without losing the real volume. A lastVolume variable stores the level so un-muting restores exactly where you were — and dragging the slider above zero auto-un-mutes, matching how native players behave. The icon swaps between muted, low, and high SVGs based on the current state, so the glyph always reflects what you'll hear.
Accessible slider semantics
The track uses role="slider" with aria-valuemin, aria-valuemax, and a live aria-valuenow, and is focusable via tabindex="0". Arrow keys nudge by 5, Home and End jump to mute and max, and the focus-visible ring plus a scaled knob make the keyboard state obvious — so it's operable and announced correctly without a mouse.
Wiring to audio
Point setVolume() at a real sink: set audioElement.volume = shown / 100 (the HTML5 media API expects 0–1) or a Web Audio GainNode.gain.value. Because every change funnels through setVolume() and paint(), syncing the UI to actual playback — including external volume changes — is a one-line addition.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing the drag math by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why fromPointer() and the initial click both funnel through setVolume() rather than each having their own logic, and how setPointerCapture on pointerdown keeps a drag tracking correctly even once the cursor moves outside the track's bounding box. It's also worth asking about the mute/lastVolume interaction — have it walk through what happens to lastVolume across a mute, an unmute, and then a fresh drag, to confirm there's no case where the remembered level gets silently lost. For extending it, have it add a scroll-wheel handler that nudges volume on the track, persist the last volume setting to localStorage across page loads, or add a small numeric tooltip that follows the knob while dragging. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a fully custom volume slider (not a styled native range input) in plain HTML, CSS, and vanilla JavaScript with no libraries, using the Pointer Events API for unified mouse/touch/pen dragging.
Requirements:
- A track element built from a div (not input type=range) containing an absolutely positioned fill bar and a circular knob, both driven by inline percentage widths/positions computed in JavaScript rather than CSS-only.
- On pointerdown on the track, call setPointerCapture with the event's pointerId so that subsequent pointermove events keep being received by the track even if the cursor moves outside its bounding rectangle during a fast drag; on pointerup, end the drag.
- A single geometry function that converts a clientX coordinate into a 0-100 volume value using the track's getBoundingClientRect, used identically for both an initial click-to-seek and continuous dragging — do not duplicate this math between the two interactions.
- A mute toggle button that hides the current volume (renders the fill at zero) without discarding the actual numeric volume value, remembering the pre-mute level in a separate variable so clicking unmute restores exactly that prior value; dragging the slider to any value above zero must also automatically clear the muted state.
- A speaker icon that swaps between three distinct SVG glyphs (muted, low volume, high volume) based on whether the control is currently muted or zero, below a threshold, or above it.
- Full keyboard operability: the track must be focusable, and ArrowUp/ArrowRight must increase volume by a fixed step, ArrowDown/ArrowLeft must decrease it, Home must set it to zero, and End must set it to one hundred, each with a visible focus ring distinguishing keyboard interaction from mouse hover.
- Proper ARIA slider semantics: role="slider" with aria-valuemin, aria-valuemax, and a live aria-valuenow that updates on every change.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA speaker icon, a track, and a percentage render in a row.
- 2Drag the knobPointer dragging sets the volume and the fill follows your finger or cursor.
- 3Click anywhere on the trackThe knob jumps to that point — click-to-seek and drag share one function.
- 4Press the speaker to muteThe fill drops to zero but your level is remembered for un-muting.
- 5Use the keyboardFocus the track and press arrows to nudge, Home and End to jump.
- 6Connect to audioSet audio.volume = value / 100 inside setVolume().
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Native range inputs are hard to style consistently — the track, thumb, and focus ring differ across browsers and need vendor pseudo-elements. A custom div track gives full control over the knob, the active scale, and the focus ring, while role=slider plus aria-valuenow restores the accessibility you'd otherwise get for free.
It uses Pointer Events, which unify mouse, touch, and pen. On pointerdown the track calls setPointerCapture so it keeps receiving moves even if the pointer leaves, and fromPointer() turns clientX into a 0–100 value using getBoundingClientRect(). touch-action:none prevents the page from scrolling mid-drag.
No. Muting sets a flag that renders the fill at zero but leaves the real volume untouched, and a lastVolume variable stores your level so un-muting returns to exactly where you were. Dragging above zero also auto-un-mutes, matching native media players.
Yes. The track is focusable and listens for keys: ArrowRight/Up and ArrowLeft/Down nudge by 5, Home mutes to 0, and End jumps to 100. A focus-visible ring and a slightly enlarged knob make the focused state clear, and aria-valuenow updates so screen readers announce the level.
Hold volume and muted in state and bind the fill width and knob left to the shown value. Attach the pointer handlers to a ref and set audio.volume = value / 100 in the setter. Put nothing in a global; the logic is self-contained. In Tailwind, position the fill and knob with inline styles for the percentage and use utilities for the rest.