CSS and Layout skill 12 of 12

CSS Anchor Positioning SKILL.md: Fix Tooltips, Menus and Popovers

The CSS Anchor Positioning skill makes Claude, Cursor or another AI agent find why a tooltip, menu or popover sits in the wrong place, and fix it. A scanner reports each fault by file and line, the agent edits only flagged lines, and you approve each batch. It asks for your browser support floor instead of choosing one.

css-anchor-positioning/SKILL.md, version 1.0.0, 2,976 words, plus scripts/anchor_check.py and three reference files.

Free to use and adapt with credit to [Afeez Adebayo](https://afeezadebayo.com) ([LinkedIn](https://www.linkedin.com/in/afeez-adebayo/)). Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-anchor-positioning/SKILL.md

In this skill

  1. When to use it
  2. Guardrails
  3. Inputs
  4. Workflow (steps 1 to 9)
  5. Worked example
  6. Templates
  7. Failure modes
  8. Quality bar
  9. Cost
  10. Handoffs, appendix, sources

1. When to use it

Use it when a stylesheet, component or template uses, or should use, anchor-name, position-anchor, position-area, anchor(), anchor-size(), anchor-scope, @position-try or position-try-fallbacks, and one of these is true: a tooltip or menu sits in the wrong place, every tooltip in a list attaches to the last item, a popover is clipped at the screen edge, a declaration is silently ignored, or the code is AI-generated and unreviewed. Use it to replace Popper or Floating UI where the layout is simple, and to write a new tooltip, dropdown or popover that is correct on the first pass.

Do not use it for dialog or popover open, close and focus behaviour (see [dialog-and-popover](https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/)), View Transitions, sticky or scroll-snap positioning, or Floating UI features with no CSS equivalent. Hand those back.

Raw SKILL.md file

.md

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

---
name: css-anchor-positioning
description: "Audits and fixes CSS anchor positioning (anchor-name, position-anchor, position-area, anchor(), position-try-fallbacks). Runs a scanner, then edits flagged lines: dangling anchor names, repeated anchors binding to the last item, missing fallbacks, invalid position-area, anchor() outside inset properties, popovers with no invoker link, and Popper or Floating UI code that can go native. Use for 'tooltip position', 'dropdown anchor', 'position-anchor not working', 'replace Popper'. Do NOT use for dialog focus or popover state, View Transitions or sticky positioning."
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/css-layout/css-anchor-positioning/
  category: frontend-development
  subcategory: css-layout
  version: "1.0.0"
  created: "2026-11-07"
  updated: "2026-11-07"
---

# CSS Anchor Positioning

Free to use and adapt with credit to [Afeez Adebayo](https://afeezadebayo.com) ([LinkedIn](https://www.linkedin.com/in/afeez-adebayo/)). Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-anchor-positioning/SKILL.md

## In this skill

1. When to use it
2. Guardrails
3. Inputs
4. Workflow (steps 1 to 9)
5. Worked example
6. Templates
7. Failure modes
8. Quality bar
9. Cost
10. Handoffs, appendix, sources

## 1. When to use it

Use it when a stylesheet, component or template uses, or should use, `anchor-name`, `position-anchor`, `position-area`, `anchor()`, `anchor-size()`, `anchor-scope`, `@position-try` or `position-try-fallbacks`, and one of these is true: a tooltip or menu sits in the wrong place, every tooltip in a list attaches to the last item, a popover is clipped at the screen edge, a declaration is silently ignored, or the code is AI-generated and unreviewed. Use it to replace Popper or Floating UI where the layout is simple, and to write a new tooltip, dropdown or popover that is correct on the first pass.

Do not use it for dialog or popover open, close and focus behaviour (see [dialog-and-popover](https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/)), View Transitions, sticky or scroll-snap positioning, or Floating UI features with no CSS equivalent. Hand those back.

## 2. Guardrails

- Edits are bulk edits. Work on a branch, write nothing until the user approves a batch, and never edit `main`. Default to a report. The scanner never writes files.
- Never choose a browser support floor. Ask for it (Input 3). Support moved during 2026 and rivals disagree with each other. Verify on MDN, web.dev or caniuse before relying on any version number. The body carries none; `references/support.md` carries dated ones.
- Never remove a JavaScript positioning library without approval. Removing a dependency is a behaviour change for users on older browsers.
- Anchoring is visual only. It never tells assistive technology that two elements are related. Do not remove an `aria-describedby`, `popovertarget` or `commandfor` link because the CSS now places the element. Wording and keyboard behaviour need a person to test; this is not an accessibility conformance statement.
- Data touched: source files only, matched by extension (`.css .scss .sass .less .html .vue .svelte .astro .php .mdx .js .jsx .ts .tsx .mjs`). The scanner prints file, line, code and a fixed message, never other source text. Do not paste whole files, `.env` files, tokens or customer text into the prompt. It skips `node_modules`, build output and minified files.
- Edit only flagged lines. Do not reformat, reorder or rewrite whole files, and do not echo unchanged code back.

## 3. Inputs

Ask for anything missing, in one message. Do not assume.

| # | Input | Ask |
|---|---|---|
| 1 | Paths to scan | "Which folders hold the CSS, components and templates?" |
| 2 | Goal | "Fix existing positioning, write a new tooltip, menu or popover, or replace a JavaScript library?" New work: read `references/patterns.md`. |
| 3 | Browser support floor | "Which browsers and versions must position the element, and which only need it usable?" If the answer includes a browser that may lack the feature, stop and tell the user to check MDN or caniuse. Do not guess a version. |
| 4 | Behaviour without support | "If positioning is unsupported, what should happen: a centred popover, a static position, or a JavaScript fallback?" Default to usable, not pretty. |
| 5 | Stack | "Plain HTML and CSS, a component framework, CSS-in-JS, Tailwind?" The scanner reads style blocks in markup files, not CSS-in-JS strings. Read those by hand. |
| 6 | Edit permission | "Branch name, and do you approve edits in batches of up to 30?" |
| 7 | A way to see it | "Dev server URL, or a browser tool?" If none, mark every visual check untested. |

## 4. Workflow

Run the script from the skill folder. Read its output only. Do not read the script or redo its checks by hand.

### Step 1: summary scan

```
python3 scripts/anchor_check.py <paths> --summary
```

Exit 0: report "no anchor positioning faults found" and stop. Exit 1: continue. Exit 2: fix the arguments.

| Code | Meaning | Handled in |
|---|---|---|
| A01 | Anchor properties on a rule with no `position: absolute` or `fixed` | Step 3 |
| A02 | Static `anchor-name` on a selector that may match many elements | Step 4 |
| A03 | `position-area` or `position-anchor` with no `position-try-fallbacks` | Step 5 |
| A04 | `anchor-name` used but never defined, or defined but never used | Step 4 |
| A05 | `position-area` mixes physical and logical values | Step 6 |
| A06 | `anchor()` outside inset properties, or on the wrong axis | Step 6 |
| A10 | `popover` element with no invoker or ARIA link in markup | Step 7 |
| A12 | `position-try-fallbacks` names a `@position-try` that is not defined | Step 5 |
| I01 | Popper or Floating UI import | Step 8 |

A01, A02 and A10 are review codes. The scanner sees rules and markup separately, so each needs a decision, not an automatic edit.

### Step 2: support strategy

Use the floor from Input 3 and the behaviour from Input 4.

| Floor | Action |
|---|---|
| Includes a browser that lacks the feature, or you could not verify one | Wrap anchored rules in `@supports (position-anchor: --a)` and leave the unguarded default usable. For a popover the unguarded default is the browser's own top-layer placement. |
| Every required browser supports it, confirmed on MDN or caniuse today | The guard is optional. Record the floor and the date in the report. |
| User cannot say | Keep the guard. |

### Step 3: positioning mode (A01)

The positioned element must be `position: absolute` or `position: fixed`. A `[popover]` element is fixed by the browser's default style; confirm that on MDN before dismissing an A01 on a popover. A non-popover needs one of the two. If it is `absolute`, the nearest positioned ancestor is its containing block, and an anchor outside that block still works as long as the anchor is laid out before the positioned element in the document. If the anchor has `display: none`, the element falls back to its nearest positioned ancestor, so a hidden anchor looks like a bug.

### Step 4: names (A02, A04)

If several elements match the same `anchor-name`, a positioned element binds to the last matching element in tree order, unless a matching ancestor exists (the nearest ancestor wins). That is why every tooltip in a list lands on the last item. Choose by case:

| Case | Fix |
|---|---|
| One anchor on the page | Keep the static name. Record "single anchor" in the report. |
| A list, each item with its own popover | Use the Popover API trigger. `popovertarget` or `commandfor` makes an implicit anchor, so no `anchor-name` is needed. With `anchor()` on an implicit anchor, set `position-anchor: auto`. |
| A repeated component, anchor and positioned element in the same component | `anchor-scope: --name` on the component root. It limits the name to that subtree and has no effect on implicit anchors. |
| Many anchors, one shared positioned element | Name each anchor in markup: `style="--a: --item-3"` on the item, `anchor-name: var(--a)` on the anchor, `position-anchor: var(--a)` on the target. |

For A04, define the missing name or correct the spelling. Dashed idents are case-sensitive. An anchor that is `display: none`, or inside skipped contents such as `content-visibility: hidden`, is not an acceptable anchor.

### Step 5: fallbacks (A03, A12)

Fallbacks are tried in the order written. The first option that does not overflow wins and stays chosen until it overflows again. Rules:

| Surface | Declarations |
|---|---|
| Tooltip above its trigger | `position-area: top; position-try-fallbacks: flip-block;` |
| Menu below its trigger | `position-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline;` and `position-try-order: most-height` when the menu is tall |
| Anything else | Name your own: `position-try-fallbacks: --above, --left;` with an `@position-try --above { position-area: top; }` block for each |

For A12, add the missing `@position-try` block or remove the name. Check MDN for which properties an `@position-try` block accepts before writing one beyond `position-area`, insets and margins. Add `position-visibility` handling only if the user's floor supports it; the October 2026 web.dev post lists new values in Chrome 157 beta, so verify.

### Step 6: values (A05, A06)

- `position-area` accepts physical or logical keywords, never a mix. `top left` and `block-start inline-end` are valid; `top inline-end` is not. Pick the family that matches the rest of the stylesheet (see [css-logical-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-logical-properties/)).
- `anchor()` is valid only in inset properties. For widths and heights use `anchor-size()`. Inside `calc()` it returns a length.
- A physical side must match the axis of the property: `left: anchor(right)` is valid; `left: anchor(top)` is not.
- `anchor-size()` in an inset or margin property sets a distance from the containing block, so the element does not follow the anchor when it moves.
- To centre on the anchor, use `justify-self: anchor-center` or `align-self: anchor-center`, not margins.
- For simple placement prefer `position-area`; reach for `anchor()` insets when you need an exact offset.

### Step 7: markup and access (A10)

Anchoring does not create a relationship. For each flagged popover:

| Element | Add |
|---|---|
| Popover opened by a button | `popovertarget="id"` or `commandfor="id"` on the button |
| Tooltip on a focusable control | `aria-describedby="id"` on the control, and the control must be focusable |
| Rich panel with interactive content | Treat as a popover or dialog, not a tooltip (see dialog-and-popover) |

Rule set by this skill, not quoted from a standard: verify it against the accessibility section of the CSS Anchor Positioning module, which this run could not read in full.

### Step 8: replace a library (I01)

Replace Popper or Floating UI only when the code does placement, flip and a fixed offset. Keep it when the code needs an arrow tied to the final side, cursor-following or other non-element anchors, or a shift that keeps the element inside padded bounds. Those are this skill's decision rules; test each case in the floor browsers, and ask the user before removing the dependency. When replacing, change one component per batch.

### Step 9: verify, then report

1. Re-run `python3 scripts/anchor_check.py <paths> --summary`. Every remaining code appears in the report with an action or a reason.
2. In each floor browser, place the trigger near each of the four screen edges and confirm the element flips and stays visible. If no browser tool exists, write "untested".
3. Safari needs its own check. A WebKit bug fixed on 11 July 2025 wrongly clipped fixed-position anchored popovers, and Safari lacked `overlay: auto`, so popover static positions differed. Explicit insets such as `bottom: anchor(top)` rendered consistently across browsers. Test on the Safari versions in the floor.
4. Tab to each trigger and confirm focus order and that the popover is announced through its link (step 7).
5. Use the report template (Templates). One row per file and code.

## 5. Worked example

Input: an invented `menu.css` (5 rules plus a `@position-try`), `page.html` and `pos.js`; `references/example.md` holds the files and the full output. Floor given by the user: current Chrome, Edge and Safari. Scan:

```
$ python3 scripts/anchor_check.py . --summary
./menu.css A01x1 A02x1 A03x1 A04x1 A05x1 A06x2 A12x1
./page.html A10x1
./pos.js I01x1
3 files scanned: 10 findings A01x1 A02x1 A03x1 A04x1 A05x1 A06x2 A10x1 A12x1 I01x1
```

Step 3: `.menu` is a popover, so A01 is dismissed after checking MDN. Step 4: `.trigger { anchor-name: --menu }` is a single trigger, so the static name stays; `--tip` has no definition, so `.tip` drops `position-anchor: --tip` and uses the implicit anchor from the `popovertarget` link added in step 7. Step 5: `.menu` gains `position-try-fallbacks: flip-block, flip-inline`; `.badge` names `--nope`, which has no block, so the name is removed. Step 6: `top left span-inline-end` becomes `top span-right`; `left: anchor(top)` becomes `left: anchor(right)`, and `width: anchor(width)` becomes `width: anchor-size(width)`. Step 7: `#orphan` gains a button with `popovertarget="orphan"`. Step 8: the `@floating-ui/dom` import in `pos.js` only computes a placement and a flip, so it is a candidate; the user is asked before it goes. The re-run prints `no anchor positioning faults found`. The edge, Safari and keyboard checks stay untested unless someone runs them.

## 6. Templates

Starting prompt:

```
Use the css-anchor-positioning skill on <paths>. Browser floor: <floor>. Without support: <behaviour>. Work on branch <name>. Run the scanner first and show me the summary. Do not edit until I approve a batch.
```

Report:

```
Scanned: <n> files. Before: <count by code>. After: <count by code>.
| File | Code | Action | Reason if left |
Floor used: <the user's>, verified <date>.
Edge flips checked: <browsers | untested>
Safari checked: <version | untested>
Keyboard and announcement checked: <checked | untested>
Needs a human: screen reader testing of the trigger and popover pair.
```

Tooltip, the shape most fixes take:

```css
.tip { position: fixed; position-anchor: --tip-anchor; position-area: top;
       position-try-fallbacks: flip-block; margin-block: 8px; }
```

## 7. Failure modes

| Failure | How to spot it | Do instead |
|---|---|---|
| Repeated static anchor name | Every tooltip lands on the last item; A02 | Step 4: implicit anchors, `anchor-scope` or per-item names |
| No fallbacks | Element clipped at an edge on small screens; A03 | Step 5: `flip-block` first, named options after |
| Invalid values | The declaration is dropped with no error; A05, A06 | Step 6: one keyword family, `anchor-size()` for sizes, matching axis |
| Visual-only relationship | Screen reader users never hear the tooltip; A10 | Step 7: invoker or ARIA link in markup |
| Support claims from an old post | A library kept for "no Firefox support", or a guard dropped on a rival's word | Step 2: ask for the floor, verify it today, test Safari |

## 8. Quality bar

Run each check before handing over.

1. Step 9.1 re-run shows no A04, A05, A06 or A12 findings, and every other code has an action or reason in the report.
2. `git diff --stat` shows only files the scanner flagged.
3. Every positioned element is `absolute`, `fixed` or a popover.
4. Every anchored element has a fallback, or a stated reason for none.
5. No repeated static anchor name serves more than one positioned element.
6. Every popover has an invoker or ARIA link in markup.
7. The browser floor in the report is the user's, with the date it was verified.
8. Each visual check in step 9 is stated as checked or untested, and every edit sits on a named branch with an approved batch.

## 9. Cost

All figures are estimates (words x 1.35 for prose, characters / 3.5 for code and tables), not from a model tokeniser.

| Item | Tokens | Read when |
|---|---|---|
| Description | 103 | Every session with the skill installed |
| This body | 4,161 | When the skill fires |
| `references/support.md` | 1,035 | Version, Baseline or source questions only |
| `references/patterns.md` | 339 | Writing a new tooltip, menu or popover only |
| `references/example.md` | 568 | Only to see full files and output |
| Typical run | 4,664 | Description, body, 25 lines of output |
| Worst case | 7,166 | All references and 60 lines of output |

Cheaper path: run `--summary`, approve one batch, edit only flagged lines, re-run, skip every reference. On the worked example the summary prints about 48 tokens, the full per-line output about 304 tokens, and the fixture files are about 208 tokens. On files this small the full output costs more than reading them. The scanner earns its cost as the codebase grows, because summary output is one line per file however long the file is. These are measured by character count on the fixtures; they are not a saving on your code.

## 10. Handoffs, appendix, sources

Handoffs: [dialog-and-popover](https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/) for popover state and focus, [css-logical-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-logical-properties/) for choosing a keyword family, [css-scroll-driven-animations](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-scroll-driven-animations/) for the sibling feature, [keyboard-focus](https://afeezadebayo.com/skills-md/frontend-development/accessibility/keyboard-focus/) for focus order.

Tool appendix (outside the tool-agnostic core): `references/support.md` holds dated support notes and the rival comparison; `references/patterns.md` holds tooltip, menu and popover patterns.

Sources (last verified 7 November 2026):

- W3C CSSWG, CSS Anchor Positioning Module Level 1 (Editor's Draft): https://drafts.csswg.org/css-anchor-position-1/
- MDN, Using CSS anchor positioning: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning/Using
- web.dev, New to the web platform in January, published 30 January 2026: https://web.dev/blog/web-platform-01-2026
- web.dev, New to the web platform in October, published 29 October 2026: https://web.dev/blog/web-platform-10-2026
- WebKit Bugzilla 295322, anchor-positioned popovers in Safari (RESOLVED FIXED): https://bugs.webkit.org/show_bug.cgi?id=295322
- Anthropic, Agent Skills best practices (body under 500 lines, 1,024 character description): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Paul Irish, modern-css skill: https://github.com/paulirish/dotfiles/blob/main/agents/skills/modern-css/SKILL.md
- AThevon, css-native skill: https://github.com/AThevon/genjutsu
- openaec-foundation, frontend-impl-popover-dialog-anchor skill: https://www.skills.sh/openaec-foundation/frontend-design-claude-skill-package/frontend-impl-popover-dialog-anchor

---
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/css-layout/css-anchor-positioning/

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.

2. Guardrails

  • Edits are bulk edits. Work on a branch, write nothing until the user approves a batch, and never edit main. Default to a report. The scanner never writes files.
  • Never choose a browser support floor. Ask for it (Input 3). Support moved during 2026 and rivals disagree with each other. Verify on MDN, web.dev or caniuse before relying on any version number. The body carries none; references/support.md carries dated ones.
  • Never remove a JavaScript positioning library without approval. Removing a dependency is a behaviour change for users on older browsers.
  • Anchoring is visual only. It never tells assistive technology that two elements are related. Do not remove an aria-describedby, popovertarget or commandfor link because the CSS now places the element. Wording and keyboard behaviour need a person to test; this is not an accessibility conformance statement.
  • Data touched: source files only, matched by extension (.css .scss .sass .less .html .vue .svelte .astro .php .mdx .js .jsx .ts .tsx .mjs). The scanner prints file, line, code and a fixed message, never other source text. Do not paste whole files, .env files, tokens or customer text into the prompt. It skips node_modules, build output and minified files.
  • Edit only flagged lines. Do not reformat, reorder or rewrite whole files, and do not echo unchanged code back.

3. Inputs

Ask for anything missing, in one message. Do not assume.

#InputAsk
1Paths to scan"Which folders hold the CSS, components and templates?"
2Goal"Fix existing positioning, write a new tooltip, menu or popover, or replace a JavaScript library?" New work: read references/patterns.md.
3Browser support floor"Which browsers and versions must position the element, and which only need it usable?" If the answer includes a browser that may lack the feature, stop and tell the user to check MDN or caniuse. Do not guess a version.
4Behaviour without support"If positioning is unsupported, what should happen: a centred popover, a static position, or a JavaScript fallback?" Default to usable, not pretty.
5Stack"Plain HTML and CSS, a component framework, CSS-in-JS, Tailwind?" The scanner reads style blocks in markup files, not CSS-in-JS strings. Read those by hand.
6Edit permission"Branch name, and do you approve edits in batches of up to 30?"
7A way to see it"Dev server URL, or a browser tool?" If none, mark every visual check untested.

4. Workflow

Run the script from the skill folder. Read its output only. Do not read the script or redo its checks by hand.

Step 1: summary scan

python3 scripts/anchor_check.py <paths> --summary

Exit 0: report "no anchor positioning faults found" and stop. Exit 1: continue. Exit 2: fix the arguments.

CodeMeaningHandled in
A01Anchor properties on a rule with no position: absolute or fixedStep 3
A02Static anchor-name on a selector that may match many elementsStep 4
A03position-area or position-anchor with no position-try-fallbacksStep 5
A04anchor-name used but never defined, or defined but never usedStep 4
A05position-area mixes physical and logical valuesStep 6
A06anchor() outside inset properties, or on the wrong axisStep 6
A10popover element with no invoker or ARIA link in markupStep 7
A12position-try-fallbacks names a @position-try that is not definedStep 5
I01Popper or Floating UI importStep 8

A01, A02 and A10 are review codes. The scanner sees rules and markup separately, so each needs a decision, not an automatic edit.

Step 2: support strategy

Use the floor from Input 3 and the behaviour from Input 4.

FloorAction
Includes a browser that lacks the feature, or you could not verify oneWrap anchored rules in @supports (position-anchor: --a) and leave the unguarded default usable. For a popover the unguarded default is the browser's own top-layer placement.
Every required browser supports it, confirmed on MDN or caniuse todayThe guard is optional. Record the floor and the date in the report.
User cannot sayKeep the guard.

Step 3: positioning mode (A01)

The positioned element must be position: absolute or position: fixed. A [popover] element is fixed by the browser's default style; confirm that on MDN before dismissing an A01 on a popover. A non-popover needs one of the two. If it is absolute, the nearest positioned ancestor is its containing block, and an anchor outside that block still works as long as the anchor is laid out before the positioned element in the document. If the anchor has display: none, the element falls back to its nearest positioned ancestor, so a hidden anchor looks like a bug.

Step 4: names (A02, A04)

If several elements match the same anchor-name, a positioned element binds to the last matching element in tree order, unless a matching ancestor exists (the nearest ancestor wins). That is why every tooltip in a list lands on the last item. Choose by case:

CaseFix
One anchor on the pageKeep the static name. Record "single anchor" in the report.
A list, each item with its own popoverUse the Popover API trigger. popovertarget or commandfor makes an implicit anchor, so no anchor-name is needed. With anchor() on an implicit anchor, set position-anchor: auto.
A repeated component, anchor and positioned element in the same componentanchor-scope: --name on the component root. It limits the name to that subtree and has no effect on implicit anchors.
Many anchors, one shared positioned elementName each anchor in markup: style="--a: --item-3" on the item, anchor-name: var(--a) on the anchor, position-anchor: var(--a) on the target.

For A04, define the missing name or correct the spelling. Dashed idents are case-sensitive. An anchor that is display: none, or inside skipped contents such as content-visibility: hidden, is not an acceptable anchor.

Step 5: fallbacks (A03, A12)

Fallbacks are tried in the order written. The first option that does not overflow wins and stays chosen until it overflows again. Rules:

SurfaceDeclarations
Tooltip above its triggerposition-area: top; position-try-fallbacks: flip-block;
Menu below its triggerposition-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline; and position-try-order: most-height when the menu is tall
Anything elseName your own: position-try-fallbacks: --above, --left; with an @position-try --above { position-area: top; } block for each

For A12, add the missing @position-try block or remove the name. Check MDN for which properties an @position-try block accepts before writing one beyond position-area, insets and margins. Add position-visibility handling only if the user's floor supports it; the October 2026 web.dev post lists new values in Chrome 157 beta, so verify.

Step 6: values (A05, A06)

  • position-area accepts physical or logical keywords, never a mix. top left and block-start inline-end are valid; top inline-end is not. Pick the family that matches the rest of the stylesheet (see [css-logical-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-logical-properties/)).
  • anchor() is valid only in inset properties. For widths and heights use anchor-size(). Inside calc() it returns a length.
  • A physical side must match the axis of the property: left: anchor(right) is valid; left: anchor(top) is not.
  • anchor-size() in an inset or margin property sets a distance from the containing block, so the element does not follow the anchor when it moves.
  • To centre on the anchor, use justify-self: anchor-center or align-self: anchor-center, not margins.
  • For simple placement prefer position-area; reach for anchor() insets when you need an exact offset.

Step 7: markup and access (A10)

Anchoring does not create a relationship. For each flagged popover:

ElementAdd
Popover opened by a buttonpopovertarget="id" or commandfor="id" on the button
Tooltip on a focusable controlaria-describedby="id" on the control, and the control must be focusable
Rich panel with interactive contentTreat as a popover or dialog, not a tooltip (see dialog-and-popover)

Rule set by this skill, not quoted from a standard: verify it against the accessibility section of the CSS Anchor Positioning module, which this run could not read in full.

Step 8: replace a library (I01)

Replace Popper or Floating UI only when the code does placement, flip and a fixed offset. Keep it when the code needs an arrow tied to the final side, cursor-following or other non-element anchors, or a shift that keeps the element inside padded bounds. Those are this skill's decision rules; test each case in the floor browsers, and ask the user before removing the dependency. When replacing, change one component per batch.

Step 9: verify, then report

  1. Re-run python3 scripts/anchor_check.py <paths> --summary. Every remaining code appears in the report with an action or a reason.
  2. In each floor browser, place the trigger near each of the four screen edges and confirm the element flips and stays visible. If no browser tool exists, write "untested".
  3. Safari needs its own check. A WebKit bug fixed on 11 July 2025 wrongly clipped fixed-position anchored popovers, and Safari lacked overlay: auto, so popover static positions differed. Explicit insets such as bottom: anchor(top) rendered consistently across browsers. Test on the Safari versions in the floor.
  4. Tab to each trigger and confirm focus order and that the popover is announced through its link (step 7).
  5. Use the report template (Templates). One row per file and code.

5. Worked example

Input: an invented menu.css (5 rules plus a @position-try), page.html and pos.js; references/example.md holds the files and the full output. Floor given by the user: current Chrome, Edge and Safari. Scan:

$ python3 scripts/anchor_check.py . --summary
./menu.css A01x1 A02x1 A03x1 A04x1 A05x1 A06x2 A12x1
./page.html A10x1
./pos.js I01x1
3 files scanned: 10 findings A01x1 A02x1 A03x1 A04x1 A05x1 A06x2 A10x1 A12x1 I01x1

Step 3: .menu is a popover, so A01 is dismissed after checking MDN. Step 4: .trigger { anchor-name: --menu } is a single trigger, so the static name stays; --tip has no definition, so .tip drops position-anchor: --tip and uses the implicit anchor from the popovertarget link added in step 7. Step 5: .menu gains position-try-fallbacks: flip-block, flip-inline; .badge names --nope, which has no block, so the name is removed. Step 6: top left span-inline-end becomes top span-right; left: anchor(top) becomes left: anchor(right), and width: anchor(width) becomes width: anchor-size(width). Step 7: #orphan gains a button with popovertarget="orphan". Step 8: the @floating-ui/dom import in pos.js only computes a placement and a flip, so it is a candidate; the user is asked before it goes. The re-run prints no anchor positioning faults found. The edge, Safari and keyboard checks stay untested unless someone runs them.

6. Templates

Starting prompt:

Use the css-anchor-positioning skill on <paths>. Browser floor: <floor>. Without support: <behaviour>. Work on branch <name>. Run the scanner first and show me the summary. Do not edit until I approve a batch.

Report:

Scanned: <n> files. Before: <count by code>. After: <count by code>.
| File | Code | Action | Reason if left |
Floor used: <the user's>, verified <date>.
Edge flips checked: <browsers | untested>
Safari checked: <version | untested>
Keyboard and announcement checked: <checked | untested>
Needs a human: screen reader testing of the trigger and popover pair.

Tooltip, the shape most fixes take:

.tip { position: fixed; position-anchor: --tip-anchor; position-area: top;
       position-try-fallbacks: flip-block; margin-block: 8px; }

7. Failure modes

FailureHow to spot itDo instead
Repeated static anchor nameEvery tooltip lands on the last item; A02Step 4: implicit anchors, anchor-scope or per-item names
No fallbacksElement clipped at an edge on small screens; A03Step 5: flip-block first, named options after
Invalid valuesThe declaration is dropped with no error; A05, A06Step 6: one keyword family, anchor-size() for sizes, matching axis
Visual-only relationshipScreen reader users never hear the tooltip; A10Step 7: invoker or ARIA link in markup
Support claims from an old postA library kept for "no Firefox support", or a guard dropped on a rival's wordStep 2: ask for the floor, verify it today, test Safari

8. Quality bar

Run each check before handing over.

  1. Step 9.1 re-run shows no A04, A05, A06 or A12 findings, and every other code has an action or reason in the report.
  2. git diff --stat shows only files the scanner flagged.
  3. Every positioned element is absolute, fixed or a popover.
  4. Every anchored element has a fallback, or a stated reason for none.
  5. No repeated static anchor name serves more than one positioned element.
  6. Every popover has an invoker or ARIA link in markup.
  7. The browser floor in the report is the user's, with the date it was verified.
  8. Each visual check in step 9 is stated as checked or untested, and every edit sits on a named branch with an approved batch.

9. Cost

All figures are estimates (words x 1.35 for prose, characters / 3.5 for code and tables), not from a model tokeniser.

ItemTokensRead when
Description103Every session with the skill installed
This body4,161When the skill fires
references/support.md1,035Version, Baseline or source questions only
references/patterns.md339Writing a new tooltip, menu or popover only
references/example.md568Only to see full files and output
Typical run4,664Description, body, 25 lines of output
Worst case7,166All references and 60 lines of output

Cheaper path: run --summary, approve one batch, edit only flagged lines, re-run, skip every reference. On the worked example the summary prints about 48 tokens, the full per-line output about 304 tokens, and the fixture files are about 208 tokens. On files this small the full output costs more than reading them. The scanner earns its cost as the codebase grows, because summary output is one line per file however long the file is. These are measured by character count on the fixtures; they are not a saving on your code.

10. Handoffs, appendix, sources

Handoffs: [dialog-and-popover](https://afeezadebayo.com/skills-md/frontend-development/html-semantics/dialog-and-popover/) for popover state and focus, [css-logical-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-logical-properties/) for choosing a keyword family, [css-scroll-driven-animations](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-scroll-driven-animations/) for the sibling feature, [keyboard-focus](https://afeezadebayo.com/skills-md/frontend-development/accessibility/keyboard-focus/) for focus order.

Tool appendix (outside the tool-agnostic core): references/support.md holds dated support notes and the rival comparison; references/patterns.md holds tooltip, menu and popover patterns.

Sources (last verified 7 November 2026):

What this skill does

Anchor positioning places one element relative to another with anchor-name, position-anchor, position-area and anchor(). It fails quietly. A declaration with an invalid value is dropped. A positioned element that is not absolute or fixed ignores the anchor. When several elements share an anchor name, every tooltip lands on the last one in the document. A menu with no fallback is clipped at the screen edge. Anchoring is also visual only, so it never links a tooltip to its trigger for a screen reader.

The skill runs scripts/anchor_check.py, which reports file, line and code for nine patterns and never edits a file. The agent fixes flagged lines on a branch, in batches you approve, using a decision rule for each code: implicit anchors, anchor-scope or per-item names for repeated anchors, fallback lists, one keyword family for position-area, anchor-size() for sizes, and an invoker or ARIA link for every popover. It asks for your browser floor and tells the agent to test Safari separately.

Who it is for

Use it if a tooltip, dropdown or popover sits in the wrong place, binds to the wrong item, clips at an edge or ignores its anchor, if you want to replace Popper or Floating UI where the layout is simple, or if you are reviewing AI-generated positioning code. It does not cover popover open, close and focus behaviour, View Transitions or sticky positioning, and it is not an accessibility sign-off. A person must test with a screen reader.

What is inside

The file is 2,976 words (version 1.0.0) with one script and three reference files. It holds:

  • Six guardrails, including edits on a branch only, no library removed without approval and no support floor chosen by the agent.
  • Seven inputs the agent must have first, including the browser floor and what should happen without support.
  • Nine workflow steps: scan, support strategy, positioning mode, names, fallbacks, values, markup and access, library replacement, then verify and report.
  • One scanner, scripts/anchor_check.py, standard library only, with nine codes. It prints file, line and a fixed message.
  • A worked example with real scanner output, a starting prompt and report template, five failure modes and an eight-item quality bar.
ItemTokensStatusRead when
Description103EstimateEvery session with the skill installed
SKILL.md body4,161EstimateWhen the skill fires
references/support.md1,035EstimateOnly for version, Safari or source questions
references/patterns.md339EstimateOnly when writing a new tooltip, menu or popover
references/example.md568EstimateOnly to see the full example files
Typical run4,664EstimateDescription, body and 25 lines of scanner output
Worst case7,166EstimateAll references and 60 lines of output
Cheaper path4,664EstimateRun the summary scan, edit flagged lines, re-run, skip the references

The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The three rivals I read are about 4,500 tokens, 2,400 tokens and unmeasured, and none ships a scanner. On the worked example the summary scan prints about 48 tokens against about 208 for the fixture files.

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

Claude. Keep the folder as css-anchor-positioning/ 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. The scanner needs Python 3 and nothing else. Then send the starting prompt from the Templates section.

Before you install. Read the scanner first. It reads files and never writes. The skill tells the agent to work on a branch, to edit in batches you approve, never to remove a positioning library without asking and never to choose your browser support floor. Install skills only from sources you trust.

A worked example

An invented menu.css has a menu with a mixed position-area and no fallbacks, a badge that uses anchor(top) on left and anchor(width) for a width, a fallback name with no @position-try block and an anchor name nothing defines. A popover in page.html has no invoker, and pos.js imports Floating UI. The scanner prints 10 findings.

The agent changes the area to top span-right, adds flip-block, flip-inline, uses anchor(right) and anchor-size(width), drops the undefined name, adds a popovertarget button for the orphan popover, and asks before removing the Floating UI import. The re-run prints no findings. Edge flips, Safari and keyboard checks are listed as untested until someone runs them.

Common mistakes

The skill is built around five ways this goes wrong:

  • A repeated static anchor name. Every tooltip binds to the last item. The skill uses implicit anchors, anchor-scope or per-item names.
  • No fallbacks. The element clips at the edge on small screens. The skill starts with flip-block.
  • Invalid values. Mixed keyword families and anchor() on a width are dropped without an error. The scanner flags them.
  • A visual-only relationship. Screen reader users never hear the tooltip. The skill adds an invoker or ARIA link.
  • Copying support claims from an old post. The skill asks for your floor, tells you to verify it and to test Safari.

This skill sits in Skills-md, Frontend Development and CSS and Layout. CSS Scroll-Driven Animations covers the sibling feature. CSS Logical Properties covers choosing a keyword family. CSS Animations and Transitions covers animating a popover in and out. Dialog and Popover covers open, close and focus behaviour.

Frequently asked questions

The common causes are a positioned element that is not absolute or fixed, an anchor name that does not match, an anchor with display none, an invalid position-area value, and a browser without support. The scanner reports A01, A04 and A05 by file and line. Check MDN for your browser floor.

When several elements share an anchor-name, the spec binds the positioned element to the last match in tree order, unless a matching ancestor exists. Use the Popover API trigger as an implicit anchor, add anchor-scope to a repeated component, or give each item its own name. The scanner marks this A02.

Replace it only when the code does placement, flip and a fixed offset. Keep it for arrows tied to the final side, cursor-following and other non-element anchors. These are the skill’s own decision rules, so test them in your browsers. It asks before removing the dependency and changes one component per batch.

position-area places the element in a three-by-three grid around the anchor, for example top or bottom span-right. anchor() gives exact inset values and works only in inset properties. For widths and heights use anchor-size(). Use position-area for simple placement and anchor() for an exact offset.

web.dev listed it as Baseline Newly available on 30 January 2026, citing Firefox 147, but the same article’s table shows different numbers, so check MDN. Safari had popover bugs that WebKit fixed in July 2025. Test the Safari versions in your floor. The skill asks you for the floor.

Add position-try-fallbacks, starting with flip-block, then flip-inline or your own @position-try options. The browser uses the first option that does not overflow and keeps it until it overflows again. The scanner marks an anchored element with no fallback as A03.

The description is about 103 tokens and loads in every session. The SKILL.md body is about 4,161 tokens when the skill fires. A typical run is about 4,664 tokens with scanner output, and the worst case with all three references is about 7,166. All figures are estimates from words and characters, not from a model tokeniser.

Changelog

  • 1.0.0, 7 November 2026: first release.

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