Concepts — HTML as an Application Platform
Phase 1 · Platform substrate · Specification area §3 (semantic HTML, forms, native validation, dialog, popover, details/summary, tables, media, responsive images, progressive enhancement, custom elements, Shadow DOM). Feeds §12 (accessibility) and §26 (design systems).
1. What is it
HTML is not a template format. It is a declarative application platform with built-in semantics, accessibility, focus management, validation, and state — most of which frontend teams reimplement in JavaScript, worse, at greater cost, and then maintain forever.
This module treats HTML as an engineering choice with measurable consequences, and asks the question the specification puts at the centre of the curriculum:
What problem existed? What does the platform provide? What abstraction was introduced? What trade-off did it make? When does the abstraction leak?
2. Why it matters
The platform version is usually smaller and more capable. Measured here: a <dialog> with
showModal() passes 7/7 behavioural tests in 222 bytes of JS. A naive custom modal passes
1/7 in more bytes. A conscientious hand-rolled implementation reaches 6/7 in 5.4× the
code — and owes maintenance forever.
Div soup costs more markup than semantic HTML and delivers nothing. Measured: div soup was 1.38× the bytes of the semantic version while exposing zero landmarks, zero headings and zero interactive elements to the accessibility tree. It is not a shortcut; it is more work for less function.
ARIA is a repair kit, not a substitute. Reconstructing the same accessibility tree from divs
took 1.69× the markup, required explicit tabindex to restore keyboard access that semantic
elements had for free, and only covers what you remembered to declare.
This is where accessibility becomes architecture rather than remediation. A team that reaches
for <div role="button" tabindex="0"> has taken on focus, keyboard activation, and state
announcement as permanent maintenance. A team that writes <button> has not. That decision is made
in Phase 1, and everything in fe-24 either inherits it or fights it.
3. How it works
+==================================================================+
| MARKUP -> ACCESSIBILITY TREE |
+==================================================================+
<button>Export</button>
|
v
role=button, name="Export", focusable, Enter/Space activate,
announced as a button, exposed in the controls list
|
v
screen reader / voice control / switch access / browser UI
<div class="btn" onclick>Export</div>
|
v
role=generic, no name, NOT focusable, no keyboard activation,
invisible to every assistive navigation mode
Measured: three implementations of one UI
| AX nodes | named | landmarks | headings | interactive | table roles | focusable | markup | |
|---|---|---|---|---|---|---|---|---|
| semantic HTML | 56 | 38 | 4 | 2 | 3 | 7 | 3 | 1.00× |
| div soup | 46 | 24 | 0 | 0 | 0 | 0 | 0 | 1.38× |
| div + ARIA | 48 | 33 | 4 | 2 | 3 | 7 | 3 | 1.69× |
Measured: dialog behaviours
| behaviour | <dialog> | custom, naive | custom, careful |
|---|---|---|---|
| opens | yes | yes | yes |
| focus moves in | yes | NO | yes |
| focus trapped | yes | NO | yes |
| background inert | yes | NO | yes |
| Escape closes | yes | NO | yes |
| focus restored | yes | NO | yes |
::backdrop | yes | NO | NO |
| JS bytes | 222 | 241 | 1200 (5.4×) |
4. Core terminology
| Term | Definition |
|---|---|
| Accessibility tree | The browser-computed tree exposed to assistive technology; derived from DOM + CSS + ARIA |
| Accessible name | The computed label for an element (from content, aria-label, <label>, etc.) |
| Landmark | A navigable region: banner, navigation, main, contentinfo, complementary |
| Implicit role | The ARIA role an element has by virtue of being that element |
| Progressive enhancement | Working baseline in HTML, improved by CSS and JS rather than depending on them |
<dialog> / showModal() | Native modal with focus trap, inertness, ::backdrop, Escape handling |
inert | Attribute removing a subtree from focus, hit-testing and the accessibility tree |
| Popover API | popover + popovertarget: top-layer, light-dismiss, no JS positioning state |
| Constraint validation | Native required/type/pattern/min/max/minlength + ValidityState |
| Dirty value flag | HTML-spec flag set when the user edits a field; gates minlength/maxlength |
:user-invalid | Matches only after user interaction — the selector you usually want, not :invalid |
| Top layer | Rendering layer above all content, used by modal dialogs and popovers; escapes clipping and z-index |
| Shadow DOM | Encapsulated subtree with scoped styles; affects the accessibility tree and focus traversal |
5. Mental models
Native elements are behaviour bundles, not tags. <button> is not "a styled box". It is:
focusable, Enter/Space activation, role=button, accessible name from content, participates in
forms, respects disabled, exposed to voice control, works with switch access. Choosing a div
opts out of all of it simultaneously, and each piece must be rebuilt and maintained separately.
ARIA's first rule is "don't use ARIA". Not because ARIA is bad, but because it only changes
the accessibility tree. It does not add focusability, keyboard handling, or state management —
measured above: the ARIA variant needed explicit tabindex to get 3 focusable elements, and still
has no keyboard activation behaviour. ARIA describes; elements behave.
The platform's abstraction cost is the styling boundary. Native elements trade customisation for
behaviour, and the honest version of "we can't use <dialog>" is usually "we couldn't style the
backdrop the way the design asked." That is a real cost — and it should be traded explicitly
against 5.4× the code and permanent ownership, not assumed away.
Progressive enhancement is a failure-mode question, not an ideology. The question is never "should this work without JS" in the abstract, but "what does the user see when the bundle fails, the CDN is blocked, or the script throws during hydration?" A form built on native constraints degrades to a working form. A form built on a JS validation library degrades to nothing.
6. Common misconceptions
-
"Semantic HTML is verbose." Measured backwards: div soup was 1.38× the bytes of the semantic version, and ARIA-patched divs 1.69× — for the same visual result and (in the div soup case) none of the function.
-
"ARIA makes divs accessible." It makes them announced. Measured: the ARIA variant matched semantic HTML on roles and landmarks but required manual
tabindexfor focus, and still lacks Enter/Space activation,disabledsemantics, and form participation. -
"We need a custom modal for design reasons." Sometimes true. But the naive custom modal measured 1/7 on behaviour while costing more bytes than native, and the careful one costs 5.4× and still lacks
::backdrop. Make it an explicit trade, not a default. -
"Native form validation isn't good enough." It correctly enforced
type=email,type=url,pattern, andmin/max, blocked submission, and exposed preciseValidityStateflags — with zero lines of validation JS. What it does not do is presentation, localisation, and cross-field rules. The correct architecture is native constraints as the source of truth with your own presentation layer; reimplementing the constraints is the mistake. -
"
:invalidis the styling hook.":invalidmatches before the user has typed anything, so an empty required field is styled as an error on first paint.:user-invalidis the one you want, and it is supported. -
"Setting
.valuein a test is equivalent to typing." Measured false, and it matters:minlengthreportedvalid=true, tooShort=falsefor a programmatically-set short value andvalid=false, tooShort=truefor the same string typed.minlength/maxlengthare gated on the spec's dirty value flag. A test that sets.valuepasses while the real form rejects the input — the same class of bug as fe-01's.click()finding.
7. Interview talking points
- "Div soup isn't a shortcut — we measured it at 1.38× the markup of semantic HTML with zero landmarks, zero headings and zero exposed controls. You pay more to get less."
- "I treat
<div role="button" tabindex="0">as taking on permanent maintenance: focus, keyboard activation, disabled semantics, and state announcement all become yours.<button>is the same visual result with none of that liability." - "We tested a native
<dialog>against a hand-rolled one behaviourally — focus trap, inertness, Escape, focus restore. Native passed 7/7 in 222 bytes; a careful custom implementation reached 6/7 in 5.4× the code. If design requires a custom one, fine, but that's the price." - "Native constraint validation should be the source of truth, read through the ValidityState API, with your own presentation layer. Teams reimplement the constraints, which is the one part they get for free, and then still have to build the part that's actually missing."
- "A subtle one:
minlengthonly applies to user-edited values because of the dirty value flag. So a test that sets.valuedirectly never seestooShortand passes while the real form rejects the same input."
8. Connections to other modules
fe-05/fe-06(CSS, layout) — the styling boundary is the real cost of native elements;::backdrop,::part, and form-control styling are where that trade is negotiated.fe-24(accessibility as engineering) — this module is its foundation. The specification's dependency is strict: ARIA-first accessibility built without semantic HTML produces compliant- looking, unusable interfaces.fe-20(unhappy path) — progressive enhancement is a failure-mode strategy; what the user gets when the bundle fails is decided by the choices in this module.fe-32/fe-33(testing) — the dirty-value-flag finding is a concrete case where synthetic interaction and real interaction have different semantics.fe-38(design systems) — whether a design system's primitives wrap native elements or replace them is the single most consequential decision it makes.browser-framework-internals.md§8 (DOM Internals) — cross when you need to know how the accessibility tree is computed rather than what it contains.