# Overlays

> Build every transient layer on the native platform — the HTML Popover API (popover/popovertarget, top-layer, light-dismiss, ::backdrop) and CSS Anchor Positioning (anchor-name/position-anchor/position-area/position-try) as the default for menus, dropdowns, tooltips, comboboxes, and popovers, retiring z-index wars, portal hacks, and JS focus-trap libraries. Three named variants (anchored popover, modal dialog, manual toast stack), a Baseline-2026 default with a scoped @supports flip fallback, and @starting-style entry/exit motion. Invoke in the Build phase for any overlay work — trigger phrases — "dropdown menu", "tooltip", "popover", "toast", "modal", "z-index war", "the menu sits behind something", "position a menu under a button", "replace the Radix/portal dropdown".

- Skill: `blyatiful1/overlays` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add blyatiful1/overlays`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blyatiful1/overlays/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: blyatiful1 (https://skillmd.com/u/blyatiful1)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blyatiful1/overlays

---


# overlays — the platform owns the top layer

**Stage:** Phase 6 — Build - **Reads:** design/SYSTEM.md §depth/§motion, design/DIRECTION.md - **Writes:** components/ui/{popover,tooltip,toast}.tsx as native-primitive wrappers + overlay usage rules

## Standard

Every transient layer — dropdown, tooltip, combobox listbox, actions menu, toast — defaults to the HTML **Popover API** plus **CSS Anchor Positioning**. This is not progressive enhancement to bolt on later: core anchor positioning (`anchor-name`, `position-anchor`, `anchor()`, `position-area`) reached **Baseline 2026** (Chrome 125+, Safari 18.2+, Firefox 132+), and the Popover API has been Baseline since 2023–24. Together they retire an entire recurring class of defects — z-index escalation, React portals, and hand-rolled focus-trap libraries — because the browser owns three things you used to fake: **top-layer stacking** (no z-index, ever), **light-dismiss** (Escape + outside-click on `auto` popovers, focus returned to the invoker), and **positioning relative to a trigger** without JS measuring rects on every scroll. JS supplies only content and open/close intent.

First-grade here: the invoker is always a real `<button popovertarget>`; positioning is declarative CSS, never `getBoundingClientRect` in an effect; entry/exit motion rides `@starting-style` + `allow-discrete`, not a mount/unmount library; and every overlay is keyboard- and reduced-motion-complete before it ships. The one real support gap is scoped and handled (see Degradation).

## Variants

**anchored popover** — menus, dropdowns, tooltips, combobox listboxes, date-picker panels. `popover` (`auto`) for anything light-dismissible; anchor it to its trigger with `anchor-name`/`position-anchor` + `position-area`. This is the default for ~90% of overlays and the reason this skill exists.

```html
<button class="acct-trigger" popovertarget="acct" aria-label="Account">…</button>
<div id="acct" popover class="acct-menu"> <!-- role/items decided below --> </div>
```

```css
/* Base: engines without anchor positioning still get a placed box */
.acct-menu { position: absolute; top: calc(100% + 0.5rem); right: 0; }

@supports (anchor-name: --a) {
  .acct-trigger { anchor-name: --acct; }
  .acct-menu {
    position-anchor: --acct;
    position-area: bottom span-left;     /* below the trigger, right edges aligned */
    top: auto; right: auto;              /* hand placement to the engine */
    margin-top: 0.5rem;
    position-try-fallbacks: flip-block;  /* flip above when the viewport bottom is tight */
  }
}
```

**modal dialog** — confirmations, forms, anything that must own focus and block the page. This is `<dialog>` + `dialogRef.showModal()`, **not** a popover: `showModal()` gives you a real focus trap, background `inert`, and a `::backdrop` scrim for free — the exact things `popover="manual"` does *not* provide. Reach for it only when the interaction is genuinely modal; a menu that inerts the whole app to show four links is modal overkill. (A non-modal `<dialog popover>` is also valid for a dismissible panel that shouldn't trap focus.)

**manual toast stack** — status notifications in a fixed corner region. `popover="manual"` (never `auto` — outside-click must not nuke a toast), multiple allowed open at once so they stack, each auto-dismissing on a timer plus an explicit close button. Announce content through an `aria-live="polite"` region, not through popover semantics. Owned in the e-commerce path by `ultraweb:cart` (add-to-cart confirmation), which consumes this variant.

## When to reach past native

- **Roving-focus menus** (`role="menu"` with arrow-key navigation, typeahead, submenus): the Popover API gives you the layer and dismissal but **not** arrow-key focus management. Either wire that JS yourself, or — usually better — render a plain list of `<a>`/`<button>` and skip `role="menu"` entirely; a short actions menu rarely needs the menu pattern.
- **Complex composite widgets** — comboboxes with `aria-activedescendant`, multi-level menus, rich date pickers: a shadcn/Radix primitive still earns its keep for the ARIA state machine. Keep it, but let it position via anchoring rather than a JS positioning engine where the version supports it.
- Everything else — tooltips, single-level dropdowns, popovers, notifications — is native. Do not install a positioning or focus-trap dependency for these.

## The graduated set — Invoker Commands, popover=hint

Open UI keeps handing declarative HTML jobs React state used to do. Take each rung as an enhancement over the baseline above, never as its replacement.

- **Invoker Commands** (`command` / `commandfor`) extend `popovertarget` past popovers to any native element: `<button command="show-modal" commandfor="confirm">` opens a `<dialog>` with no `showModal()` call in an effect, `command="close"` closes it, `command="toggle-popover"` covers the popover cases. Every `useState(false)` that exists only to open and shut a native element deletes with it — and with it, usually, the client component that held it.
- **`popover=hint`** is the tooltip-shaped third state. A hint light-dismisses like `auto`, but opening one does NOT close an open `auto` popover — so a tooltip inside a menu stops nuking the menu the instant it appears. `auto` still owns menus; `manual` still owns toasts.
- **Support gate.** Both land later than the Baseline-2026 core this skill defaults to (Chromium first) — check the current table before either becomes load-bearing. Keep `popovertarget` / `showModal()` as the real implementation and layer the attribute over it, or feature-detect (`"command" in HTMLButtonElement.prototype`) before deleting the handler. A dialog that never opens in Firefox is a defect, not a Baseline bet.

## Motion & degradation

Entry/exit animates across the top-layer `display` toggle via discrete-property transitions — no `AnimatePresence`, no client component:

```css
.acct-menu {
  opacity: 0; transform: translateY(-4px);
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out),
              overlay 180ms allow-discrete, display 180ms allow-discrete;
}
.acct-menu:popover-open { opacity: 1; transform: translateY(0); }
@starting-style { .acct-menu:popover-open { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .acct-menu { transition: none; transform: none; } }
```

Durations and easing come from SYSTEM.md §motion (micro band, 150–250ms) — never a fresh magic number. **Degradation is narrowly scoped:** only the `@position-try` viewport-flip lags full convergence (Safari 18.4+/26+, Firefox 147+). The base rule above already ships a static offset for pre-anchor engines, and where flip isn't honored the popover simply stays in its declared `position-area` instead of flipping — anchored and usable, never broken. Reach for `motion/react` only when an overlay's content needs spring physics CSS can't express; the layer, dismissal, and positioning stay native.

## A11y

- Invoker is a real `<button popovertarget>`; the browser reflects open state as `aria-expanded` and wires `aria-details`. Never a `<div onClick>`.
- `auto` popovers are non-modal by design — focus is *not* trapped, which is correct for menus and tooltips. Escape and outside-click dismiss, returning focus to the invoker. If you need a trap + inert background, that's `<dialog>.showModal()`, not a manual popover.
- Tooltips: link trigger and tip with `aria-describedby`; the tip must be hoverable and Escape-dismissible (WCAG 2.2 §1.4.13), and never the only home for essential info.
- Toasts announce via `aria-live="polite"`; timing gives readers time (non-critical ≥5s or dismissible), and dismissal is keyboard-reachable.
- `::backdrop` on an `auto` popover is decorative only — it does not inert the page; a real scrim implies a modal, so use `<dialog>`.

## Anti-patterns

- `z-[9999]` (or any hand-tuned z-index) on something that should live in the top layer — grep overlay components for `z-\[`; a popover/dialog needs none.
- Installing a JS focus-trap or portal library for a plain dropdown, tooltip, or single dialog — the platform does it now.
- `popover="auto"` on a toast — light-dismiss deletes it on the next outside click; toasts are `manual`.
- `popover="manual"` used as a modal expecting a focus trap or inert background — it provides neither; that's `<dialog>.showModal()`.
- `role="menu"` with no arrow-key roving focus wired — broken menu semantics; either implement it or drop the role for a link/button list.
- Anchoring with no `@supports (anchor-name)` / static-offset fallback — older engines render an unplaced box in flow.
- `command`/`commandfor` or `popover="hint"` shipped as the only path, with no `popovertarget`/`showModal()` implementation underneath and no feature detection — these are the graduated set, not the baseline.
- A `transition` on a popover with no `allow-discrete` on `display`/`overlay` — it pops in and snaps out with no exit motion.
- A gray drop-shadowed panel with `rounded-xl shadow-lg` — overlay depth comes from SYSTEM.md §depth (tinted shadow), like every other surface.

## Worked example — Tidepool, SaaS account menu

Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.

## Composes with

Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.

