Slider

A filled-track input for picking a value from a continuous span.

Default

Add .slider to an <input type="range">. The filled portion paints in primary; the thumb sits as a narrow vertical pill at the value's position.

<div style="max-width: 24rem;">
  <label for="basicSlider" class="form-label">Brightness</label>
  <input type="range" class="slider" id="basicSlider" value="40" />
</div>

Value display

Pair with <output> for a live readout. A one-line oninput handler keeps the display reactive; no framework required.

30
<div style="max-width: 24rem;">
  <div class="d-flex justify-content-between align-items-baseline" style="margin-bottom: 0.375rem;">
    <label for="outputSlider" class="form-label mb-0">Opacity</label>
    <output for="outputSlider" class="text-muted-foreground">30</output>
  </div>
  <input type="range" class="slider" id="outputSlider" value="30"
         oninput="this.parentElement.querySelector('output').value = this.value" />
</div>

Min and max

Set min and max to bound the slider. Defaults to 0 through 100.

<div style="max-width: 24rem;">
  <label for="minMaxSlider" class="form-label">Year</label>
  <input type="range" class="slider" min="2000" max="2030" value="2026" id="minMaxSlider" />
</div>

Steps

Add step to snap the thumb to fixed increments. Drop it (or use any) for fully continuous movement.

<div style="max-width: 24rem;">
  <label for="stepSlider" class="form-label">Rating</label>
  <input type="range" class="slider" min="0" max="5" step="0.5" value="3" id="stepSlider" />
</div>

Sizes

Three sizes match the .input family. The thumb is shorter than the track so the clipped sliver at value extremes is less prominent, and every dimension scales with --st-density.

<div class="demo-stack" style="max-width: 24rem;">
  <div>
    <label for="smSlider" class="form-label">Small</label>
    <input type="range" class="slider slider--sm" id="smSlider" value="40" />
  </div>
  <div>
    <label for="defaultSlider" class="form-label">Default</label>
    <input type="range" class="slider" id="defaultSlider" value="60" />
  </div>
  <div>
    <label for="lgSlider" class="form-label">Large</label>
    <input type="range" class="slider slider--lg" id="lgSlider" value="80" />
  </div>
</div>

Disabled

Add the disabled attribute to dim the track and thumb and block interaction.

<div style="max-width: 24rem;">
  <label for="disabledSlider" class="form-label">Playback speed</label>
  <input type="range" class="slider" id="disabledSlider" value="40" disabled />
</div>

Browser validation

Native :user-invalid fires after the user moves the thumb past the allowed range. Usually a step mismatch on a constrained input. A danger border paints around the track; fill and thumb keep their brand colors.

<div style="max-width: 24rem;">
  <label for="invalidSlider" class="form-label">Quantity (must land on a multiple of 10)</label>
  <input type="range" class="slider" id="invalidSlider" min="0" max="100" step="10" value="35" />
</div>

The seeded value sits between steps. Click the thumb then click away to trip :user-invalid; drag to a multiple of 10 to clear it.

Server validation

Set aria-invalid="true" from your form library. The attribute is sticky, so the red border persists until the library clears it.

<div style="max-width: 24rem;">
  <label for="srvSlider" class="form-label">Threshold</label>
  <input type="range" class="slider" id="srvSlider" aria-invalid="true" value="60" />
</div>

How the fill works

No JavaScript. Two browser paths:

  • Firefox exposes ::-moz-range-progress, a native pseudo whose width tracks the input's value. We paint it with --slider-fill.
  • Webkit / Blink doesn't expose a progress pseudo. The thumb gets a giant box-shadow (-100vmax 0 0 100vmax var(--slider-fill)) whose right edge lands at the thumb's right edge; overflow: hidden on the runnable track clips the shadow to the track shape. The shadow follows the thumb because the browser moves the thumb natively.

Customization

Ten variables retune .slider without touching component CSS. Override on .slider itself, on a parent scope, or on :root. The cascade scopes the change.

Variable Default Use
--slider-h 2.25rem Track + shell height (matches .input)
--slider-thumb-w 0.5rem Thumb width (narrow vertical pill)
--slider-thumb-h 1.25rem Thumb height (centered inside track)
--slider-thumb-gap 0.25rem Visible space between thumb's right edge and unfilled track
--slider-radius 999px Track + thumb corner radius (pill)
--slider-track-bg var(--st-neutral) Unfilled segment
--slider-fill var(--st-primary) Filled segment
--slider-thumb-bg #fff Thumb fill
--slider-thumb-shadow 0 1px 3px oklch(0 0 0 / 0.25) Thumb edge definition
--slider-ring var(--st-ring) Focus halo color

Track and thumb opt out of --st-radius. Pill is semantic. It isn't a style choice. Override --slider-radius per-component if you really want a square track.