Slider
A range slider - the filled range and thumb take 4 semantic tones across 3 sizes, single or multi-thumb, horizontal or vertical.
import { Slider } from "@/components/ui/slider"
export default function SliderDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<Slider defaultValue={[50]} tone="brand" />
<Slider defaultValue={[25, 75]} tone="success" size="lg" />
<Slider defaultValue={[40]} tone="danger" size="sm" disabled />
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/sliderDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/slider.tsx"use client"
import * as React from "react"
import { Slider as SliderPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const sliderTrackVariants = cva(
"relative grow overflow-hidden rounded-full bg-surface-3 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full",
{
variants: {
size: {
sm: "data-[orientation=horizontal]:h-1 data-[orientation=vertical]:w-1",
md: "data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5",
lg: "data-[orientation=horizontal]:h-2 data-[orientation=vertical]:w-2",
},
},
defaultVariants: { size: "md" },
}
)
const sliderRangeVariants = cva(
"absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
{
variants: {
tone: {
neutral: "bg-primary",
brand: "bg-brand",
success: "bg-success",
danger: "bg-danger",
},
},
defaultVariants: { tone: "neutral" },
}
)
const sliderThumbVariants = cva(
"relative block shrink-0 rounded-full border bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] duration-(--motion-fast) after:absolute after:-inset-1.5 hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
{
variants: {
size: {
sm: "size-3.5",
md: "size-4",
lg: "size-5",
},
tone: {
neutral: "border-primary",
brand: "border-brand",
success: "border-success",
danger: "border-danger",
},
},
defaultVariants: { size: "md", tone: "neutral" },
}
)
interface SliderProps
extends React.ComponentProps<typeof SliderPrimitive.Root>,
VariantProps<typeof sliderThumbVariants> {}
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
size,
tone,
...props
}: SliderProps) {
const values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
data-tone={tone ?? "neutral"}
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none select-none items-center data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(sliderTrackVariants({ size }))}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(sliderRangeVariants({ tone }))}
/>
</SliderPrimitive.Track>
{Array.from({ length: values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className={cn(sliderThumbVariants({ size, tone }))}
/>
))}
</SliderPrimitive.Root>
)
}
export {
Slider,
sliderTrackVariants,
sliderRangeVariants,
sliderThumbVariants,
type SliderProps,
}Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import { Slider } from "@/components/ui/slider"
<Slider defaultValue={[50]} tone="brand" max={100} step={1} />Pass an array to defaultValue (or value) - the number of thumbs follows its length. Both axes are optional; the default is the neutral medium slider.
Examples
Tones
The tone colors the filled range and the thumb border; the track stays on a neutral surface token.
Sizes
Range
A two-entry value renders two thumbs, so people can drag both ends of a range independently.
Vertical
Set orientation="vertical" for a vertical control. Give the container enough height for the range to travel.
Disabled
Props
Slider also accepts every radix Slider prop - min, max, step, onValueChange and disabled included.
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "neutral" | "brand" | "success" | "danger" | "neutral" | Semantic color of the filled range and the thumb border, mapped to theme tokens. Emitted as data-tone on the root. |
size | "sm" | "md" | "lg" | "md" | Track thickness and thumb diameter, scaled together. |
defaultValue | number[] | [min, max] | Uncontrolled starting value. The number of thumbs follows the array length, so one entry renders a single thumb and two render a range. |
value | number[] | - | Controlled value. Drive it together with onValueChange from React state. |
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction. A vertical slider gets a built-in minimum height so it has room to travel. |
ai2 Slider: a range slider for React with semantic tones and sizes
The ai2 Slider is a shadcn-compatible slider component for React, built on the radix-ui Slider primitive and styled with Tailwind CSS v4. It lets people pick a number or a range by dragging a thumb along a track, with a 4-tone by 3-size matrix from a single cva: the filled range and the thumb border take a semantic color while the track stays on a neutral surface token.
It ships through the shadcn registry format, so one CLI command, an MCP agent, or a copy-paste puts the source in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Slider?
It is a single Slider export that renders the radix Slider.Root, a Track with a filled Range, and one Thumb per value. The anatomy matches shadcn/ui exactly, extended with two cva axes: tone (neutral, brand, success, danger) and size (sm, md, lg).
The number of thumbs follows the length of value or defaultValue, so one entry gives a single-value slider and two give a range. The tone colors both the range fill and the thumb border, the size scales the track and the thumb together, and an orientation prop switches the same component between horizontal and vertical.
Why use it
- Accessible by construction: The radix-ui Slider primitive renders real slider roles with aria-valuenow, moves with arrow keys, Home and End, and supports multiple thumbs out of the box.
- Semantic tones: neutral for ordinary controls, brand for primary settings like volume, success for healthy ranges, danger for risky limits. The tone colors the filled range and thumb border, and is emitted as data-tone for styling.
- Three coherent sizes: sm, md and lg scale the track thickness and the thumb diameter together from one cva, so a compact slider and a large one feel like the same component.
- Single, range and vertical: Pass one value for a single thumb or two for a range, and set orientation="vertical" for a vertical control. The vertical layout carries a built-in minimum height so it has room to travel.
- Agent-readable metadata: The registry item states the tone and size matrix and the single, multi and vertical support in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its radix-ui dependency and the @ai2/tokens theme to your project.
- 4 tones x 3 sizes: Twelve combinations from two cva axes, all resolved against theme tokens rather than hardcoded colors.
- Multi-thumb ranges: The thumb count follows the value array, so a two-entry value renders a draggable low and high handle for min and max selection.
- Horizontal and vertical: One orientation prop flips the layout; the vertical variant gets a minimum height so the track is always usable.
- Controlled and uncontrolled state: Use defaultValue for simple cases or drive value and onValueChange from React state when other UI depends on the number.
- Data attributes for styling: The parts expose data-slot="slider", "slider-track", "slider-range" and "slider-thumb", plus data-tone and data-orientation, so you can restyle from CSS without forking the component.
Production tips
- Match the value array to the thumbs you want: One number renders a single thumb, two render a range. Keep the array length stable across renders so thumbs are not added or removed while dragging.
- Set min, max and step for real units: The defaults are 0 to 100 with a step of 1. Pass explicit min, max and step when the slider maps to a domain like price, volume or opacity.
- Give vertical sliders their space: A vertical slider needs height from its container. The component adds a minimum height, but place it in a tall enough parent so the range reads clearly.
- Reserve danger for real limits: A red range reads as a warning. Use tone="danger" for thresholds like spend caps or rate limits, not for ordinary settings.
- Show the current value nearby: A slider communicates position, not an exact number. Pair it with a label or readout so people can see the precise value they picked.
Works with the rest of ai2
The slider composes naturally with the rest of the registry. Pair it with an ai2 Label to name the setting, lay out controls in a ai2 Field, and group related sliders inside an ai2 Card with ai2 Separator rows between sections.
When the choice is a plain on or off rather than a range, use Switch instead, and reach for Input when people need to type an exact number. All three share the same tone vocabulary and token source, so mixed forms stay visually consistent in both modes.