Accessibility work fails in a predictable way: it gets scheduled. It becomes an audit near the end of a quarter, which produces a list of findings against components that are already everywhere, which produces a set of patches that fix the symptoms an automated tool can see. The alternative is not more diligence at the end. It is treating each interaction pattern as a contract with an agreed set of obligations, and building the component to the contract the first time, when it costs an hour instead of a release.
Every pattern is a promise
A thing that looks like a tab list is promising arrow-key navigation between tabs and a single stop in the tab order for the whole group. A thing that looks like a modal dialog is promising that focus moves into it, cannot leave it while it is open, and returns to whatever opened it. A thing that looks like a menu button is promising that Enter and the down arrow open it and Escape closes it back onto the trigger. The visual is the advertisement; the behaviour is the product, and a user who has learned the pattern elsewhere will arrive expecting it here.
The obligations are not a matter of taste. They are written down, pattern by pattern, in the authoring practices that accompany the ARIA specification, which turns most accessibility arguments into a lookup rather than a debate.
What a combobox owes its user
The combobox is the pattern that exposes whether a library takes this seriously, because it is the one where the naive implementation is convincingly wrong. A text input with a popup list of options owes a programmatic relationship between the input and the list, an expanded state that is exposed rather than implied by appearance, and a reference from the input to the option currently highlighted, so that assistive technology can announce it.
The difficult part is that the highlighted option is not focused. Focus stays in the input the whole time, because the user is still typing, and the arrow keys move a pointer that exists only as an attribute. Get that wrong by moving real focus into the list and typing breaks; skip it entirely and the widget works perfectly with a mouse while announcing nothing at all. Then there is the rest of the contract: Escape closes without committing, Enter commits the highlighted option, the list filters as the user types, and the number of remaining results is announced politely rather than after every keystroke.
Focus is state, and something has to own it
Most keyboard bugs are ownership bugs. A dialog opens and nobody moves focus, so the keyboard user is still at the top of the page behind it. A dialog closes and nobody returns focus, so the next Tab starts from the document again. A row is deleted while focus is inside it and focus falls to the body, which is the interface equivalent of being put down in a different room.
Composite widgets need the same deliberateness in the other direction. A toolbar or a menu should be one stop in the tab order with the arrow keys moving within it, which means exactly one item is tabbable at a time and the component is responsible for moving that property around as the selection changes. And whatever owns focus must leave it visible: removing the focus ring without replacing it with something at least as clear is the single most common way a component becomes unusable without a mouse.
Contrast, motion and the tokens underneath
Some of this belongs in the token layer rather than the component. Contrast is a property of a pair of colours, so it can only be guaranteed where pairs are defined, which means semantic tokens that carry a surface and the ink meant to sit on it together. Non-text contrast matters just as much and is checked half as often: borders that separate controls, the focus indicator itself, the icon that is the only label a button has.
Motion is the same kind of decision. A user who has asked their operating system to reduce motion has told you something specific, and a component that honours it by shortening transitions rather than removing feedback entirely respects both the request and the reason the animation existed.
The automated pass finds the easy half
Linters and automated auditors are genuinely worth running: they catch missing labels, wrong roles, broken relationships between elements and low contrast on static pairs, and they catch them in seconds. What they cannot tell you is that the tab order walks through the page in an order nobody would choose, that a live region announces the wrong thing at the wrong moment, or that a control is technically labelled and the label is meaningless.
Those come from using the thing. Put the mouse away and walk the screen with the keyboard, turn on a screen reader and listen to what a change actually announces, zoom to two hundred percent and see what reflows. It takes a few minutes per component and it is the difference between a component that passes a checker and one that works.




