SuperAPI AI Directory

frontend-impl-popover-dialog-anchor

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

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, 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)

  1. NEVER set tabindex on <dialog>. The dialog is a container; focus belongs on its descendants. Use autofocus for initial focus.
  2. NEVER combine popover attribute with dialog.showModal() on the same element. Popovers are non-modal by spec; mixing yields undefined cross-API state.
  3. NEVER write a custom click-outside handler for an auto or hint popover. The spec provides light-dismiss; rolling your own breaks focus restoration.
  4. 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.
  5. 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.
  6. NEVER omit display ... allow-discrete (and overlay ... allow-discrete for top-layer elements) from the transition shorthand. Exit animations cut off otherwise.
  7. NEVER place @starting-style BEFORE its target rule. Equal specificity = source order decides; the main rule will win.
  8. ALWAYS capture document.activeElement BEFORE calling dialog.showModal() and restore on close. Popover API does this automatically; <dialog> does NOT.
  9. 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