Start with a native range input, a label, and a visible value. A range slider styled with CSS retains the browser’s built-in control behavior, while a CSS-styled slider bar can show where the handle travels.
The example below sets a volume from 0 to 100 in increments of 5. Save it as an HTML file and open it in a browser. Drag the handle or focus it and use the arrow keys; the number beside the control updates as you move. The initial number is present in the markup, so it is visible even before the script runs.
CSS slider bar: a complete example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Volume control</title>
<style>
.control { max-width: 24rem; font-family: system-ui, sans-serif; }
.control input { display: block; width: 100%; margin: 1rem 0; }
.control input[type="range"] {
appearance: none;
-webkit-appearance: none;
background: transparent;
cursor: pointer;
}
.control input:focus-visible { outline: 3px solid #0645ad; outline-offset: 5px; }
.control input::-webkit-slider-runnable-track {
height: 8px; border-radius: 4px; background: #52657a;
}
.control input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px; height: 24px; margin-top: -8px;
border: 2px solid #102a43; border-radius: 50%; background: #fff;
}
.control input::-moz-range-track {
height: 8px; border-radius: 4px; background: #52657a;
}
.control input::-moz-range-thumb {
width: 20px; height: 20px;
border: 2px solid #102a43; border-radius: 50%; background: #fff;
}
@media (forced-colors: active) {
.control input[type="range"] { appearance: auto; -webkit-appearance: auto; }
}
</style>
</head>
<body>
<div class="control">
<label for="volume">Volume</label>
<input id="volume" name="volume" type="range"
min="0" max="100" step="5" value="40">
<span>Value: <output id="volume-value" for="volume">40</output>%</span>
</div>
<script>
const volume = document.querySelector('#volume');
const value = document.querySelector('#volume-value');
volume.addEventListener('input', () => { value.textContent = volume.value; });
</script>
</body>
</html>
The min and max attributes set the endpoints; step sets the allowed increments from the minimum, and value sets the initial position. Here, 40 is valid because it is eight steps above zero. Choose a step that suits the decision: a coarse control is quick to use but cannot select values between its increments. The HTML range reference describes these attributes and their defaults.
The label gives the input an accessible name. The output element displays the initial value, and the input listener updates its text as the value changes. If your application changes the input value from script, update the output there too; assigning a value programmatically does not run this listener automatically. Use a number input alongside the control if users must enter an exact value. If you add one, keep both fields synchronized and apply the same bounds to each. Decide how to handle invalid typed values before saving: show an error or correct them according to an explicit rule, rather than silently accepting a conflicting value.
Style the track and thumb
A css range slider has two visible parts: the track and the thumb. The track indicates the available path; the thumb is the handle the user moves. The example removes native appearance and sets a transparent input background so the track rules can supply its color. Its 24-pixel handle is larger than the track, making it easier to see and grab.
The CSS slider bar uses separate browser-specific selectors. Chromium and Safari use ::-webkit-slider-runnable-track and ::-webkit-slider-thumb; Firefox uses ::-moz-range-track and ::-moz-range-thumb. Keep the rules separate: an unsupported selector in a selector list can invalidate the whole rule. Styling a slider’s bar with CSS does not change its numeric behavior. The negative WebKit thumb margin centers the taller handle on the track. Firefox centers its thumb through its own rendering. These hooks are nonstandard, so test the result in each target engine; MDN’s form styling guide explains the browser-specific approach.
Preserve interaction and focus
A custom range slider in CSS should remain usable without a pointer. The native input provides keyboard behavior; the :focus-visible outline makes keyboard focus apparent. Keep the label visible and choose colors that distinguish the handle, track, and focus indicator from their surroundings. The forced-colors rule requests native appearance on the input in that mode. Check the track, thumb, and focus indicator with system colors too.
Before shipping a range slider with custom CSS, open the page in each target browser, press Tab until the control is focused, then press the right arrow once: from 40, the displayed value should become 45 and the focus outline should remain visible.
