Measured Results

Chrome for Testing 149.0.7827.55 (arm64 macOS) via playwright-core/CDP. Reproduce: cd ../src && npm run all. Accessibility trees come from Accessibility.getFullAXTree — the browser's real computed tree, not inferred from markup.


1. Accessibility tree — three implementations of one UI

variantAX nodesnamedlandmarksheadingsinteractivetable rolesfocusable
semantic HTML563842373
div soup462400000
div + ARIA483342373

Landmarks exposed:

  • semantic — banner, navigation, main, contentinfo
  • div soup — (none)
  • div + ARIA — banner, navigation, main, contentinfo

Markup size, normalised:

variantbytesvs semantic
semantic HTML4591.00×
div soup6341.38×
div + ARIA7781.69×

Div soup costs 38% more markup and delivers nothing. It is not a shortcut. ARIA can rebuild the tree at 69% more markup — and note the focusable column: the ARIA version needed explicit tabindex to restore keyboard access that semantic elements provide for free. ARIA changes the accessibility tree; it does not confer behaviour.


2. Dialog behaviours — driven with real keyboard input

behaviour<dialog> showModal()custom, naivecustom, careful
opensyesyesyes
focus moves inyesNOyes
focus trapped (8 × Tab)yesNOyes
background inertyesNOyes
Escape closesyesNOyes
focus restored to triggeryesNOyes
::backdrop availableyesNONO
implementationJS bytesvs native
<dialog> showModal()2221.0×
custom, naive2411.1×
custom, careful12005.4×

The naive custom modal costs more bytes than native and passes 1 of 7 behavioural checks — and it is what "just use a div with role="dialog"" produces in practice.

The careful version is a good-faith implementation with focus trap, inert, Escape and focus restore. It reaches 6/7 at 5.4× the code, and it must keep working across new focusable element types, shadow DOM, iframes, and browser changes — forever.


3. Native constraint validation — zero lines of validation JS

fieldconstraintvaluevalidValidityState flag
emailtype=email"not-an-email"NOtypeMismatch
emailtype=email"a@b.co"yes
agemin=18"9"NOrangeUnderflow
agemin=18"30"yes
codepattern"abc-12"NOpatternMismatch
codepattern"ABC-1234"yes
sitetype=url"nope"NOtypeMismatch
sitetype=url"https://x.dev"yes
  • submission blocked while invalid: yes
  • submission allowed once valid: yes
  • :invalid matches: yes
  • :user-invalid supported: yes — and it is the one you want

The dirty-value trap

how the value was setvalidtooShort
el.value = 'short' (programmatic)truefalse
user types "short" (real keystrokes)falsetrue

minlength/maxlength apply only to values the user edited — the HTML spec's dirty value flag. A test that sets .value directly never sees tooShort and passes while the real form rejects the same input.

This is the same class of finding as fe-01's .click() result: synthetic interaction and real interaction have different semantics, and the test harness is the thing that is wrong.

What native validation does not do

  • inline error text positioned in your layout (the bubble is unstyleable)
  • aria-describedby wiring between input and error
  • localisation — browser messages follow browser locale, not application locale
  • cross-field rules (confirm password, end-date after start-date)
  • server-side validation, which is the only one that is a security control

Correct architecture: native constraints as the source of truth, read via ValidityState, with your own presentation layer on top. Reimplementing the constraints is the mistake; building the presentation is the work.