Primitives
Slider
Single-value range control with branded and plain treatments.
Composition
Value control
- <Slider />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/slider.tsx.
npx shadcn@latest add https://control-ui.dev/r/slider.jsonRaw code
Primary installed source
"use client";import { Slider as SliderPrimitive } from "@base-ui/react/slider";import type { CSSProperties } from "react";import type { RangeKnobStyle } from "@/components/control-ui/knob-contracts/range-knobs";import { cn } from "@/components/control-ui/lib/cn";export type SliderVariant = "default" | "plain";export type SliderProps = Omit< { variant?: SliderVariant; value?: number; defaultValue?: number; onValueChange?: (value: number) => void; onValueCommitted?: (value: number) => void; orientation?: "horizontal" | "vertical"; min?: number; max?: number; step?: number; disabled?: boolean; label?: string; showValue?: boolean; formatValue?: (value: number) => string; className?: string; "aria-label"?: string; }, "style"> & { style?: CSSProperties & RangeKnobStyle };const MAX_VISIBLE_STEP_TICKS = 50;function tickPositions(min: number, max: number, step: number | undefined): number[] { if (typeof step !== "number" || step <= 0 || !Number.isFinite(step)) return []; const range = max - min; if (range <= 0) return []; const stepCount = Math.round(range / step); if (stepCount <= 1 || stepCount > MAX_VISIBLE_STEP_TICKS) return []; return Array.from({ length: stepCount - 1 }, (_, i) => ((i + 1) / stepCount) * 100);}function displaySliderValue( formattedValues: readonly string[], values: readonly number[], formatValue?: (value: number) => string,): string { const value = values[0]; if (value === undefined) return ""; return formatValue ? formatValue(value) : (formattedValues[0] ?? "");}export function Slider({ className, variant = "default", label, showValue, formatValue, value, defaultValue, onValueChange, onValueCommitted, orientation = "horizontal", min = 0, max = 100, step, disabled, style, "aria-label": ariaLabel, ...props}: SliderProps) { const showValueResolved = showValue ?? Boolean(label); const labeled = variant === "plain" && (label !== undefined || showValue === true); const ticks = labeled ? tickPositions(min, max, step) : []; return ( <SliderPrimitive.Root<number> data-control-ui="slider" data-range-kind="slider" data-slot="root" data-control-family="range" data-variant={variant} orientation={orientation} data-labeled={labeled ? "true" : undefined} value={value} defaultValue={defaultValue} min={min} max={max} step={step} disabled={disabled} onValueChange={onValueChange} onValueCommitted={onValueCommitted} className={cn( "group relative flex cursor-pointer select-none items-center data-[disabled]:cursor-not-allowed", orientation === "vertical" ? "min-h-20 w-fit touch-pan-x" : "w-full touch-pan-y", className, )} style={style} {...props} > <SliderPrimitive.Control data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="control" data-labeled={labeled ? "true" : undefined} className={cn("flex items-center", orientation === "vertical" ? "h-full" : "w-full")} > <SliderPrimitive.Track data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="track" data-variant={variant} data-labeled={labeled ? "true" : undefined} className={cn("relative grow", orientation === "vertical" ? "h-full" : "w-full")} style={style} > <SliderPrimitive.Indicator data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="indicator" data-variant={variant} style={style} /> {ticks.map((pct) => ( <span key={pct} aria-hidden data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="tick" className="pointer-events-none absolute" style={orientation === "vertical" ? { bottom: `${pct}%` } : { left: `${pct}%`, bottom: 0 }} /> ))} <SliderPrimitive.Thumb aria-label={ariaLabel ?? (labeled ? undefined : label)} getAriaValueText={formatValue ? (_, thumbValue) => formatValue(thumbValue) : undefined} data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="thumb" data-variant={variant} data-focus-ring="within" className="block" style={style} /> </SliderPrimitive.Track> </SliderPrimitive.Control> {labeled && <SliderLabelOverlay label={label} showValue={showValueResolved} formatValue={formatValue} />} </SliderPrimitive.Root> );}function SliderLabelOverlay({ label, showValue, formatValue }: Pick<SliderProps, "label" | "showValue" | "formatValue">) { return ( <div data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="label-overlay" className="pointer-events-none absolute inset-0 z-10 flex items-center justify-between" > {label ? ( <SliderPrimitive.Label data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="label" className="select-none" > {label} </SliderPrimitive.Label> ) : ( <span /> )} {showValue && ( <SliderPrimitive.Value data-control-ui="slider" data-control-family="range" data-range-kind="slider" data-slot="value" className="select-none" > {(formattedValues, values) => displaySliderValue(formattedValues, values, formatValue)} </SliderPrimitive.Value> )} </div> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-range-* · 11 knobsHow the cascade resolves--cui-range-level-warning-background<color>var(--warning)--cui-range-level-clip-background<color>var(--destructive)--cui-range-indeterminate-size<length-percentage>40%--cui-range-track-radius<length-percentage>9999px--cui-range-track-background<color>var(--muted)--cui-range-indicator-radius<length-percentage>9999px--cui-range-indicator-background<color>var(--primary)--cui-range-thumb-radius<length-percentage>9999px--cui-range-thumb-background<color>var(--background)--cui-range-thumb-border-color<color>var(--control-rim)--cui-range-thumb-shadow*var(--shadow-sm)