States are a grid, not a pile of names. The columns are the interaction: rest, hover, focus, and pressed. The rows are the application: default, error, disabled, and selected. A token exists only where the cell is real. Filling every intersection invents controls that cannot happen.
Disabled does not have a hover, so that cell is empty. It does not have a focus or a pressed state either. Publishing a faded colour for those cells invites a control that looks interactive when it cannot be used. Readonly is not disabled. A readonly field still takes focus, and it uses the same ring as every other focus cell. Do not give error its own ring colour and selected another.
The demo is that grid. The disabled cells that cannot happen are an em dash. Each focus cell that exists shows the one vermilion ring.
How to check it
- Search for a disabled hover style. It should be missing, not a faded copy of hover.
- Tab to a readonly field. It takes the shared ring. A disabled control does not.
- Check focus on default, error, and selected. The ring is the same vermilion.
.button:focus-visible,
.field[readonly]:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.button:disabled {
color: var(--fg-3);
pointer-events: none;
}