HTML and Semantics skill 4 of 4

Dialog and Popover SKILL.md: Native Modals, Focus and Esc

The Dialog and Popover skill makes Claude, Cursor or another AI agent audit and fix modals, popovers and their trigger buttons. A read-only checker flags div-based modals, unnamed dialogs, broken popover targets and blocked cancel buttons, and a decision table picks the native element. The agent edits in small approved batches and tests focus, Esc and scrolling.

dialog-and-popover/SKILL.md, version 1.0.0, 3,913 words, plus scripts/dialog_check.py.

An agent audits and fixes overlay markup in this order: run a read-only checker, choose the right native primitive for each overlay with a decision table, apply fixed rules for naming, focus, Esc, scrolling and motion, then edit in small approved batches and test the behaviour the checker cannot see.

It works on plain HTML and on the rendered output of any framework or UI library. Source facts sit in references/appendix.md and code in references/patterns.md.

When to use this skill, and when not to

Use it to audit, fix or build modal dialogs, confirm boxes, drawers, lightboxes, popovers, filter panels, toasts and their trigger buttons, or to replace a hand-built modal and focus trap with the native elements.

RequestUse instead
Fields, labels and validation inside a dialoghtml-forms.md
Landmarks, headings, links and buttons outside overlayssemantic-html.md
Cookie, consent or age-gate wording and whether dismissing counts as consentA person with legal responsibility. This skill marks up the dialog only
Tabs, comboboxes, date pickers, carousels, tree viewsA component accessibility skill. This skill covers dialog and popover only
Visual design of the overlayOut of scope

Guardrails

  1. Propose, then change. Report and plan first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
  2. Never trigger real actions. Confirm dialogs guard deletes, payments and sends. Test only in a local or test environment with fake data. The agent never clicks a live "Delete", "Pay" or "Send".
  3. The dialog is not the authority. A confirm button is user experience. The server must authorise every destructive action, and the report says so if it does not.
  4. Personal data. Dialogs show names, emails, addresses and account details. Use invented test data such as test@example.com, never paste entered or displayed values into reports, and never store them.
  5. Batches stay small. At most 3 overlays or 30 attribute changes per batch (defaults), each shown as a diff.
  6. Nothing is invented. Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry verify before relying on this.
  7. Not legal advice. Consent, cookie and age dialogs carry legal duties. The agent routes wording and the dismiss-equals-consent question to a person.
  8. Refuses to: disable Esc without a documented data-loss reason and a guard (step 3), remove the only visible close control, build a JavaScript focus trap where a native dialog fits, put role="dialog" or aria-modal on a popover, or fake modality with aria-hidden on the page behind.

Inputs the agent must have before starting

Ask for what is missing in one message and wait. Do not proceed on assumptions.

#InputWhat to ask if missing
1Target"Which pages, templates or components? File paths, or rendered HTML saved with each overlay in its closed and open state."
2Stack"Plain HTML, a framework, or a UI library that renders overlays (Radix, Headless UI, MUI, Bootstrap)?"
3Browsers"Which browsers and versions must work?"
4Purpose"What is each overlay for: confirm, form, information, menu, toast, hover hint, navigation?"
5Data loss"Does closing any overlay lose typed or unsaved data?"
6Test setup"Is there a test environment where I may click through with fake data, and can you run Python 3?"
7Dependants"Do tests, analytics or CSS read the ids, classes or roles I might change?"

If input 3 is unknown, the agent uses only dialog with showModal() (MDN: widely available since March 2022) and JavaScript for everything else, and lists command, closedby and popover="hint" as options pending an answer. Static checks cannot see overlays that scripts build at run time. If input 1 gives only source for such a component, the agent works from the rendered DOM or says so.

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as dialog-and-popover/SKILL.md.

---
name: dialog-and-popover
description: "Audits and fixes modal dialogs, popovers and their trigger buttons. Runs a read-only checker for div-based modals, unnamed dialogs, tabindex on dialog, broken popovertarget or commandfor targets, invalid command values, empty close buttons and cancel buttons blocked by required fields, then applies rules for choosing dialog, auto, manual or hint, focus, Esc, scroll lock and animation. Use for modal, dialog, popup, overlay, popover, lightbox, drawer, showModal or focus trap work. Do NOT use for form fields (html-forms), landmarks (semantic-html), consent wording, tabs or comboboxes."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/
  category: frontend-development
  subcategory: html-semantics
  version: "1.0.0"
  created: "2026-10-24"
  updated: "2026-10-24"
---

# Dialogs and Popovers: Native Modals, Overlays and Focus

An agent audits and fixes overlay markup in this order: run a read-only checker, choose the right native primitive for each overlay with a decision table, apply fixed rules for naming, focus, Esc, scrolling and motion, then edit in small approved batches and test the behaviour the checker cannot see.

It works on plain HTML and on the rendered output of any framework or UI library. Source facts sit in `references/appendix.md` and code in `references/patterns.md`.

## When to use this skill, and when not to

Use it to audit, fix or build modal dialogs, confirm boxes, drawers, lightboxes, popovers, filter panels, toasts and their trigger buttons, or to replace a hand-built modal and focus trap with the native elements.

| Request | Use instead |
|---|---|
| Fields, labels and validation inside a dialog | `html-forms.md` |
| Landmarks, headings, links and buttons outside overlays | `semantic-html.md` |
| Cookie, consent or age-gate wording and whether dismissing counts as consent | A person with legal responsibility. This skill marks up the dialog only |
| Tabs, comboboxes, date pickers, carousels, tree views | A component accessibility skill. This skill covers dialog and popover only |
| Visual design of the overlay | Out of scope |

## Guardrails

1. **Propose, then change.** Report and plan first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Never trigger real actions.** Confirm dialogs guard deletes, payments and sends. Test only in a local or test environment with fake data. The agent never clicks a live "Delete", "Pay" or "Send".
3. **The dialog is not the authority.** A confirm button is user experience. The server must authorise every destructive action, and the report says so if it does not.
4. **Personal data.** Dialogs show names, emails, addresses and account details. Use invented test data such as `test@example.com`, never paste entered or displayed values into reports, and never store them.
5. **Batches stay small.** At most 3 overlays or 30 attribute changes per batch (defaults), each shown as a diff.
6. **Nothing is invented.** Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry **verify before relying on this**.
7. **Not legal advice.** Consent, cookie and age dialogs carry legal duties. The agent routes wording and the dismiss-equals-consent question to a person.
8. **Refuses to:** disable Esc without a documented data-loss reason and a guard (step 3), remove the only visible close control, build a JavaScript focus trap where a native `dialog` fits, put `role="dialog"` or `aria-modal` on a popover, or fake modality with `aria-hidden` on the page behind.

## Inputs the agent must have before starting

Ask for what is missing in one message and wait. Do not proceed on assumptions.

| # | Input | What to ask if missing |
|---|---|---|
| 1 | Target | "Which pages, templates or components? File paths, or rendered HTML saved with each overlay in its closed and open state." |
| 2 | Stack | "Plain HTML, a framework, or a UI library that renders overlays (Radix, Headless UI, MUI, Bootstrap)?" |
| 3 | Browsers | "Which browsers and versions must work?" |
| 4 | Purpose | "What is each overlay for: confirm, form, information, menu, toast, hover hint, navigation?" |
| 5 | Data loss | "Does closing any overlay lose typed or unsaved data?" |
| 6 | Test setup | "Is there a test environment where I may click through with fake data, and can you run Python 3?" |
| 7 | Dependants | "Do tests, analytics or CSS read the ids, classes or roles I might change?" |

If input 3 is unknown, the agent uses only `dialog` with `showModal()` (MDN: widely available since March 2022) and JavaScript for everything else, and lists `command`, `closedby` and `popover="hint"` as options pending an answer. Static checks cannot see overlays that scripts build at run time. If input 1 gives only source for such a component, the agent works from the rendered DOM or says so.

## Workflow

### Step 0: Confirm scope

Confirm inputs 1 to 7 and the branch name. Tell the user the plan: checker, primitive choice, rules, batches, behaviour tests, report. Gate 1: wait for a yes.

### Step 1: Run the checker

Run `python3 scripts/dialog_check.py page.html`, pipe it through `head -60` and read the output only. Do not read the script. It reads files, writes nothing, exits 1 on any error and prints `file:line level code message`. Keep the output as the baseline.

| Code | Level | Fix |
|---|---|---|
| `custom-dialog` | warn | Step 2, then step 3 |
| `tabindex-on-dialog` | error | Delete it. MDN: never set `tabindex` on `dialog` |
| `open-attribute` | warn | Step 3.6 |
| `closedby-invalid` | error | Use `any`, `closerequest` or `none` (step 3.5) |
| `dialog-no-name`, `labelledby-missing-target` | error | Step 3.1 |
| `multiple-autofocus` | warn | Step 3.2 |
| `no-close-control` | warn | Step 3.3. Ignore if script adds one and the test in step 10 passes |
| `empty-button` | error | Give the button text or `aria-label` |
| `cancel-blocked-by-required` | warn | Add `formnovalidate` (step 3.4) |
| `popover-value` | warn | Step 4.1 |
| `invoker-not-button`, `invoker-target-missing`, `invoker-target-wrong` | error | Step 4.2 |
| `command-invalid`, `command-target-wrong` | error | Step 5 |
| `manual-aria-expanded` | warn | Step 4.3 |

The checker reads static markup only. It cannot see focus, scripts, CSS or overlays created at run time, so a clean result proves nothing about behaviour. Step 10 does.

### Step 2: Choose the primitive for each overlay

Apply the first matching row.

| Overlay | Use |
|---|---|
| Must be answered before the user continues (confirm, required form, blocking error) | Modal `dialog`, opened with `showModal()` or `command="show-modal"` |
| Panel the user may ignore while using the page (chat, inspector) | Non-modal `dialog`, opened with `show()` |
| Short-lived panel anchored to a button, dismissed by clicking away (filters, share menu, options) | `popover` (auto) |
| Panel that must stay open until the user or code closes it (toast, status) | `popover="manual"` |
| Hover or focus preview | `popover="hint"`, never the only home of important information. Verify browser support |
| A popover with a title and its own controls | `dialog` with the `popover` attribute, so it has a role and a name (MDN lists this pattern) |
| Content that expands in place | Not an overlay. Use `details` and `summary`, outside this skill |
| Site navigation menu | A disclosure that reveals a list of links. WebAIM Million 2026 found 22% of `role="menu"` elements had barriers, so reserve `role="menu"` for application action menus that implement arrow keys |

A `dialog` that traps the user without a reason is a defect, and a popover used where the user must decide is also one. Ask input 4 again if the row is unclear.

### Step 3: Modal dialog rules

Read `references/patterns.md` once, only when writing or rewriting markup, CSS or script (steps 3, 6 and 7).

1. **Name.** Every dialog has `aria-labelledby` pointing to a visible heading, or `aria-label`. Use `aria-describedby` only for short plain text. Do not describe structured content (W3C APG).
2. **Initial focus.** Put `autofocus` on the control named below. With no `autofocus`, `showModal()` focuses the first focusable element (MDN). Use one `autofocus` per dialog.

| Dialog content | Focus on open |
|---|---|
| Irreversible action | The least destructive button, such as "Keep account" |
| Informs or continues a process | The most-used button |
| Long text, a list or a table | A static heading with `tabindex="-1"`, with no `aria-describedby` |
| A form | The first field |

3. **Close control.** At least one visible close button in the tab order inside the dialog (APG: strongly recommended). Icon-only buttons need an accessible name.
4. **Forms.** Use `form method="dialog"` so a button closes the dialog and sets `returnValue`. A cancel button needs `formnovalidate`, because a required empty field blocks a submit button from closing the dialog (MDN). Read `returnValue` in the `close` event. Field rules go to `html-forms.md`.
5. **Esc and outside click.** Esc closes a modal (MDN) through the `cancel` event. `closedby="closerequest"` is the default for modals. Use `closedby="any"` only if clicking outside should close it, and **verify before relying on this** in the target browsers. If closing loses data (input 5), call `preventDefault()` on `cancel` while the form is dirty and ask first. Never remove Esc for convenience.
6. **Open it with the methods.** `showModal()` or `command="show-modal"` makes the rest of the page inert and exposes `aria-modal="true"` (MDN). The `open` attribute opens a non-modal dialog, so it is wrong for a modal. Do not add `aria-hidden` to the page or hand-written focus traps.
7. **Return focus.** On close, focus returns to the control that opened it, unless that control no longer exists, and then to a sensible target such as the list heading (APG). Test it (step 10). Do not assume it.
8. **Known bug.** `autofocus` on a slotted element inside a dialog in shadow DOM may not take focus (Mozilla Bugzilla 2050985, unconfirmed). If the overlay lives in a web component, test focus in Firefox and Safari.

### Step 4: Popover rules

1. **Kind.** Plain `popover` is `auto`: it closes on outside click and Esc. `manual` has no light dismiss and can stack with others. `hint` closes other hints only. A value outside `auto`, `manual` and `hint` is a defect.
2. **Invokers.** The trigger is a `button` with `popovertarget="id"` (and `popovertargetaction` of `show`, `hide` or `toggle` where the default toggle is not wanted), or `commandfor`. The id must exist and carry the `popover` attribute. A link is not an invoker.
3. **State.** The invoker gets an implicit `aria-expanded` (MDN). Do not add one by hand.
4. **Behaviour.** A popover is not modal and does not trap focus. Esc returns focus to the invoker (MDN). Keep controls inside reachable by Tab in reading order.
5. **Events.** Use `beforetoggle` (cancellable) and `toggle` for logic, and check `:popover-open` before calling `showPopover()` or `hidePopover()`.

### Step 5: Declarative or JavaScript

1. If input 3 includes only browsers with `command` and `commandfor` (InfoQ, 28 January 2026: Chrome 135, Firefox 144, Safari 26.2, so **verify before relying on this**), use them with built-in commands only: `show-modal`, `close`, `request-close`, `show-popover`, `hide-popover`, `toggle-popover`.
2. Otherwise use `popovertarget` for popovers and `showModal()` in script for dialogs. Use one approach per project.
3. Custom commands must start with `--` and need a `command` event listener. The checker accepts them. Prefer built-ins.

### Step 6: The page behind

1. A modal does not stop the page scrolling (whatwg/html issue 7732, open). Apply the CSS in `references/patterns.md`.
2. `overflow: hidden` on the body fails on iOS Safari (OpenReplay, 30 January 2026). If iOS support matters, use the fixed-body method in the same reference and test on a real device.
3. Cap the dialog with `max-height` and `overflow-y: auto` so a tall dialog stays reachable at 320 CSS pixels (WCAG 1.4.10 Reflow).

### Step 7: Motion

Animate entry with `@starting-style` and animate `display` and `overlay` with `transition-behavior: allow-discrete` (MDN). The `::backdrop` is removed at once on close, so keyframes cannot fade it out. Respect `prefers-reduced-motion` (this skill's rule). Snippet in `references/patterns.md`.

### Step 8: Libraries

If a UI library renders the overlay, run the checker on the rendered HTML and apply steps 3 and 4 as tests, not as a rewrite. Do not replace a working library component with native elements unless the user asks. Report any library behaviour that fails step 10.

### Step 9: Plan and apply in batches

1. Turn the checker output and decisions into a change list: file and line, current markup, new markup, rule (step number).
2. Search for dependants (commands below). Ids, classes and roles that tests or scripts read stay unless the plan updates them.
3. Group under guardrail 5. Gate 2: show each batch's diff and wait for a yes. Edit in place. Do not echo unchanged markup back.

### Step 10: Verify

1. Re-run `dialog_check.py`. Pass: 0 errors on changed files and no new warnings.
2. Keyboard, in the test environment. Open each overlay with the keyboard. Tab and Shift+Tab stay inside a modal and cycle. Esc closes it (unless step 3.5 guards it). Focus returns to the opener.
3. Modality. In the console, with the dialog open: `[!!document.querySelector('dialog:modal'), document.activeElement.closest('dialog') !== null]`. Pass: `[true, true]` for a modal. Clicking the page behind does nothing.
4. Screen reader or accessibility tree. The dialog announces its role and name on open. Popover invokers report expanded and collapsed.
5. Reflow. At 320 CSS pixels wide and 200% zoom, every control is reachable and nothing is cut off.
6. Scroll. With the modal open, the page behind does not move on desktop, and on iOS if it was in scope.
7. Motion. With reduced motion on, no transition runs.
8. Server. The destructive action is rejected without server authorisation (guardrail 3), tested with fake data.

### Step 11: Report

Use the report template. State what was changed, which behaviours were tested and by whom, what was not tested (browsers, iOS, screen readers), library failures and legal wording routed to a person.

## Worked example

The page is invented. The checker output is real: it came from running the script in this skill on this file.

**Input.** An account settings page. Plain HTML. Target browsers: current Chrome, Firefox and Safari. Closing the edit-profile overlay loses typed data. The server authorises deletes.

**Step 1, checker (before):**

```
page_before.html:4  warn   manual-aria-expanded  aria-expanded on a popovertarget button: MDN says the relationship is implicit
page_before.html:5  error  invoker-not-button  popovertarget on <a>: use a button
page_before.html:6  error  invoker-target-missing  target id 'missing-panel' not found in this file
page_before.html:7  error  command-target-wrong  show-modal needs a dialog, target is <div>
page_before.html:8  error  command-invalid  command='open' is not a built-in command or a --custom name
page_before.html:9  warn   popover-value  popover='sticky' is not auto, manual or hint
page_before.html:10  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:10  warn   custom-dialog  <div role=dialog> instead of native dialog: focus handling, inert and Esc must be built by hand
page_before.html:11  error  closedby-invalid  closedby='click' is not any, closerequest or none
page_before.html:11  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:11  error  tabindex-on-dialog  tabindex on dialog (MDN: never set it)
page_before.html:11  warn   multiple-autofocus  2 autofocus elements inside one dialog
page_before.html:11  warn   open-attribute  static open attribute opens non-modal; use showModal() or command=show-modal for a modal
page_before.html:15  warn   cancel-blocked-by-required  cancel button in a method=dialog form with required fields has no formnovalidate
page_before.html:19  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:19  warn   no-close-control  no method=dialog form or command=close button found inside; JS may add one
page_before.html:20  error  empty-button  button has no text or accessible name
page_before.html:20  error  labelledby-missing-target  aria-labelledby points to missing id 'nothere'
page_before.html: 11 errors, 7 warnings
```

**Steps 2 to 7, decisions.**

| Line | Change | Step |
|---|---|---|
| 4, 9 | Filters panel becomes a plain `popover` (anchored, click-away). Remove `aria-expanded` and `sticky` | 2, 4.1, 4.3 |
| 5 | Link invoker becomes a button | 4.2 |
| 6 | Dangling target: the user confirms the help panel was removed, so delete the button | 4.2 |
| 7, 10 | Edit-profile `div role="dialog"` becomes a native `dialog` with `aria-labelledby`, opened by `command="show-modal"` | 2, 3.1, 3.6, 5.1 |
| 8 | `command="open"` becomes `show-modal` on the delete dialog | 5.1 |
| 11 | Delete dialog: remove `open` and `tabindex`, set `closedby="closerequest"`, one `autofocus` on "Keep account", name via `aria-labelledby` | 3.1, 3.2, 3.5, 3.6 |
| 15 | Cancel button gets `formnovalidate` | 3.4 |
| 19, 20 | Newsletter dialog gets `aria-labelledby` and a close button. The unnamed dialog on line 20 is unused, so the user approves its removal | 3.1, 3.3 |
| Edit-profile | `cancel` handler asks before discarding typed data, because input 5 says closing loses data | 3.5 |

The user approves one batch. **Step 10.** The checker on the rebuilt file reports `0 errors, 0 warnings`. The user runs the keyboard, `:modal` and focus-return checks in the test environment and confirms them. Safari 26.2 is the oldest target for `command`, which the user confirmed. The report lists one open item: scroll lock on iOS is untested.

## Templates

### Finding dependants

```
grep -rnE "(showModal|show\(\)|showPopover|togglePopover|\.close\()" --include=*.js --include=*.ts --include=*.tsx .
grep -rnE "role=.(dialog|alertdialog).|aria-modal|getElementById\(.(modal|dialog)" --include=*.js --include=*.html --include=*.tsx .
```

### Report

```
DIALOG AND POPOVER REPORT: [pages], [date], branch [name]
Baseline: [n] errors, [n] warnings        After: [n] errors, [n] warnings
Overlays: [id: primitive chosen and why, one line each]
Changed: custom to native [n], names [n], focus [n], invokers [n], commands [n]
Data-loss guards added: [list or none]
Behaviour tests: keyboard [pass/fail], :modal [pass/fail], focus return [pass/fail],
                 reflow [pass/fail], scroll lock [pass/fail/not tested], reduced motion [pass/fail]
Server authorises destructive actions: [yes / unconfirmed]
Not tested: [browsers, iOS, screen readers, library internals]
Legal wording routed to: [person, or none]
```

### Starting prompt

```
Use the dialog-and-popover skill on [files]. Stack: [stack]. Target browsers:
[list]. Closing [overlay] loses data. Run the checker and choose a primitive for
each overlay first, and change nothing until I approve the plan.
```

Code for dialogs, popovers, animation and scroll lock sits in `references/patterns.md`.

## Failure modes and anti-patterns

| # | Failure | How to spot it | What to do instead |
|---|---|---|---|
| 1 | Div modal with a hand-built focus trap and `aria-hidden` on the page | `custom-dialog`. Tab reaches the page behind, or a screen reader can read it | Native `dialog` with `showModal()` (step 3.6). WebAIM found pages with ARIA averaged more errors than pages without |
| 2 | A modal opened with the `open` attribute or `show()` | `open-attribute`. Esc does nothing and the page behind stays clickable | `showModal()` or `command="show-modal"` |
| 3 | Wrong initial focus or lost return focus | Step 10.2 fails, or focus lands on a destructive button | The table in step 3.2. Test return focus. Check shadow DOM (step 3.8) |
| 4 | The wrong primitive: a popover for a decision, a dialog for a hint, `role="menu"` for navigation | Step 2 row does not match the purpose | Re-run step 2 with the purpose from input 4 |
| 5 | Page scrolls behind the modal, or a tall dialog cannot be reached | Step 10.5 and 10.6 fail, especially on iOS | Step 6 |

## Quality bar

Check every item before handing over. Rules are in the step named, not repeated here.

1. Gate 1 and gate 2 approvals are recorded and the work is on a named branch.
2. Baseline and final checker output are in the report, and the final run shows 0 errors on changed files.
3. Every overlay has a primitive chosen from the step 2 table, with the reason in the report.
4. Every dialog has a name, a visible close control and exactly one initial-focus decision (step 3).
5. Every popover invoker is a button whose target exists and carries `popover` (step 4).
6. No `aria-modal`, `role="dialog"` or `aria-hidden` workaround remains where a native element is used.
7. Step 10.2, 10.3 and 10.5 were run for each modal and the results are in the report.
8. Any overlay where closing loses data has a guard, or the user declined it in writing.
9. The report states whether the server authorises destructive actions.
10. No real personal data, credentials or live actions appear in tests or in the report.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the sizes shown on the pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 113 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 5,768 | Estimate | When the skill fires |
| C. `references/patterns.md` | 1,085 | Estimate | Only when writing or rewriting markup, CSS or script |
| C. `references/appendix.md` | 981 | Estimate | Only to check a claim |
| Script output: `dialog_check.py` (this page's example) | 571 | Estimate | Every run. Capped at 60 lines by `head -60`, about 1,700 tokens |
| D. Typical run (A + B + patterns + script output) | 7,537 | Estimate | Common path |
| D. Worst case (A + B + both references + 60 lines of output) | 9,647 | Estimate | Large page, claim check |
| E. Cheaper path | 6,452 | Estimate | Run the checker, fix only flagged lines that need no new markup, skip the references |

Rival sizes, estimated from the pages I read: Community-Access modal-specialist about 10,000 characters, so roughly 2,900 tokens; thedaviddias popover about 2,300 characters of SKILL.md plus a `references/pattern.md` I could not size, so roughly 660 tokens before that file; workspace-brfis create-accessible-modals could not be sized from the page. This skill costs more to load than all three. What the script saves is the agent reading and reasoning over every overlay's markup, and I have not measured that saving.

## Handoffs

`html-forms.md` covers fields inside dialogs. `semantic-html.md` covers the page around them. `responsive-images.md` covers images inside lightboxes.

## Appendix

Tool-specific facts and the rival comparison sit in `references/appendix.md`. Read it only to check a claim.

## Sources

Last verified 24 October 2026 for every source, except where a date says otherwise.

- MDN, The Dialog element: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog
- MDN, Using the Popover API: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using
- W3C WAI-ARIA Authoring Practices Guide, Dialog (Modal) pattern (no update date shown): https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
- whatwg/html, Consider preventing page scroll when modal dialog is visible (issue 7732, open): https://github.com/whatwg/html/issues/7732
- OpenReplay, How to stop a page from scrolling while a dialog is open (30 January 2026): https://blog.openreplay.com/stop-page-scrolling-dialog-open/
- Mozilla Bugzilla 2050985, autofocus in dialog in shadow DOM (unconfirmed): https://bugzilla.mozilla.org/show_bug.cgi?id=2050985
- InfoQ, HTML invoker commands (28 January 2026): https://www.infoq.com/news/2026/01/html-invoker-commands
- WebAIM, The WebAIM Million 2026 (data February 2026): https://webaim.org/projects/million/
- Community-Access, accessibility-agents, modal-specialist (via skillselion.com). Rival implementation: https://skillselion.com/skills/community-access/accessibility-agents/modal-specialist
- workspace-brfis, create-accessible-modals (via tessl.io). Rival implementation: https://tessl.io/registry/workspace-brfis/create-accessible-modals/skills
- thedaviddias, ux-patterns-for-developers, popover (via skillselion.com). Rival implementation: https://skillselion.com/skills/thedaviddias/ux-patterns-for-developers/popover
- Anthropic, Agent Skills best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.

Workflow

Step 0: Confirm scope

Confirm inputs 1 to 7 and the branch name. Tell the user the plan: checker, primitive choice, rules, batches, behaviour tests, report. Gate 1: wait for a yes.

Step 1: Run the checker

Run python3 scripts/dialog_check.py page.html, pipe it through head -60 and read the output only. Do not read the script. It reads files, writes nothing, exits 1 on any error and prints file:line level code message. Keep the output as the baseline.

CodeLevelFix
custom-dialogwarnStep 2, then step 3
tabindex-on-dialogerrorDelete it. MDN: never set tabindex on dialog
open-attributewarnStep 3.6
closedby-invaliderrorUse any, closerequest or none (step 3.5)
dialog-no-name, labelledby-missing-targeterrorStep 3.1
multiple-autofocuswarnStep 3.2
no-close-controlwarnStep 3.3. Ignore if script adds one and the test in step 10 passes
empty-buttonerrorGive the button text or aria-label
cancel-blocked-by-requiredwarnAdd formnovalidate (step 3.4)
popover-valuewarnStep 4.1
invoker-not-button, invoker-target-missing, invoker-target-wrongerrorStep 4.2
command-invalid, command-target-wrongerrorStep 5
manual-aria-expandedwarnStep 4.3

The checker reads static markup only. It cannot see focus, scripts, CSS or overlays created at run time, so a clean result proves nothing about behaviour. Step 10 does.

Step 2: Choose the primitive for each overlay

Apply the first matching row.

OverlayUse
Must be answered before the user continues (confirm, required form, blocking error)Modal dialog, opened with showModal() or command="show-modal"
Panel the user may ignore while using the page (chat, inspector)Non-modal dialog, opened with show()
Short-lived panel anchored to a button, dismissed by clicking away (filters, share menu, options)popover (auto)
Panel that must stay open until the user or code closes it (toast, status)popover="manual"
Hover or focus previewpopover="hint", never the only home of important information. Verify browser support
A popover with a title and its own controlsdialog with the popover attribute, so it has a role and a name (MDN lists this pattern)
Content that expands in placeNot an overlay. Use details and summary, outside this skill
Site navigation menuA disclosure that reveals a list of links. WebAIM Million 2026 found 22% of role="menu" elements had barriers, so reserve role="menu" for application action menus that implement arrow keys

A dialog that traps the user without a reason is a defect, and a popover used where the user must decide is also one. Ask input 4 again if the row is unclear.

Step 3: Modal dialog rules

Read references/patterns.md once, only when writing or rewriting markup, CSS or script (steps 3, 6 and 7).

  1. Name. Every dialog has aria-labelledby pointing to a visible heading, or aria-label. Use aria-describedby only for short plain text. Do not describe structured content (W3C APG).
  2. Initial focus. Put autofocus on the control named below. With no autofocus, showModal() focuses the first focusable element (MDN). Use one autofocus per dialog.
Dialog contentFocus on open
Irreversible actionThe least destructive button, such as "Keep account"
Informs or continues a processThe most-used button
Long text, a list or a tableA static heading with tabindex="-1", with no aria-describedby
A formThe first field
  1. Close control. At least one visible close button in the tab order inside the dialog (APG: strongly recommended). Icon-only buttons need an accessible name.
  2. Forms. Use form method="dialog" so a button closes the dialog and sets returnValue. A cancel button needs formnovalidate, because a required empty field blocks a submit button from closing the dialog (MDN). Read returnValue in the close event. Field rules go to html-forms.md.
  3. Esc and outside click. Esc closes a modal (MDN) through the cancel event. closedby="closerequest" is the default for modals. Use closedby="any" only if clicking outside should close it, and verify before relying on this in the target browsers. If closing loses data (input 5), call preventDefault() on cancel while the form is dirty and ask first. Never remove Esc for convenience.
  4. Open it with the methods. showModal() or command="show-modal" makes the rest of the page inert and exposes aria-modal="true" (MDN). The open attribute opens a non-modal dialog, so it is wrong for a modal. Do not add aria-hidden to the page or hand-written focus traps.
  5. Return focus. On close, focus returns to the control that opened it, unless that control no longer exists, and then to a sensible target such as the list heading (APG). Test it (step 10). Do not assume it.
  6. Known bug. autofocus on a slotted element inside a dialog in shadow DOM may not take focus (Mozilla Bugzilla 2050985, unconfirmed). If the overlay lives in a web component, test focus in Firefox and Safari.

Step 4: Popover rules

  1. Kind. Plain popover is auto: it closes on outside click and Esc. manual has no light dismiss and can stack with others. hint closes other hints only. A value outside auto, manual and hint is a defect.
  2. Invokers. The trigger is a button with popovertarget="id" (and popovertargetaction of show, hide or toggle where the default toggle is not wanted), or commandfor. The id must exist and carry the popover attribute. A link is not an invoker.
  3. State. The invoker gets an implicit aria-expanded (MDN). Do not add one by hand.
  4. Behaviour. A popover is not modal and does not trap focus. Esc returns focus to the invoker (MDN). Keep controls inside reachable by Tab in reading order.
  5. Events. Use beforetoggle (cancellable) and toggle for logic, and check :popover-open before calling showPopover() or hidePopover().

Step 5: Declarative or JavaScript

  1. If input 3 includes only browsers with command and commandfor (InfoQ, 28 January 2026: Chrome 135, Firefox 144, Safari 26.2, so verify before relying on this), use them with built-in commands only: show-modal, close, request-close, show-popover, hide-popover, toggle-popover.
  2. Otherwise use popovertarget for popovers and showModal() in script for dialogs. Use one approach per project.
  3. Custom commands must start with -- and need a command event listener. The checker accepts them. Prefer built-ins.

Step 6: The page behind

  1. A modal does not stop the page scrolling (whatwg/html issue 7732, open). Apply the CSS in references/patterns.md.
  2. overflow: hidden on the body fails on iOS Safari (OpenReplay, 30 January 2026). If iOS support matters, use the fixed-body method in the same reference and test on a real device.
  3. Cap the dialog with max-height and overflow-y: auto so a tall dialog stays reachable at 320 CSS pixels (WCAG 1.4.10 Reflow).

Step 7: Motion

Animate entry with @starting-style and animate display and overlay with transition-behavior: allow-discrete (MDN). The ::backdrop is removed at once on close, so keyframes cannot fade it out. Respect prefers-reduced-motion (this skill's rule). Snippet in references/patterns.md.

Step 8: Libraries

If a UI library renders the overlay, run the checker on the rendered HTML and apply steps 3 and 4 as tests, not as a rewrite. Do not replace a working library component with native elements unless the user asks. Report any library behaviour that fails step 10.

Step 9: Plan and apply in batches

  1. Turn the checker output and decisions into a change list: file and line, current markup, new markup, rule (step number).
  2. Search for dependants (commands below). Ids, classes and roles that tests or scripts read stay unless the plan updates them.
  3. Group under guardrail 5. Gate 2: show each batch's diff and wait for a yes. Edit in place. Do not echo unchanged markup back.

Step 10: Verify

  1. Re-run dialog_check.py. Pass: 0 errors on changed files and no new warnings.
  2. Keyboard, in the test environment. Open each overlay with the keyboard. Tab and Shift+Tab stay inside a modal and cycle. Esc closes it (unless step 3.5 guards it). Focus returns to the opener.
  3. Modality. In the console, with the dialog open: [!!document.querySelector('dialog:modal'), document.activeElement.closest('dialog') !== null]. Pass: [true, true] for a modal. Clicking the page behind does nothing.
  4. Screen reader or accessibility tree. The dialog announces its role and name on open. Popover invokers report expanded and collapsed.
  5. Reflow. At 320 CSS pixels wide and 200% zoom, every control is reachable and nothing is cut off.
  6. Scroll. With the modal open, the page behind does not move on desktop, and on iOS if it was in scope.
  7. Motion. With reduced motion on, no transition runs.
  8. Server. The destructive action is rejected without server authorisation (guardrail 3), tested with fake data.

Step 11: Report

Use the report template. State what was changed, which behaviours were tested and by whom, what was not tested (browsers, iOS, screen readers), library failures and legal wording routed to a person.

Worked example

The page is invented. The checker output is real: it came from running the script in this skill on this file.

Input. An account settings page. Plain HTML. Target browsers: current Chrome, Firefox and Safari. Closing the edit-profile overlay loses typed data. The server authorises deletes.

Step 1, checker (before):

page_before.html:4  warn   manual-aria-expanded  aria-expanded on a popovertarget button: MDN says the relationship is implicit
page_before.html:5  error  invoker-not-button  popovertarget on <a>: use a button
page_before.html:6  error  invoker-target-missing  target id 'missing-panel' not found in this file
page_before.html:7  error  command-target-wrong  show-modal needs a dialog, target is <div>
page_before.html:8  error  command-invalid  command='open' is not a built-in command or a --custom name
page_before.html:9  warn   popover-value  popover='sticky' is not auto, manual or hint
page_before.html:10  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:10  warn   custom-dialog  <div role=dialog> instead of native dialog: focus handling, inert and Esc must be built by hand
page_before.html:11  error  closedby-invalid  closedby='click' is not any, closerequest or none
page_before.html:11  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:11  error  tabindex-on-dialog  tabindex on dialog (MDN: never set it)
page_before.html:11  warn   multiple-autofocus  2 autofocus elements inside one dialog
page_before.html:11  warn   open-attribute  static open attribute opens non-modal; use showModal() or command=show-modal for a modal
page_before.html:15  warn   cancel-blocked-by-required  cancel button in a method=dialog form with required fields has no formnovalidate
page_before.html:19  error  dialog-no-name  dialog has no aria-labelledby or aria-label
page_before.html:19  warn   no-close-control  no method=dialog form or command=close button found inside; JS may add one
page_before.html:20  error  empty-button  button has no text or accessible name
page_before.html:20  error  labelledby-missing-target  aria-labelledby points to missing id 'nothere'
page_before.html: 11 errors, 7 warnings

Steps 2 to 7, decisions.

LineChangeStep
4, 9Filters panel becomes a plain popover (anchored, click-away). Remove aria-expanded and sticky2, 4.1, 4.3
5Link invoker becomes a button4.2
6Dangling target: the user confirms the help panel was removed, so delete the button4.2
7, 10Edit-profile div role="dialog" becomes a native dialog with aria-labelledby, opened by command="show-modal"2, 3.1, 3.6, 5.1
8command="open" becomes show-modal on the delete dialog5.1
11Delete dialog: remove open and tabindex, set closedby="closerequest", one autofocus on "Keep account", name via aria-labelledby3.1, 3.2, 3.5, 3.6
15Cancel button gets formnovalidate3.4
19, 20Newsletter dialog gets aria-labelledby and a close button. The unnamed dialog on line 20 is unused, so the user approves its removal3.1, 3.3
Edit-profilecancel handler asks before discarding typed data, because input 5 says closing loses data3.5

The user approves one batch. Step 10. The checker on the rebuilt file reports 0 errors, 0 warnings. The user runs the keyboard, :modal and focus-return checks in the test environment and confirms them. Safari 26.2 is the oldest target for command, which the user confirmed. The report lists one open item: scroll lock on iOS is untested.

Templates

Finding dependants

grep -rnE "(showModal|show\(\)|showPopover|togglePopover|\.close\()" --include=*.js --include=*.ts --include=*.tsx .
grep -rnE "role=.(dialog|alertdialog).|aria-modal|getElementById\(.(modal|dialog)" --include=*.js --include=*.html --include=*.tsx .

Report

DIALOG AND POPOVER REPORT: [pages], [date], branch [name]
Baseline: [n] errors, [n] warnings        After: [n] errors, [n] warnings
Overlays: [id: primitive chosen and why, one line each]
Changed: custom to native [n], names [n], focus [n], invokers [n], commands [n]
Data-loss guards added: [list or none]
Behaviour tests: keyboard [pass/fail], :modal [pass/fail], focus return [pass/fail],
                 reflow [pass/fail], scroll lock [pass/fail/not tested], reduced motion [pass/fail]
Server authorises destructive actions: [yes / unconfirmed]
Not tested: [browsers, iOS, screen readers, library internals]
Legal wording routed to: [person, or none]

Starting prompt

Use the dialog-and-popover skill on [files]. Stack: [stack]. Target browsers:
[list]. Closing [overlay] loses data. Run the checker and choose a primitive for
each overlay first, and change nothing until I approve the plan.

Code for dialogs, popovers, animation and scroll lock sits in references/patterns.md.

Failure modes and anti-patterns

#FailureHow to spot itWhat to do instead
1Div modal with a hand-built focus trap and aria-hidden on the pagecustom-dialog. Tab reaches the page behind, or a screen reader can read itNative dialog with showModal() (step 3.6). WebAIM found pages with ARIA averaged more errors than pages without
2A modal opened with the open attribute or show()open-attribute. Esc does nothing and the page behind stays clickableshowModal() or command="show-modal"
3Wrong initial focus or lost return focusStep 10.2 fails, or focus lands on a destructive buttonThe table in step 3.2. Test return focus. Check shadow DOM (step 3.8)
4The wrong primitive: a popover for a decision, a dialog for a hint, role="menu" for navigationStep 2 row does not match the purposeRe-run step 2 with the purpose from input 4
5Page scrolls behind the modal, or a tall dialog cannot be reachedStep 10.5 and 10.6 fail, especially on iOSStep 6

Quality bar

Check every item before handing over. Rules are in the step named, not repeated here.

  1. Gate 1 and gate 2 approvals are recorded and the work is on a named branch.
  2. Baseline and final checker output are in the report, and the final run shows 0 errors on changed files.
  3. Every overlay has a primitive chosen from the step 2 table, with the reason in the report.
  4. Every dialog has a name, a visible close control and exactly one initial-focus decision (step 3).
  5. Every popover invoker is a button whose target exists and carries popover (step 4).
  6. No aria-modal, role="dialog" or aria-hidden workaround remains where a native element is used.
  7. Step 10.2, 10.3 and 10.5 were run for each modal and the results are in the report.
  8. Any overlay where closing loses data has a guard, or the user declined it in writing.
  9. The report states whether the server authorises destructive actions.
  10. No real personal data, credentials or live actions appear in tests or in the report.

Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the sizes shown on the pages I read.

ItemTokensStatusRead condition
A. Description (always loaded)113EstimateEvery session with the skill installed
B. SKILL.md body5,768EstimateWhen the skill fires
C. references/patterns.md1,085EstimateOnly when writing or rewriting markup, CSS or script
C. references/appendix.md981EstimateOnly to check a claim
Script output: dialog_check.py (this page's example)571EstimateEvery run. Capped at 60 lines by head -60, about 1,700 tokens
D. Typical run (A + B + patterns + script output)7,537EstimateCommon path
D. Worst case (A + B + both references + 60 lines of output)9,647EstimateLarge page, claim check
E. Cheaper path6,452EstimateRun the checker, fix only flagged lines that need no new markup, skip the references

Rival sizes, estimated from the pages I read: Community-Access modal-specialist about 10,000 characters, so roughly 2,900 tokens; thedaviddias popover about 2,300 characters of SKILL.md plus a references/pattern.md I could not size, so roughly 660 tokens before that file; workspace-brfis create-accessible-modals could not be sized from the page. This skill costs more to load than all three. What the script saves is the agent reading and reasoning over every overlay's markup, and I have not measured that saving.

Handoffs

html-forms.md covers fields inside dialogs. semantic-html.md covers the page around them. responsive-images.md covers images inside lightboxes.

Appendix

Tool-specific facts and the rival comparison sit in references/appendix.md. Read it only to check a claim.

Sources

Last verified 24 October 2026 for every source, except where a date says otherwise.

What this skill does

Overlays fail in ways a mouse user never sees. A hand-built modal lets Tab reach the page behind it, a close button has no name, or focus lands on a destructive button the moment a confirm box opens. The WebAIM Million 2026 report (data collected February 2026) found pages with ARIA averaged 59.1 errors against 42 without, so more ARIA is not the fix.

This skill runs a read-only Python checker that reports div-based modals, unnamed dialogs, tabindex on a dialog, broken popovertarget and commandfor targets, invalid command values, empty close buttons and cancel buttons that a required field blocks. A decision table then picks the right native element for each overlay, and fixed rules cover naming, focus, Esc, scroll lock and motion. Edits go in batches of at most 3 overlays or 30 attribute changes, and each batch needs your yes.

I read three published skills on the same ground before writing this one. One covers modal focus in depth but has no script and no popover decision. One is lean and has no popover coverage. One covers popovers and asks for role="dialog" on the container. I did not find a checker script in any of the pages I read.

Who it is for

Use it if you build or review modals, confirm boxes, drawers, lightboxes, filter panels or toasts and want the native dialog and popover elements doing the work. It suits frontend developers, accessibility reviewers and anyone checking AI-generated overlay code. It reads rendered HTML, so it works with any framework or UI library.

It is the wrong skill for fields and validation inside a dialog (use HTML Forms), for headings and landmarks (use Semantic HTML), for tabs and comboboxes, for the wording of consent dialogs and for visual design.

What is inside

The file is 3,913 words (version 1.0.0) with one script and two reference files. It holds:

  • Eight guardrails, including no clicks on live delete, pay or send buttons, no real personal data in tests and no consent wording decided by the agent.
  • Seven inputs the agent must have first, with the exact question for each.
  • A twelve-step workflow with a primitive decision table, focus-target table, popover rules, declarative-or-script choice, scroll lock, motion and a behaviour test pass.
  • One standard-library script, scripts/dialog_check.py, with 17 problem codes.
  • A worked example with real checker output (11 errors and 7 warnings before, 0 after), plus templates for dependants, report and starting prompt.
  • Five failure modes, a ten-item quality bar and a cost section.

The token costs below are estimates from the skill’s own count: words times 1.35 for prose and characters divided by 3.5 for code and tables. They are not from a model tokeniser.

ItemTokensStatus
Description (always loaded)113Estimate
SKILL.md body (when the skill fires)5,768Estimate
references/patterns.md, read only when writing or rewriting markup, CSS or script1,085Estimate
references/appendix.md, read only to check a claim981Estimate
Typical run (description, body, patterns, checker output)7,537Estimate
Cheaper path (checker, fix flagged lines, no references)6,452Estimate
Worst case (both references, 60 lines of checker output)9,647Estimate

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as dialog-and-popover/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. Then send the starting prompt from the Templates section.

Before you install. Read the script first. It reads files and writes nothing. The skill tells the agent to test only against a local or test environment with fake data. Install skills only from sources you trust.

A worked example

An invented account settings page has a link used as a popover button, a button pointing at an id that does not exist, a div role="dialog" with no name, a delete dialog with a static open attribute, a tabindex, closedby="click" and two autofocus elements, and a cancel button blocked by a required field. The checker reports 11 errors and 7 warnings.

You tell the agent that closing the edit-profile overlay loses typed data and that the server authorises deletes. It turns the div modal into a native dialog, moves focus to “Keep account”, adds formnovalidate to cancel, adds a cancel guard for unsaved changes and asks before deleting the two dead elements. After one approved batch the checker reports 0 errors and 0 warnings. The report lists scroll lock on iOS as untested.

Common mistakes

The skill is built around five ways this goes wrong:

  • A div modal with a hand-built focus trap. It rebuilds what showModal() already does. The skill replaces it with a native dialog.
  • Opening a modal with the open attribute. That opens a non-modal dialog, so the page behind stays clickable.
  • Wrong or lost focus. Focus on a destructive button, or no return to the opener, is tested in step 10 and not assumed.
  • The wrong primitive. A popover for a decision or a dialog for a hint. A table picks one per overlay.
  • The page scrolling behind the modal. The platform does not stop it (whatwg/html issue 7732, open), and overflow: hidden fails on iOS Safari.

HTML Forms covers fields inside dialogs, Semantic HTML covers the page around them and Responsive Images covers images inside lightboxes. Browse the HTML and Semantics set or the full Frontend Development category.

Frequently asked questions

Use a modal dialog when the user must answer before continuing, such as a confirm box or a required form. Use a popover for a short panel anchored to a button that can be dismissed by clicking away. This skill’s decision table picks one per overlay, and a popover that needs a title and controls becomes a dialog with the popover attribute.

Not for a modal opened with showModal() or command show-modal. MDN says it makes the rest of the page inert, so Tab stays in the dialog. The skill refuses to add a hand-written trap or aria-hidden on the page where a native dialog fits, and it tests focus and Esc in step 10.

It depends on the content. Use the least destructive button for an irreversible action, the most-used button for an informative dialog, a static heading for long text and the first field for a form. Put autofocus on that one control. The skill’s table follows the W3C APG guidance and the checker warns about more than one autofocus.

In a form with method dialog, a submit button cannot close the dialog while a required field is empty, per MDN. Add formnovalidate to the cancel button. The checker flags this as cancel-blocked-by-required, and the skill leaves field rules to the HTML Forms skill.

InfoQ reported them shipped in Chrome 135, Firefox 144 and Safari 26.2 on 28 January 2026. The skill uses them only if every browser you target has them, and otherwise uses popovertarget and showModal(). It marks the choice verify before relying on this, because browser support changes.

The platform does not do it (whatwg/html issue 7732, open). Use body:has(dialog:modal) with overflow hidden and a stable scrollbar gutter, plus a max-height on the dialog. That fails on iOS Safari, so the skill gives the fixed-body method and tells you to test on a real iPhone.

The description is about 113 tokens and loads in every session. The SKILL.md body is about 5,800 tokens when the skill fires. A typical run is about 7,500 tokens including the patterns file and checker output, the cheapest path is about 6,500 and the worst case is about 9,600. All figures are estimates, not from a model tokeniser.

Changelog

  • 1.0.0, 24 October 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.