---
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.
