Shadcn Angle Slider for React and Tailwind
A control for selecting an angle in a circular range.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/angle-slider.jsonpnpm dlx shadcn@latest add https://kit.dev/r/angle-slider.jsonnpx shadcn@latest add https://kit.dev/r/angle-slider.jsonyarn shadcn@latest add https://kit.dev/r/angle-slider.jsonInstall the following dependencies:
bun add @ark-ui/reactpnpm add @ark-ui/reactnpm install @ark-ui/reactyarn add @ark-ui/reactCopy and paste the following code into your project.
"use client";
import {
AngleSlider as ArkAngleSlider,
useAngleSlider as useArkAngleSlider,
useAngleSliderContext as useArkAngleSliderContext,
} from "@ark-ui/react/angle-slider";
import type React from "react";
import { cn } from "@/lib/utils";
export const useAngleSlider = useArkAngleSlider;
export const useAngleSliderContext = useArkAngleSliderContext;
export const AngleSliderContext = ArkAngleSlider.Context;
const angleSliderRootClassName = cn(
"inline-flex flex-col items-center gap-4",
"data-disabled:pointer-events-none data-disabled:opacity-64 data-disabled:grayscale"
);
export const AngleSlider = (
props: React.ComponentProps<typeof ArkAngleSlider.Root>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.Root
className={cn(angleSliderRootClassName, className)}
data-slot="angle-slider"
{...rest}
/>
);
};
export const AngleSliderRootProvider = (
props: React.ComponentProps<typeof ArkAngleSlider.RootProvider>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.RootProvider
className={cn(angleSliderRootClassName, className)}
data-slot="angle-slider"
{...rest}
/>
);
};
export const AngleSliderLabel = (
props: React.ComponentProps<typeof ArkAngleSlider.Label>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.Label
className={cn("select-none font-medium text-sm", className)}
data-slot="angle-slider-label"
{...rest}
/>
);
};
export const AngleSliderControl = (
props: React.ComponentProps<typeof ArkAngleSlider.Control>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.Control
className={cn(
"relative flex size-[var(--size)] items-center justify-center rounded-full",
"select-none bg-muted",
"[--size:100px] [--thumb-width:3px] [--track-width:4px]",
"shadow-[inset_0_2px_4px_rgb(0_0_0/0.06)]",
"before:pointer-events-none before:absolute before:inset-[var(--track-width)] before:rounded-full before:bg-background before:shadow-[inset_0_1px_3px_rgb(0_0_0/0.08)]",
"after:pointer-events-none after:absolute after:z-1 after:size-1.5 after:rounded-full after:bg-muted-foreground",
"has-focus-visible:ring-[3px] has-focus-visible:ring-ring/32",
className
)}
data-slot="angle-slider-control"
{...rest}
/>
);
};
export const AngleSliderThumb = (
props: React.ComponentProps<typeof ArkAngleSlider.Thumb>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.Thumb
className={cn(
"absolute inset-y-0 z-2 w-[var(--thumb-width)] outline-none",
"left-[calc(50%-var(--thumb-width)/2)]",
"before:absolute before:top-1 before:left-1/2 before:size-2.5 before:-translate-x-1/2",
"before:rounded-full before:border-2 before:border-background before:bg-primary",
"before:shadow-xs before:transition-transform",
"after:absolute after:top-3.5 after:left-1/2 after:w-0.5 after:-translate-x-1/2",
"after:h-[calc(var(--size)/2-18px)] after:rounded-px",
"after:bg-linear-to-b after:from-primary after:to-transparent",
"active:before:-translate-x-1/2 active:before:scale-110",
"motion-reduce:before:transition-none!",
className
)}
data-slot="angle-slider-thumb"
{...rest}
/>
);
};
export const AngleSliderMarkerGroup = (
props: React.ComponentProps<typeof ArkAngleSlider.MarkerGroup>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.MarkerGroup
className={cn(
"pointer-events-none absolute inset-0 z-0 rounded-full",
className
)}
data-slot="angle-slider-marker-group"
{...rest}
/>
);
};
export const AngleSliderMarker = (
props: React.ComponentProps<typeof ArkAngleSlider.Marker>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.Marker
className={cn(
"absolute inset-y-0 w-0.5",
"left-[calc(50%-1px)]",
"before:absolute before:top-1.5 before:left-1/2 before:h-1.5 before:w-0.5 before:-translate-x-1/2",
"before:rounded-px before:bg-border",
"data-[state=at-value]:before:bg-primary",
"data-[state=under-value]:before:bg-primary/40",
className
)}
data-slot="angle-slider-marker"
{...rest}
/>
);
};
export const AngleSliderValueText = (
props: React.ComponentProps<typeof ArkAngleSlider.ValueText>
) => {
const { className, ...rest } = props;
return (
<ArkAngleSlider.ValueText
className={cn(
"font-semibold text-xl tabular-nums leading-none",
className
)}
data-slot="angle-slider-value-text"
{...rest}
/>
);
};
export const AngleSliderHiddenInput = (
props: React.ComponentProps<typeof ArkAngleSlider.HiddenInput>
) => (
<ArkAngleSlider.HiddenInput data-slot="angle-slider-hidden-input" {...props} />
);Update the import paths to match your project setup.
For a progress-ring style dial, see Circular Slider.
Anatomy
AngleSlider
├── AngleSliderContext
├── AngleSliderLabel
├── AngleSliderControl
│ ├── AngleSliderMarkerGroup
│ │ └── AngleSliderMarker
│ └── AngleSliderThumb
├── AngleSliderValueText
└── AngleSliderHiddenInputUsage
import {
AngleSlider,
AngleSliderControl,
AngleSliderHiddenInput,
AngleSliderLabel,
AngleSliderMarker,
AngleSliderMarkerGroup,
AngleSliderThumb,
AngleSliderValueText,
} from "@/components/ui/angle-slider";<AngleSlider>
<AngleSliderLabel>Rotation</AngleSliderLabel>
<AngleSliderControl>
<AngleSliderMarkerGroup>
{[0, 45, 90, 135, 180, 225, 270, 315].map((value) => (
<AngleSliderMarker key={value} value={value} />
))}
</AngleSliderMarkerGroup>
<AngleSliderThumb />
</AngleSliderControl>
<AngleSliderValueText />
<AngleSliderHiddenInput />
</AngleSlider>Controlled
Use value and onValueChange to control the angle.
Root Provider
Use useAngleSlider with AngleSliderRootProvider when you need the API outside the tree.
States
Disabled
Examples
Steps
Use step to snap the thumb to discrete angles.
Context
Use AngleSliderContext or useAngleSliderContext to read the current value.
API Reference
asChild merges props onto a single child element. The thumb is a role="slider" with values from 0 to 360.
AngleSlider
Root. Renders a div. Sets --value and --angle on the element.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessible name for the thumb when there is no visible label. |
aria-labelledby | string | - | Id of the element that labels the thumb. Defaults to the label id. |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
defaultValue | number | 0 | Uncontrolled initial angle in degrees. |
dir | "ltr" | "rtl" | - | Text direction. Usually inherited from LocaleProvider. |
disabled | boolean | - | Disable pointer and keyboard input. |
id | string | - | Unique id for the machine. |
ids | Partial<{ root: string; thumb: string; hiddenInput: string; control: string; valueText: string; label: string }> | - | Element ids for composition. |
invalid | boolean | - | Marks the slider as invalid. |
name | string | - | Name for form submission (hidden input). |
onValueChange | (details: ValueChangeDetails) => void | - | Called when the value changes. ValueChangeDetails is { value: number }. |
onValueChangeEnd | (details: ValueChangeDetails) => void | - | Called when dragging or stepping ends. |
readOnly | boolean | - | Focusable but not editable. |
step | number | 1 | Snap increment in degrees. |
value | number | - | Controlled angle in degrees (0–360). |
| Attribute | Description |
|---|---|
data-slot | angle-slider |
data-scope | angle-slider |
data-part | root |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
| CSS variable | Description |
|---|---|
--value | Current numeric value |
--angle | Display angle used to rotate the thumb (accounts for dir) |
AngleSliderLabel
Visible label. Clicking it focuses the thumb. Renders a label.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the label. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-label |
data-scope | angle-slider |
data-part | label |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
AngleSliderControl
Circular track. Pointer down on the ring or thumb starts dragging. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the control. Override --size, --track-width, or --thumb-width here. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-control |
data-scope | angle-slider |
data-part | control |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
| CSS variable | Default | Description |
|---|---|---|
--size | 100px | Diameter of the dial |
--track-width | 4px | Thickness of the outer ring |
--thumb-width | 3px | Width of the rotating needle |
AngleSliderThumb
Draggable needle. Renders a div with role="slider". Rotated with --angle.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the thumb. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-thumb |
data-scope | angle-slider |
data-part | thumb |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
AngleSliderMarkerGroup
Container for tick marks. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-marker-group |
data-scope | angle-slider |
data-part | marker-group |
AngleSliderMarker
A tick at a given angle. Renders a div. Rotated to its value.
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Angle of the marker in degrees. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the marker. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-marker |
data-scope | angle-slider |
data-part | marker |
data-value | The marker angle |
data-state | "under-value", "at-value", or "over-value" |
data-disabled | Present when disabled |
AngleSliderValueText
Displays valueAsDegree unless you pass children. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the value. |
children | ReactNode | - | Custom content. Defaults to the formatted degree string. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-value-text |
data-scope | angle-slider |
data-part | value-text |
AngleSliderHiddenInput
Hidden input for form submission. Renders an input type="hidden".
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
| Attribute | Description |
|---|---|
data-slot | angle-slider-hidden-input |
AngleSliderRootProvider
Root alternative that takes the API from useAngleSlider. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseAngleSliderReturn | required | Return value of useAngleSlider(). |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
Pass defaultValue, step, disabled, and other machine options to useAngleSlider(), not to AngleSliderRootProvider.
useAngleSlider
Creates the angle slider API for AngleSliderRootProvider. Accepts the same options as AngleSlider except asChild and className.
const angleSlider = useAngleSlider({ defaultValue: 45, step: 15 });AngleSliderContext / useAngleSliderContext
Render-prop or hook access to slider state. Use inside AngleSlider or AngleSliderRootProvider.
| Property | Type | Description |
|---|---|---|
value | number | Current angle in degrees. |
valueAsDegree | string | Formatted degree string (for example "45deg"). |
dragging | boolean | Whether the thumb is being dragged. |
setValue | (value: number) => void | Set the angle. |
AngleSliderContext children: (context) => ReactNode.
Accessibility
The thumb is a role="slider" with aria-valuemin={0}, aria-valuemax={360}, and aria-valuenow set to the current value. Pair it with AngleSliderLabel, or pass aria-label / aria-labelledby on AngleSlider.
Keyboard support
| Key | Description |
|---|---|
ArrowRight | Increase the value by step. |
ArrowLeft | Decrease the value by step. |
ArrowUp | Decrease the value by step. |
ArrowDown | Increase the value by step. |
Home | Set the value to 0. |
End | Set the value to 360. |
Shift + Arrow | Larger step (native event step multiplier). |