Home / Snippets / UI Components /

Range slider

A fully styled range input with custom track and thumb — appearance: none and vendor-prefixed pseudo-elements, no JavaScript.

Volume
Brightness (with accent fill)
Widely Supported
uino-js

Quick implementation

.range-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 0.35rem;
  border-radius: 99rem;
  background: oklch(0.3 0.02 260);
  outline: none;
  cursor: pointer;
}

/* Webkit track */
.range-slider::-webkit-slider-runnable-track {
  height: 0.35rem;
  border-radius: 99rem;
  background: oklch(0.3 0.02 260);
}

/* Webkit thumb */
.range-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: oklch(0.95 0.01 260);
  border: 2px solid oklch(0.52 0.22 265);
  box-shadow: 0 1px 4px oklch(0 0 0 / 0.4);
  margin-top: -0.45rem;
  transition: transform 0.15s, box-shadow 0.15s;
}

.range-slider:focus::-webkit-slider-thumb,
.range-slider:hover::-webkit-slider-thumb {
  transform: scale(1.15);
  box-shadow: 0 0 0 3px oklch(0.52 0.22 265 / 0.3);
}

/* Firefox track */
.range-slider::-moz-range-track {
  height: 0.35rem;
  border-radius: 99rem;
  background: oklch(0.3 0.02 260);
}

/* Firefox thumb */
.range-slider::-moz-range-thumb {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: oklch(0.95 0.01 260);
  border: 2px solid oklch(0.52 0.22 265);
  box-shadow: 0 1px 4px oklch(0 0 0 / 0.4);
  cursor: pointer;
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building a CSS form component library.

Goal: A fully styled range input with custom track and thumb — no JavaScript.

Technical constraints:
- Use appearance: none (and -webkit-appearance: none) to remove all browser default styles.
- Style the track with ::-webkit-slider-runnable-track and ::-moz-range-track.
- Style the thumb with ::-webkit-slider-thumb and ::-moz-range-thumb.
- Thumb: white circle (oklch(0.95 0.01 260)), accent border (oklch(0.52 0.22 265)), drop shadow.
- Track: dark background (oklch(0.3 0.02 260)), rounded with border-radius: 99rem.
- Use oklch() for all color values, not hex or rgba().
- Add a focus/hover state on the thumb: scale(1.15) and a glowing ring using box-shadow.

Framework variant (pick one):
A) Vanilla CSS class .range-slider applicable to any <input type="range">.
B) React component — accept min, max, value, onChange, and label as props.

Edge cases to handle:
- Webkit requires margin-top on the thumb to vertically center it over the track.
- Firefox and Webkit use completely separate pseudo-elements — provide both.
- Ensure the slider is keyboard-accessible (arrow keys work natively, do not override).

Return HTML and CSS.

Why this matters in 2026

The default range input looks different across every browser and operating system — inconsistent height, colour, and thumb size make it stand out awkwardly in a polished UI. With appearance: none and vendor-prefixed pseudo-elements, you can achieve a consistent, on-brand slider in pure CSS without reaching for a JavaScript slider library.

The logic

appearance: none strips all browser default styling from the <input type="range">. Webkit (Chrome, Safari, Edge) uses ::-webkit-slider-runnable-track for the track and ::-webkit-slider-thumb for the handle; Firefox uses ::-moz-range-track and ::-moz-range-thumb. The thumb requires -webkit-appearance: none as well to fully reset it in Webkit. A negative margin-top on the Webkit thumb vertically centers it over the track because the thumb sits relative to the track height. The focus/hover state uses transform: scale(1.15) and a box-shadow ring to give clear interactive feedback.

Accessibility & performance

Range inputs are natively keyboard-accessible — arrow keys increment and decrement the value. Do not override or remove the default outline without providing an equally visible alternative. Always add an aria-label or a visible <label> associated via for/id; a range with no label has no accessible name. For live value display (e.g., showing the number next to the slider), use a linked <output> element with for pointing to the input's id — no JavaScript needed for the initial render.