DialKit
A compound for building front-panel control surfaces — chips, segmented toggles, and switches sit inside collapsible labelled panels. Dial is the outer kit; Dial.Panel, Dial.Chips, Dial.Segment, and Dial.Toggle are the parts you compose inside.
Mode
View
Customize
Shape
3
State
Installation
npx shadcn@latest add https://craftbits.dev/r/dialkit.jsonUsage
DialKit is a compound — Dial owns the LayoutGroup so segmented controls inside don't collide across instances, Dial.Panel is a disclosable section, and the three control parts each own their value state with controlled + uncontrolled modes.
import { Dial } from "@craft-bits/core";
<Dial>
<Dial.Panel title="Tone">
<Dial.Chips
label="Mode"
options={["wave", "saw", "square"]}
defaultValue="wave"
/>
<Dial.Segment
label="View"
options={["S", "M", "L"]}
defaultValue="M"
/>
<Dial.Toggle label="Mute" defaultChecked={false} />
</Dial.Panel>
</Dial>Controlled — pair each part's value (or checked) with its onValueChange (or onCheckedChange):
const [mode, setMode] = useState("wave");
const [mute, setMute] = useState(false);
<Dial>
<Dial.Panel title="Tone">
<Dial.Chips
label="Mode"
options={["wave", "saw", "square"]}
value={mode}
onValueChange={setMode}
/>
<Dial.Toggle
label="Mute"
checked={mute}
onCheckedChange={setMute}
/>
</Dial.Panel>
</Dial>Understanding the component
- Compound parts.
Dialis the outer card + LayoutGroup;Dial.Panelis a disclosable section with a header button;Dial.Chips,Dial.Segment,Dial.Toggleare the labelled control rows. Composing them as children rather than passing a flatcontrolsarray keeps the call-site declarative. - Shared LayoutGroup, isolated layoutIds.
Dialcreates auseId()-scopedLayoutGroupso eachSegment's sliding active background animates only within its own kit — multiple panels on the same page never tween into each other. - Independent value state per control.
Chips,Segment,Toggleeach own theirvalue/checkedstate (controlled + uncontrolled). There is no parent "values" object — the kit is a layout, not a form. - Panel disclosure.
Dial.Panelis a button-triggered region witharia-expanded+aria-controls. The body collapses withSPRINGS.damped(critically-damped, no overshoot) so the height settle stays sharp. - Keyboard model for Chips & Segment. Arrow Left/Right (and Up/Down) cycle selection with wrap-around; Home / End jump to the ends. The active option is the only tab-stop (
tabIndex={0}), matching the W3C radio-group pattern. - Toggle as a switch.
Dial.Toggleisrole="switch"(not a button) so screen readers announce on/off. The knob slides between two pinned positions withSPRINGS.snap— mechanical, not bouncy.
Props
Dial
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the rendered <div>. |
children | ReactNode | — | Panels and labelled rows. |
...rest | HTMLAttributes<HTMLDivElement> | — | Any other <div> prop. |
Dial.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Header label — rendered uppercase + mono. |
defaultOpen | boolean | true | Initial open state (uncontrolled). |
open | boolean | — | Controlled open state. Pair with onOpenChange. |
onOpenChange | (open: boolean) => void | — | Called when the header button toggles the panel. |
className | string | — | Merged onto the rendered <div>. |
Dial.Chips
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Row label, also the accessible name of the group. |
options | readonly T[] | required | Selectable chip values. |
value | T | — | Controlled selection. |
defaultValue | T | options[0] | Uncontrolled starting selection. |
onValueChange | (value: T) => void | — | Called when the user picks a new chip. |
renderOption | (option: T) => ReactNode | — | Custom per-chip render. |
className | string | — | Merged onto the outer row <div>. |
Dial.Segment
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Row label, also the accessible name of the group. |
options | readonly T[] | required | Segmented values. |
value | T | — | Controlled selection. |
defaultValue | T | options[0] | Uncontrolled starting selection. |
onValueChange | (value: T) => void | — | Called on selection change. |
formatOption | (option: T) => ReactNode | — | Format each segment's visible label. |
className | string | — | Merged onto the outer row <div>. |
Dial.Toggle
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Visible label — doubles as the accessible name. |
checked | boolean | — | Controlled checked state. |
defaultChecked | boolean | false | Uncontrolled initial state. |
onCheckedChange | (checked: boolean) => void | — | Called on toggle. |
className | string | — | Merged onto the rendered <button>. |
Accessibility
Dial.Panelis a<button>drivingaria-expandedandaria-controls. The body isrole="region"labelled by the header for landmark navigation.Dial.ChipsandDial.Segmentarerole="radiogroup"(labelled by their row label) with each optionrole="radio"+aria-checked. Only the active option is in the tab sequence; arrow keys cycle with wrap-around, Home / End jump to the ends — the canonical W3C radio-group keyboard model.Dial.Toggleisrole="switch"witharia-checkedandaria-labelledbypointing at its visible label. Screen readers announce "on / off" rather than "pressed / not pressed."- Focus is visible via
focus-visible:rings keyed to--cb-accent, offset from--cb-bgso they stay legible on every theme surface. - The sliding active background in
Dial.Segmentuses Motion'slayoutIdand respectsprefers-reduced-motion— Motion collapses the layout transition to an instant swap. - Color contrast in the default theme: row labels use
--cb-fg-mutedand selected text on the active segment uses--cb-accent-fgagainst--cb-accent; both pass WCAG AA.
Credits
- Extracted from:
terminal-dreams(src/components/ui/dialkit/). The original five files (Dial.tsx,DialPanel.tsx,DialChips.tsx,DialSegment.tsx,DialToggle.tsx) were renamed and re-architected as a Radix-style compound. The originalDialwas a numeric scrubber slider; in the library,Dialis the container — the slider concern is covered by other primitives in the buttons & inputs section.