Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Slider

The native range input, restyled.

Installation

pnpm dlx zazz-ui add slider
npx zazz-ui add slider
yarn dlx zazz-ui add slider
bunx zazz-ui add slider

Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

<input
  type="range"
  name="volume"
  min="0"
  max="100"
  step="10"
  value="50"
  aria-label="Volume"
  style="--max-w: 96"
/>
/**
 * slider.css — Range input (input[type="range"])
 *
 * @layer      variables, reset
 * @requires   _layers.css, _variables.css
 * @uses       appearance: none — redraws track + thumb from theme tokens
 * @tokens     --ui-slider-* (@layer variables, on :root so subtree hooks work)
 *
 * Note: The active region is NOT filled (parity across engines); add a paint
 * via JS if a filled track is needed. The thumb is centered on the track with a
 * calc'd negative margin so retuning either size keeps it aligned.
 */

@layer variables {
  :root {
    --ui-slider-ring-color: var(--color-ring);

    --ui-slider-track-h: calc(var(--spacing) * 1.5);
    --ui-slider-track-bg: var(--color-muted);
    --ui-slider-track-border: 1px solid var(--color-muted);
    --ui-slider-track-rounded: var(--radius-full);
    --ui-slider-thumb-size: calc(var(--spacing) * 4);
    --ui-slider-thumb-bg: var(--color-background);

    --ui-slider-thumb-shadow: var(--shadow-xs);
    --ui-slider-thumb-border: 1px solid var(--color-border);
    --ui-slider-thumb-rounded: var(--radius-full);
  }
}

@layer reset {
  :where(input[type="range"]) {
    /* derived on the element so a subtree --ui-slider-track-h keeps the thumb centered */
    --ui-slider-thumb-offset: calc(var(--ui-slider-track-h) * -1);

    -webkit-appearance: none;
    appearance: none;
    inline-size: 100%;
    background: transparent;
    cursor: grab;
    border-radius: var(--ui-slider-track-rounded);

    /* transparent outline keeps focus visible in forced-colors / high-contrast
       modes; the visible focus ring lives on the thumb via --shadow-ring */
    outline: var(--outline-width) var(--outline-style) transparent;
    outline-offset: var(--outline-offset);
  }

  :where(input[type="range"])::-webkit-slider-runnable-track {
    block-size: var(--ui-slider-track-h);
    inline-size: 100%;
    background: var(--ui-slider-track-bg);
    border: var(--ui-slider-track-border);
    border-radius: var(--ui-slider-track-rounded);
  }

  :where(input[type="range"])::-moz-range-track {
    block-size: var(--ui-slider-track-h);
    inline-size: 100%;
    background: var(--ui-slider-track-bg);
    border: var(--ui-slider-track-border);
    border-radius: var(--ui-slider-track-rounded);
  }

  :where(input[type="range"])::-webkit-slider-thumb {
    /* the ring is a box-shadow so it composes with --ui-slider-thumb-shadow;
       widths animate from 0 so it grows in on focus */
    --_ring-offset-width: 0px;
    --_ring-width: 0px;
    --_ring-fill: color-mix(in oklch, var(--ui-slider-ring-color) var(--ring-opacity), transparent);
    box-shadow:
      0 0 0 var(--_ring-offset-width) var(--ring-offset-shadow-color),
      0 0 0 calc(var(--_ring-offset-width) + var(--_ring-width))
        var(--_ring-fill, var(--ring-shadow-color)),
      var(--ui-slider-thumb-shadow);

    -webkit-appearance: none;
    appearance: none;
    margin-block-start: var(--ui-slider-thumb-offset);
    inline-size: var(--ui-slider-thumb-size);
    block-size: var(--ui-slider-thumb-size);
    background-color: var(--ui-slider-thumb-bg);
    border: var(--ui-slider-thumb-border);
    border-radius: var(--ui-slider-thumb-rounded);

    transition: var(--default-transition);
  }

  :where(input[type="range"])::-moz-range-thumb {
    /* the ring is a box-shadow so it composes with --ui-slider-thumb-shadow;
       widths animate from 0 so it grows in on focus */
    --_ring-offset-width: 0px;
    --_ring-width: 0px;
    --_ring-fill: color-mix(in oklch, var(--ui-slider-ring-color) var(--ring-opacity), transparent);

    appearance: none;
    inline-size: var(--ui-slider-thumb-size);
    block-size: var(--ui-slider-thumb-size);
    background-color: var(--ui-slider-thumb-bg);
    border: var(--ui-slider-thumb-border);
    border-radius: var(--ui-slider-thumb-rounded);

    box-shadow:
      0 0 0 var(--_ring-offset-width) var(--ring-offset-shadow-color),
      0 0 0 calc(var(--_ring-offset-width) + var(--_ring-width))
        var(--_ring-fill, var(--ring-shadow-color)),
      var(--ui-slider-thumb-shadow);

    transition: var(--default-transition);
  }

  @media (hover: hover) {
    :where(input[type="range"]:hover) {
      --ui-slider-thumb-border: 1px solid var(--color-primary);
    }
  }

  :where(input[type="range"]:active) {
    --ui-slider-thumb-shadow: var(--shadow-xs), var(--shadow-ring);

    cursor: grabbing;
  }

  :where(input[type="range"]:focus-visible) {
    --ui-slider-thumb-shadow: var(--shadow-xs), var(--shadow-ring);
  }

  :where(input[type="range"]:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

Attributes

This primitive is a composition of other primitives and utilities: it has no presets, slots, or hooks of its own. Copy its example and adapt it.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

slider hooks
HookDefaultState
--ui-slider-ring-colorvar(--color-ring)—
--ui-slider-thumb-bgvar(--color-background)—
--ui-slider-thumb-border1px solid var(--color-border)—
--ui-slider-thumb-offsetcalc(var(--ui-slider-track-h) * -1)—
--ui-slider-thumb-roundedvar(--radius-full)—
--ui-slider-thumb-shadowvar(--shadow-xs)—
--ui-slider-thumb-sizecalc(var(--spacing) * 4)—
--ui-slider-track-bgvar(--color-muted)—
--ui-slider-track-border1px solid var(--color-muted)—
--ui-slider-track-hcalc(var(--spacing) * 1.5)—
--ui-slider-track-roundedvar(--radius-full)—