skip to content

Across a large component library, `<div>`-based controls with hand-written ARIA keep reappearing even though native elements exist for them. How would you make native-first the default, and when is a custom ARIA widget genuinely justified?

level: principalimportance: nice to knowfreq 28%

answer

  1. recurrence is a systems signal
  2. move the shortest path
  3. the reset is the real culprit
  4. ARIA is for patterns HTML lacks
  5. few custom widgets, each owned

basics

~20 s

Make the native element the easiest path: ship styled primitives, remove CSS resets that punish real elements, lint for role and tabindex on generic tags, and require a keyboard pass in review. Custom widgets are justified only where HTML has no equivalent.

solid answer

~50 s

Recurrence means the incentives, not the knowledge, are wrong. I attack the causes: ship a small set of reviewed primitives — button, link, checkbox, radio, select, disclosure — built on native elements so the styled path *is* the accessible path; delete the global CSS resets and design constraints that made teams abandon real elements in the first place; add lint rules that flag `role` or `tabindex` on generic tags and require justification; and put a keyboard-only pass in the definition of done for anything interactive, since that is what automated rules cannot check. Custom ARIA is justified when HTML has no element for the pattern — tabs, tree views, comboboxes, grids — or when a native control cannot meet a hard product requirement. Those get treated as owned components with tests and a named maintainer, not markup pasted into features. The measure of success is that writing a div-button is more work than writing a button.

go deeper

for a junior

Know the outcome you are aiming for: use the library's button component, and if you find yourself typing role or tabindex on a div, that is the moment to ask whether a native element exists.

for a middle

Be ready to explain why a lint rule on role/tabindex and a keyboard pass in review catch different classes of defect, and why an automated accessibility score can be green while controls remain unusable.

for a senior

Show that you would find the cause — the reset, the design constraint, the generic wrapper — before adding policy, and describe the verification that closes the loop rather than the checklist that documents it.

for a principal

Own the incentive design and the exception budget: which primitives the organisation guarantees, how a custom widget gets approved and maintained, what you measure instead of rule counts, and how you keep the accessible path the lowest-effort path as the system grows.

## Diagnose before you legislate When the same defect keeps returning after being fixed, education is not the missing input. Ask why an engineer chose a div in the moment. The honest answers are usually structural: - **The styled path is the wrong path.** The design system's `.btn` class was written for a div, or a global reset stripped the native button so thoroughly that it is easier to start from nothing. - **The design forbade the native control.** A picker mock-up that no native `<select>` can render, so the whole control was rebuilt — and then the pattern was copied for controls that *could* have been native. - **The framework primitive encouraged it.** A generic wrapper component that renders a div and spreads props, so the tag is invisible at the call site. - **Nothing caught it.** Review looked at the diff's logic, the automated checker passed, and no one used the keyboard. Each cause has a different fix, and a policy that ignores them produces a rule people route around. ## Make the correct thing the cheapest thing The governing principle: engineers do not choose accessibility, they choose the shortest path. Move the shortest path. **Ship the primitives.** A small, non-negotiable set — button, link, text input with its label, checkbox, radio, select, disclosure — each built on the native element, each fully styled to the design language, each with the focus indicator already correct. If the styled component is the native element, using it is not a virtue, it is the default. **Remove the traps.** Audit the global stylesheet for resets that make native elements unusable and the design constraints that made teams abandon them. This is where a design-system owner earns their keep: negotiating a select that is stylable-enough rather than shipping a hand-built listbox to every product. **Make the exception visible.** A generic box primitive that lets any tag be passed in encourages accidental divs. Require interactive components to name their element explicitly. ## Enforce at the cheapest checkpoint Layer the checks so each catches what the previous cannot: - **Lint** for the syntactic tells: `role` on a generic tag, `tabindex` on anything that is not a documented pattern, a click handler on a non-interactive element, `role` values that duplicate an element's implicit role. Fast, in-editor, no judgment required. - **Automated rule engines** in CI as a floor — they catch invalid roles, unresolved references, missing names. - **Review** carries the part machines cannot: does the declared pattern implement its keyboard contract? A concrete definition of done for interactive work — *Tab to it, operate it with the keyboard, see the focus indicator* — is worth more than a checklist of attributes. - **Tests** that assert computed role, name and state rather than the presence of an attribute string, so replacing a div with a button does not fail the suite and dropping a native element does. ## When custom is genuinely justified Hold two conditions. First, **HTML has no element for the pattern** — tabs, tree views, comboboxes, grids, carousels, drag-and-drop reordering. ARIA exists precisely for this, and refusing to use it is as wrong as reaching for it first. Second, **a native element exists but cannot meet a hard requirement** — a genuine one, verified, not a styling preference that a designer would trade away if told the price. When you accept a custom widget, accept the whole cost: an owning team, an implementation of the full keyboard contract for that pattern, tests at the accessibility-tree level, verification on the assistive technologies you support, and a review each time the platform ships a native replacement. A custom widget is a *product* with a maintenance budget, and the organisation should have very few of them. ## What you measure Avoid rule counts — they reward attribute hygiene and can improve while users stay blocked. Better signals: how many hand-rolled interactive patterns exist outside the library and whether that number falls; whether the top user flows are completable keyboard-only, retested each release; how many `role`/`tabindex` lint exceptions are open and who owns each. Those track the thing you actually care about. ## The point to land Say that recurrence is a systems signal, not an ignorance signal. Fix the incentive — make the native element the styled, documented, lowest-effort choice — then enforce with the cheapest check that can see the defect, and treat the small set of legitimate custom widgets as owned components rather than as markup.

  • A product team insists their designed dropdown cannot be a native `<select>`. How do you decide?
    Separate the requirement from the preference. Ask what specifically the native control cannot do — rich option content, multi-select with search, inline actions — and whether the design would survive without it. If a native control covers the use case, ship it and bank the mobile platform picker. If not, the team gets a custom listbox as a library component with an owner, a keyboard contract and tests, not a one-off in a feature.
  • How do you keep the custom widgets you did accept from decaying over time?
    Treat each as a product: a named owner, a documented keyboard contract, tests asserting computed role, name and state, and verification on the assistive technologies you support before release. Add a periodic review that asks whether the platform has since shipped a native replacement — when it has, migrating deletes code rather than adding it.
  • Which enforcement mechanism gives the best return early on, if you can only add one?
    A lint rule that flags `role` and `tabindex` on generic elements, with an explicit allowlist. It is instant, needs no judgment, and points directly at the pattern you are trying to eliminate — and the allowlist doubles as a live inventory of every custom widget the organisation owns, which is the number you want to watch.

saying these in an interview costs you the question

  • Answers with training and a wiki page and nothing structural
  • Bans ARIA outright instead of reserving it for missing patterns
  • Relies on an automated score as the definition of done
  • Ignores the CSS reset or design constraint that caused the divs
  • Lets every team own its own hand-built dropdown

context