Interaction — States
Visual + behavioral contract for the 8 interaction states every Koder control passes through: enabled, hovered, focused, pressed, dragged, selected, activated, disabled. Material parity (`/foundations/interaction/states`). Token-level recipes for applying state overlays without per-control duplication.
Spec — Interaction: States
Facet *isual*do Koder Design. Material parity: https://m3.material.io/foundations/interaction/states.
The 8 states
| State | Trigger | Persistence |
|---|---|---|
| *nabled* | Default | Until another state takes precedence |
| *overed* | Pointer over | While hover |
| *ocused* | Keyboard nav lands; or explicit focus() |
Until blur |
| *ressed* | Pointer down OR Space/Enter on focused | While pressed |
| *ragged* | Pointer down + move past threshold | While dragging |
| *elected* | User explicit selection (selection.kmd) |
Until deselected |
| *ctivated* | Persistent expanded/open state (nav item current) | Until view changes |
| *isabled* | App logic | Until logic changes |
States are *omposable* a button can be focused AND hovered AND pressed simultaneously. Visuals layer additively.
R1 — Overlay opacity recipe
Every state visual is an *verlay*on top of the base control color. Opacity per state:
| State | Overlay opacity |
|---|---|
| Hover | 8% |
| Focus | 12% |
| Pressed | 12% (+ ripple on touch) |
| Dragged | 16% |
| Selected | 12% (persistent) |
| Activated | 12% (persistent) |
Combined states sum opacities (capped at 24%): focused+pressed = 24%.
Token: overlay color = var(--on-surface) for surface controls, var(--accent-on) for accent-filled controls. Applied as background-image: linear-gradient(<overlay>, <overlay>) so it composes on top of the base.
R2 — Focus ring contract
Focus indication must be *lways visible*for keyboard navigation — never :focus { outline: none }.
.control:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}focus-visible(notfocus) — avoids ring on mouse click (peruser agent heuristic), preserves it on keyboard nav
- 2px stroke minimum (3px on TV per
adaptive-design.kmdR7) outline-offsetso the ring sits OUTSIDE the control border- Color:
var(--focus)— typically the accent at higher contrast
R3 — Disabled state
Disabled controls:
- Reduced opacity: 38% of normal (
opacity: 0.38) - No hoverfocuspress response
cursor: not-allowed(web), no ripple (Flutter)- Reads as "dimmed, not interactive" — does NOT use error/warning red
aria-disabled="true"(NOTdisabledHTML attribute when thecontrol should still be focusable for keyboard nav announcement)
- Tooltip explaining WHY disabled (best practice — not enforced)
R4 — Pressed state on touch
On touch surfaces, "pressed" is brief — the press ends when the finger lifts. Visual:
- Ripple emanating from touch point (Material default)
- OR scale-down (
transform: scale(0.97)for ~100ms) — for variantsthat don't want ripple
- Audio feedback: optional (per
voice/wake-word.kmdif voice enabled)
Pressed visual ends on touch-up OR after a maximum 600ms (whichever first) to avoid stuck-pressed appearance.
R5 — Dragged state
For draggable elements (file list items, sortable rows, kanban cards):
- Elevation boost (shadow +2 levels) during drag
- Slight scale (1.04) so the dragged item visually "lifts"
- Drop targets show 12% accent-tinted overlay (consistent with R1)
- Drag preview shows the item content (not just a placeholder)
R6 — Selected vs activated
These are easily confused. Rule:
- *elected*— user picked this thing as part of an action they
haven't completed (
selection.kmd). Multi-select examples. - *ctivated*— this thing represents the CURRENT view/section.
Navigation item showing "you are here". Persists across the current view.
A nav item for the current page = activated. A list item picked for bulk-delete = selected.
R7 — State announcement (a11y)
Screen readers announce state changes. Requirements:
| State | Announcement |
|---|---|
| Focused | Element role + label + state ("Button, Save, focused") |
| Pressed | Implicit in focus + native role |
| Selected | "Selected" appended; via aria-selected="true" |
| Activated | "Current" appended; via aria-current="page" (or "step", "true") |
| Disabled | "Dimmed" / "Disabled" appended; via aria-disabled="true" |
| Indeterminate | "Mixed" appended; via aria-checked="mixed" |
R8 — Animation duration
State transitions use *ast*motion tokens (per future motion.kmd):
- Hover → 100ms
- Focus → 50ms (near-instant; aids keyboard speed)
- Press → 50ms in, 100ms out
- Drag start → 200ms (lift)
- Drag end → 150ms (settle)
Reduced-motion users get all of these set to 0ms (instant transitions) via @media (prefers-reduced-motion: reduce).
Cross-link
interaction/selection.kmd— selection state detailsfoundations/elements.kmd— element families per control typethemes/color-schemes.kmd— overlay color tokens