Step 2 — Native <dialog> vs Your Modal

Goal

Test modals behaviourally rather than visually, and price what a custom one actually costs.

Predict first

Three modals, same appearance: native <dialog> + showModal(); a naive custom <div role="dialog" aria-modal="true">; and a careful custom one with focus trap, inert, Escape and focus restore.

For each, predict pass/fail on: focus moves in · focus trapped over 8 Tabs · background inert · Escape closes · focus restored to trigger · ::backdrop available.

Then predict JS bytes for each. Most people expect the naive custom modal to be the smallest.

Run

npm run dialog

Expected output:

  behaviour          | <dialog> showModal()| 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 required (normalised bytes):
    <dialog> showModal()     222 bytes  (1.0x native)
    custom, naive            241 bytes  (1.1x native)
    custom, careful         1200 bytes  (5.4x native)

What just happened

The naive custom modal passes 1 of 7 — and costs more bytes than native. It is exactly what "just use a div with role="dialog" and aria-modal="true"" produces. aria-modal is a promise to assistive technology that the rest of the page is unavailable; it does not make it so. Focus walks straight out the back.

The careful version reaches 6/7 at 5.4× the code. It is a genuine good-faith implementation — focus trap with wrap-around, inert on siblings, Escape handling, focus restore. It is also now yours forever: it must keep working as new focusable element types appear, across shadow DOM boundaries, inside iframes, and through browser focus-behaviour changes.

Note how the tests are written. Not "is there a focus trap in the code" but: press Tab eight times through CDP and ask where focus is. Press Escape and ask whether it closed. Try to focus a background button and see whether the browser allows it. Behaviour is the requirement, so behaviour is what is measured — and this is the test suite you should demand of any component library before adopting it.

The one native gap is ::backdrop, available only to modal <dialog>. Which points at the real trade: the objection to native elements is almost always styling, and the platform has been closing that gap (::backdrop, Popover API, anchor positioning, appearance: base-select). See docs/analysis.md.

The decision, stated properly

You are not choosing markup. You are choosing an ownership boundary. The question is not "can we build this?" — you can — but "will it still be correct in three years, after four team changes, a framework upgrade, and a browser focus-behaviour change?"

Measured cost for the simplest possible case: 5.4× the code and still one behaviour short. A combobox is an order of magnitude beyond that.

Checkpoint

docs/verification.md Checkpoint 3.