Interaction states
Guide index · Surfaces · Components
Shared philosophy
Action, selection, keyboard focus, and business status are four independent channels. Blue is the emphasized action or tab. Violet identifies navigation, cyan identifies focus, and neutral surfaces with bold labels identify graphical selection. Data rows add a persistent primary-text ▸ glyph; the terminal uses that glyph for navigation and rows. Status is wording, with a semantic color only as support.
Resolve combined states in this order: disabled prevents activation; busy prevents a duplicate activation; an error supplies its message; focus adds its indicator; selection keeps its marker; hover or press then adjusts an enabled face. None of these removes a required status label. An invalid focused field retains its danger edge and uses the lighter surface.focus fill.
State is never color alone. A check, dot, thumb, marker, or explicit word still has to be present. Focus moves when the person moves it, when an overlay opens, or when a disclosure hides the focused control. Refresh, incoming logs, and status polling do not take focus. Content that is already on screen stays in place, and freshness is labeled separately from record status.
Platform implementation
Desktop and mobile are one graphical system. They share faces, roles, state rules, type families, type sizes and weights, and icons at the same sizes and strokes. Both use icons. Layout, padding, and component target dimensions adapt to available space and touch input; mobile has no hover. Typography and iconography diverge between the graphical system and the terminal, as specified on their own pages. The terminal keeps the same channels and expresses them with one cell size, glyphs, and foreground color.
Desktop
Controls use contoured faces and the shared gradients. Hover and press change the enabled face without moving or resizing it. Keyboard focus recolors the existing contour cyan and does not add a second ring. Use the existing 1px contour at 0 offset, never an outer stroke. When an edge must retain on, checked, or invalid meaning, preserve that semantic edge and use the opaque lighter surface.focus fill (#52677E) for focus instead. Apply this exception consistently to pressed toggles, on switches, checked or indeterminate checkboxes, checked radios, and invalid fields; keep their glyphs and error text visible. Pointer and keyboard are both first-class.
Mobile
Mobile uses the desktop faces, colors, and states. It has no hover, so press is the only transient face. Targets use the 48px touch size. Focus uses the same cyan contour when an external keyboard or switch device is present. Labels stay visible, and nothing depends on a hover tooltip. Text follows the host text scale, as specified in typography.
Terminal
The terminal has no hover, press, gradient, or chamfer. Brackets, ▸, [x], (•), [on], [!], and (disabled) carry the structure that desktop draws with edges and bars. Foreground color keeps the same roles: pale text for a selected action, cyan for focus, violet for navigation, primary text for persistent ▸ selection, and semantic colors for status words. A cell background stays the surface color. Focus recolors only the control label cyan, preserves its value and selection glyph, and adds no prefix or substitute cyan frame. Nerd Font is required and guaranteed; do not replace ▸ with >. Every platform shows one nearby sentence explaining why the control is disabled. Keep that reason identical across platforms and expose it as an accessible description. Only when the sentence will not fit may the terminal append “(disabled)”; never append “(unavailable)”.
| State | Desktop and mobile | Terminal |
|---|---|---|
| Focus | Existing 1px cyan contour at 0 offset; semantic-edge exceptions use surface.focus | Cyan control label; unchanged value and persistent selection glyph; no prefix or cyan frame |
| Selected navigation | Neutral selection face, violet leading edge, and bold violet label | ▸ in primary text in a fixed one-cell slot; violet navigation label |
| Selected data | Persistent ▸ glyph and neutral selection face | ▸ in primary text in a fixed one-cell slot |
| Selected action or tab | Control face, blue edge or top marker | Pale bold label with | Label \ endcaps; actions use brackets |
| Hover | Desktop hover face. Mobile has no hover | None |
| Press | Pressed face. Mobile uses this as its transient face | None |
| Disabled | Flat face, readable muted label, nearby reason | Readable label plus a nearby reason; (disabled) only when the sentence will not fit |
| Busy | Enabled face, reserved width, operation-specific replacement label | Operation-specific replacement label, supplemented by the quadrant-block spinner |
| Checkbox | Empty, pale check, or dash on the dark face | [ ] [x] [-] |
| Radio | Empty, or a centered pale dot | ( ) (•) |
| Switch | 32×20 track and 16px pale thumb, inside the full target | [off] [on] |
| Invalid | Danger edge plus the associated message | Danger cue plus [!] and the message |
| Invalid and focused | Danger edge and cyan focus together | Danger cue and cyan focus together |
Independent state channels
Each row below is one step of the precedence order: the channel by itself, then the combinations that have to stay readable together. Desktop and mobile use the same faces. Mobile omits hover and uses the 48px target. The terminal keeps the same order and draws it with glyphs and foreground color.
Disabled
Busy
Busy and disabled
Error
Enter a single token.
Focus
Selected
Hover
Pressed
Selected and focused
Invalid and focused
Enter a single token.
Disabled
Busy
Busy and disabled
Error
Enter a single token.
Focus
Selected
Pressed
Selected and focused
Invalid and focused
Enter a single token.
Disabled
Busy
Busy and disabled
Error
Enter a single token.
Focus
Selected
Selected and focused
Invalid and focused
Enter a single token.
Standard graphical controls
This table is the desktop face contract. Mobile uses the same faces, at the 48px target, and omits hover. The terminal uses the glyph mapping in Platform implementation for this same set.
| State | Face | Boundary and text |
|---|---|---|
| Default primary | Control gradient | 1px blue edge; primary text, weight 600 |
| Default secondary | Same control gradient | 1px essential neutral edge; primary text, weight 500 |
| Hover | Control-hover gradient | Keep primary/secondary edge role and label |
| Pressed | Control-pressed gradient | Same edge; no movement or size change |
| Keyboard focus | Current state face | Replace the existing contour border with cyan; do not add an outer ring |
| Disabled | Flat content | Quiet edge, full-opacity secondary text; no hover/press change |
| Busy | Existing enabled face | Reserved progress slot and specific label; no duplicate activation |
| Quiet action | Transparent | Primary text at rest; cyan underlined links remain distinct; no persistent filled face |
| Destructive | Danger surface | Danger edge and pale/danger label; explicit destructive verb |
| Warning · default / hover | Warning surface; unchanged on hover | Warning edge and label; explicit warning verb |
| Warning · press | Warning surface | Preserve edge and label; underline label during press |
| Warning · focus | Lighter --focus-fill: #52677E | Preserve warning edge and label; no outer ring |
| Warning · disabled | Disabled surface | Quiet edge and secondary text; no hover/press change; explain availability |
Both primary and secondary standard buttons remain dark and translucent-looking. Hierarchy comes from boundary, weight and placement. Do not make the primary brighter than a selected tab. A quiet action is the explicit unfilled exception.
Selected content tabs use the default control gradient, pale text and 3px blue top marker. On desktop, hover may use the hover gradient but does not remove selection. Inactive tabs use dark content and secondary text. A focused inactive tab has cyan focus without selecting itself. A selected tab with focus keeps its blue top marker independently of its cyan contour. Mobile has no tab hover. The terminal renders the selected tab as a pale bold label between a square left join and one diagonal top-right end (| Overview \), and focus as cyan, without painting a blue cell. Inactive tabs use secondary text without brackets. All terminal controls keep their containing background. Primary actions are bold pale labels in brackets; secondary actions use the essential edge color. Focus recolors only the label cyan, preserving values and selection glyphs.
Default
Hover
Pressed
Focus
Disabled
Busy
Quiet
Destructive
Selected tab
Selected tab, hover
Focused inactive tab
Selected tab with focus
Toggle
Default
Pressed
Focus
Disabled
Busy
Quiet
Destructive
Selected tab
Focused inactive tab
Selected tab with focus
Toggle
Default
Focus
Disabled
Busy
Quiet
Destructive
Selected tab
Focused inactive tab
Toggle
Focus and keyboard
Use semantic native controls and visible focus for every keyboard-operable element. Focus recolors the existing 1px contour cyan at 0 offset, including diagonal cuts, without an outer stroke. Keep the contour visible through decorative clipping. Reserve a transparent 1px contour for quiet actions and links so focus does not change geometry. Preserve semantic value/error edges with the lighter surface.focus fill exception. Ensure at least 3:1 against adjacent colors. Selection is not a focus indicator.
Use a single tab stop for a tab strip and roving focus among enabled tabs. Arrows move focus, Home/End reach ends, Enter/Space activates manually. Pointer activation focuses and selects. Manual activation prevents arrow exploration from unexpectedly loading data. Form text inputs keep their native editing keys.
Opening menus and dialogs moves focus into them. Escape dismisses a dismissible overlay and returns focus to its trigger. If a disclosure hides focused content, return focus to its heading control first. Reflow, incoming logs and status polling do not take focus. Desktop and mobile share this contour. The terminal uses cyan foreground or a cyan frame, and it has no pointer hover.
Button
Link
Tab strip
Field
Row
Pane
┌ Focused pane ──────────────┐ │ north-gateway │ └────────────────────────────┘
Selection
Navigation is violet and focus is cyan. Graphical selected destinations use a neutral selection face, violet leading edge, and bold violet label. Selected data rows add a persistent primary-text ▸ glyph in a reserved slot. The terminal uses that glyph for navigation and rows. Selected-and-focused examples below are the canonical combined state; selection remains after focus moves.
A card is not implicitly clickable. If selection is needed, provide an explicit selection control and selection semantics. Do not nest action buttons inside a clickable-card button. If a list row is an activation control, its behavior and keyboard target must be explicit while retaining the contextual selection style. Desktop and mobile share the neutral selection face for navigation and the persistent row glyph for data. The terminal preserves the primary-text ▸ glyph while focus recolors the control label cyan; values remain unchanged.
Navigation
Data
Gateway
Navigation
Data
Gateway
Navigation
Data
Gateway
Fields and errors
Field hover uses the named --field-hover-edge: #91A0B7. Read-only fields retain the input face, essential edge, and primary text; they stay keyboard-focusable, selectable, and copyable, prevent editing, and expose native readonly semantics (or aria-readonly="true" for a custom control). Focus recolors the existing 1px contour cyan at 0 offset. Include a visible Read-only label.
An enabled field uses the flat input surface and essential neutral boundary. Focus recolors the existing 1px edge cyan at 0 offset; validation errors use a danger edge plus a visible associated error. Invalid focus preserves that edge and uses surface.focus. Required state is expressed in the label/hint. Placeholders are secondary hints, not labels.
Keep a checkbox, radio, or switch value distinct from hover and focus. A checked control uses a dark blue face, a blue boundary, and a pale check or thumb. A radio has a centered pale dot. A switch uses a chamfered 32×20 control track with 4px cuts, not a pill, and a 16px pale thumb inside the full target. Desktop and mobile share these faces. Mobile omits hover and uses the 48px target. The terminal keeps the same values and draws [ ], [x], [-], ( ), (•), [off], and [on]. Always expose the state independently of color.
Default
Hover
Focus
Invalid
Enter a single token.
Invalid and focused
Enter a single token.
Disabled
Read-only
Unchecked
Hover
Checked
Checked, hover
Mixed
Focused
Disabled
Invalid
Select a source.
Invalid and focused
Select a source.
Group
Unchecked
Hover
Checked
Checked, hover
Focused
Disabled
Off
On
On, focused
Off, disabled
On, disabled
Default
Focus
Invalid
Enter a single token.
Invalid and focused
Enter a single token.
Disabled
Read-only
Unchecked
Checked
Mixed
Focused
Disabled
Invalid
Select a source.
Invalid and focused
Select a source.
Group
Unchecked
Checked
Focused
Disabled
Off
On
On, focused
Off, disabled
On, disabled
Default
Focus
Invalid
Enter a single token.
Invalid and focused
Enter a single token.
Disabled
Read-only
Unchecked
Checked
Mixed
Focused
Disabled
Invalid
Select a source.
Invalid and focused
Select a source.
Group
Unchecked
Checked
Focused
Disabled
Off
On
On, focused
Off, disabled
On, disabled
Loading, retention and feedback
Busy actions replace their idle label with operation-specific wording, such as “Save” becoming “Saving changes”. Reserve enough width for both labels and the indicator so the control does not move or resize. Desktop and mobile supplement the words with the four-segment spinner; the terminal uses the same words and the quadrant-block spinner. For an unresolved submission, keep “Accepted, outcome pending” or “Outcome uncertain” visible outside collapsible content. Do not unlock a replacement action because a spinner stopped.
Retain existing rows during refresh and label snapshot freshness separately from record status. Empty means a successful query returned no content; loading and unavailable get their own text. Invalid JSON remains editable and visible; guided mutation may be blocked while the last valid value remains preserved. Desktop and mobile share these messages. The terminal uses the same words, with [!] instead of a graphical edge.
Busy
Outcome pending
Submission details
Received by the queue. The external result has not arrived.
Refreshing
Loading
Loading records…
Empty
No records match this query.
Unavailable
Records are unavailable. The last snapshot is still shown above.
Invalid JSON
{ "title": "Research notes"
Line 1: expected a closing brace.
Busy
Outcome pending
Submission details
Received by the queue. The external result has not arrived.
Refreshing
Loading
Loading records…
Empty
No records match this query.
Unavailable
Records are unavailable. The last snapshot is still shown above.
Invalid JSON
{ "title": "Research notes"
Line 1: expected a closing brace.
Busy
Outcome pending
Submission details
Received by the queue. The external result has not arrived.
Refreshing
Loading
Loading records…
Empty
No records match this query.
Unavailable
Records are unavailable. The last snapshot is still shown above.
Invalid JSON
{ "title": "Research notes"
Line 1: expected a closing brace.