Frontend Impl : Popover, Dialog, Anchor Positioning
Authoritative reference for the modern top-layer authoring stack. Three APIs share one mental model : promote element to top layer, position relative to anchor, animate enter and exit with discrete-property transitions. Pick <dialog> for modal interruption; pick the Popover API for transient non-modal surfaces.
Quick Reference
Baseline status
| Feature | Baseline | Source |
|---|---|---|
<dialog> element |
Widely Available since March 2022 | MDN : dialog (verified 2026-05-19) |
Popover API (popover attribute) |
Newly Available since January 2025 | MDN : Popover API (verified 2026-05-19) |
@starting-style, transition-behavior: allow-discrete |
Newly Available since August 2024 | MDN : @starting-style (verified 2026-05-19) |
Anchor Positioning core (anchor-name, position-anchor, anchor()) |
Limited / rolling out 2024 to 2026 | MDN : CSS Anchor Positioning (verified 2026-05-19) |
position-area, position-try-fallbacks |
Newly Available since January 2026 | MDN : position-try-fallbacks (verified 2026-05-19) |
overlay property |
Limited (Chromium-led) | MDN : overlay (verified 2026-05-19) |
Single most useful default
Modal interrupts user workflow -> <dialog> + showModal(). Transient surface anchored to a trigger -> popover="auto" with implicit anchor.
closedby defaults (memorize)
| Open path | Default closedby |
Escape closes? | Light dismiss? |
|---|---|---|---|
dialog.showModal() |
closerequest |
YES | NO |
dialog.show() |
none |
NO | NO |
<dialog open> (attribute) |
none |
NO | NO |
popover="auto" |
(built-in) | YES | YES |
popover="manual" |
(built-in) | NO | NO |
popover="hint" |
(built-in) | YES | YES |
Decision Trees
Tree 1 : <dialog> showModal vs Popover API auto?
Does it interrupt user workflow and require explicit action to dismiss?
YES -> <dialog> + showModal(). Top layer + implicit inert background +
Escape + ::backdrop. Manual focus restoration on close.
NO -> next question
Is it a transient surface anchored to a trigger button (dropdown menu,
tooltip, date picker, command palette, combobox listbox)?
YES -> popover="auto" + popovertarget on the trigger button. Implicit
anchor for positioning, built-in light dismiss, automatic focus
restoration on Esc-close.
NO -> next question
Should it stay open while the user interacts elsewhere (tear-off settings
panel, persistent help overlay)?
YES -> popover="manual". Author closes via popovertargetaction="hide" or
.hidePopover(). No light dismiss.
NO -> next question
Is it a hint surface, low-stakes, ephemeral (hover tooltip, autocomplete
hint)?
YES -> popover="hint". Light dismiss; closes other hint popovers when
opened but not auto popovers.
Tree 2 : Anchor positioning vs JS getBoundingClientRect?
Is the popover triggered by a button with popovertarget?
YES -> implicit anchor. Write position-area: bottom span-inline-end;
directly on the popover. No anchor-name needed.
Is anchor positioning required and the target browser audience is 2025+?
YES -> anchor-name on source + position-anchor + position-area + (often)
position-try-fallbacks: flip-block, flip-inline;.
Must the pattern work on browsers older than 2025-2026?
YES -> gate with @supports (anchor-name: --x) { ... } and provide a JS
fallback (track scroll / resize, set top / left via element.style).
Is the value not a top / left / inset (e.g., sizing relative to anchor)?
YES -> anchor-size(width) inside inline-size / block-size declarations.
Tree 3 : Combined @starting-style + allow-discrete recipe selection
Animate from display: none -> visible via popover or dialog?
YES -> REQUIRED LINES in transition shorthand :
opacity 0.3s,
transform 0.3s,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
PLUS @starting-style { :popover-open / [open] { opacity: 0; ... } }
declared AFTER the open-state rule.
Animate opacity-only with NO display change (element already visible)?
YES -> regular transition: opacity works. No @starting-style needed,
no allow-discrete needed.
Backdrop animation desired too?
YES -> apply the same recipe to ::backdrop selector. transition includes
background-color + display + overlay allow-discrete; @starting-style
declares background-color transparent.
Patterns
Pattern A : Modal <dialog> with focus restoration
<button id="open">Open dialog</button>
<dialog id="dlg" closedby="closerequest">
<form method="dialog">
<h2>Confirm</h2>
<p>Are you sure?</p>
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
const dlg = document.getElementById('dlg');
const opener = document.getElementById('open');
let trigger = null;
opener.addEventListener('click', () => {
trigger = document.activeElement;
dlg.showModal();
});
dlg.addEventListener('close', () => {
trigger?.focus();
trigger = null;
if (dlg.returnValue === 'confirm') { /* act on result */ }
});
<form method="dialog"> closes the dialog with returnValue set to the clicked button's value. The close event fires for ALL dismissal paths.
Pattern B : Popover with implicit anchor
<button popovertarget="menu" id="menu-btn">Menu</button>
<div id="menu" popover="auto">
<button popovertarget="menu" popovertargetaction="hide">Close</button>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
#menu {
position-area: bottom span-inline-end;
margin-block-start: 0.5rem;
padding: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
inset: unset; /* clear UA default centering for popovers */
}
The popover acquires an implicit anchor (the trigger button) because popovertarget references it. position-area: bottom span-inline-end places the popover below the button, aligned to its inline-end edge.
Pattern C : Anchored popover with @supports gate and fallback
@supports (anchor-name: --x) {
#menu-btn { anchor-name: --menu-anchor; }
#menu {
position: absolute;
position-anchor: --menu-anchor;
position-area: bottom span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
margin-block-start: 0.5rem;
}
}
@supports not (anchor-name: --x) {
/* JS-positioned fallback */
#menu {
position: absolute;
/* top / left set via element.style by JS on open + scroll + resize */
}
}
position-try-fallbacks lists the rescue strategies the browser tries in order when the primary placement would overflow. flip-block mirrors top -> bottom; flip-inline mirrors left -> right; the third option composes both for diagonal flip.
Pattern D : Combined enter / exit animation recipe (popover)
[popover] {
/* Closed state (also exit-animation target) */
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
[popover]:popover-open {
/* Open state (entry-animation target) */
opacity: 1;
transform: scale(1);
}
/* @starting-style MUST come AFTER the :popover-open rule */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
@media (prefers-reduced-motion: reduce) {
[popover] { transition: opacity 0.1s linear, display 0.1s allow-discrete, overlay 0.1s allow-discrete; transform: none; }
@starting-style { [popover]:popover-open { opacity: 0; transform: none; } }
}
For a <dialog> opened with showModal(), replace :popover-open with [open] (or dialog:open) and apply the same shape. Add a ::backdrop block with the same transitions to animate the dim layer.
Pattern E : Modal <dialog> enter / exit animation (with backdrop)
dialog {
opacity: 0;
transform: translateY(8px) scale(0.98);
transition:
opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
dialog[open] { opacity: 1; transform: translateY(0) scale(1); }
@starting-style {
dialog[open] { opacity: 0; transform: translateY(8px) scale(0.98); }
}
dialog::backdrop {
background: rgb(0 0 0 / 0%);
transition:
background-color 0.25s,
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
dialog[open]::backdrop { background: rgb(0 0 0 / 0.4); }
@starting-style {
dialog[open]::backdrop { background: rgb(0 0 0 / 0%); }
}
Pattern F : closedby="any" for light-dismiss modal
<dialog id="prefs" closedby="any">
<h2>Preferences</h2>
<form method="dialog">
<button autofocus>Done</button>
</form>
</dialog>
closedby="any" opts into outside-click dismissal AND keeps Escape working. Useful for non-destructive modals where users may click away. Listen for the close event to handle any dismissal path; restore focus there.
Out of Scope
- ARIA roles and states deep-dive :
role="dialog",aria-modal,aria-labelledby, combobox / listbox patterns (covered in[[frontend-a11y-aria-patterns]]). - Focus management mechanics :
:focus-visible,tabindex, roving tabindex,aria-activedescendant,inertalgorithm (covered in[[frontend-a11y-focus-keyboard-inert]]). - Modal / toast / drawer component templates (covered in
[[frontend-component-modal-toast-system]]). - Scroll-driven animations and View Transitions API (covered in
[[frontend-impl-view-transitions-scroll-animations]]).
Hard Rules (Binding)
- NEVER set
tabindexon<dialog>. The dialog is a container; focus belongs on its descendants. Useautofocusfor initial focus. - NEVER combine
popoverattribute withdialog.showModal()on the same element. Popovers are non-modal by spec; mixing yields undefined cross-API state. - NEVER write a custom click-outside handler for an
autoorhintpopover. The spec provides light-dismiss; rolling your own breaks focus restoration. - NEVER use Anchor Positioning without an
@supports (anchor-name: --x)gate (and a JS fallback for non-supporting browsers) until full cross-engine Baseline lands. - NEVER omit
@starting-stylefor entry animations on<dialog>/ popover. Without it, the property switch happens before the transition can start; the element appears instantly. - NEVER omit
display ... allow-discrete(andoverlay ... allow-discretefor top-layer elements) from the transition shorthand. Exit animations cut off otherwise. - NEVER place
@starting-styleBEFORE its target rule. Equal specificity = source order decides; the main rule will win. - ALWAYS capture
document.activeElementBEFORE callingdialog.showModal()and restore onclose. Popover API does this automatically;<dialog>does NOT. - ALWAYS use
<form method="dialog">+ buttonvalueattributes to return a result from a dialog declaratively. Setsdialog.returnValuewithout JS submit handlers.
Reference Links
references/methods.md: full method tables for<dialog>, Popover API, anchor positioning, and the combined animation recipereferences/examples.md: three renderable demos (modal withclosedby, anchored popover withposition-try-fallbacks, animated popover with full recipe)references/anti-patterns.md: 7 anti-patterns with symptom, root cause, fix- MDN : dialog (verified 2026-05-19)
- MDN : Popover API (verified 2026-05-19)
- MDN : CSS Anchor Positioning (verified 2026-05-19)
- MDN : @starting-style (verified 2026-05-19)
- MDN : transition-behavior (verified 2026-05-19)
- MDN : overlay (verified 2026-05-19)
- MDN : position-try-fallbacks (verified 2026-05-19)
- WHATWG HTML : popover (verified 2026-05-19)
Cross-References
[[frontend-syntax-html5-semantic]]:<dialog>semantics, when to use<dialog>vs ARIArole="dialog"[[frontend-a11y-aria-patterns]]: ARIA role / state model for dialog, combobox, menu, listbox[[frontend-a11y-focus-keyboard-inert]]::focus-visible, tabindex, inert, focus-trap mechanics[[frontend-visual-micro-interactions]]: timing tokens, easing curves,@starting-stylegeneral use[[frontend-component-modal-toast-system]]: component-level modal / toast / drawer templates