Component

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.

primary
secondary
quiet
danger
focus
disabled
loading
Focus2px brand-700 · offset 2
Disabledink-200 fill · ink-400 label
Loading16px spinner · width held
Button variant token bindings
VariantFillBorderLabelHover and press
Primaryaction.primary ink-900nonepaperfill ink-800, press ink-950
Secondarysurfaceink-300text-primaryborder ink-500
Quietnonenonetext-primarywash ink-a04
Dangersurfacedanger-600danger-600fill 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 · 16px icon
40 · quiet
40 · secondary
44 · focus
Frame32 / 40 / 44 square
Icon16px at 32 · 20px from 40
Target44px effective
Icon button token bindings
PartBinding
Framecontrol.height.sm/md/lg, square, radius.sm
Iconicon.size.sm at 32 · icon.size.md at 40 and 44
Appearancesecondary and quiet rows of the button matrix
Targetcontrol.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.

Do give a view one Midnight action and keep its companions quiet.
Don't build pills, cast shadows, or fill with yellow. Yellow marks passages; Midnight acts.

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.

rest
focus · ring only, boundary unchanged
error

Enter a title before you get feedback.

disabled
Boundaryborder-strong #707B8D
Errordanger-600 + body-sm danger-800
Disabledink-100 fill · ink-400 text

States. 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.

Inset20 × 24
Growthwith content, inside its scroll owner

States. 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.

Course
Writing seminar 201
Writing seminar 201
Rhetoric 110
Independent study
open state · focus stays on the trigger
Trigger40px · border-strong · radius 4
Listboxhairline · radius 8 · shadow-overlay
Selectedleading check + ink-100 wash
Custom select token bindings and behavior
PartBinding
Triggercontrol.height.md 40px · 1px border.strong · radius.sm 4px · chevron icon.size.sm 16px ink-600
Listboxsurface · 1px line · radius.md 8px · shadow.overlay · z.overlay · 6px outer padding
Option8 × 12px row · radius.sm · body-md · hover wash ink.a04
Selected optionleading 16px check · ink.100 wash · aria-selected
Motionoverlay recipe: fade + 4px rise · duration.sm · ease.out
Keyboardarrows 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.

Orderlabel · control · helper or error
Error textbody-sm danger-800 · icon optional
Requiredthe word, written out
Text field token bindings
PartBinding
Heightcontrol.height.md 40px · control.height.lg 44px touch · textarea free
Boundary1px border.strong #707B8D
Radiusradius.sm 4px
Valuebody-md · text-primary
Placeholderink-400, example content only
Inset16px · long writing textarea 20px vertical, 24px horizontal
Chevronicon.size.sm 16px · ink-600
Focus2px brand-700 ring, offset 2, boundary unchanged
Errorborder danger-600 · message body-sm danger-800
Disabledfill ink-100 · text ink-400
Labellabel 13/500 · text-primary
Helperbody-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.

Do label above the control and let the placeholder show an example a student could overwrite.
Don't let a placeholder stand in for the label or the instruction. It disappears at the first character.

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.

report options
feedback scope
Checkedink-900 fill · paper glyph
Emptyborder-strong
Dashed linethe 44px row target
Checkbox and radio token bindings
PartBinding
Visualcontrol.selection-visual 20px
Targetcontrol.target-min 44px row, label included
Shapebox radius.sm 4px · radio radius.full
Empty1px border.strong on surface
Checkedfill ink-900 · glyph paper
Focusshared 2px brand-700 ring
Motioncheck 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.

Track40 × 24 · full round
Offink-200
Onink-900 · paper thumb
Switch token bindings
PartBinding
Trackcontrol.switch.width × control.switch.height, 40 × 24px, radius.full
Thumbcontrol.selection-visual 20px · paper
Offtrack ink-200
Ontrack ink-900
Targetcontrol.target-min 44px row with the label
Focusshared ring around the track
Motion16px 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.

Do state what turning it on does. On always means yes to one sentence.
Don't label a switch with a bare noun. Off becomes a guess about what stopped.

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.

check · draws in
dot · fades and settles
thumb · slides and crossfades
Checkstroke draw · sm · ease-out
Radio dotfade + settle from 0.6 · sm
Switch16px slide + crossfade · sm

Motion. 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.

live · click or arrow keys
focus on a segment
Trackbg-subtle · radius 4
Activesurface + hairline · radius 2
Items32px
Slidetransform · dur.sm · ease.out
Segmented control token bindings
PartBinding
Trackbg-subtle fill · radius.sm · 2px inset
Item32px · label 13/500 · ink-600
Active itemsurface fill · line hairline · text-primary · radius.xs
Hoverwash ink-a04 on inactive items
Focusshared 2px brand-700 ring
Counttwo 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.

Feedback Your draft
Do switch views with it. Both sides show the same work from a different angle.
Save draft Get feedback
Don't put actions in segments. Anything that submits or saves is a button.

Rules

Eight decisions that keep every control quiet, reachable, and honest.

  1. One primary action per view. One Midnight fill; supporting acts are secondary, quiet, or links.
  2. Height follows density, never importance. The 32/40/44 ladder is regional; primary does not mean taller.
  3. Labels are visible. A placeholder is an example, never the label or the instruction.
  4. One focus signal. The 2px brand-700 ring at 2px offset serves every control, and nothing else changes at focus.
  5. Destructive is a variant, not a color accident. Danger buttons live in destructive rows and confirm dialogs and stay white until hover.
  6. Loading never collapses width. The label holds its box while a 16px spinner takes the icon slot.
  7. Every control reaches 44px effectively. Visuals may be 20 or 32px; targets and rows may not.
  8. Segments and switches act instantly. A segmented control never submits; a switch never waits for a save.