Controls
The interactive vocabulary of the product: one Midnight action per view, border-strong fields, inked selection marks, and a single Butter Yellow focus ring for everything. Heights and targets come from the sizing ladder; the ring and the input boundary come from borders.
Buttons
One Midnight fill marks the single primary act on a view. Every other button stays structural: hairline, quiet, or, for destruction only, danger.
Button
A button runs one action named as a verb: Get feedback, Save draft, Rerun feedback. Anatomy: a 500 weight label, an optional leading icon slot, an inset of 12, 16, or 20px on the 32, 40, or 44px ladder, and a 4px radius. Never a pill, never a shadow, never yellow.
2px brand-700 · offset 2ink-200 fill · ink-400 label16px spinner · width held| Variant | Fill | Border | Label | Hover and press |
|---|---|---|---|---|
| Primary | action.primary ink-900 | none | paper | fill ink-800, press ink-950 |
| Secondary | surface | ink-300 | text-primary | border ink-500 |
| Quiet | none | none | text-primary | wash ink-a04 |
| Danger | surface | danger-600 | danger-600 | fill danger-600, label white |
States. Rest, hover, press, focus with the shared ring, disabled, and loading. Disabled flattens any variant to an ink-200 fill with an ink-400 label. Loading keeps the label and its width, swaps a 16px linear spinner into the icon slot, and ignores clicks until it resolves. Sizes follow the sizing ladder: 32, 40, and 44px with 12, 16, and 20px insets and a 44px effective target.
Accessibility. A button is a button element. Keep the label in the DOM while loading and set aria-busy, so the accessible name never disappears mid action. Danger belongs only in destructive rows and confirm dialogs.
Icon button
An icon button is a square button for one compact utility act whose glyph is unambiguous: dismiss, copy, rerun. It takes the secondary or quiet appearance, never primary, and its only accessible name is a required aria-label.
32 / 40 / 44 square16px at 32 · 20px from 4044px effective| Part | Binding |
|---|---|
| Frame | control.height.sm/md/lg, square, radius.sm |
| Icon | icon.size.sm at 32 · icon.size.md at 40 and 44 |
| Appearance | secondary and quiet rows of the button matrix |
| Target | control.target-min 44px |
States. The same line as the button: rest, hover, press, focus, disabled.
Accessibility. The aria-label names the action, not the glyph: Rerun feedback, not circular arrow. At 32px the effective target still reaches 44px through surrounding space.
Text fields
Fields collect the words around the draft: titles, course details, context for the reader. The input boundary must be found, so it is the one border set in border-strong.
Text input
A text input takes a single line. Anatomy: a 40px box (44 on touch) with a border-strong boundary, 4px radius, body-md value text, and an ink-400 placeholder that shows an example, never the instruction.
Enter a title before you get feedback.
border-strong #707B8Ddanger-600 + body-sm danger-800ink-100 fill · ink-400 textStates. Rest, focus, error, and disabled, as staged above. Focus adds the ring and nothing else: darkening the boundary as well would double stroke the box.
Accessibility. Every input pairs with a visible label from the field pattern below; the placeholder vanishes at the first character, so it never carries requirements.
Textarea
A textarea is the same contract with room to write. Long writing surfaces get a 20 by 24px inset and grow with their content; the surrounding scroll owner absorbs the length, never the page.
20 × 24with content, inside its scroll ownerStates. The text input line applies; only the inset and the free height differ.
Accessibility. Keep growth visual: once the scroll owner's limit is reached the textarea scrolls internally rather than pushing the actions below it out of reach.
Select
A select is a custom control in input clothing: a 40px border-strong trigger with a 16px ink-600 chevron, opening a listbox built on the menu overlay surface. The native element remains only where the platform demands it; email renders no controls at all.
40px · border-strong · radius 4hairline · radius 8 · shadow-overlayleading check + ink-100 wash| Part | Binding |
|---|---|
| Trigger | control.height.md 40px · 1px border.strong · radius.sm 4px · chevron icon.size.sm 16px ink-600 |
| Listbox | surface · 1px line · radius.md 8px · shadow.overlay · z.overlay · 6px outer padding |
| Option | 8 × 12px row · radius.sm · body-md · hover wash ink.a04 |
| Selected option | leading 16px check · ink.100 wash · aria-selected |
| Motion | overlay recipe: fade + 4px rise · duration.sm · ease.out |
| Keyboard | arrows move · Home and End jump · type-ahead · Enter selects · Escape closes |
States. Rest, focus, error, and disabled follow the text input on the trigger; the chevron never changes. Open adds the listbox on the overlay recipe while the trigger keeps focus and its ring.
Accessibility. The trigger takes role="combobox" with aria-expanded; the list is a listbox of option rows carrying aria-selected. Focus stays on the trigger while the list is open and returns to it on close; the keyboard line above is part of the contract, not an enhancement.
Field
Field wraps any control so it explains itself: a 13/500 label first, the control, then helper or error in body-sm. Required is the written word beside the label; an asterisk alone explains nothing.
Appears at the top of your feedback report.
Enter the full code from your syllabus, like WRT 201.
label · control · helper or errorbody-sm danger-800 · icon optionalthe word, written out| Part | Binding |
|---|---|
| Height | control.height.md 40px · control.height.lg 44px touch · textarea free |
| Boundary | 1px border.strong #707B8D |
| Radius | radius.sm 4px |
| Value | body-md · text-primary |
| Placeholder | ink-400, example content only |
| Inset | 16px · long writing textarea 20px vertical, 24px horizontal |
| Chevron | icon.size.sm 16px · ink-600 |
| Focus | 2px brand-700 ring, offset 2, boundary unchanged |
| Error | border danger-600 · message body-sm danger-800 |
| Disabled | fill ink-100 · text ink-400 |
| Label | label 13/500 · text-primary |
| Helper | body-sm · text-secondary |
States. Helper and error share one slot below the control; error replaces helper instead of stacking under it.
Accessibility. Point the control at its helper or error with aria-describedby and set aria-invalid on error, so the message reads with the label instead of getting lost below.
Selection controls
Choices read as marks: a 20px visual inked when chosen, inside a 44px row that includes its label. Nothing here is smaller to hit than it looks.
Checkbox and radio
A checkbox answers one independent yes or no; radios choose one of a visible few. Anatomy: a 20px box at 4px radius or a full round circle, border-strong when empty, ink-900 with a paper glyph when chosen, and a label completing the 44px row.
ink-900 fill · paper glyphborder-strongthe 44px row target| Part | Binding |
|---|---|
| Visual | control.selection-visual 20px |
| Target | control.target-min 44px row, label included |
| Shape | box radius.sm 4px · radio radius.full |
| Empty | 1px border.strong on surface |
| Checked | fill ink-900 · glyph paper |
| Focus | shared 2px brand-700 ring |
| Motion | check draws in · dot fades and settles · duration.sm · ease.out |
States. Empty, checked, focus, and disabled, which mutes the mark to ink-200 with an ink-400 label. Checking animates: the check draws in and the radio dot fades and settles over duration.sm; see selection motion below.
Accessibility. Keep the native input inside the label element: the row toggles from anywhere and the platform announces the state without extra wiring.
Switch
A switch flips a setting that applies the moment it moves; nothing waits for a save. Anatomy: a 40 by 24 full round track with a 20px paper thumb, ink-200 when off, ink-900 when on. The label states what turning it on does.
40 × 24 · full roundink-200ink-900 · paper thumb| Part | Binding |
|---|---|
| Track | control.switch.width × control.switch.height, 40 × 24px, radius.full |
| Thumb | control.selection-visual 20px · paper |
| Off | track ink-200 |
| On | track ink-900 |
| Target | control.target-min 44px row with the label |
| Focus | shared ring around the track |
| Motion | 16px thumb translate + track crossfade · duration.sm · ease.out |
States. Off, on, focus, and disabled, muted like the checkbox. The thumb slides its 16px over duration.sm on ease.out while the track crossfades between ink-200 and ink-900.
Accessibility. A native checkbox with role="switch" works; position and fill carry the state together, so on and off never differ by color alone.
Selection motion
Checking is drawn, not swapped: the check strokes in, the radio dot settles, the switch thumb slides. All three run live below; click a row or toggle it with the keyboard to play the change.
stroke draw · sm · ease-outfade + settle from 0.6 · sm16px slide + crossfade · smMotion. The three marks share one family from the motion page recipes: color changes ride the hover timing, the draw and settle run duration.sm with ease.out, and nothing bounces or overshoots. Reduced motion collapses all three to instant flips through the global rule, so every state stays legible in the resting frame.
Segmented control
The review screen moves between Feedback and Your draft with one segmented control. It changes what is shown, never what is saved.
Anatomy: a bg-subtle track with a 2px inner inset, 32px items, the active item lifted to a white fill with a hairline and ink-900 text, inactive items in ink-600. Two segments are the norm; three is the ceiling.
bg-subtle · radius 4surface + hairline · radius 232pxtransform · dur.sm · ease.out| Part | Binding |
|---|---|
| Track | bg-subtle fill · radius.sm · 2px inset |
| Item | 32px · label 13/500 · ink-600 |
| Active item | surface fill · line hairline · text-primary · radius.xs |
| Hover | wash ink-a04 on inactive items |
| Focus | shared 2px brand-700 ring |
| Count | two segments by default, three at most |
States. Active, inactive, inactive hover, and focus. On change the white active pill slides to the chosen segment, transform only, over duration.sm with ease.out; reduced motion snaps it into place.
Accessibility. Expose it as tabs: a tablist with aria-selected and arrow key movement, because segments switch views in place.
Rules
Eight decisions that keep every control quiet, reachable, and honest.
- One primary action per view. One Midnight fill; supporting acts are secondary, quiet, or links.
- Height follows density, never importance. The 32/40/44 ladder is regional; primary does not mean taller.
- Labels are visible. A placeholder is an example, never the label or the instruction.
- One focus signal. The 2px brand-700 ring at 2px offset serves every control, and nothing else changes at focus.
- Destructive is a variant, not a color accident. Danger buttons live in destructive rows and confirm dialogs and stay white until hover.
- Loading never collapses width. The label holds its box while a 16px spinner takes the icon slot.
- Every control reaches 44px effectively. Visuals may be 20 or 32px; targets and rows may not.
- Segments and switches act instantly. A segmented control never submits; a switch never waits for a save.