Slider
An input for selecting a value from a range. Built on native <input type="range"> with custom track and thumb styling, filled track indicator, and full keyboard support.
Default
Range slider with default value.
<input class="slider" type="range" min="0" max="100" value="50">
With label
<label class="label" for="vol">Volume</label>
<input class="slider" type="range" id="vol" min="0" max="100" value="75">
With value display
Shows current value using a native <output> element.
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;">
<label class="label" for="temp" style="margin:0;">Temperature</label>
<output id="temp-output" class="text-muted-foreground" style="font-size:0.875rem;font-family:var(--font-mono);" for="temp">0.5</output>
</div>
<input class="slider" type="range" id="temp" min="0" max="1" step="0.1" value="0.5"
oninput="document.getElementById('temp-output').value = this.value">
With steps
Slider with discrete step increments.
<input class="slider" type="range" min="0" max="100" step="25" value="50">
Vertical
Vertical orientation using data-orientation="vertical".
<input class="slider" type="range" data-orientation="vertical" min="0" max="100" value="60">
Disabled
Slider in disabled state.
<input class="slider" type="range" min="0" max="100" value="30" disabled>
CSS view file
/* -- Slider component ------------------------------------------- */
@layer components {
.slider {
--slider-value: 50%;
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 0.5rem;
border-radius: 9999px;
background: var(--secondary);
cursor: pointer;
outline: none;
border: none;
accent-color: var(--primary);
/* Track — WebKit (filled via gradient) */
&::-webkit-slider-runnable-track {
height: 0.5rem;
border-radius: 9999px;
background: linear-gradient(
to right,
var(--primary) 0%,
var(--primary) var(--slider-value),
var(--secondary) var(--slider-value),
var(--secondary) 100%
);
}
/* Track — Firefox */
&::-moz-range-track {
height: 0.5rem;
border-radius: 9999px;
background: var(--secondary);
border: none;
}
/* Filled portion — Firefox */
&::-moz-range-progress {
height: 0.5rem;
border-radius: 9999px;
background: var(--primary);
}
/* Thumb — WebKit */
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--background);
border: 2px solid var(--primary);
margin-top: -0.375rem;
cursor: pointer;
transition: box-shadow 150ms, transform 150ms;
}
/* Thumb — Firefox */
&::-moz-range-thumb {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--background);
border: 2px solid var(--primary);
cursor: pointer;
transition: box-shadow 150ms, transform 150ms;
}
/* Hover */
&:hover:not(:disabled) {
&::-webkit-slider-thumb {
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 15%, transparent);
}
&::-moz-range-thumb {
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 15%, transparent);
}
}
/* Focus */
&:focus-visible {
&::-webkit-slider-thumb {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
&::-moz-range-thumb {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
}
/* Disabled */
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&::-webkit-slider-thumb { cursor: not-allowed; }
&::-moz-range-thumb { cursor: not-allowed; }
}
/* Vertical orientation */
&[data-orientation="vertical"] {
writing-mode: vertical-lr;
direction: rtl;
width: 0.5rem;
height: 12rem;
&::-webkit-slider-runnable-track {
width: 0.5rem;
background: linear-gradient(
to top,
var(--primary) 0%,
var(--primary) var(--slider-value),
var(--secondary) var(--slider-value),
var(--secondary) 100%
);
}
&::-webkit-slider-thumb {
margin-top: 0;
margin-left: -0.375rem;
}
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.slider {
&::-webkit-slider-thumb { transition: none; }
&::-moz-range-thumb { transition: none; }
}
}
/* High contrast */
@media (prefers-contrast: more) {
.slider {
&::-webkit-slider-thumb {
border-width: 3px;
}
&::-moz-range-thumb {
border-width: 3px;
}
}
}
/* Forced colors (Windows High Contrast Mode) */
@media (forced-colors: active) {
.slider {
&::-webkit-slider-runnable-track {
background: ButtonFace;
border: 1px solid ButtonText;
}
&::-moz-range-track {
background: ButtonFace;
border: 1px solid ButtonText;
}
&::-moz-range-progress {
background: Highlight;
}
&::-webkit-slider-thumb {
background: ButtonText;
border-color: ButtonText;
}
&::-moz-range-thumb {
background: ButtonText;
border-color: ButtonText;
}
&:focus-visible {
&::-webkit-slider-thumb {
outline-color: Highlight;
}
&::-moz-range-thumb {
outline-color: Highlight;
}
}
}
}
}
JS view file
/* -- Slider component ------------------------------------------- */
function updateSliderValue(el) {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
}
function init() {
document.querySelectorAll('.slider:not([data-init])').forEach((el) => {
el.dataset.init = '';
updateSliderValue(el);
el.addEventListener('input', () => updateSliderValue(el));
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });