---
title: "Interaction states"
description: "State layers, the gap-then-ring focus indicator, and how disabled is drawn."
url: "https://tanrei.chnotchy.com/foundations/states/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Interaction states

> State layers, the gap-then-ring focus indicator, and how disabled is drawn.

Interactive components share one vocabulary of states: enabled, hovered, focused, pressed, dragged, selected, activated, error, loading and disabled. A component’s spec lists the states it supports, and every platform must implement all of them.

## State layers

Hover, focus, press and drag are shown with a **state layer**: a translucent overlay that covers the component’s container.

- The layer takes the color of the **content**, that is the `on-` role of whatever it covers. On a filled button it is `on-primary`; on a text button it is the label color.
- Its opacity comes from a token, not from the component.
- **Only one layer is visible at a time.** When states overlap, the strongest wins: pressed over focused over hovered.
- It fades in over a short effects transition and appears immediately on press.

The quiet selection is a layer too, and the one that stays: a list’s selected option, a menu’s current link item and the tonal tabs’ indicator are drawn in their content color at `selected-state-layer-opacity`, twice the hover layer, over whatever the item stands on. So a selected item stands as far from a hovered one as a hovered one stands from one at rest, on any surface. The hover, focus and press layers are laid over it, and the selection is never told by the fill alone: a check, a mark or the label says it too.

| Token | Value | Specimen |
| --- | --- | --- |
| `--tanrei-sys-state-disabled-container-opacity` | 0.12 (12%) |  |
| `--tanrei-sys-state-disabled-content-opacity` | 0.38 (38%) |  |
| `--tanrei-sys-state-dragged-state-layer-opacity` | 0.16 (16%) |  |
| `--tanrei-sys-state-focus-state-layer-opacity` | 0.1 (10%) |  |
| `--tanrei-sys-state-hover-state-layer-opacity` | 0.08 (8%) |  |
| `--tanrei-sys-state-pressed-state-layer-opacity` | 0.1 (10%) |  |
| `--tanrei-sys-state-selected-state-layer-opacity` | 0.16 (16%) |  |

Generated from `design/tokens/tanrei.light.tokens.json`, `tokens/sys/state.tokens.json`

Hover and press the buttons below to see the layer on different containers:

## Focus: gap, then ring

Keyboard focus is drawn as a 2px gap in the surface color followed by a 2px ring in the `focus-ring` role. The gap guarantees that the ring is visible even when the component’s own fill is close to the ring color, and the role is checked for 3:1 contrast against surfaces.

- The ring is shown for keyboard and assistive-technology focus, not for pointer clicks.
- It is never animated.
- It follows the component’s shape, offset outward.

## No ripple by default

Tanrei does not draw an expanding ripple on press. The pressed state layer is the feedback. A theme may opt into ripple, but no component depends on it.

## Disabled

A disabled component draws its container at the `disabled-container-opacity` and its content at the `disabled-content-opacity` of `on-surface`, shows no state layer, and leaves the focus order. Disabled elements are exempt from contrast requirements, which is a reason to use them sparingly.

> **Tip**
>
> Prefer keeping an action enabled and explaining what is missing when it is used. A disabled button cannot tell anyone why it is disabled.
