States must be controllable and observable from outside the component
Aliases: controllable state · data-state · testable state · Storybook controls
What it is
A state that lives only in :hover or in an internal timer cannot be held by a parent, a test, or the docs. States must be driven from outside the component — opened with a prop, a data attribute, or an argument — and observed from outside, via data-state, ARIA, or a stable class. Fail either, and the state table holds only for the author’s mouse, not for review or regression.
Drive lets Storybook and automation skip a real hover or a real slow network. Observation lets assertions skip pixel matching. Missing either, the state is still a side effect.
Why it happens
CSS pseudo-classes bind state to pointer position. Test runners then fire unreliable hover events, or never reach focus-visible cells that appear only for the keyboard. An internal setTimeout binds loading to wall-clock time, so the loading frame in the docs finishes before the screenshot. External drive turns those cells into inputs: data-state="loading" or isDisabled lets any caller — tests included — set the bit.
Observation is the dual. Without a stable outward hook, visual regression can only screenshot the page, and screen-reader tests can only fuzzy-match text. aria-busy, aria-invalid, and data-state make queries look like the contract. Parents need observation too: a table may disable bulk actions while a row is submitting. If state will not cross the component boundary, the composition layer pokes internal DOM and shatters when a class name changes.
Where it stops holding
A static site with no script and purely decorative pieces has no external driver; observation can degrade to “you can see it.” Some states on native controls (platform :disabled) are already observable to selectors and assistive technology; a custom data attribute is optional. Custom controls do not get that channel for free. Transient hover that cannot be server-rendered may stay a pseudo-class, but an equivalent force-prop should still exist for docs. Security-sensitive internal flags (a risk lock) may be hidden from ordinary parents, yet they still need to be observable to a test account or a dedicated query channel, or that cell can never regress.
Applying it
- Give every public state an input (prop or data attribute) and an output (ARIA or
data-state), and name both in the component docs. - Light every cell from Storybook controls. “Hover it with the mouse” must not be the only demo.
- Open disabled, loading, and error from tests by setting the input, then assert the output hook. Pixel diff is not the sole oracle.
- How to check: without moving the pointer or mocking the clock, can an outer test open and read back every cell. Any cell it cannot is still locked inside the component.
Related
- Same group: R1.03.1 Every component needs a complete interactive state set · R1.03.2 Loading, empty, and error are the states most often left unspecified · R1.03.3 Missing states get improvised during implementation · R1.03.4 Transitions between states fail more often than the states themselves · R1.03.5 Overlapping states need an explicit priority rule
- Nearby: R3.09 Semantic structure and accessibility implementation · R3.01 Semantic structure
- Search terms:
externally driven observable state·data-state·aria-busy
Cards in the same group
- R1.03.1Every component needs a complete interactive state set
- R1.03.2Loading, empty, and error are the states most often left unspecified
- R1.03.3Missing states get improvised during implementation
- R1.03.4Transitions between states fail more often than the states themselves
- R1.03.5Overlapping states need an explicit priority rule