Interaction — States

mandatory

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 (not focus) — avoids ring on mouse click (per

    user agent heuristic), preserves it on keyboard nav

  • 2px stroke minimum (3px on TV per adaptive-design.kmd R7)
  • outline-offset so 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" (NOT disabled HTML attribute when the

    control 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 variants

    that don't want ripple

  • Audio feedback: optional (per voice/wake-word.kmd if 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).

  • interaction/selection.kmd — selection state details
  • foundations/elements.kmd — element families per control type
  • themes/color-schemes.kmd — overlay color tokens

Source: ../home/koder/dev/koder/meta/docs/stack/specs/interaction/states.kmd