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.

0.5
<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.

50
<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 });