stream/ui
Browse the library

Slider

A native range input with a hard-edged track and thumb, for volume, seeking, and any value on a scale.

stableSlider.tsx

Examples

With a label and readout

60%
slider/basic.tsx
"use client";

import { useState } from "react";
import { Slider } from "@streamuilib/react";

export default function SliderBasic() {
  const [value, setValue] = useState(60);

  return (
    <div className="w-full max-w-xs">
      <div className="mb-2 flex justify-between">
        <label htmlFor="slider-brightness" className="font-bold">
          Brightness
        </label>
        <span className="font-mono text-sm tabular-nums">{value}%</span>
      </div>
      <Slider id="slider-brightness" value={value} onValueChange={setValue} aria-valuetext={`${value}%`} />
    </div>
  );
}

Tones

slider/tones.tsx
"use client";

import { useState } from "react";
import { Slider, type Tone } from "@streamuilib/react";

const tones: Tone[] = ["lime", "pink", "blue", "violet", "orange"];

export default function SliderTones() {
  const [values, setValues] = useState([20, 40, 60, 80, 50]);

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      {tones.map((tone, i) => (
        <Slider
          key={tone}
          aria-label={`${tone} level`}
          tone={tone}
          value={values[i]}
          onValueChange={(v) => setValues((all) => all.map((old, j) => (j === i ? v : old)))}
        />
      ))}
    </div>
  );
}

Installation

bun add @streamuilib/react
usage.tsx
import { Slider } from "@streamuilib/react";

<Slider aria-label="Volume" value={volume} onValueChange={setVolume} />

API

PropTypeDefaultDescription
valuenumber—Current value. Required; the slider is always controlled.
onValueChange(value: number) => void—Called with the new value as a number.
min / maxnumber0 / 100Range of the slider.
stepnumber | "any"1Increment; any allows continuous values.
tone"lime" | "pink" | "blue" | "violet" | "orange""lime"Color of the filled part.
aria-valuetextstring—What screen readers announce, e.g. "1:23 of 3:05".
...propsComponentProps<"input">—Native input attributes, e.g. disabled or id.

Accessibility

  • A real <input type="range">: arrow keys, Page Up/Down, Home/End, and touch all work.
  • Give it a name with a <label> or aria-label, and use aria-valuetext when the raw number isn't meaningful.
  • The focus ring sits around the whole track, and the thumb fills with the tone while dragging.

Source

packages/react/src/Slider.tsx
"use client";

import type { ChangeEvent, ComponentProps, CSSProperties } from "react";
import { cn } from "./cn";
import type { Tone } from "./tones";

export type SliderProps = Omit<ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange"> & {
  value: number;
  min?: number;
  max?: number;
  step?: number | "any";
  /** Called with the new numeric value. */
  onValueChange?: (value: number) => void;
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
  /** Color of the filled part of the track. */
  tone?: Tone;
};

/**
 * A native range input with a brutal track and thumb (styles in theme.css).
 * Native means arrow keys, Page Up/Down, Home/End, touch, and screen
 * reader support for free. Pass `aria-valuetext` for human-readable values
 * like "1:23 of 3:05".
 */
export function Slider({
  value,
  min = 0,
  max = 100,
  step = 1,
  onValueChange,
  onChange,
  tone = "lime",
  className,
  style,
  ...props
}: SliderProps) {
  const span = max - min;
  const fill = span > 0 ? ((Math.min(max, Math.max(min, value)) - min) / span) * 100 : 0;

  return (
    <input
      type="range"
      value={value}
      min={min}
      max={max}
      step={step}
      onChange={(e) => {
        onChange?.(e);
        onValueChange?.(Number(e.target.value));
      }}
      className={cn("sui-range w-full", className)}
      style={{ ...style, "--sui-fill": `${fill}%`, "--sui-range-color": `var(--${tone})` } as CSSProperties}
      {...props}
    />
  );
}