Expressive

Selection & input

Slider

Expressive slider with a bar handle; single value or range.

Volume40
Price range

Sliders pick a value or range from a continuous scale. The M3 Expressive slider has a thick track and a bar-shaped handle that narrows while you drag it.

Installation

npx @grenishrai/expressive add slider

This copies slider.tsx into your components folder, along with what it needs from Expressive (utils, motion, styles), and installs @base-ui/react.

Usage

import { Slider } from "@/components/expressive/slider";

<Slider defaultValue={40} thumbLabels={["Volume"]} />
<Slider defaultValue={[20, 80]} thumbLabels={["Minimum price", "Maximum price"]} />

Props

Slider

PropTypeDefaultDescription
value / defaultValuenumber | number[]—One number for a single thumb, an array for a range.
onValueChange(value) => void—Called while dragging.
min / max / stepnumber0 / 100 / 1Range and granularity.
thumbLabelsstring[]—Accessible names for each thumb.
orientation"horizontal" | "vertical""horizontal"Layout direction.
disabledboolean—Prevents interaction.
motion"expressive" | "standard" | "fluid" | "none"theme defaultMotion scheme for this component's transitions. none turns animation off.

Accessibility

  • Each thumb is a role="slider" input with aria-valuenow, min and max.
  • Arrow keys step by step; Page Up/Down and Home/End jump.
  • Name every thumb with thumbLabels (or aria-labelledby on the slider).

Notes

  • The gap around the handle is a ring in --ex-slider-gap-color (default surface). Set it to the background color when the slider sits on another surface.

Source

This is the file the CLI writes into your project, shown with the default import paths. It's yours to change.

components/slider.tsx
"use client";

import { Slider as BaseSlider } from "@base-ui/react/slider";
import { cn } from "@/lib/expressive/utils";
import type { MotionProp } from "@/lib/expressive/use-motion";

export interface SliderProps extends Omit<BaseSlider.Root.Props, "className"> {
  className?: string;
  motion?: MotionProp;
  /** Accessible names for the thumbs, e.g. `["Minimum price", "Maximum price"]`. */
  thumbLabels?: string[];
}

/*
 * M3 Expressive slider: a 16px track with a bar-shaped handle that narrows
 * while dragged. The gap around the handle is drawn with a ring in
 * `--ex-slider-gap-color` (defaults to `surface`); set it to the slider's
 * background when it sits on another surface.
 */
export function Slider({ className, motion, thumbLabels, value, defaultValue, ...props }: SliderProps) {
  const current = value ?? defaultValue;
  const count = Array.isArray(current) ? current.length : 1;

  return (
    <BaseSlider.Root
      data-slot="slider"
      data-motion={motion}
      value={value}
      defaultValue={defaultValue}
      className={cn("group w-full data-disabled:opacity-(--ex-disabled-content)", className)}
      {...props}
    >
      <BaseSlider.Control className="flex h-11 w-full touch-none items-center select-none">
        <BaseSlider.Track className="relative h-4 w-full rounded-shape-full bg-secondary-container">
          <BaseSlider.Indicator className="rounded-shape-full bg-primary group-data-disabled:bg-on-surface" />
          {Array.from({ length: count }, (_, index) => (
            <BaseSlider.Thumb
              key={index}
              index={index}
              aria-label={thumbLabels?.[index]}
              className={cn(
                "h-11 w-1 cursor-grab rounded-shape-full bg-primary select-none",
                "ring-6 ring-(--ex-slider-gap-color,var(--ex-color-surface))",
                "transition-[width] spring-spatial-fast data-dragging:w-0.5 data-dragging:cursor-grabbing",
                "has-focus-visible:outline-(length:--ex-focus-ring-width) has-focus-visible:outline-offset-(--ex-focus-ring-offset) has-focus-visible:outline-(--ex-focus-ring-color) has-focus-visible:outline-solid",
                "group-data-disabled:bg-on-surface",
              )}
            />
          ))}
        </BaseSlider.Track>
      </BaseSlider.Control>
    </BaseSlider.Root>
  );
}