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') { }
});
<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;
}
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) {
#menu {
position: absolute;
}
}
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] {
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 {
opacity: 1;
transform: scale(1);
}
@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; : 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 { : ( / ); }
{
dialog { : ( / ); }
}
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, inert algorithm (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
tabindex on <dialog>. The dialog is a container; focus belongs on its descendants. Use autofocus for initial focus.
- NEVER combine
popover attribute with dialog.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
auto or hint popover. 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-style for 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 (and overlay ... allow-discrete for top-layer elements) from the transition shorthand. Exit animations cut off otherwise.
- NEVER place
@starting-style BEFORE its target rule. Equal specificity = source order decides; the main rule will win.
- ALWAYS capture
document.activeElement BEFORE calling dialog.showModal() and restore on close. Popover API does this automatically; <dialog> does NOT.
- ALWAYS use
<form method="dialog"> + button value attributes to return a result from a dialog declaratively. Sets dialog.returnValue without JS submit handlers.
Reference Links
Cross-References
[[frontend-syntax-html5-semantic]] : <dialog> semantics, when to use <dialog> vs ARIA role="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-style general use
[[frontend-component-modal-toast-system]] : component-level modal / toast / drawer templates