Skip to content
    Style: Wireframe

    Components · Forms

    Slider

    A native range input with a filled track and a live value. Arrows, Page Up/Down, Home and End.

    40%
    5
    30 days

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/slider.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/slider.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS and the script.

    slider.tsx
    import { useState, type CSSProperties } from 'react';
    import { cn } from '@/lib/utils';
    import { fillOf, slider } from './slider.variants';
    
    export interface SliderProps {
      id: string;
      label: string;
      /** Uncontrolled start value (use value + onValueChange to control it). */
      defaultValue?: number;
      value?: number;
      onValueChange?: (value: number) => void;
      min?: number;
      max?: number;
      step?: number;
      /** Shown after the number, e.g. "%" or " GB". */
      unit?: string;
      name?: string;
      disabled?: boolean;
      className?: string;
    }
    
    /** A native range input with a filled track and a live output. */
    export function Slider({ id, label, defaultValue = 50, value, onValueChange, min = 0, max = 100, step = 1, unit = '', name, disabled, className }: SliderProps) {
      const [own, setOwn] = useState(defaultValue);
      const v = value ?? own;
      return (
        <div className={cn(slider.root, className)}>
          <div className={slider.head}>
            <label htmlFor={id} className={slider.label}>{label}</label>
            <output htmlFor={id} data-unit={unit} className={slider.output}>{v}{unit}</output>
          </div>
          <input
            type="range" id={id} name={name} min={min} max={max} step={step} value={v} disabled={disabled} data-slider="" className={slider.input}
            style={{ '--slider-fill': fillOf(v, min, max) } as CSSProperties}
            onChange={(e) => {
              const n = Number(e.target.value);
              setOwn(n);
              onValueChange?.(n);
            }}
          />
        </div>
      );
    }
    slider.variants.ts
    /** A slider on the native range input: arrow keys, Page Up/Down, Home and End come from the browser. The track fills
     *  up to the thumb (a gradient driven by --slider-fill, kept in step by a few lines of script or by React). The
     *  thumb grows on hover and press; the focus ring goes round the whole slider. */
    export const slider = {
      root: 'grid w-full gap-3',
      head: 'flex items-baseline justify-between gap-4 text-sm',
      label: 'font-medium',
      output: 'text-muted-foreground tabular-nums',
      input:
        "h-6 w-full cursor-pointer appearance-none rounded-full bg-transparent focus-visible:outline-offset-4 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--primary)_var(--slider-fill,50%),var(--muted)_var(--slider-fill,50%))] [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-muted [&::-moz-range-progress]:h-1.5 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-primary [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-primary [&::-webkit-slider-thumb]:bg-background [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:transition-transform enabled:hover:[&::-webkit-slider-thumb]:scale-115 enabled:active:[&::-webkit-slider-thumb]:scale-125 enabled:active:[&::-webkit-slider-thumb]:bg-pressed [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-primary [&::-moz-range-thumb]:bg-background",
    };
    
    export const fillOf = (value: number, min: number, max: number) => `${((value - min) / (max - min)) * 100}%`;