R1.03.6externally driven observable statedesign

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

Quick Actions

Share

Share this page

ios_share

https://hci.top/en/handbook/R1.03.6