2026-10 · Navigation · GitHub
A selector you turn: choices round a knurled dial set in a cloth-dotted plate, carried one detent at a time under a mark that never moves. Underneath, it is a radio group.
The same radio group under each: names that stay upright, icons that say their name when reached for, and words engraved round the arc that turn with it.
Names · views for an archive. Arrow keys turn it a detent; the word under the mark is the choice.
.mode-dial
Icons · the same views. Hover or focus raises the name; on touch the chosen one keeps its name up.
.mode-dial--icons
Curled text · a dashboard’s time range, Day to All, read clockwise.
.mode-dial--curled
The display reads what is under the mark, and what is under the pointer before you turn to it.
Range · hover a word to preview its span; turn to apply it.
.mode-dial--curled + __display
Views · the screen says what each view is for before you pick it.
.mode-dial--icons + __display
For a choice that costs something. Turning browses, the key equips, Escape turns it back.
Kit · a game’s item wheel. The mark is what you are looking at; the pip is what you are holding.
.mode-dial--icons + __display + __commit
The geometry is a share of the dial and the cloth is in pixels, so a small dial has fewer dots, not smaller ones.
Compact · 8.5rem, the size it takes beside a search field in a site header.
--mode-dial-size: 8.5rem
Large · 26rem, a HUD control a thumb can turn; on a phone it takes the column.
--mode-dial-size: 26rem
The same dial and the same mechanics, flat: white fills, hairlines and plain type, the version you would drop into an ordinary website. Switch Sound on above and the two finishes part by ear as well: a ringing metal tick against a dry plastic one.
Plain names · the archive’s views in a system face on a white plate. Where the 120 teeth were, one tick per detent.
.mode-dial--plain
Plain curled · the time range still turns with the dial, in the same sans and no longer cut into it.
.mode-dial--plain + --curled
Plain icons · with the display: no glass and no dot matrix, the same readout in the text face.
.mode-dial--plain + --icons + __display
Plain kit · the key still travels, and still takes a ring while a choice is pending. Only the material went.
.mode-dial--plain + --icons + __display + __commit
One variation with a voice of its own, opt-in like every sound on this page: a scratch for the teeth going past the mark, and a snap as the dial lands in a detent.
Ratchet · switch Sound on and turn it. The knurl scratches tooth by tooth, quicker the quicker it turns and silent when it stops, and the face snaps into each detent it lands in.
.mode-dial--ratchet
A rotary selector for a mode you stay in. Its choices sit round a
knurled charcoal dial set into a cloth-dotted plate, and turning it
brings one at a time under a fixed index mark. Underneath it is a
radio group in a form, so arrow keys, a screen reader and a server
all have it with no script; turning it by dragging, and the feel of
each detent, are what component.js adds. Three item sets show where
it belongs: an archive's views as icons that say their name when
reached for, a dashboard's time range engraved round the arc, and a
game's kit with a display and a centre key that commits. A second
finish, .mode-dial--plain, is the same dial with the
material taken off, and .mode-dial--ratchet gives either
finish a voice of its own.
:has() rules write --mode-dial-sel and
--mode-dial-n on the root, and the knurl, the engraving
and the options layer all turn to
-(sel + laps·n + drag) · 360deg/n on one transition,
each option turning back by the same angle, so names and icons stay
upright in lockstep without @property. A detent is an
overshoot: 420ms on a linear() curve that passes the
stop by 6% of the move and settles. CSS alone spins first-to-last the
long way round; component.js keeps a winding count,
--mode-dial-laps, so every turn takes the near way.
--mode-dial-light: 27deg sets every shadow, the
knurl's rim light, the plate's chamfer, the face's streak, the cloth
and the engraving lip, and it sits on the face, which never turns, so
the teeth slide through a highlight that stays put.
.mode-dial--plain keeps the mechanism and every mark
that says something — the index, the chosen weight, the hollow index
and ringed key of a pending choice, the pip, the focus rings, the
tooltips — and takes off the cloth, the teeth, the light and every
shadow: white fills, 1px hairlines, a system face in sentence case,
and one tick per detent on the rim so it still reads as something
that turns. Its signal is darker, #cf5a22, because the
polished orange is about 2:1 on white.
A drag goes through x − k·sin(2πx)/2π, with
k = --mode-dial-notch (.6): slow near a stop, quick
between, and continuous when the radio flips. Every detent the face
passes is a tick — an arrow key, each detent a drag crosses, each one
a click or an Escape sweeps over, timed to when the eased angle
reaches it — and a tick dips the index, fires
mode-dial:tick, buzzes on touch and pen, and clicks if
--mode-dial-click is above 0. The key travels down, away
from the light, and springs back, clicking on the way down and more
softly on the way up. The sound is opt-in, synthesised with Web
Audio, and follows only a real key, pointer or click on the dial; at
0 no AudioContext is ever made. The polished finish rings like metal
and the plain one snaps like plastic. .mode-dial--ratchet
is a voice of its own on either: a grain of bright noise for every
tooth the face carries past the index, so the scratch's rate is the
turn's speed, and a snap as the face lands in a detent — a transient,
a thock falling from 170Hz, the ball settling 15ms later — lighter
for a detent passed on the way.
With the centre key present, the checked radio is the pending choice
and :default is the committed one: the index goes hollow
and the key takes a ring while they differ, and a pip stays with what
was committed. A hidden status line names what is held, and the key
is described by it. Without script the key submits the form as GET;
with script it rewrites defaultChecked, Enter on a radio
commits, and Escape is form.reset(). A view or a filter
has no key and applies as it turns.
An option’s target is its whole slice of the face: between the
half-way lines to its neighbours, from the key’s edge, or the
middle’s without one, out to the plate’s, turning with the
face. A tap on the knurl picks the option at that angle, and the key
is never under 44px across. Names rise as tooltips on :hover, on
:focus-visible and, under (hover: none), on
:active and on the checked item, moving toward the centre
so a wide word clears its own icon. A sideways swipe across the face
turns the dial and a vertical one still scrolls the page. Every text
pair clears 4.5:1 and every signal mark 3:1, in both finishes: the
display is one ink on one glass, because a second, dimmer ink was the
first thing to fall under the floor.
notes.md — the full record, including what was tried and dropped