The Attribute Isn't the Behavior
Open any component library's Dialog implementation. You'll find role="dialog" and aria-modal="true" on the content panel. Check the box, ship it, call it accessible.
Now try using it with a keyboard.
Open the dialog. Press Tab. Where does focus go? If it goes behind the dialog to the page content, the dialog isn't accessible — a keyboard user is now interacting with elements they can't see, behind a modal overlay. Press Tab twelve more times. If focus never wraps back to the first element inside the dialog, it's not trapped. Press Escape. If the dialog doesn't close and return focus to the button that opened it, a keyboard user is stranded.
role="dialog" tells a screen reader "this is a dialog." It doesn't trap focus. It doesn't handle Escape. It doesn't restore focus on close. It doesn't prevent clicks outside from reaching the page behind. It's a label, not a behavior.
The gap between "has the right ARIA attributes" and "actually works for someone using a keyboard or screen reader" is where most component libraries cut corners. Not out of malice — out of complexity. A fully accessible modal dialog requires a focus trap with tabbable element detection, click-outside dismissal that doesn't false-positive on drag events, Escape key handling that doesn't leak to parent components, focus restoration to the trigger element, and initial focus placement with a priority chain. That's five independent behaviors converging on one component.
I built all of it from scratch for . When the styled layer wraps these headless primitives with token-driven CSS, the flat hex values in the generated output mean every semantic change requires a rebuild. var() chains would cascade at runtime. For a published design system, that's the right trade-off — and the first thing I'll revisit in Phase 3.
All three primitives, the hooks that power them, and 173 tests are in the flintwork repo. The next phase is a styled layer that consumes these primitives and applies token-driven CSS — connecting the design token pipeline to the components that use them.
The thing I keep coming back to: accessibility isn't a feature you add to components. It's the architecture of the component. The focus trap isn't a wrapper around the dialog — it's what makes the dialog a dialog. The roving tabindex isn't an enhancement to the tab bar — it's what makes keyboard navigation usable. When you build the behavior layer first, the accessibility comes with it. When you build the visual layer first and add accessibility later, it never quite fits.
SOCIAL SHARE CARD GENERATOR