Slider
A shadcn-style slider component built with Ark UI primitives.
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderDemo = () => {
return (
<Slider defaultValue={72} name="volume" className="w-full max-w-xs">
<SliderLabel>Volume</SliderLabel>
<SliderField />
</Slider>
);
};
export default SliderDemo;
Installation
npx shadcn@latest add @ark-cn/sliderInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/slider.tsx
"use client";
import {
Slider as SliderPrimitive,
useSlider,
useSliderContext,
} from "@ark-ui/react/slider";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export type {
SliderFocusChangeDetails,
SliderValueChangeDetails,
} from "@ark-ui/react/slider";
export { useSlider, useSliderContext };
const normalizeValues = (
v: number | number[] | undefined,
): number[] | undefined => {
if (v === undefined) {
return undefined;
}
return typeof v === "number" ? [v] : v;
};
export type SliderProps = Omit<
SliderPrimitive.RootProps,
"defaultValue" | "value"
> & {
defaultValue?: number | number[];
value?: number | number[];
};
export const Slider = ({
className,
defaultValue,
value,
...props
}: SliderProps) => (
<SliderPrimitive.Root
className={cn(
"group/slider flex w-full flex-col gap-2 text-foreground",
"data-disabled:opacity-50 data-invalid:text-destructive",
className,
)}
data-slot="slider"
defaultValue={normalizeValues(defaultValue)}
value={normalizeValues(value)}
{...props}
/>
);
export type SliderRootProviderProps = SliderPrimitive.RootProviderProps;
export const SliderRootProvider = ({
className,
...props
}: SliderRootProviderProps) => (
<SliderPrimitive.RootProvider
className={cn(
"group/slider flex w-full flex-col gap-2 text-foreground",
"data-disabled:opacity-50 data-invalid:text-destructive",
className,
)}
data-slot="slider-root-provider"
{...props}
/>
);
export type SliderLabelProps = SliderPrimitive.LabelProps;
export const SliderLabel = ({ className, ...props }: SliderLabelProps) => (
<SliderPrimitive.Label
className={cn(
"font-medium text-foreground text-sm leading-none select-none",
className,
)}
data-slot="slider-label"
{...props}
/>
);
export type SliderValueTextProps = SliderPrimitive.ValueTextProps;
export const SliderValueText = ({
className,
...props
}: SliderValueTextProps) => (
<SliderPrimitive.ValueText
className={cn(
"font-variant-numeric text-foreground text-sm tabular-nums",
className,
)}
data-slot="slider-value-text"
{...props}
/>
);
export const SliderValue = SliderValueText;
export type SliderControlProps = SliderPrimitive.ControlProps;
export const SliderControl = ({ className, ...props }: SliderControlProps) => (
<SliderPrimitive.Control
className={cn(
"relative flex w-full touch-none select-none items-center py-1.5",
"data-[orientation=vertical]:h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[orientation=vertical]:py-0",
className,
)}
data-slot="slider-control"
{...props}
/>
);
export type SliderTrackProps = SliderPrimitive.TrackProps;
export const SliderTrack = ({ className, ...props }: SliderTrackProps) => (
<SliderPrimitive.Track
className={cn(
"relative h-2 w-full grow overflow-hidden rounded-full bg-muted",
"group-data-invalid/slider:bg-destructive/15 dark:group-data-invalid/slider:bg-destructive/24",
"data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2 data-[orientation=vertical]:shrink-0",
className,
)}
data-slot="slider-track"
{...props}
/>
);
export type SliderRangeProps = SliderPrimitive.RangeProps;
export const SliderRange = ({ className, ...props }: SliderRangeProps) => (
<SliderPrimitive.Range
className={cn(
"absolute h-full rounded-full bg-primary",
"group-data-invalid/slider:bg-destructive",
"data-[orientation=vertical]:w-full",
className,
)}
data-slot="slider-range"
{...props}
/>
);
export type SliderThumbProps = SliderPrimitive.ThumbProps;
export const SliderThumb = ({ className, ...props }: SliderThumbProps) => (
<SliderPrimitive.Thumb
className={cn(
"relative block size-5 shrink-0 cursor-grab rounded-full border-2 border-primary bg-background shadow-sm outline-none",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"group-data-invalid/slider:border-destructive group-data-invalid/slider:focus-visible:ring-destructive/24 dark:group-data-invalid/slider:focus-visible:ring-destructive/40",
"data-dragging:cursor-grabbing",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
data-slot="slider-thumb"
{...props}
/>
);
export type SliderHiddenInputProps = SliderPrimitive.HiddenInputProps;
export const SliderHiddenInput = (props: SliderHiddenInputProps) => (
<SliderPrimitive.HiddenInput data-slot="slider-hidden-input" {...props} />
);
export type SliderDraggingIndicatorProps =
SliderPrimitive.DraggingIndicatorProps;
export const SliderDraggingIndicator = ({
className,
style,
...props
}: SliderDraggingIndicatorProps) => (
<SliderPrimitive.DraggingIndicator
className={cn(
// Ark positions this element via `getDraggingIndicatorProps` (absolute + transform).
// Only add visual styling + an extra offset via the `translate` property
// (doesn't override `transform`).
"pointer-events-none z-10 rounded-md bg-foreground px-2 py-1.5 font-medium text-background text-xs tabular-nums whitespace-nowrap shadow-sm",
"data-[state=closed]:opacity-0",
// Horizontal: above the thumb. Vertical: to the side of the thumb.
"[translate:0_calc(-100%-8px)] data-[orientation=vertical]:[translate:calc(100%)_0]",
className,
)}
data-slot="slider-dragging-indicator"
style={style}
{...props}
/>
);
export type SliderMarkerGroupProps = SliderPrimitive.MarkerGroupProps;
export const SliderMarkerGroup = ({
className,
...props
}: SliderMarkerGroupProps) => (
<SliderPrimitive.MarkerGroup
className={cn(
"mt-2 flex w-full justify-between px-0.5",
"data-[orientation=vertical]:mt-0 data-[orientation=vertical]:h-full data-[orientation=vertical]:flex-col data-[orientation=vertical]:justify-between data-[orientation=vertical]:ps-3",
className,
)}
data-slot="slider-marker-group"
{...props}
/>
);
export type SliderMarkerProps = SliderPrimitive.MarkerProps;
export const SliderMarker = ({ className, ...props }: SliderMarkerProps) => (
<SliderPrimitive.Marker
className={cn(
"relative text-center text-muted-foreground text-xs",
"before:absolute before:-top-2.5 before:left-1/2 before:size-1 before:-translate-x-1/2 before:rounded-full before:bg-border",
"data-[state=at-value]:before:bg-primary data-[state=under-value]:before:bg-primary",
className,
)}
data-slot="slider-marker"
{...props}
/>
);
export type SliderContextProps = SliderPrimitive.ContextProps;
export const SliderContext = (props: SliderContextProps) => (
<SliderPrimitive.Context {...props} />
);
export type SliderFieldProps = Omit<SliderControlProps, "children"> & {
trackProps?: SliderTrackProps;
rangeProps?: SliderRangeProps;
thumbsProps?: {
children?: (index: number) => ReactNode;
};
};
export const SliderField = ({
className,
trackProps,
rangeProps,
thumbsProps,
...controlProps
}: SliderFieldProps) => (
<SliderControl className={className} {...controlProps}>
<SliderTrack {...trackProps}>
<SliderRange {...rangeProps} />
</SliderTrack>
<SliderThumbs>{thumbsProps?.children}</SliderThumbs>
</SliderControl>
);
export const SliderThumbs = ({
children,
}: {
children?: (index: number) => ReactNode;
}) => (
<SliderPrimitive.Context>
{({ value }) =>
value.map((_, index) => (
<SliderThumb key={index} index={index}>
{children?.(index)}
<SliderHiddenInput />
</SliderThumb>
))
}
</SliderPrimitive.Context>
);
Update import aliases to match your project setup.
Usage
import * as Slider from "@/components/ui/slider"Read exported parts in src/components/ui/slider.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderBasic = () => (
<div className="w-full max-w-xs">
<Slider aria-label={["Volume"]} defaultValue={50}>
<SliderLabel>Volume</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderBasic;
Range
import {
Slider,
SliderContext,
SliderField,
SliderLabel,
} from "@/components/ui/slider";
const SliderRange = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={[30, 60]}>
<SliderContext>
{({ value }) => (
<div className="flex justify-between gap-2">
<SliderLabel>Range</SliderLabel>
<span className="font-medium text-foreground text-xs tabular-nums">
{value.join(" - ")}
</span>
</div>
)}
</SliderContext>
<SliderField />
</Slider>
</div>
);
export default SliderRange;
With marks
import {
Slider,
SliderField,
SliderLabel,
SliderMarker,
SliderMarkerGroup,
} from "@/components/ui/slider";
const SliderWithMarks = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={50}>
<SliderLabel>With marks</SliderLabel>
<SliderField />
<SliderMarkerGroup>
<SliderMarker value={0}>0</SliderMarker>
<SliderMarker value={25}>25</SliderMarker>
<SliderMarker value={50}>50</SliderMarker>
<SliderMarker value={75}>75</SliderMarker>
<SliderMarker value={100}>100</SliderMarker>
</SliderMarkerGroup>
</Slider>
</div>
);
export default SliderWithMarks;
Min / max
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderMinMax = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={0} max={10} min={-10}>
<SliderLabel>-10 to 10</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderMinMax;
Step
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderStep = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={7.5} max={10} min={5} step={0.01}>
<SliderLabel>Step 0.01 (5-10)</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderStep;
Events
onValueChange: -
onValueChangeEnd: -
import { useState } from "react";
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderOnEvent = () => {
const [live, setLive] = useState("");
const [end, setEnd] = useState("");
return (
<div className="flex max-w-xs flex-col gap-2 w-full">
<Slider
defaultValue={40}
onValueChange={(details) => {
setLive(details.value.join(", "));
}}
onValueChangeEnd={(details) => {
setEnd(details.value.join(", "));
}}
className="w-full"
>
<SliderLabel>Events</SliderLabel>
<SliderField />
</Slider>
<p className="text-muted-foreground text-xs">
onValueChange:{" "}
<span className="font-medium text-foreground">{live || "-"}</span>
</p>
<p className="text-muted-foreground text-xs">
onValueChangeEnd:{" "}
<span className="font-medium text-foreground">{end || "-"}</span>
</p>
</div>
);
};
export default SliderOnEvent;
Vertical
import { Slider, SliderField } from "@/components/ui/slider";
const SliderVertical = () => (
<Slider
aria-label={["Storage size in GB"]}
defaultValue={15}
max={35}
min={5}
orientation="vertical"
>
<SliderField />
</Slider>
);
export default SliderVertical;
Vertical dragging indicator
import {
Slider,
SliderDraggingIndicator,
SliderField,
} from "@/components/ui/slider";
const SliderVerticalDraggingIndicator = () => (
<Slider
aria-label={["Storage size in GB"]}
defaultValue={15}
max={35}
min={5}
orientation="vertical"
>
<SliderField
thumbsProps={{ children: () => <SliderDraggingIndicator /> }}
/>
</Slider>
);
export default SliderVerticalDraggingIndicator;
Vertical two ranges
5-35 GB
0-100 GB
Two vertical range sliders (2 thumbs each) with different bounds.
import { Slider, SliderField } from "@/components/ui/slider";
const SliderVerticalTwoRanges = () => (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex items-start justify-between gap-6">
<div className="flex flex-col gap-2">
<p className="font-medium text-foreground text-xs">5-35 GB</p>
<Slider
aria-label={["Min storage", "Max storage"]}
defaultValue={[12, 28]}
max={35}
min={5}
orientation="vertical"
>
<SliderField />
</Slider>
</div>
<div className="flex flex-col gap-2">
<p className="font-medium text-foreground text-xs">0-100 GB</p>
<Slider
aria-label={["Min storage", "Max storage"]}
defaultValue={[25, 75]}
max={100}
min={0}
orientation="vertical"
>
<SliderField />
</Slider>
</div>
</div>
<p className="text-muted-foreground text-xs">
Two vertical range sliders (2 thumbs each) with different bounds.
</p>
</div>
);
export default SliderVerticalTwoRanges;
Center origin
import {
Slider,
SliderField,
SliderLabel,
SliderValueText,
} from "@/components/ui/slider";
const SliderCenterOrigin = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={20} origin="center">
<SliderLabel>Center origin</SliderLabel>
<div className="flex justify-between">
<span className="text-muted-foreground text-xs">
Offset from center
</span>
<SliderValueText />
</div>
<SliderField />
</Slider>
</div>
);
export default SliderCenterOrigin;
Dragging indicator
import {
Slider,
SliderDraggingIndicator,
SliderField,
SliderLabel,
} from "@/components/ui/slider";
const SliderDraggingIndicatorDemo = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={40}>
<SliderLabel>Drag me</SliderLabel>
<SliderField
thumbsProps={{ children: () => <SliderDraggingIndicator /> }}
/>
</Slider>
</div>
);
export default SliderDraggingIndicatorDemo;
Thumb overlap
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderThumbOverlap = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={[30, 70]} minStepsBetweenThumbs={10} step={1}>
<SliderLabel>Min gap (10 steps)</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderThumbOverlap;
Thumb collision behavior
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderThumbCollision = () => (
<div className="flex w-full max-w-lg flex-col gap-6 justify-center items-center">
<div className="w-full max-w-xs">
<Slider defaultValue={[25, 60]} thumbCollisionBehavior="push">
<SliderLabel>push</SliderLabel>
<SliderField />
</Slider>
</div>
<div className="w-full max-w-xs">
<Slider defaultValue={[25, 60]} thumbCollisionBehavior="swap">
<SliderLabel>swap</SliderLabel>
<SliderField />
</Slider>
</div>
</div>
);
export default SliderThumbCollision;
Context
import {
Slider,
SliderContext,
SliderField,
SliderLabel,
} from "@/components/ui/slider";
const SliderContextDemo = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={40}>
<SliderContext>
{(ctx) => (
<div className="flex justify-between gap-2">
<SliderLabel>Dragging: {String(ctx.dragging)}</SliderLabel>
<span className="font-medium text-foreground text-sm tabular-nums">
{ctx.value.join(", ")}
</span>
</div>
)}
</SliderContext>
<SliderField />
</Slider>
</div>
);
export default SliderContextDemo;
Thumb alignment
import {
Slider,
SliderField,
SliderLabel,
SliderValueText,
} from "@/components/ui/slider";
const SliderThumbAlignment = () => (
<div className="flex w-full max-w-lg flex-col gap-6">
<div className="w-full max-w-xs">
<Slider defaultValue={50} thumbAlignment="contain">
<div className="flex justify-between">
<SliderLabel>contain</SliderLabel>
<SliderValueText />
</div>
<SliderField />
</Slider>
</div>
<div className="w-full max-w-xs">
<Slider defaultValue={50} thumbAlignment="center">
<div className="flex justify-between">
<SliderLabel>center</SliderLabel>
<SliderValueText />
</div>
<SliderField />
</Slider>
</div>
</div>
);
export default SliderThumbAlignment;
With field
Helper text (Field + Slider).
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Slider, SliderField } from "@/components/ui/slider";
const SliderWithField = () => (
<Field className="w-full max-w-xs">
<FieldLabel>Opacity</FieldLabel>
<FieldDescription>Helper text (Field + Slider).</FieldDescription>
<Slider defaultValue={60}>
<SliderField />
</Slider>
</Field>
);
export default SliderWithField;
With number input
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderWithNumberInput = () => {
const [value, setValue] = useState(50);
return (
<div className="flex max-w-xs flex-col gap-3 w-full">
<Slider
max={100}
min={0}
onValueChange={(details) => {
setValue(details.value[0] ?? 0);
}}
value={value}
className="w-full"
>
<SliderLabel>Opacity</SliderLabel>
<SliderField />
</Slider>
<Input
aria-label="Matching numeric value"
className="max-w-32"
max={100}
min={0}
onChange={(event) => {
setValue(Number(event.target.value));
}}
type="number"
value={value}
/>
</div>
);
};
export default SliderWithNumberInput;
Disabled
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderDisabled = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={40} disabled>
<SliderLabel>Disabled</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderDisabled;
Form
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderForm = () => {
const [out, setOut] = useState<string | null>(null);
return (
<form
className="flex max-w-xs flex-col gap-2 w-full"
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
setOut(String(formData.get("volume") ?? ""));
}}
>
<Slider defaultValue={72} name="volume">
<SliderLabel>Volume</SliderLabel>
<SliderField />
</Slider>
<Button size="sm" type="submit" variant="default">
Submit
</Button>
{out !== null ? (
<p className="text-muted-foreground text-xs">
FormData volume:{" "}
<span className="font-medium text-foreground">{out}</span>
</p>
) : null}
</form>
);
};
export default SliderForm;
Read only
import { Slider, SliderField, SliderLabel } from "@/components/ui/slider";
const SliderReadOnly = () => (
<div className="w-full max-w-xs">
<Slider defaultValue={55} readOnly>
<SliderLabel>Read-only</SliderLabel>
<SliderField />
</Slider>
</div>
);
export default SliderReadOnly;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
Slider
| Prop | Type | Description |
|---|---|---|
| defaultValue? | number | number[] | Accepts scalar or array; normalized to Ark’s array form. |
| value? | number | number[] | Controlled value; accepts scalar or array. |
SliderField
| Prop | Type | Description |
|---|---|---|
| trackProps? | SliderTrackProps | Forwarded to the track element. |
| rangeProps? | SliderRangeProps | Forwarded to the range fill element. |
| thumbsProps? | { children?: (index: number) => ReactNode } | Custom thumb contents per index. |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.