DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

CircularSlider

A range slider bent into a ring. One or two handles, an arc fill, tick dots, wrapping clock scales, and open-arc gauges. Full keyboard operation and a per-handle accessible name.

Playground

Installation

npx shadcn@latest add https://tessinaui.com/r/circular-slider.json

Usage

import { CircularSlider } from "@tessinaui/ui";
<CircularSlider
  label="Brightness"
  value={value}
  onValueChange={(v) => setValue(v as number)}
  formatValue={(v) => `${v}%`}
>
  <span>{value}%</span>
</CircularSlider>

When to use it

A ring, not a bar, when the value is cyclical or has no natural end — a time of day, a compass heading, a hue — or when the control has to sit inside a round read-out, which is what every sleep, thermostat and dimmer screen in the research does. For a plain bounded quantity in a form, use Slider: a straight track is easier to aim at and reads faster.

It is a separate component rather than a slider variant because the interaction model differs, not just the paint. A linear slider maps one axis to a value; this maps an angle, and on a wrapping scale the shorter way between two values can be through the end of the range.

Examples

Default

A single handle with a centre read-out.

Sleep range

Two handles on a wrapping 24-hour scale. Bedtime after wake-up is the normal way to say "overnight", so the arc runs forward through midnight rather than backwards around the dial. Each handle carries its own name — "Sleep, 22:00 to 06:00" on one control does not tell a screen-reader user which end is about to move.

Gauge

An open arc: sweep={270} with startAngle={225}, the shape thermostats use. wrap is ignored when the arc has a gap — there is no seam to travel through, and silently teleporting a handle across it would be worse than clamping.

Accessibility

  • Every handle is a role="slider" with aria-valuemin / aria-valuemax / aria-valuenow, focusable in source order.
  • Pass formatValue. A bare 22 on a clock face means nothing; it becomes aria-valuetext="22:00".
  • A range takes two labels, one per handle.
  • Keyboard: arrows step, Shift+arrow and PageUp/PageDown take the large step, Home/End jump to the ends. On a wrapping scale the arrows carry through the seam.
  • The SVG ring is aria-hidden and focusable="false" — the handles carry all the semantics, so assistive tech never meets a nameless graphic.
  • Handles keep a 44px touch target on phones via an invisible centred pad, so the visible handle can stay proportionate to a small ring (WCAG 2.5.5).
  • readOnly keeps focus and announces aria-readonly; disabled removes the handles from the tab order.

API Reference

CircularSlider

PropTypeDefaultDescription
valuenumber | [number, number]—Controlled value. A two-number array makes it a range
defaultValuenumber | [number, number]minUncontrolled initial value
minnumber0Minimum
maxnumber100Maximum
stepnumber1Step increment
largeStepnumberstep × 10Step for PageUp/PageDown and Shift+arrow
onValueChange(value) => void—Fires on every change
onValueCommitted(value) => void—Fires once at the end of a drag, or immediately for a keyboard step
size"xs" | "sm" | "md" | "lg" | "xl""md"Ring diameter preset
diameternumberfrom sizeDiameter in px, overriding size
thicknessnumber7.5% of diameterTrack thickness in px
startAnglenumber0Where min sits, in degrees clockwise from 12 o'clock
sweepnumber360How much of the circle the scale spans, in degrees
wrapbooleanfalseWhether the scale wraps at the ends. Forced off when sweep < 360
intent"none" | "error" | "warning" | "success" | "info""none"Arc and focus-ring colour
ticksnumber | number[]—Tick dots: a count, or explicit values
handleIconReactNode | [ReactNode, ReactNode]—Icon inside the handle; an array names each end
labelstring | [string, string]"Value"Accessible name. A range takes two
formatValue(value: number) => string—Human text for aria-valuetext and your own read-out
disabledbooleanfalse—
readOnlybooleanfalseFocusable, not changeable
childrenReactNode—Rendered in the middle of the ring

Sizes:

SizeDiameterHandle
xs112px24px
sm136px28px
md160px32px
lg200px36px
xl248px40px

CircularSliderSkeleton

Placeholder ring. Its box comes from the component's own diameter map, so the two cannot drift.

PropTypeDefaultDescription
sizeCircularSliderSize"md"Diameter preset
diameternumberfrom sizeDiameter in px
SliderMediaTrimmer

On this page

PlaygroundInstallationUsageWhen to use itExamplesDefaultSleep rangeGaugeAccessibilityAPI ReferenceCircularSliderCircularSliderSkeleton