Dial

Circular numeric control with a tick ring that fills from min to the current value, drag axis, wrap, invert, progress outline, sizes, and label placement. Prefer Slider for a linear track, and Tick Slider for bound labels with a tick gauge.

Also known as: knob, circular slider, radial slider, rotary, circular control, radial control

Installation

Install with the CLI, or copy the component source into your project.

Add dial (npm)

npx --yes -p @default-file/ui df-ui add dial

Default

Single value on a 0 to 100 ring. Drag in any direction. scrubSound follows position.

Sizes

sm, md, and lg remap ring diameter, ticks, thumb, and type.

Inside label

A short label and the live value sit in the center.

M42
T18
D81

Outside labels

Place the label at the top, right, bottom, or left of the ring. labelGap sets the space between the label and the ring.

Top
Right
Bottom
Left

Percent value

valueFormat percent maps min to max onto 0% to 100%. The inside label stays muted so the percent is primary.

Vol63%

Tick indicator

thumb tick accents the nearest mark and hides the dot.

48

Filled face

faceBackground, faceBorder, and accent restyle the ring.

A35

Drag axis

dragAxis all uses every direction. horizontal uses only left and right. vertical uses only up and down. The indicator still travels around the ring.

40
All
40
Horizontal
40
Vertical

Full turn

wrap with min 0 and max 360 spins with no stop. Ticks do not fill from an origin. invert reverses drag so the value can go negative when min is below 0.

Turn40
Invert0

Progress outline

progress draws a ring on the face edge from startAngle to the current value. Ticks sit inside that rim. wrap with min 0 and max 360 keeps the ring spinning.

X35°
Y287°
Z258°

Disabled

Blocks pointer and keyboard interaction.

Off40

Properties

Circular numeric control with a tick ring that fills from min to the current value, optional center or edge label, and a moving indicator. Pointer travel raises or lowers the value. wrap spins through a full turn with no origin fill. invert reverses drag so reverse rotation can go negative. progress draws a rotation outline on the face. Prefer Slider for a linear track, and Tick Slider for bound labels with a tick gauge.

Value

PropTypeDefaultNotes
minnumber0Minimum value.
maxnumber100Maximum value.
stepnumber1Increment between committed values. Values of 0 or less fall back to 1.
valuenumber-Controlled value. Snapped to step and clamped, or wrapped when wrap is set.
defaultValuenumberminUncontrolled initial value.
onValueChange(value: number) => void-Called when a committed value changes.
wrapbooleanfalseWhen true, dragging or stepping past max continues at min, and past min continues at max. Ticks do not fill from an origin unless tickFill is set. Leave false to clamp.
invertbooleanfalseReverse pointer travel and arrow keys. Right and up decrease. Set min below 0 so reverse rotation can go negative.
dragAxis"all" | "horizontal" | "vertical""all"Pointer axis that changes the value. all maps right and up to increase, left and down to decrease, and diagonals combine. horizontal uses only left and right. vertical uses only up and down. The indicator still travels around the ring.
startAnglenumber0Ring heading in degrees clockwise from the top. Gauge min sits here when min is 0 or greater. When min is below 0 and max is above 0, value 0 sits here.
sweepnumber360How much of the ring the range uses. 360 is a full ring. Use less than 360 to keep the ends on distinct points.
formatValue(value: number) => string-Formats the visible value and aria valuetext. Overrides valueFormat.
valueFormat"number" | "percent""number"Built in readout format. number uses step precision. percent shows position across min to max. Ignored when formatValue is set.
showValuebooleanfalseShow the formatted value in the center of the ring.
disabledboolean-Disables pointer and keyboard interaction.

Label

PropTypeDefaultNotes
labelReact.ReactNode-Optional label. Use a short mark for inside, or any copy for an outside edge. An inside label next to a value uses smaller muted type so the value stays primary.
labelPosition"inside" | "top" | "bottom" | "left" | "right""inside"Where the label sits. inside places it in the center of the ring. top, bottom, left, and right sit outside the ring.
labelGapstring-Gap between an outside label and the ring. Prefer a kit length token such as calc(4 * var(--spacing-unit)). Ignored when the label is inside.

Appearance

PropTypeDefaultNotes
size"sm" | "md" | "lg""md"Ring diameter and matching tick, thumb, and type sizes. sm is compact, md is the default, lg is a large ring.
thumb"dot" | "tick""dot"Value indicator. dot is a circular thumb on the ring. tick hides the thumb and accents the nearest tick.
tickCountnumber36Number of ticks around the ring. Minimum 2.
showTicksbooleantrueShow the tick ring.
tickFillbooleantrue when wrap is off and min is 0 or greaterFill ticks from min to the current value. Defaults to on for a gauge. Off when wrap is set or min is below 0 so the ring has no origin. Set true to force fill.
tickRadius"none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "full""none"End of each default tick. none is square. full is round.
thumbRadius"none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "full""full"Corner radius of the dot thumb.
renderTick(tick: { index: number; count: number; active: boolean; filled: boolean; angle: number; value: number }) => React.ReactNode-Replace each default tick with custom content. filled is true from min to the current value. active is true on the nearest tick when thumb is tick.
accentstring-Fill for the dot thumb and the nearest tick when thumb is tick. Prefer a kit color token.
tickMutedstring-Fill for ticks past the current value. Prefer a kit color token.
tickFilledstring-Fill for ticks from min to the current value. Prefer a kit color token.
tickActivestring-Fill for the nearest tick when thumb is tick. Prefer a kit color token.
labelColorstring-Label text color. Prefer a kit color token.
valueColorstring-Value text color. Prefer a kit color token.
faceBackgroundstring-Fill behind the ring. Transparent by default.
faceBorderstring-Border color of the ring. Transparent by default.
progressbooleanfalseDraw a ring on the face edge from startAngle to the current value. The unused part of the rim stays empty. Ticks inset from that ring so they do not sit on the edge.
progressColorstring-Color of the progress ring. Prefer a kit color token.
tickWidthstring-Tick thickness. Prefer a kit length token.
tickLengthstring-Tick length along the radius. Prefer a kit length token.
thumbSizestring-Dot thumb diameter. Prefer a kit length token.

Sound

PropTypeDefaultNotes
tickSoundbooleantruePlay a short tick on each stepped value change while dragging or using the keyboard. Skipped when the document is hidden or reduced motion is preferred.
scrubSoundbooleanfalsePlay a smooth scrub tone while dragging. Pitch follows position and level follows drag speed. Off by default. Stops on release, unmount, or when the document is hidden. Skipped when reduced motion is preferred.

Keyboard and accessibility

Slider (role="slider"): The face is the slider. Arrow keys step the value. invert flips that direction. Home goes to min. End goes to max, or to the last rest when wrap is set. PageUp and PageDown move by one tenth of the range.

PropTypeDefaultNotes
aria-labelstring-Accessible name when label is omitted or is not a string.
aria-labelledbystring-ID of a labeling element. Defaults to the label element when label is set and aria-label is omitted.
classNamestring-Additional styles on the root.
styleReact.CSSProperties-Inline styles on the root. Can also override Dial CSS variables directly.
refReact.Ref<HTMLDivElement>-Ref to the root element.

Tokens

Theme defaults live on :root. Override these globally, on a local host, or with the matching appearance props. size remaps diameter, tick, thumb, and type tokens. Prop and style overrides still win.

TokenTypeDefaultNotes
--df-dial-sizeCSS lengthcalc(18 * var(--spacing-unit))Ring diameter for size md.
--df-dial-size-smCSS lengthcalc(12 * var(--spacing-unit))Ring diameter for size sm.
--df-dial-size-lgCSS lengthcalc(28 * var(--spacing-unit))Ring diameter for size lg.
--df-dial-accentCSS colorvar(--df-coral-430)Dot thumb fill and default active tick fill.
--df-dial-tick-mutedCSS colorcolor-mix(in oklch, var(--foreground) 14%, var(--background))Fill for ticks past the current value.
--df-dial-tick-filledCSS colorcolor-mix(in oklch, var(--foreground) 32%, var(--background))Fill for ticks from min to the current value.
--df-dial-tick-activeCSS colorvar(--df-dial-accent)Nearest tick fill when thumb is tick.
--df-dial-tick-widthCSS lengthvar(--border-width-thick)Tick thickness for size md.
--df-dial-tick-lengthCSS lengthcalc(2 * var(--spacing-unit))Tick length for size md.
--df-dial-tick-radiusCSS length0pxEnd radius of a custom HTML tick.
--df-dial-thumb-sizeCSS lengthcalc(var(--df-dial-tick-length) + var(--spacing-unit))Dot thumb diameter for size md. The center sits on the tick midpoint. One spacing unit longer so the dot covers the mark.
--df-dial-thumb-radiusCSS lengthvar(--radius-full)Dot thumb corner radius.
--df-dial-face-bgCSS colortransparentFill inside the ring. Transparent by default so the ticks sit on the page.
--df-dial-face-fillCSS colorcolor-mix(in oklch, var(--muted) 88%, var(--background))Filled face color. Pass it as faceBackground for a disc behind the ticks.
--df-dial-face-borderCSS colortransparentRing border color.
--df-dial-face-border-widthCSS lengthvar(--border-width-thick)Ring border width.
--df-dial-face-radiusCSS lengthvar(--radius-full)Corner radius of the ring face.
--df-dial-ring-insetCSS lengthcalc(1 * var(--spacing-unit))Space from the face edge to the outer tick tips when the face is filled or progress is set.
--df-dial-progress-colorCSS colorvar(--df-yellow-orange-230)Progress ring color.
--df-dial-progress-widthCSS lengthvar(--border-width-thick)Progress ring thickness.
--df-dial-label-colorCSS colorvar(--foreground)Label text color.
--df-dial-label-font-familyCSS font familyvar(--df-font-sans)Label font.
--df-dial-label-font-weightCSS font weightvar(--df-font-weight-medium)Label weight.
--df-dial-label-sizeCSS lengthvar(--df-text-xs)Label type size for size md.
--df-dial-label-caption-colorCSS colorvar(--muted-foreground)Inside label color when a value is also shown.
--df-dial-label-caption-sizeCSS lengthvar(--df-text-2xs)Inside label type size when a value is also shown, for size md.
--df-dial-value-colorCSS colorvar(--foreground)Value text color.
--df-dial-value-font-familyCSS font familyvar(--df-font-sans)Value font.
--df-dial-value-font-weightCSS font weightvar(--df-font-weight-semibold)Value weight.
--df-dial-value-sizeCSS lengthvar(--df-text-sm)Value type size for size md.
--df-dial-label-gapCSS lengthcalc(2 * var(--spacing-unit))Gap between an outside label and the ring. size remaps this. labelGap and style overrides still win.
--df-dial-center-gapCSS lengthcalc(1 * var(--spacing-unit))Gap between an inside label and the value.
--df-dial-motion-durationCSS timevar(--df-duration-slow)Duration for thumb settle, face press, and active tick length.
--df-dial-motion-easeCSS easingvar(--df-ease-emphasized)Easing for thumb settle, face press, and active tick length.
--df-dial-drag-scaleCSS number0.98Face scale while dragging.
--df-dial-tick-active-growCSS lengthvar(--spacing-unit)Extra tick length when thumb is tick and the mark is nearest the value.
--df-dial-label-max-widthCSS lengthcalc(16 * var(--spacing-unit))Maximum width of an outside label.