Design Principle
ARIA is not a magic semantic layer for divs; a component is implementable only when native semantics, keyboard behavior, accessible name, state attributes, and visual state agree.
Large Theory
Custom components often fail when the visual already looks like a button, tab, combobox, or menu, while the DOM, keyboard behavior, focus model, and screen-reader semantics remain plain divs. aria-component-contract freezes the behavior contract before implementation: decide whether native HTML can do the job, choose a mature primitive or APG pattern when needed, and only then add ARIA, data-state, and styling. This keeps ARIA from becoming a late patch and makes it part of the component state machine.
Small Points
- Native-first comes first: prefer
button,a,input,select,textarea,details/summary, anddialogwhen they fit. Move to ARIA/APG/primitives only when native HTML cannot express the compound behavior. - The component contract includes at least role, accessible name, description, value, owned/controlled elements, keyboard keys, focus entry/exit, state attributes, and failure states.
- Name, Role, and Value must be testable: icon buttons have names, tabs expose selected state, switch/button pressed or checked state matches visuals, and invalid inputs are connected to error descriptions.
- ARIA attributes cannot be separated from behavior:
aria-expandedreflects real expanded content,aria-selectedis not a generic current-page marker,aria-disableddoes not block clicks by itself, andaria-busyneeds visible state plus a completion path. - Keyboard matrices are component-specific: Tab enters and exits, Enter/Space activate, Arrow/Home/End move within composite widgets, Escape closes or cancels, and typeahead or
aria-activedescendantis used only when the pattern fits. - Choose and document one focus model: DOM focus moves among items, or focus stays on the container/input while
aria-activedescendantexposes the active item. Do not mix models casually. - Visual states cover hover, focus-visible, active, disabled, loading, invalid/error, selected/open/pressed, forced-colors, and reduced-motion; critical state cannot rely only on color or motion.
- For Radix, shadcn, React Aria, and similar primitives, labels, descriptions, portals, focus behavior, and data attributes are behavior boundaries. Styling or motion changes need replacement tests.
- Tests prioritize
getByRoleand accessible name, keyboard paths, state attributes, focus destinations, and live text. Screenshots prove visuals, not semantics.
Design Judgment
A component is ready for frontend implementation when its contract can answer why native HTML is not enough, what happens for each key, what name/role/value screen readers receive, how visual and ARIA state stay synchronized, how failures recover, and how tests assert the behavior. If those answers are unclear, fall back to native controls, Radix, React Aria, Ariakit, or APG patterns instead of adding more roles and CSS.
Implementation Notes
Output an AriaComponentContract: componentType, userTask, nativeAlternative, chosenPattern, primitiveSource, role, accessibleNameSource, descriptionSource, valueModel, ownedElements, controlledElements, stateMap, keyboardMatrix, focusModel, portalContext, formIntegration, loadingPolicy, invalidPolicy, disabledPolicy, forcedColorsPolicy, reducedMotionPolicy, dataStateSelectors, and testAssertions. In React, use data-state for styling and ARIA for semantics; do not let CSS classes become the only source of state.
Counterexamples/Risks
A div role=button without Space/Enter activation; icon buttons with only SVG and no name; visual selected state disagreeing with aria-selected; custom selects that cannot be operated by keyboard; aria-disabled=true while the click handler still runs; loading spinners without text status; red-border-only errors; Radix Dialog animation breaking focus return; and virtual lists whose aria-activedescendant points to a missing node.
Case Study
W3C Using ARIA's first rule is to use native HTML elements and attributes when they already provide the needed semantics and behavior. ARIA in HTML clarifies which roles and attributes are allowed on specific HTML elements. WCAG Name, Role, Value requires user interface component names, roles, states, properties, and values to be programmatically determinable. Testing Library's ByRole query turns role and accessible name into test entry points, making the contract automatable. Radix Primitives and React Aria are valuable not for their default visuals, but because they stabilize focus, keyboard, state, and labelling behavior before local styling. The reusable lesson is that an accessible component is not a set of ARIA attributes; it is consistency from DOM semantics through test queries.
Source Links
W3C Using ARIA: https://www.w3.org/TR/using-aria/ W3C ARIA in HTML: https://www.w3.org/TR/html-aria/ WAI-ARIA APG Patterns: https://www.w3.org/WAI/ARIA/apg/patterns/ WCAG Name, Role, Value: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html MDN ARIA guides: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides Testing Library ByRole: https://testing-library.com/docs/queries/byrole/ Radix Primitives Accessibility: https://www.radix-ui.com/primitives/docs/overview/accessibility Carbon Accessibility Testing: https://carbondesignsystem.com/guidelines/accessibility/testing/
Agent Directive
Before writing a custom interactive component, output the native-first decision, name/role/value, state attribute table, keyboard matrix, focus model, visual states, and test assertions; do not use ARIA to patch div components that lack keyboard behavior.