Component Archaeology
Field Study 01 — Overlays

The Dialog

Focus management and accessibility: the 30-year arc of the web rediscovering what desktop GUIs solved in the 1980s.

20 min read · runnable demos Updated

Every dialog is the same dialog. A rectangle floats above the work; the rest of the screen goes quiet; you answer a question and the rectangle disappears. The behavior has barely changed since the first desktop computers grew windows. What changed — relentlessly, fashionably, sometimes destructively — is how we built it.

The hard parts were always identical: trap keyboard focus inside the box, restore it on close, make the rest of the page inert, escape on Esc, and render above everything else. The web spent thirteen years re-solving those five problems from scratch — and the two eras before the web had already solved most of them. We start there, in the dirt, because the thesis only makes sense if you can see the convention being invented.1

00 Context · before the web

Desktop GUI Dialogs, 1984–1995

The dialog was invented before the web existed. The desktop GUI shipped the whole pattern — modality, a focused default button, Esc to cancel — and the browser would spend the next thirty years catching up to it.

The modal alert with a heavy-bordered default button — press Return to accept — and the pinstripe title bar.Recreation after Apple Macintosh System 7 (1991). Drawn in CSS; not a screenshot.
The MessageBox — a beveled grey panel, a navy title bar, and the focus ring drawn as a dotted rectangle.Recreation after Microsoft Windows 3.1 (1992). Drawn in CSS; not a screenshot.

By the early 1990s the modal dialog was a solved problem on the desktop. The operating system owned the window manager, so it could do what the web could not for decades: genuinely freeze the application behind the box, place the dialog unambiguously on top, route every keystroke into it, and mark one button as the default that Return would press. Esc meant cancel. Tab cycled the controls and stayed inside.

None of this was negotiable or re-implementable — it was the platform. That is the whole thesis of this study in one sentence: the web spent its entire history trying to rebuild, in user space, a thing the desktop got for free from the kernel. Keep these two boxes in mind. Every era that follows is measured against them.

01 Context · the first web dialog

window.alert & confirm, 1995

The web shipped with exactly one dialog, and it was a single function call. It worked perfectly — and you could not style one pixel of it. That tension defines everything after.

window.confirm() · native, blocking Live

Netscape’s confirm() was the platform dialog, inherited. It blocked the JS thread, trapped focus, returned a boolean, and was drawn by the browser chrome — perfectly accessible, perfectly modal, perfectly impossible to brand. One line of code; zero control. For a decade that was the only correct dialog on the web, and almost nobody could live with how it looked.

It did everything right except the one thing designers cared about.

The entire history that follows is the field rejecting this box — trading its bulletproof behavior for the freedom to style it, and then spending fifteen years trying to win the behavior back.

if (confirm("Delete this item?")) {
  remove(item);
}

The whole API. It blocks, traps focus, and returns a boolean — and you cannot change a single pixel of its appearance.

so we built our own — out of a div

02 The DIY era

The hand-rolled <div>, 2008

To escape the ugly native box, we built our own from a positioned <div> and a semi-transparent overlay. It looked however we wanted. It also re-introduced every problem the platform had already solved.

Vanilla JS · absolute positioning Live

The recipe spread across every codebase: an absolutely-positioned <div> for the box, a second full-screen <div> at fifty-percent black for the overlay, and a hard-coded negative margin to “centre” it. Finally the dialog matched the brand. Finally a designer was happy.

And every desktop guarantee evaporated. Tab walked straight out the back of the box into the page behind it. Esc did nothing. The overlay only looked like it disabled the page — links underneath were still clickable. Screen readers were told nothing; there was no ARIA at all. And the centring broke the moment the content was taller than you guessed.

We traded a box we couldn’t style for a box that didn’t work.

Why it mattered anyway

It established the ambition: the dialog is ours now, part of the document, styleable with CSS. Every era after this is an attempt to keep that freedom while clawing back the correctness we’d just thrown away.

<div id="overlay"></div>
<div id="box">
  <p>Delete this item?</p>
  <button onclick="hide()">OK</button>
</div>

A hard-coded margin-left stands in for real centring. The accessibility cost is entirely invisible — until someone audits it.

then a widget promised to do it for us

03 The widget era

jQuery UI Dialog, 2010

You wrote a <div>, called .dialog() on it, and a whole window system unfolded out of your markup. It felt like magic. It was a coat of paint over the same structural problem.

jQuery UI 1.8 · Smoothness theme Live

jQuery UI took an ordinary element and upgraded it in place. The title came from an attribute; the buttons from a config object; the whole thing was lifted out of the document and re-parented to the end of <body> so it could float free. ThemeRoller let you click together a colour scheme and download a stylesheet. For a generation, this was the first time a UI component “just worked.”

It looked like a window. Underneath, it was a div with delusions of grandeur.

What it cost

Focus still wasn’t trapped — Tab wandered out the back. The DOM rewrite meant your markup and your runtime tree no longer matched, and the second overlay <div> kicked off a decade-long z-index arms race.

What it won

Speed, and a shared vocabulary. .dialog() meant the same thing on every project — and that consistency, not the correctness, is why it long outlived its expiry date.

// the widget reads from markup, then rewrites it
$( "#dialog" ).dialog({
  modal: true,
  width: 320,
  buttons: {
    "Delete item": function() {
      $( this ).dialog( "close" );
    }
  }
});

An imperative API consuming a markup template — then rewriting your DOM out from under you.

then the framework ate the page

04 Markup is the API

Bootstrap 3 Modal, 2014

Bootstrap moved configuration out of JS and into data- attributes. You described the modal in HTML and wired the trigger with two attributes. The component disappeared into the markup.

Bootstrap 3.0 · fade + slide Live

The Bootstrap 3 modal was a cultural reset. There was no .modal() call to remember; you added data-toggle="modal" to a button, pointed it at an id, and a delegated handler did the rest. Suddenly someone who’d never written a line of JS could ship a working dialog.

It also standardised the anatomy everyone still draws today — modal-header, modal-body, modal-footer, primary action bottom-right. That skeleton is now so ingrained it reads as a law of nature. It isn’t; Bootstrap decided it.

Bootstrap didn’t fix focus. It made the broken thing easy enough that everyone shipped it.

The reckoning

Accessibility improved on paper — role="dialog", aria-hidden toggling — but focus management stayed shaky for years, and the single global backdrop made stacking modals a documented hazard. The invisible cost: you now shipped a whole framework’s CSS to render one box.

<div class="modal fade" id="confirm">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">…</div>
      <div class="modal-body">…</div>
      <div class="modal-footer">…</div>
    </div>
  </div>
</div>

The header / body / footer anatomy Bootstrap canonised is still the default mental model for every modal.

then everything became a component

05 The dialog becomes a component

Material-UI Dialog, 2017

Component frameworks absorbed the dialog into the application's object graph. You stopped writing markup and started declaring state: <Dialog open={open}>.

Material · elevation 24 · scrim Live

Material Design gave the dialog a physics. It sat on the elevation system — a surface lifted above the page, casting a layered shadow, dimming everything below behind a 32%-black scrim. The motion was specified: scale up from 90% on a named curve. For the first time the dialog’s appearance derived from a design system rather than being invented per project.

Architecturally it went further than anything before. In React, MUI gave you a declarative <Dialog> driven by a state variable; Angular Material made it an injectable service. Focus management, scroll-locking and ARIA were finally handled for you.

The dialog stopped being markup and became a unit of application architecture.

The bill

Lock-in and weight. Your “delete?” confirmation now depended on the framework’s runtime, theming layer and dependency-injection system — and pulled in a design language with opinions about everything down to the ripple on the button.

<Dialog open={open} onClose={close}>
  <DialogTitle>Use location?</DialogTitle>
  <DialogContent>…</DialogContent>
  <DialogActions>
    <Button onClick={close}>Agree</Button>
  </DialogActions>
</Dialog>

Declarative state in React; an injectable service in Angular — two answers to the same question: who owns the open dialog?

then we took the styling back

06 Behaviour without opinion

Radix Dialog, 2021

The headless turn split the component in two. Radix shipped the behaviour — focus trap, ARIA, escape, portalling — and shipped exactly zero pixels of style. You brought the CSS.

Radix UI · headless + your CSS Live

By 2021 the field had learned the lesson of the Material years: the hard, universal part of a dialog is its behaviour; the part you actually want to control is its look. Headless libraries inverted the deal. Radix gives you compound parts — Dialog.Root, Dialog.Trigger, Dialog.Portal, Dialog.Content — that wire up a fully accessible, focus-trapping, portalled modal, then get out of your way.

The accessibility everyone had failed to hand-roll for a decade was finally a solved, audited dependency. In the demo: Tab is trapped, Esc closes, focus returns to the trigger. You wrote none of that — and every pixel of the styling.

Give me the keyboard handling. Keep your shadows.

The trade

Freedom returned, with responsibility attached. A headless dialog is a blank canvas: get the CSS wrong and you ship an invisible focus ring or a broken mobile layout. The behaviour is bulletproof; the design is entirely on you again.

<Dialog.Root>
  <Dialog.Trigger>Edit</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className="overlay" />
    <Dialog.Content className="content">
      <Dialog.Title>Edit profile</Dialog.Title>
      <Dialog.Close>Save</Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Compound components describe structure; the library owns behaviour, your stylesheet owns appearance.

then the platform finally shipped it

07 The platform catches up

Native <dialog> + ::backdrop, 2023

After nearly three decades, the browser shipped the thing. <dialog> with showModal() gives focus trapping, the top layer, the inert background and ::backdrop — natively, with no dependency at all.

Native element · top layer Live

The native <dialog> is the quiet endpoint of the story. Call showModal() and the browser promotes it to the top layer — a rendering layer above everything, which kills the z-index wars dead. The background becomes inert. Focus is trapped and restored. Esc works. A <form method="dialog"> closes it and returns a value. Dim it with ::backdrop.2

What took jQuery UI a thousand lines and Radix a careful dependency is now five problems the platform solves for free. With @starting-style and allow-discrete, even the entrance animation — the last thing libraries held over the platform — is built in.

Three decades to standardise a box that floats. The web gets there; it just takes its time.

The catch

It isn’t quite free. Click-outside-to-close still needs a few lines; default styles are spartan; older browsers wanted a polyfill until recently. But the trajectory is unmistakable — the component is dissolving back into the platform it was always trying to be.

<dialog id="dlg">
  <form method="dialog">
    <h2>Subscribe</h2>
    <button value="cancel">Cancel</button>
    <button value="ok">OK</button>
  </form>
</dialog>

Five lines of platform replace a decade of libraries. The dialog ends where it began: in HTML.

The shape of the dig

Read top to bottom, the strata tell one story twice. First the responsibility for a dialog’s behaviour migrated outward — from the operating system, briefly into the browser’s confirm(), then out into our own code, into a widget, a framework, a headless dependency, and finally back into the browser itself. Then the responsibility for its appearance swung like a pendulum: owned by the platform, seized by us, imposed by frameworks, and handed firmly back.

The component never got more capable. A 2023 native dialog does almost exactly what a 1991 System 7 alert did. What got better was where the hard parts lived. That’s the real artefact in the dirt: not the dialog, but forty years of us relearning, slowly, what we should never have been hand-writing in the first place.

Notes & sources
  1. Desktop dialog conventions — default button, Esc to cancel, focus containment — are codified in Apple’s Human Interface Guidelines (Addison-Wesley, 1992) and Microsoft’s The Windows Interface Guidelines for Software Design (1995).
  2. The <dialog> element, showModal(), the top layer and ::backdrop are specified in the WHATWG HTML Living Standard; cross-browser support reached Baseline in 2022–2023.
  3. Modal-dialog keyboard and ARIA expectations follow the W3C WAI-ARIA Authoring Practices, “Dialog (Modal) Pattern.”