Slider

Range input styled to the tokens, with an optional value label and formatter.

import { Slider }from "@/components/ui"

Specification

Selects a numeric value from a continuous range, with an optional live value label and custom formatter.

Props (10)
Every prop this component accepts, its type, default, and how to use it.
PropTypeDefaultDescription
labelstring—Visible label shown above the track and used as the aria-label.
value*number—Controlled current value.
onChange*(value: number) => void—Called with the new value as the slider moves.
minnumber0Minimum selectable value.
maxnumber100Maximum selectable value.
stepnumber1Increment between selectable values.
disabledboolean—Disables the slider.
showValueboolean—Displays the current value beside the label.
format(value: number) => stringString(v)Formats the displayed value (e.g. append a % or unit).
classNamestring—Extra classes merged onto the wrapper.

Volume & threshold

60%
25
Full source
The complete component, read from src/components/ui/slider.tsx.
slider.tsx
"use client";

import { useId } from "react";
import { cn } from "@/lib/utils";

interface SliderProps {
  label?: string;
  value: number;
  onChange: (value: number) => void;
  min?: number;
  max?: number;
  step?: number;
  disabled?: boolean;

  showValue?: boolean;

  format?: (value: number) => string;
  className?: string;
}

export function Slider({
  label,
  value,
  onChange,
  min = 0,
  max = 100,
  step = 1,
  disabled,
  showValue,
  format = (v) => String(v),
  className,
}: SliderProps) {
  const id = useId();
  const pct = ((value - min) / (max - min)) * 100;

  return (
    <div className={cn("w-full", className)}>
      {(label || showValue) && (
        <div className="mb-1.5 flex items-center justify-between text-[13px]">
          {label && <label htmlFor={id} className="font-medium text-text">{label}</label>}
          {showValue && <span className="tabular-nums text-text-secondary">{format(value)}</span>}
        </div>
      )}
      <input
        id={id}
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        disabled={disabled}
        aria-label={label}
        onChange={(e) => onChange(Number(e.target.value))}
        className={cn("ui-slider w-full", disabled && "cursor-not-allowed opacity-50")}
        style={{
          background: `linear-gradient(to right, var(--color-accent) ${pct}%, var(--color-bg-active) ${pct}%)`,
        }}
      />
    </div>
  );
}

Dependencies

To use Slider you also need the following. Copy these into your project alongside it.

Recommended folder structure

project structure
src/components/ui/
  └─ slider.tsx
src/lib/
  └─ utils.ts

Dependency files

These supporting files aren’t components — copy each one into the matching path above.

src/lib/utils.ts
utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

/**
 * Merge class names with clsx semantics + Tailwind conflict resolution.
 * Prefer this in all components.
 */
export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

/**
 * Concatenate class names, filtering out falsy values.
 * @deprecated Prefer {@link cn} — it also resolves Tailwind class conflicts.
 */
export function cx(...args: (string | false | null | undefined)[]): string {
  return args.filter(Boolean).join(" ");
}

/**
 * Focus ring style helper.
 * @deprecated Focus rings are now handled globally via the
 * `:focus-visible` rule in `globals.css` and the `focus-visible:*`
 * utilities. This remains only for backwards compatibility.
 */
export function ring(color: string = "var(--color-accent)") {
  return { outlineColor: color };
}