ComponentsForm
Knob
Chooses a number by turning a dial, with the value in its middle.
Import
import { Knob } from "@booleanpress/ui/knob"Usage
A knob is a slider drawn as a dial, for dashboards and settings where a round control fits better than a track.
import { Knob } from "@booleanpress/ui/knob"
export function SendingRate() {
return <Knob defaultValue={50} aria-label="Sending rate" />
}It is uncontrolled with defaultValue (min by default), or controlled with value and onValueChange; onValueCommit fires once when a drag or a key press ends. min, max (0 and 100 by default) and step bound it; when the range spans zero, the value's arc starts at zero. Drag round the dial with a pointer, or use the arrow keys, Page Up/Down, Home and End. formatValue turns the number into the text in the middle and the value screen readers announce ((value) => ${value}%``); without it the number is formatted in the provider's locale. size is sm (80 px), default (100 px) or lg (150 px) and follows the provider's controlSize; a size-* class sets any other size. strokeWidth sets the arc's thickness, and valueColor, rangeColor and textColor its colours. readOnly keeps the dial focusable and announced but fixed; disabled dims it and takes it out of the tab order. With name, the value is submitted in a form (not while disabled), and a form reset brings back the first value.
Examples
Basic
A dial from 0 to 100, at 50.
Min and max
min={-50} and max={50}: the value's arc starts at zero.
Step
step={10} moves the value in steps of 10, by key and by drag.
Value template
formatValue shows and announces "42%".
Stroke width
strokeWidth={5} draws a thin arc.
Sizes
sm is 80 px, default 100 px and lg 150 px; the text scales with the dial.
import { Knob } from "@booleanpress/ui/knob"
export default function KnobSizes() {
return (
<div className="flex items-center gap-6">
<Knob size="sm" defaultValue={30} aria-label="Small" />
<Knob defaultValue={50} aria-label="Default" />
<Knob size="lg" defaultValue={70} aria-label="Large" />
</div>
)
}Colours
valueColor and rangeColor take theme tokens for a delivered, bounced and opened dial.
import { Knob } from "@booleanpress/ui/knob"
export default function KnobColours() {
return (
<div className="flex items-center gap-6">
<Knob defaultValue={75} valueColor="var(--success)" rangeColor="var(--success-tag)" aria-label="Delivered" />
<Knob defaultValue={12} valueColor="var(--destructive)" rangeColor="var(--destructive-tag)" aria-label="Bounced" />
<Knob defaultValue={40} valueColor="var(--chart-2)" aria-label="Opened" />
</div>
)
}Controlled
value and onValueChange, also changed by two buttons.
import { useState } from "react"
import { MinusIcon, PlusIcon } from "lucide-react"
import { Button } from "@booleanpress/ui/button"
import { Knob } from "@booleanpress/ui/knob"
export default function KnobControlled() {
const [workers, setWorkers] = useState(0)
return (
<div className="flex flex-col items-center gap-2">
<Knob value={workers} onValueChange={setWorkers} size="lg" aria-label="Queue workers" />
<div className="flex gap-2">
<Button size="icon-sm" aria-label="Add a worker" disabled={workers >= 100} onClick={() => setWorkers((n) => Math.min(100, n + 1))}>
<PlusIcon />
</Button>
<Button size="icon-sm" variant="secondary" aria-label="Remove a worker" disabled={workers <= 0} onClick={() => setWorkers((n) => Math.max(0, n - 1))}>
<MinusIcon />
</Button>
</div>
</div>
)
}Read only
readOnly keeps the dial in the tab order and announced, but fixed.
Disabled
disabled dims the dial and takes it out of the tab order.
Accessibility
- Semantics
- The dial is an SVG with
role="slider",aria-valuenow,aria-valueminandaria-valuemax;formatValueaddsaria-valuetext. Read only, it carriesaria-readonly="true"; disabled,aria-disabled="true". The arcs and the text in the middle are decoration. Withname, a hidden input carries the value for the form. - Labels
- Name it with
aria-labeloraria-labelledby. Put a hint or an error inaria-describedby. - Focus
- The dial is one tab stop (none when disabled). Keyboard focus is a 1px
--ringoutline 2px round it. A drag focuses it, so the keys continue from there. - Known limits
- The dial turns clockwise to raise the value in every reading direction, so โ and โ raise it in right-to-left pages too.
- A drag is hard to stop on an exact value; the arrow keys, or a number field beside it, are exact.
- Colours given to
valueColor,rangeColorortextColorare yours to check for contrast against the page.
Keyboard
| Key | Behaviour |
|---|---|
| โorโ | Raises the value by one step. |
| โorโ | Lowers the value by one step. |
| Shiftโ | With any arrow key, moves ten steps that way. |
| Page UporPage Down | Raises or lowers the value by ten steps. |
| HomeorEnd | Sets the minimum or the maximum. |
API
Knob
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | The value at the start, when it controls itself. min by default. | |
disabled | boolean | false | Dims the dial and takes it out of the tab order. |
form | string | The id of the form the value belongs to, for a control placed outside it. | |
formatValue | ((value: number) => string) | The text in the middle, and the value screen readers announce: (value) => \${value}%``. The number by default. | |
max | number | 100 | The highest value. 100 by default. |
min | number | 0 | The lowest value. 0 by default. |
name | string | The form field name: the value is submitted with the form, and a form reset brings back the first value. | |
onValueChange | ((value: number) => void) | Called with the new value while it changes. | |
onValueCommit | ((value: number) => void) | Called with the value once a drag or a key press ends. | |
rangeColor | string | The colour of the rest of the arc. --border by default. | |
readOnly | boolean | false | Keeps the dial focusable and announced but ignores the pointer and the keys. |
showValue | boolean | true | Shows the value in the middle. True by default. |
size | "default" | "sm" | "lg" | The dial's width and height: 80, 100 or 150 px. Defaults to the provider's controlSize; a size-* class sets any other. | |
step | number | 1 | The amount each move changes the value by. 1 by default. |
strokeWidth | number | 14 | The arc's thickness, in hundredths of the dial's width. 14 by default. |
textColor | string | The colour of the value text. --muted-foreground by default. | |
value | number | The value, when you control it. | |
valueColor | string | The colour of the value's arc, any CSS colour. --primary by default. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="knob" (Knob), and data-size, data-disabled, data-readonly.
Theming
The arc is --border, the value's arc --primary and the text --muted-foreground; valueColor, rangeColor and textColor set them as the variables --knob-value, --knob-range and --knob-text. Focus is --ring. A disabled knob is drawn at 60% opacity.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--ring | outline |