Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 17 – Forms, tables, popovers and anchor positioning

Popovers, dialogs and disclosure widgets

Style dialogs, popovers and details with ::backdrop, :modal, :popover-open and :open, and animate them in and out with @starting-style.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Choose between a modal dialog, a popover and a details element for a piece of interface
  • Use ::backdrop, :modal, :popover-open and :open to style open states and the top layer
  • Build entry and exit transitions with @starting-style and transition-behavior allow-discrete
  • Build an accordion from details and summary with ::details-content and the name attribute
  • Explain which behaviour HTML provides (focus, Escape, light dismiss) and what CSS only styles

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between a modal dialog, a popover and a details element for a piece of interface
  • Use ::backdrop, :modal, :popover-open and :open to style open states and the top layer
  • Build entry and exit transitions with @starting-style and transition-behavior allow-discrete
  • Build an accordion from details and summary with ::details-content and the name attribute
  • Explain which behaviour HTML provides (focus, Escape, light dismiss) and what CSS only styles

Before you start

On this page

HTML has three built-in ways to show content that opens and closes. A <dialog> opened with showModal() is modal: it sits above the whole page, the rest of the page is inert, and Escape closes it. The popover attribute makes any element a non-modal overlay that closes when the person clicks elsewhere or presses Escape, for menus, tooltips and help panels. <details> with its <summary> opens and closes content in the page itself, for disclosures and accordions. HTML provides the behaviour: focus, keyboard handling, light dismiss. CSS styles the result. Open dialogs and popovers live in the top layer, above every z-index; ::backdrop styles the layer behind a modal dialog; :modal, :popover-open and :open select the open states; and @starting-style with transition-behavior: allow-discrete animates them in and out. The results below come from Chromium 153, Firefox 155 and WebKit 26.6, which differ most at closing time.

Which element for which job

  • Modal dialog, opened with showModal() or a button with command="show-modal": it sits in the top layer and the rest of the page is inert. Escape closes it; a click outside closes it only with closedby="any". It matches :modal and :open.
  • Non-modal dialog, opened with show(): it stays in the page, which stays usable, and neither Escape nor a click outside closes it. It matches :open.
  • Popover auto, opened with showPopover() or a button with popovertarget: it sits in the top layer, the page stays usable, and Escape or a click outside closes it. It matches :popover-open.
  • Popover manual: the same, except that only your script closes it.
  • details: a click on its summary opens it in the page, not in the top layer. Escape and outside clicks leave it open; another click on the summary closes it, and so does opening another details with the same name. It matches :open.

Use a modal dialog when the page must wait for an answer, such as “Confirm the payment?”. Use a popover for something the person can ignore or dismiss: a menu, a date picker, a help panel. popover="hint" is for tooltips, and popover="manual" for things that stay until your code hides them, such as a chat panel. Use details when the content belongs in the page and only needs to fold away. A modal dialog closes on Escape because the HTML standard gives it the “close request” state by default; a dialog opened with show() gets the “none” state and stays open.

The top layer and ::backdrop

A banner with position: fixed and z-index: 2147483647, the largest 32-bit integer, covers the page. The dialog and the popover sit inside a card with z-index: 1, a stacking context of its own:

A dialog and a popover against the highest z-index HTML · top-layer.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A dialog against a banner with the highest z-index</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .banner { position: fixed; inset: 0; z-index: 2147483647; background: rgb(255 200 0 / 0.5); }   /* covers the page */
    .card { position: relative; z-index: 1; }    /* a stacking context around the dialog and the popover */
    dialog, [popover] { inline-size: 200px; block-size: 100px; padding: 0; }
  </style>
</head>
<body>
  <div class="banner" id="banner"></div>
  <div class="card">
    <dialog id="dialog">Confirm the payment?</dialog>
    <div popover id="help">Help text</div>
  </div>
  <details id="details"><summary>Refund rules</summary>Five to seven working days.</details>
  <script>
    const dialog = document.getElementById('dialog');
    const help = document.getElementById('help');
    const onTop = () => document.elementFromPoint(innerWidth / 2, innerHeight / 2).id;
    const states = (el) => [':modal', ':popover-open', ':open'].filter((s) => el.matches(s)).join(' ') || 'none';
    dialog.showModal();
    console.log(`showModal(): #${onTop()} on top; matches ${states(dialog)}; ::backdrop ${getComputedStyle(dialog, '::backdrop').backgroundColor}`);
    dialog.close();
    dialog.show();
    console.log(`show(): #${onTop()} on top; matches ${states(dialog)}`);
    dialog.close();
    help.showPopover();
    console.log(`showPopover(): #${onTop()} on top; matches ${states(help)}; ::backdrop ${getComputedStyle(help, '::backdrop').backgroundColor}`);
    help.hidePopover();
    document.getElementById('details').open = true;
    console.log(`details opened: matches ${states(document.getElementById('details'))}`);
  </script>
</body>
</html>
  • showModal(): the dialog was on top, and it matched :modal and :open.
  • show(): the banner covered the dialog, which matched :open.
  • showPopover(): the popover was on top, and it matched :popover-open.
  • details.open = true: not in the top layer; it matched :open.

All three engines agreed. showModal() and showPopover() move the element out of its stacking contexts into the top layer, which is painted after the whole page, in the order elements were added to it, so no z-index can cover it. No ancestor’s overflow: hidden clips it either: inside a 40px box with overflow: hidden, the modal dialog and the popover still showed in full in all three engines, while a dialog opened with show() was clipped to the box. Each top-layer element gets a ::backdrop just below it. The browser’s default style dims the page behind a modal dialog with 10 % black, rgba(0, 0, 0, 0.1), and leaves a popover’s backdrop transparent, as all three engines reported. ::backdrop inherits from its dialog, so custom properties set on the dialog work inside it, as all three engines showed too.

The top layer above all of the page, a z-index 2147483647 banner included; showModal and showPopover lift a box into it.The top layermodal dialogs and open popovers,in the order they opened,each above its own ::backdropThe page's stacking contextseven a banner withz-index: 2147483647 stays hereA card with z-index: 1where the dialog sits in the HTMLshowModal() and showPopover()lift it into the top layer;show() leaves it herepainted abovepainted above

Where an open dialog or popover is painted

Text description of the diagram

The diagram has three layers, from the top down, each painted above the next.

  1. The top layer holds modal dialogs and open popovers, in the order they opened, each above its own ::backdrop.
  2. Below it are the page's stacking contexts. Even a banner with z-index 2147483647 stays here, under the top layer.
  3. At the bottom is a card with z-index 1, where the dialog sits in the HTML. showModal() and showPopover() lift the box into the top layer; a dialog opened with show() stays inside this card.

A dimmed page should still let the dialog stand out. The program works out, with the WCAG formula, how dark a black backdrop must be for a white dialog without a border to reach 3:1 against the white page behind it:

How dark should the backdrop be? JavaScript · backdrop_contrast.mjs
// How dark must a dialog's ::backdrop be before a white dialog stands out from the white page behind it? The
// backdrop is black at an alpha, composited over the page in sRGB; the ratio is WCAG 2.2's contrast formula. The
// browser's default ::backdrop for a modal dialog is rgba(0, 0, 0, 0.1).

const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const luminance = (grey) => lin(grey / 255);            // a grey has three equal channels
const ratio = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);

const page = 255;                                        // a white page
const dialog = luminance(255);                           // a white dialog without a border
for (const alpha of [0.1, 0.25, 0.4, 0.42, 0.5, 0.6]) {
  const dimmed = page * (1 - alpha);                     // black at `alpha` over the page
  const r = ratio(dialog, luminance(dimmed));
  console.log(`backdrop alpha ${alpha.toFixed(2)}: page dimmed to ${dimmed.toFixed(1)}/255, dialog edge ${r.toFixed(2)}:1${r >= 3 ? '  (3:1 or more)' : ''}`);
}

Output

backdrop alpha 0.10: page dimmed to 229.5/255, dialog edge 1.25:1
backdrop alpha 0.25: page dimmed to 191.3/255, dialog edge 1.83:1
backdrop alpha 0.40: page dimmed to 153.0/255, dialog edge 2.85:1
backdrop alpha 0.42: page dimmed to 147.9/255, dialog edge 3.04:1  (3:1 or more)
backdrop alpha 0.50: page dimmed to 127.5/255, dialog edge 3.98:1  (3:1 or more)
backdrop alpha 0.60: page dimmed to 102.0/255, dialog edge 5.74:1  (3:1 or more)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node backdrop_contrast.mjs

The default 10 % leaves only 1.25:1, so give the dialog a border or a shadow, or darken the backdrop to about 42 % or more. backdrop-filter: blur(4px) on ::backdrop softens the page too, and is Baseline newly available.

Open states: :modal, :popover-open and :open

:modal matches only a dialog opened with showModal(), the right hook for styles that a modal dialog needs and a non-modal one doesn’t. :popover-open matches an open popover. :open is newer and broader: it matches an open dialog, an open details and a select whose picker is open, but not a popover, as the top-layer test showed.

:open is Baseline newly available, and dialogs, popovers and :open are an Interop 2026 focus area, so engines are still aligning details. Two habits keep older engines working. First, a selector an engine doesn’t know makes it drop the whole rule, every selector of the list included, so write dialog[open] and details[open] in rules that must always apply; the open attribute is set whenever these elements are open. Second, test that open content is reachable with the keyboard and that focus lands inside a modal dialog: HTML moves it there when the dialog opens, and back to the button that opened it when it closes.

Animating in and out

An element that appears has no “before” style to transition from, so a fade-in does nothing on its own. @starting-style supplies that before style. Closing is harder: a closed dialog or popover gets display: none, which removes it at once. transition-behavior: allow-discrete (here in the shorthand) lets display wait until the end of the transition, and overlay, also with allow-discrete, keeps the element in the top layer for the same time:

Fade and slide in, fade out CSS · dialog-motion.css
/* A confirmation dialog and a help popover that fade and slide in, and fade out where the engine allows it. */
dialog,
[popover] {
  opacity: 0;
  translate: 0 16px;
  transition:
    opacity 300ms,
    translate 300ms,
    display 300ms allow-discrete,   /* stay displayed until the fade-out ends */
    overlay 300ms allow-discrete;   /* stay in the top layer until then (Chromium only) */
}
dialog[open],                        /* [open], not :open: an older engine would drop the whole rule */
:popover-open {
  opacity: 1;
  translate: 0 0;
}
@starting-style {                    /* the values to animate from when the element appears */
  dialog[open],
  :popover-open {
    opacity: 0;
    translate: 0 16px;
  }
}

dialog::backdrop {
  background-color: rgb(0 0 0 / 0);
  transition:
    background-color 300ms,
    display 300ms allow-discrete,
    overlay 300ms allow-discrete;
}
dialog[open]::backdrop {
  background-color: rgb(0 0 0 / 0.5);
}
@starting-style {
  dialog[open]::backdrop {
    background-color: rgb(0 0 0 / 0);
  }
}

dialog :focus-visible {
  outline: 3px solid #1f4e8c;
  outline-offset: 2px;
}

/* Motion the person did not start can make some people dizzy: keep the fade, drop the slide. Repeat the whole
   list: changing only transition-property would pair allow-discrete with the wrong properties. */
@media (prefers-reduced-motion: reduce) {
  dialog,
  [popover] {
    transition:
      opacity 300ms,
      display 300ms allow-discrete,
      overlay 300ms allow-discrete;
  }
}
Measuring the transitions while they run HTML · dialog-motion.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Entry and exit transitions of a dialog and a popover</title>
  <link rel="stylesheet" href="dialog-motion.css">
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
  </style>
</head>
<body>
  <button type="button" commandfor="confirm" command="show-modal">Pay ₹1,250</button>
  <dialog id="confirm" aria-labelledby="confirm-title">
    <h2 id="confirm-title">Confirm the payment?</h2>
    <button type="button" commandfor="confirm" command="close">Cancel</button>
  </dialog>
  <div popover id="help">The amount includes delivery.</div>
  <script>
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const running = (el) => el.getAnimations({ subtree: true }).map((a) => a.transitionProperty).sort().join(', ') || 'none';
    async function check(el, open, close) {
      open();
      await wait(100);
      console.log(`#${el.id} 100ms after opening: transitions ${running(el)}`);
      await wait(400);
      close();
      await wait(100);
      console.log(`#${el.id} 100ms after closing: display ${getComputedStyle(el).display}, transitions ${running(el)}`);
      await wait(400);
    }
    addEventListener('load', async () => {
      const dialog = document.getElementById('confirm');
      const help = document.getElementById('help');
      await check(dialog, () => dialog.showModal(), () => dialog.close());
      await check(help, () => help.showPopover(), () => help.hidePopover());
    });
  </script>
</body>
</html>
  • 100ms after showModal(): the opacity, translate and backdrop transitions were running in all three engines.
  • 100ms after close() or hidePopover(): Chromium 153 still showed the element, with its display and overlay transitions running; Firefox 155 and WebKit 26.6 had already set display: none, with nothing running.

The entry animation runs everywhere. The exit animation runs only in Chromium: Firefox doesn’t transition display at all, and WebKit, which does transition display on ordinary elements, removed the closing dialog and popover at once. That is a safe fallback, because in those engines the element simply disappears. overlay matters in Chromium: a test without it, screenshotted 150ms into the exit, showed the dialog still fading, at opacity 0.78, but out of the top layer and so hidden behind a covering element of the page; with overlay it faded out on top.

Respect prefers-reduced-motion: reduce, as the stylesheet does by keeping the fade and dropping the slide. WCAG 2.2’s success criterion 2.3.3 (level AAA) asks that motion started by an interaction can be switched off unless it is essential. Write the whole transition list in that block. Changing only transition-property there is a trap: the comma lists pair up by position again, allow-discrete lands on the wrong property, and in a reduced-motion run in Chromium the dialog then vanished on close instead of fading.

CSS Animation & Keyframes Generator Try timing functions and durations for an entry animation, then move the start values into @starting-style.

Details and summary: disclosures and accordions

A details element is open when it has the open attribute. Its summary is the button that toggles it, with a disclosure triangle drawn as its ::marker, so list-style: none on the summary or a ::marker rule restyles it. ::details-content, Baseline newly available, is the part that opens and closes, so you can give it padding or animate it. Several details with the same name form an exclusive accordion, where opening one closes the others:

An accordion from details and summary HTML · accordion.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>An accordion from details and summary</title>
  <style>
    body { margin: 16px; font: 16px/24px Arial, sans-serif; }
    :root { interpolate-size: allow-keywords; }       /* lets block-size animate to and from auto (Chromium only) */
    details { border-block-end: 1px solid #c5ccd6; }
    summary { padding: 8px 0; cursor: pointer; }
    summary:focus-visible { outline: 3px solid #1f4e8c; outline-offset: 2px; }
    details::details-content {                         /* the part that opens and closes */
      block-size: 0;
      overflow: clip;
      transition: block-size 400ms, content-visibility 400ms allow-discrete;
    }
    details[open]::details-content { block-size: auto; }   /* [open], not :open: older engines know ::details-content but not :open */
    details p { margin: 0 0 8px; block-size: 96px; }
    @media (prefers-reduced-motion: reduce) {
      details::details-content { transition: none; }
    }
  </style>
</head>
<body>
  <details name="faq" id="refunds">
    <summary>How long does a refund take?</summary>
    <p>Refunds go back to the card or account you paid from in five to seven working days.</p>
  </details>
  <details name="faq" id="delivery">
    <summary>Can I change the delivery address?</summary>
    <p>Yes, until the order is packed. Open the order and choose a new address.</p>
  </details>
  <script>
    const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const refunds = document.getElementById('refunds');
    const delivery = document.getElementById('delivery');
    addEventListener('load', async () => {
      refunds.open = true;
      await wait(100);
      console.log(`100ms after opening: ${refunds.getBoundingClientRect().height.toFixed(1)}px tall, :open ${refunds.matches(':open')}`);
      await wait(500);
      console.log(`600ms after opening: ${refunds.getBoundingClientRect().height.toFixed(1)}px tall`);
      delivery.open = true;                              // same name: opening one closes the other
      await wait(50);
      console.log(`after opening the second: refunds open ${refunds.open}, delivery open ${delivery.open}`);
    });
  </script>
</body>
</html>
  • 100ms after opening: 80.2px tall and still growing in Chromium 153; already fully open in Firefox 155 (145.5px) and WebKit 26.6 (145.0px).
  • 600ms after opening: 145.0px in Chromium too.
  • Opening the second: it closed the first in all three engines.

Animating to the content’s natural height needs interpolate-size: allow-keywords, which lets block-size move to and from auto, and only Chromium supports it, so Firefox and WebKit opened the panel at once, which is fine. Closed content gets content-visibility: hidden from the browser, which is why the transition lists content-visibility with allow-discrete as well. The rule that opens the content uses details[open]: Safari 18.4 to 26.4 and Chrome 131 and 132 know ::details-content but not :open, and would drop a :open rule and keep the content at 0px.

What HTML handles and CSS can’t

CSS only styles these widgets. The behaviour comes from HTML, so use the real elements instead of styled divs:

  • A modal dialog moves focus inside when it opens and back to the opener when it closes, keeps Tab inside, makes the page inert and closes on Escape. closedby="any" adds closing on a backdrop click: Chromium and Firefox list it, and the WebKit build we ran already supports it, though web-features lists no released Safari yet.
  • Popovers get light dismiss and Escape from the browser, and a popovertarget button is wired to its popover for assistive technology. In all three engines, showing an auto popover closed the other auto and hint popovers, showing a hint closed only other hints, Escape closed the most recent one, and manual popovers stayed open through everything.
  • command="show-modal" and commandfor="dialog-id" on a button open a modal dialog with no script, Baseline newly available. In all three engines a click opened the dialog and moved focus to its Close button.

Practise: which popovers stay open

Write the rules that decide which popovers are open after a sequence of steps:

Exercise · Medium · JavaScript

Predict which popovers stay open

A page has menus (popover="auto"), tooltips (popover="hint") and a chat panel (popover="manual"). Before you style them, know which ones are open. Write openPopovers(types, steps) in popovers.mjs and export it.

types maps each popover's id to 'auto', 'hint' or 'manual'. steps is a list of strings: 'show <id>', 'hide <id>', 'escape' (the person presses Escape) and 'outside' (the person clicks outside every popover). Return the ids that are open after the last step, in the order they were opened, oldest first. These are the rules all three engines followed in the lesson's run, for popovers that are not nested inside each other:

  • Showing an auto popover closes every other open auto and hint popover.
  • Showing a hint popover closes the other open hint popovers, but not the auto ones.
  • Showing a manual popover closes nothing, and nothing closes a manual popover except 'hide <id>'.
  • 'escape' closes the most recently opened auto or hint popover that is still open.
  • 'outside' closes every open auto and hint popover.
  • Showing a popover that is already open changes nothing.

The sample tests import the function from popovers.mjs and run in your browser.

Starter code · popovers.mjs

/** The ids of the open popovers after the steps, oldest first. */
export function openPopovers(types, steps) {
  // Replace this line with your code.
  return [];
}
The sample tests · popovers.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { openPopovers } from './popovers.mjs';

const types = { menu: 'auto', account: 'auto', tip: 'hint', price: 'hint', chat: 'manual' };

test('an auto popover closes the other auto and hint popovers', () => {
  assert.deepEqual(openPopovers(types, ['show menu', 'show account']), ['account']);
  assert.deepEqual(openPopovers(types, ['show account', 'show tip', 'show menu']), ['menu']);
});

test('a hint closes other hints, but leaves the auto popover open', () => {
  assert.deepEqual(openPopovers(types, ['show menu', 'show tip']), ['menu', 'tip']);
  assert.deepEqual(openPopovers(types, ['show menu', 'show tip', 'show price']), ['menu', 'price']);
});

test('a manual popover stays open whatever happens, until it is hidden', () => {
  assert.deepEqual(openPopovers(types, ['show chat', 'show menu', 'show tip', 'outside', 'escape']), ['chat']);
  assert.deepEqual(openPopovers(types, ['show chat', 'show menu', 'hide chat']), ['menu']);
});

test('escape closes the most recent auto or hint popover, one at a time', () => {
  assert.deepEqual(openPopovers(types, ['show menu', 'show tip', 'escape']), ['menu']);
  assert.deepEqual(openPopovers(types, ['show menu', 'show chat', 'show tip', 'escape', 'escape']), ['chat']);
  assert.deepEqual(openPopovers(types, ['show chat', 'escape']), ['chat']);
});

test('a click outside closes every auto and hint popover', () => {
  assert.deepEqual(openPopovers(types, ['show menu', 'show tip', 'outside']), []);
});

test('showing an open popover changes nothing', () => {
  assert.deepEqual(openPopovers(types, ['show menu', 'show tip', 'show menu']), ['menu', 'tip']);
  assert.deepEqual(openPopovers(types, ['show chat', 'show chat']), ['chat']);
});
A hint

Keep one array of the open ids, oldest first. Each step filters it or appends to it: for 'show', filter out the types that the new popover closes, then push its id; for 'escape', find the last id whose type is not 'manual' and remove it.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A cookie banner has position: fixed and z-index: 2147483647. A dialog opened with showModal() sits inside a card with z-index: 1. Which is on top?

    Choose one answer.

    Show the answer to question 1

    Answer: The dialog: it is in the top layer, above every stacking context

    showModal() and showPopover() move the element into the top layer, which is painted above the whole page, whatever its z-index values. In the lesson's run the dialog was on top in all three engines. A dialog opened with show() stays in the page, and there the banner covered it.

  2. Question 2 of 6 Which of these does the :open pseudo-class match?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • A dialog opened with showModal()
    • A details element with its open attribute
    • A dialog opened with show()

    :open matches elements with an open and a closed state: dialogs, details and selects with an open picker. Popovers have :popover-open instead; in the lesson's run an open popover matched :popover-open and not :open. Only the dialog from showModal() also matched :modal.

  3. Question 3 of 6 A dialog fades in with @starting-style, but when it closes it vanishes at once in Chromium. Which declaration is missing from its transition?

    Choose one answer.

    Show the answer to question 3

    Answer: display 300ms allow-discrete

    A closed dialog gets display: none, which normally switches off at once and ends every other transition. With allow-discrete, display flips to none only at the end of the duration, so the fade-out can play. Add overlay 300ms allow-discrete too, or the dialog leaves the top layer and fades out behind the page. Firefox and WebKit removed the closing dialog at once in the lesson's run whatever the CSS said.

  4. Question 4 of 6 A white dialog has no border and sits on a white page. Roughly what alpha must a black ::backdrop have before the dialog's edge reaches 3:1 against the dimmed page? Give it to two decimals.

    Type a number.

    Show the answer to question 4

    Answer: 0.42 (anything from 0.4 to 0.44 counts)

    The backdrop darkens the page to 255 × (1 − alpha). The lesson's program found 2.85:1 at 0.40 and 3.04:1 at 0.42. The browser's default, 0.1, gives only 1.25:1, so either darken the backdrop or give the dialog a border.

  5. Question 5 of 6 A menu is a popover="auto", and a tooltip inside the page is a popover="hint". The menu is open when the tooltip opens. What happens to the menu?

    Choose one answer.

    Show the answer to question 5

    Answer: It stays open: a hint closes only other hints

    popover="hint" exists for tooltips: showing one closes other hints but leaves auto popovers alone. In the lesson's run, Escape then closed the tooltip first and the menu second, and a click outside closed both.

  6. Question 6 of 6 An accordion hides closed content with details::details-content { block-size: 0; } and opens it with details:open::details-content { block-size: auto; }. What goes wrong in an engine that supports ::details-content but not :open?

    Choose one answer.

    Show the answer to question 6

    Answer: The second rule is dropped, so the content stays 0px tall even when open

    A selector the engine doesn't know makes it drop the whole rule. Safari 18.4 to 26.4 and Chrome 131 and 132 know ::details-content but not :open, so they would apply the closed size and never the open one. Write details[open]::details-content, which every engine understands.

Interview questions

Fresher · What is the top layer, and why can’t z-index place an element above a modal dialog?

The top layer is a layer that browsers paint after the whole document, above every stacking context. A dialog opened with showModal(), an open popover and a fullscreen element are moved into it, in the order they open, each with a ::backdrop just below it. z-index only orders boxes within a stacking context, and every stacking context of the page is painted before the top layer, so even z-index: 2147483647 on a fixed banner stays underneath; in a measured test a modal dialog inside a z-index: 1 card covered such a banner in Chromium, Firefox and WebKit. An ancestor’s overflow: hidden doesn’t clip a top-layer element either. A dialog opened with show() is not in the top layer, so it is stacked like any other box.

Key takeaways

  • Modal dialog for decisions, popover for dismissible overlays, details for content that folds away.
  • showModal() and showPopover() put the element in the top layer, above every z-index; show() doesn’t.
  • :modal, :popover-open and :open select open states; use dialog[open] and details[open] where an older engine must not drop the rule.
  • @starting-style animates the entry; display and overlay with allow-discrete keep the exit visible in Chromium, while Firefox and WebKit remove the element at once.
  • The default backdrop is 10 % black, 1.25:1 against a white dialog; add a border or darken it.
  • HTML provides focus handling, Escape and light dismiss; CSS only styles them.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.