# Anchored Auto Layout > A CSS library that writes CSS Anchor Positioning as HTML attributes, with iOS Auto Layout-style names: `data-⚓️-to`, `data-⚓️-below`, `data-⚓️-sync-width`. One CSS file. No JavaScript, ResizeObservers, portals or z-index juggling. Install with `npm i anchored-auto-layout`, then `@import "anchored-auto-layout";` (or `@import "anchored-auto-layout/min";` for the minified file). Needs anchor positioning and typed `attr()`: Chrome and Edge 133+. Safari has typed `attr()` only in Technology Preview, and Firefox is untested. When in doubt about behaviour, read the shipped files for the installed version. ## Library - [anchored-auto-layout.css](anchored-auto-layout.css): The library. One file, no build step or preprocessor; link it and use the attributes. ## Docs - [SPEC.md](SPEC.md): The API reference. Every `data-⚓️-*` attribute and its values, the syntax rules (named `--x` values vs. boolean attributes that use `data-⚓️-to`), built-in behaviour (top-layer reset, sub-menu contexts, anchored container queries, AIM entry morphing) and the known browser limitations with workarounds. Read it before answering anything about attribute syntax. ## Notes for LLMs assisting a developer with this library - Every attribute is namespaced `data-⚓️-*`, with the literal ⚓️ emoji in the name. That's on purpose (it keeps layout attributes visually apart from styling classes like Tailwind). It is not a typo or a placeholder to remove. - A value starting with `--` (e.g. `data-⚓️-below="--header"`) targets the anchor named by `data-⚓️-name="--header"` elsewhere. The boolean form (`data-⚓️-below` with no value) targets whatever anchor `data-⚓️-to="--x"` names on the same element. - Don't suggest JavaScript (ResizeObserver, portals, Popper-style libraries) for anything the library already covers. - Before recommending an attribute combination, check the "Known Browser Limitations" section of SPEC.md. Some combinations have confirmed browser gaps that aren't library bugs, e.g. an automatic `flip-block`/`flip-inline` fallback on `[popover]`/`` targets in specific configurations. ## On this site - [/llms-full.txt](/llms-full.txt): llms.txt, SPEC.md, and the attribute reference generated from the CSS (every attribute, every value, the exact declarations it applies, and the custom properties) as one plain-text file. Fetch this one if you can only fetch one. - [/SPEC.md](/SPEC.md): SPEC.md as raw markdown (rendered at [/docs/spec/](/docs/spec/)). - [/anchored-auto-layout.css](/anchored-auto-layout.css): The library, the same file the npm package ships. - [/anchored-auto-layout.html-data.json](/anchored-auto-layout.html-data.json): Every `data-⚓️-*` attribute with a description and its value suggestions, in VS Code's HTML custom data format. ## Docs pages - [Getting started](/docs/) - [Concepts](/docs/concepts/) - [Attributes](/docs/reference/) - [Browser support](/docs/browser-support/) - [Full spec](/docs/spec/) - [Playground](/playground/) ## Recipes Each link is a complete standalone HTML page: the demo's markup, CSS and JS with the library linked from `/anchored-auto-layout.css`. Copy one whole as a working starting point. The annotated page for each is at `/recipes//`. - [DevTools dock side with data-⚓️-side](/recipes-live/01-devtools-dock/): A DevTools panel docked before/below/after the page it inspects. - [Drag to dock: chat-head snapping with data-⚓️-side](/recipes-live/02-drag-to-dock/): Drag a picture-in-picture camera bubble and release: it snaps to the nearest side of the call and swings there. - [RTL mirror: logical anchor sides](/recipes-live/03-rtl-mirror/): A chat UI whose tooltips, timestamps, reaction badge, presence dot, hint and menu are all placed with logical data-⚓️-* sides (before/after/align-trailing). - [Chat sides](/recipes-live/04-chat-sides/): Chat bubbles placed with data-⚓️-side="before|after" around dots on a centre spine. - [Compass menu](/recipes-live/05-radial-menu/): A radial/compass action menu: four items placed above/after/below/before a hub with data-⚓️-side, opening out of the hub with data-⚓️-morph-from. - [The tooltip that argues](/recipes-live/06-argumentative-tooltip/): Click to cycle the requested side, or drag the button into a corner: data-⚓️-try="flip-all" lets the browser overrule the request, and @container anchored(fallback) queries report asked vs. got. - [Coach-mark tour that walks around each target](/recipes-live/07-coach-mark-tour/): A product tour whose one coach mark hops between targets and sides (below, after, above, before) with data-⚓️-to + data-⚓️-side + data-⚓️-tween, and a data-⚓️-fill spotlight that follows along. - [Corner toasts that pick a side](/recipes-live/08-corner-toasts/): Toasts that stack below top-corner anchors and above bottom-corner anchors. - [Filter, sort & drag grid that glides](/recipes-live/09-filter-sort-grid/): Filter chips hide cells, sort buttons and drag/Alt+arrow reorder only change CSS order; every swatch is anchored to its grid cell with data-⚓️-fill + data-⚓️-transition="glide" + data-⚓️-scope-name, so it glides to it… - [Masonry that glides](/recipes-live/10-masonry-reflow/): A CSS multi-column masonry board whose cards glide to their new column when the column width changes or the board is resized. - [Card ↔ list view toggle](/recipes-live/11-card-list-toggle/): Switch a music library between cards and a list: only the invisible grid changes, and three anchored layers per item (background, cover, text) morph to their new slots with data-⚓️-fill + data-⚓️-transition="morph". - [Resizable heat-map that re-flows](/recipes-live/12-resizable-heatmap/): A GitHub-style contribution calendar: drag the handle and the weeks wrap into bands while 371 day squares glide to their new spots (data-⚓️-fill + data-⚓️-transition="glide", one scoped --day name per cell). - [iOS app shell: nav bar, tab bar, push, sheet and split view](/recipes-live/13-ios-app-shell/): A UITabBarController + UINavigationController shell built from constraints. - [iOS profile with proportional constraints](/recipes-live/14-ios-profile-proportional/): A UIKit-style profile screen built from Auto Layout constraints written as data-⚓️-* attributes, with proportional widths from sync-width and --⚓️-mult-x. - [iOS sign-up form + keyboardLayoutGuide](/recipes-live/15-ios-form-keyboard/): A UIKit-style sign-up form built as a constraint chain, with the primary button anchored data-⚓️-side="above" a simulated keyboardLayoutGuide so it rides the keyboard up. - [iOS adaptive dashboard: size classes, readable guide & priorities](/recipes-live/16-ios-adaptive-dashboard/): A Health-style summary built like a UIKit screen, with widget cards that go from one column to two by swapping one data-⚓️-side value and their width multipliers. - [Magnetic table columns: values glide to new anchors](/recipes-live/17-split-table/): Recreates the 'tables that split, stretch and snap into place' interaction (Operate, Alvish Baldha). - [Barrel roll number counter: digits glide between anchored slots](/recipes-live/18-barrel-counter/): A live count up/count down number where every digit is a ten-numeral barrel. - [Archive an email: the row's height glides shut and the rest slide up](/recipes-live/19-archive-list/): An email list where each message is an invisible slot with a visible card data-⚓️-fill on it and data-⚓️-transition=glide. - [Animated grid reflow (after Bramus)](/recipes-live/bramus-grid-reflow/): Bramus's animated CSS grid technique rebuilt with the data-⚓️-* attribute library: anchor-scope + data-⚓️-fill, zero JS. --- # SPEC.md A zero-JS, HTML-first constraint layout engine mapping CSS Anchor Positioning to an iOS Auto Layout-style DOM API. `tests/` has a live, runnable version of everything below — one demo+test HTML file per feature, viewable directly in a browser or checked headlessly via `npm test`. Treat it as the executable source of truth for what actually works, not just this document. **Editor autocomplete:** `anchored-auto-layout.html-data.json` describes every `data-⚓️-*` attribute (with value suggestions where applicable) in VS Code's [HTML custom data](https://code.visualstudio.com/api/extension-guides/custom-data-tutorial) format. Point your project's `.vscode/settings.json` at it: ```json { "html.customData": ["./node_modules/anchored-auto-layout/anchored-auto-layout.html-data.json"] } ``` (This repo's own `.vscode/settings.json` already wires it up locally.) ## Core Principles - **Relational & Extrinsic:** Position relative to any element, breaking strict DOM hierarchy. - **Zero-JS Execution:** Math, collisions, and visibility handled entirely by the browser's C++ engine. - **HTML-First & Distinct:** Layout logic lives in markup. The `data-⚓️-*` namespace visually separates constraints from styling classes. - **i18n-Native:** Driven by CSS logical properties for automatic RTL layout mirroring. ## Syntax & Routing - **Explicit (Multi-Anchor):** String values target specific anchors (e.g., `data-⚓️-below="--header"`). - **Implicit (Boolean):** Setting `data-⚓️-to="--btn"` allows modifiers to act as booleans (e.g., `data-⚓️-below`, `data-⚓️-sync-width`). ## API Reference | Category | Attributes | Details | |---|---|---| | Anchor | `data-⚓️-name="--name"` | Makes the element an anchor: sets `anchor-name` from the value via typed `attr()`, so the value must be a dashed-ident. | | Target | `data-⚓️-to="--name"` | Binds the element to a named anchor: sets `position-anchor`, `position: absolute`, `container-type: anchored` (see Built-In Intelligence Layers) and `box-sizing: border-box` (so `sync-*` sizes match the anchor's border box), plus `interpolate-size: allow-keywords`. An element with both `data-⚓️-to` and `data-⚓️-name` also gets `transform-style: preserve-3d` (Sub-Menu Contexts). | | Root | `data-⚓️-root` | `position: relative` (plus `interpolate-size: allow-keywords`): the containing block for parent-pinned (Parent Pins) children. | | Parent Pins | `data-⚓️-pin-[top\|bottom\|leading\|trailing\|x\|y\|all]` | Pins target to its relative containing block. | | Outside Edges | `data-⚓️-[above\|below\|before\|after]` | Positions target entirely outside the anchor's edge. | | Side | `data-⚓️-side="[above\|below\|before\|after]"` | The same four outside edges as one attribute whose value is the side. Behaves identically to the booleans (logical, RTL-mirroring, native `data-⚓️-try` flips, tweens with `data-⚓️-tween`) but a state change is one value swap instead of remove-one/add-another, and two sides can't be set at once. Implicit anchor only (`data-⚓️-to`). | | Inside Align | `data-⚓️-align-[top\|bottom\|leading\|trailing]` | Flushes target to the inside edge of the anchor. | | Stretch Bounds | `data-⚓️-stretch-[x\|y]` | Spans the target across the anchor's full axis. | | Fill | `data-⚓️-fill` | Fills the anchor's full box on both axes at once: `inset: anchor(inside)` plus `width`/`height` from `anchor-size()` — the animatable alternative to `stretch-x` + `stretch-y` combined (see Known Browser Limitations). Author `width`/`height` override the built-in size. | | Tweenable edges | `data-⚓️-tween` | Opt-in: puts `above`/`before`/`align-bottom`/`align-trailing` on the START inset plus a percentage `translate` so a below↔above (or after↔before, top↔bottom) swap tweens instead of snapping. `stretch-x`/`stretch-y` keep the start inset and size from `anchor-size()` instead of the end inset, and `align-x`/`align-y="center"`/`center-all` use `anchor(center)` minus half their own size instead of `anchor-center`, so swaps between edge, stretch and center states tween too. `sync-width`/`sync-height` and author sizes still win over the stretch size. Horizontal writing modes only (RTL handled); don't combine with `data-⚓️-try`. Use with `morph` or `glide`. | | Sizing & Math | `data-⚓️-sync-[width\|height]` | Sizes via: `(Anchor * Multiplier) + Offset`. Controlled via CSS variables: `--⚓️-mult-[x\|y]` (default: `1`) and `--⚓️-offset-[x\|y]` (default: `0px`). | | Position Area | `data-⚓️-area="[top\|bottom\|leading\|trailing]"` | Coarse placement on one side of the anchor via `position-area`: `block-start`, `block-end`, `inline-start`, `inline-end` respectively (logical, so it mirrors in RTL). Takes the element out of flow (`position: absolute`). | | Anchor Center | `data-⚓️-align-x="center"`, `data-⚓️-align-y="center"`, `data-⚓️-center-all` | Centers the target on the anchor via `anchor-center`: `align-x` sets `justify-self` (inline axis), `align-y` sets `align-self` (block axis), `center-all` sets `place-self` (both). `center` is the only value. Each takes the element out of flow (`position: absolute`). With `data-⚓️-tween` they switch to `anchor(center)` minus half their own size (see Tweenable edges). | | Margin Align | `data-⚓️-align-self="[center\|start\|end]"` | Aligns a target inside the space its insets define, via auto margins: `center` sets `margin: auto` (both axes), `start` sets `margin-inline-end: auto`, `end` sets `margin-inline-start: auto` (inline axis only). Meant to pair with `data-⚓️-stretch-[x\|y]` or Parent Pins. Doesn't take the element out of flow on its own. | | Collisions | `data-⚓️-try="flip-[x\|y\|all]"`, `data-⚓️-try-order="most-[x\|y]"` | Flips on axis if colliding. Try-order actively hunts for the quadrant with the most available space. | | Visibility State | `data-⚓️-visibility="[always\|valid\|visible\|no-overflow\|valid-no-overflow\|visible-no-overflow\|all]"` | Maps to the CSS `position-visibility` property (spec: `always \|\| anchor-valid \|\| anchor-visible \|\| no-overflow`, default `anchor-visible`; the plural `anchors-*` spellings are the deprecated aliases, and the library declares both so older engines keep working). `valid`/`visible`/`no-overflow` are independently combinable; `all` applies all three (strictest). `always` opts out entirely. | | Scoping | `data-⚓️-scope="[all\|--name]"`, `data-⚓️-scope-name="--name"` | Fences anchor names to a subtree for component reuse via `anchor-scope`. `scope="all"` scopes every anchor name; `scope="--name"` and `scope-name="--name"` are equivalent and scope just that name. | | Morphing (AIM) | `data-⚓️-transition="morph\|glide"`, `data-⚓️-morph-from[="--anchor"]` | Animates an element from a collapsed state inside an anchor's bounding box to its final layout. `morph-from` sets the `@starting-style` origin (explicit anchor, or implicit/boolean to default to the element's own target); `transition="morph"` applies the spatial transition (`inset`, `width`, `height`, `position-area`, `translate`, `opacity`). Requires `interpolate-size: allow-keywords` (auto-applied to every `data-⚓️-root`/`data-⚓️-to`). `--⚓️-tx`/`--⚓️-ty` (the translate-workaround custom properties `above`/`before`/`align-bottom`/`align-trailing` use — see Known Browser Limitations) are registered via `@property` as ``, so switching between any of those and a plain inset-based attribute (e.g. below → above) tweens smoothly instead of snapping — verified live by sampling the computed `translate` value every frame across a real transition. | ## Built-In Intelligence Layers - **Top-Layer Reset:** Applying a target to `` or `[popover]` natively strips the browser's aggressive User-Agent centering margins. - **Sub-Menu Contexts:** Elements acting as both target and anchor automatically establish a pristine 3D containing block (`preserve-3d`) for nested dropdowns. - **Anchored Container Queries:** Targets automatically inject `container-type: anchored`, allowing internal elements to restyle themselves based on fallback collision flips. **Side effect worth knowing:** like any non-`normal` `container-type`, this makes the target a containing block for `position: fixed` descendants too (confirmed live), not just a query container — a plain `position: fixed` element nested anywhere inside a target no longer pins to the true viewport, it pins to the target's own box instead. Site-wide consequence here: `
` is itself a target (`data-⚓️-to="--header"`), so any page content wanting real viewport-fixed UI (a floating action button, a toast) needs to render outside `
` — e.g. portaled to `document.body` — rather than relying on plain `position: fixed` from within page content. Found building the playground's mobile "Controls" FAB, which rendered ~200px below the visible viewport before the fix. - **Anchor Interpolated Morphing (AIM):** Elements can animate their entry from perfectly collapsed inside an anchor's geometry (`data-⚓️-morph-from`) to their resolved layout, using native `@starting-style` — no JS FLIP technique required. ## Trade-offs & Constraints - **Extrinsic Handling:** Elements are `position: absolute`. Internal text overflow must be handled manually (`overflow: auto`). - **Unidirectional:** Circular dependencies (A ↔ B) are ignored by the browser. - **Prefix Required:** HTML string values must explicitly include double dashes (e.g., `"--header"`). - **Same-Axis Sizing Conflicts:** `data-⚓️-stretch-[x|y]` (implicit sizing via opposing insets) and `data-⚓️-sync-[width|height]` (explicit `width`/`height`) both resolve the same axis differently. Combine them on different axes freely (e.g. stretch-x + sync-height), but on the *same* axis the explicit `width`/`height` from `sync-*` wins over the stretch insets, per standard over-constrained absolute-positioning sizing rules — the stretch is silently dropped on that axis, not merged. ## Browser Support The library needs CSS Anchor Positioning **and typed `attr()`** (`attr(data-⚓️-x type())`), which is how every `--name` gets from markup into CSS. Without typed `attr()` the attributes are inert, so feature-detect that, not anchoring: `@supports (anchor-name: attr(data-x type()))`. | Engine | Status | |---|---| | Chrome / Edge 133+ | Supported. The library, its test suite (`npm test`, Playwright Chromium) and the docsite are built and verified here. | | Safari | Not supported in a stable release: anchor positioning shipped in 26, but typed `attr()` is Technology Preview only. | | Firefox | Untested by the library's own suite. MDN lists anchor positioning from 147 and typed `attr()` from 155, but the parts marked "no" below don't apply there. | Per feature (Chromium versions from the docsite's browser-support page; Firefox/Safari columns from MDN browser-compat-data, checked 2026-10-30, not verified by this library's tests): | Feature | Used by | Chrome/Edge | Firefox | Safari | |---|---|---|---|---| | Typed `attr()` (**required**) | every attribute that takes a `--name` | 133 | 155 | Preview only | | Anchor positioning (**required**) | everything: `anchor()`, `position-anchor`, `anchor-size()` | 125 | 147 | 26 | | `position-area` | `data-⚓️-area` | 129 | 147 | 26 | | `anchor-center` | `data-⚓️-align-x`, `-align-y`, `-center-all` | 125 | 147 | 26 | | `position-try-fallbacks` | `data-⚓️-try` | 128 | 147 | 26 | | `position-try-order` | `data-⚓️-try-order` | 125 | 148 | 26 | | `anchor-scope` | `data-⚓️-scope`, `-scope-name` | 131 | 147 | 26 | | `position-visibility` (`anchors-visible`, `no-overflow`) | `data-⚓️-visibility` | 125 | 147 | 26.2 | | `position-visibility: anchor-valid` | `data-⚓️-visibility` values that include `valid` | 157 | No (plural `anchors-valid` 147) | 27 | | `container-type: anchored` | every target (fallback-aware container queries) | 143 | No | No | | `@starting-style` | `data-⚓️-morph-from` | 117 | 129 | 17.5 | | `interpolate-size` | morphs to and from `auto` sizes | 129 | No | No | ## Known Browser Limitations (verified live, not authoring bugs) - **Resolved — the "end-only inset" problem was the UA popover stylesheet, not a browser bug.** `[popover]`/`` get `inset: 0` and `fit-content` sizing from the UA sheet, which over-constrains any box that sets only one inset (an `inset-block-end: anchor(...)` alone rendered at the wrong place). The top-layer reset (section 1) clears `inset`/`width`/`height`/margins inside `:where()`, so it has zero specificity and never beats an anchor()-driven longhand or an author class. `above`, `before`, `align-bottom` and `align-trailing` are therefore plain logical `inset-block-end`/`inset-inline-end` on every target type (an earlier `translate` workaround, `--⚓️-tx`/`--⚓️-ty`, was removed). Consequences: RTL and vertical writing modes work, native `flip-block`/`flip-inline` work on popovers in both directions (custom `position-area` flip blocks removed), and `data-⚓️-stretch-[x|y]` sizes popovers correctly. Trade-off: switching between a start attribute and an end one (`below` <-> `above`) changes which inset is used and inset cannot interpolate from `auto`, so that swap snaps instead of tweening; same-attribute changes and size still morph. - **`anchor-valid` (`data-⚓️-visibility="valid"`) needs Chrome 157+.** The CSSWG renamed `anchors-visible`/`anchors-valid` to singular `anchor-visible`/`anchor-valid` (blink-dev intents, Chrome 157, plurals kept as deprecated aliases). The old plural `anchors-valid` was never shippable (reverted over spec ambiguity), so in Chromium 153 `CSS.supports("position-visibility", "anchors-valid")` and the singular forms all return `false`, while plural `anchors-visible`, `no-overflow` and `always` work. The library declares plural then singular for every value (an engine drops the declaration it doesn't parse), so `visible` works everywhere and gains the singular spelling for free; any value including `valid` (`valid`, `valid-no-overflow`, `all`) silently falls back to the initial value until the engine supports `anchor-valid`. `tests/10-visibility-state.html` probes support with `CSS.supports` and only treats `valid` values as a known gap when neither spelling parses. - **`data-⚓️-try` measures overflow against the target's containing block *and* the scrollable area.** Confirmed while testing: an extra `position: absolute` wrapper between a popover's anchor and the viewport, or a tall (scrollable) document, changes what counts as overflowing, so a box hanging off the bottom of the viewport can simply not flip. Anchors placed directly on a non-scrolling viewport (`position: fixed`) behave as expected (tests 09 and 16). - **`position-try-fallbacks` does not re-evaluate on scroll — confirmed live, current Chromium (153.0.8010.12).** An anchor-positioned element (plain or `[popover]`, root-document scroll or an `overflow: auto` scroll container, `data-⚓️-try` on any axis) picks its fallback exactly once — apparently checking overflow against the anchor's position as of `scrollY: 0`/initial layout, never the actual current scroll-adjusted viewport position. Verified 5 independent ways: scrolling an already-open popover into an overflowing position (no re-flip, holds for 4.5s+ across 15 polled frames); resizing the viewport instead of scrolling (same); closing and reopening a popover while already scrolled to an overflowing position (still no flip — rules out a stale/cached fallback choice, since this is a fresh `showPopover()` call); and inserting a brand-new DOM node (never laid out before) at an already-scrolled, already-overflowing position (still no flip — rules out any per-element caching entirely). A page scrolled via `window.scrollTo()` *before* the browser's first paint pass still doesn't get correct fallback selection either, which is what pins this down precisely: the algorithm isn't stale-caching a past result, it's not factoring scroll position into its overflow check at all, ever. This directly contradicts MDN's own description of the feature ("re-evaluates continuously as the user scrolls") — worth re-testing against a future Chromium release rather than assuming MDN is simply wrong. **Practical impact:** any UI built on the assumption that scrolling an anchor toward a viewport edge live-triggers its `data-⚓️-try` fallback (e.g. a tooltip that's expected to flip sides as its anchor scrolls near the top/bottom of the screen) will not work today — the fallback is only correctly chosen at the moment the element is first laid out (its very first style/layout pass, `scrollY` baked in at that instant), not on later scroll events. No workaround identified; this sits below the CSS layer entirely. - **Animating a fill across a reflow: `data-⚓️-fill` handles it, `stretch-x` + `stretch-y` don't.** Confirmed live building the animated-grid-reflow demo (Bramus's technique of anchoring absolutely-positioned content to a grid cell so it can animate independently of the grid's own instant reflow): a box sized purely by 4 inset longhands (`stretch-x` + `stretch-y`), or by the `inset: anchor(inside)` shorthand with no explicit size, visibly collapses to ~0px wide partway through a `transition: inset` when its anchor moves, even though old and new size are identical. `data-⚓️-fill` therefore sets both the single `inset: anchor(inside)` shorthand and an explicit `width`/`height` from `anchor-size()`, which makes the size authoritative so only the position animates (verified sampling every frame: no collapse, in real Chrome). Chrome still reports four separate `inset-*` transitions for the shorthand, so why the shorthand plus explicit size works isn't established. Author `width`/`height` override the built-in ones. --- # Attribute reference (generated from anchored-auto-layout.css) ## 1. The Core Relationship & Resets ### data-⚓️-name Defines this element as an anchor. Value must be a dashed-ident (e.g. "--header") — sets `anchor-name`. - data-⚓️-name [data-⚓️-name] { anchor-name: attr(data-⚓️-name type()) } ### data-⚓️-to Defines this element as a target and binds it to a named anchor. Value must start with "--" — sets `position-anchor` and takes the element out of document flow. - data-⚓️-to [data-⚓️-to] { position-anchor: attr(data-⚓️-to type()); container-type: anchored; box-sizing: border-box } :where(:is(dialog, [popover])[data-⚓️-to]) { margin: 0; inset: auto; width: auto; height: auto; max-width: none; max-height: none } ### data-⚓️-root Marks this element as a positioned container (`position: relative`) — pair with data-⚓️-pin-* children. - data-⚓️-root [data-⚓️-root] { position: relative } ## 2. Anchor Scoping (Component Isolation) ### data-⚓️-scope Anchor scoping shorthand: `all`, or a dashed-ident (`--name`) to scope just that anchor name (same as data-⚓️-scope-name). - data-⚓️-scope="--name" [data-⚓️-scope^="--"] { anchor-scope: attr(data-⚓️-scope type()) } - data-⚓️-scope="all" — Scopes all anchor names within this subtree — sets `anchor-scope: all`. [data-⚓️-scope="all"] { anchor-scope: all } ### data-⚓️-scope-name Fences anchor names to this subtree for component reuse (e.g. a repeated card component). Value should be a dashed-ident matching the anchor-name used inside — sets `anchor-scope`. - data-⚓️-scope-name [data-⚓️-scope-name] { anchor-scope: attr(data-⚓️-scope-name type()) } ## 3. Parent Constraints (Pin to Bounding Box) ### data-⚓️-pin-all Pins the target to all four edges of its containing block. - data-⚓️-pin-all [data-⚓️-pin-all] { inset: 0 } ### data-⚓️-pin-x Pins the target across the full inline axis of its containing block. - data-⚓️-pin-x [data-⚓️-pin-x] { inset-inline: 0 } ### data-⚓️-pin-y Pins the target across the full block axis of its containing block. - data-⚓️-pin-y [data-⚓️-pin-y] { inset-block: 0 } ### data-⚓️-pin-top Pins the target to the top edge of its containing block. - data-⚓️-pin-top [data-⚓️-pin-top] { inset-block-start: 0 } ### data-⚓️-pin-bottom Pins the target to the bottom edge of its containing block. - data-⚓️-pin-bottom [data-⚓️-pin-bottom] { inset-block-end: 0 } ### data-⚓️-pin-leading Pins the target to the leading (left, in LTR) edge of its containing block. - data-⚓️-pin-leading [data-⚓️-pin-leading] { inset-inline-start: 0 } ### data-⚓️-pin-trailing Pins the target to the trailing (right, in LTR) edge of its containing block. - data-⚓️-pin-trailing [data-⚓️-pin-trailing] { inset-inline-end: 0 } ## 4. Anchor Edge Bindings ### data-⚓️-above Positions the target entirely above (outside) the anchor's edge. Empty value targets the anchor set via data-⚓️-to; a "--name" value targets a specific named anchor. - data-⚓️-above [data-⚓️-above=""] { inset-block-end: anchor(self-start) } - data-⚓️-above="--name" [data-⚓️-above^="--"] { inset-block-end: anchor(attr(data-⚓️-above type()) self-start) } ### data-⚓️-below Positions the target entirely below (outside) the anchor's edge. Empty value targets the anchor set via data-⚓️-to; a "--name" value targets a specific named anchor. - data-⚓️-below [data-⚓️-below=""] { inset-block-start: anchor(self-end) } - data-⚓️-below="--name" [data-⚓️-below^="--"] { inset-block-start: anchor(attr(data-⚓️-below type()) self-end) } ### data-⚓️-before Positions the target entirely before (left of, in LTR) the anchor's edge. Empty value targets the anchor set via data-⚓️-to; a "--name" value targets a specific named anchor. - data-⚓️-before [data-⚓️-before=""] { inset-inline-end: anchor(self-start) } - data-⚓️-before="--name" [data-⚓️-before^="--"] { inset-inline-end: anchor(attr(data-⚓️-before type()) self-start) } ### data-⚓️-after Positions the target entirely after (right of, in LTR) the anchor's edge. Empty value targets the anchor set via data-⚓️-to; a "--name" value targets a specific named anchor. - data-⚓️-after [data-⚓️-after=""] { inset-inline-start: anchor(self-end) } - data-⚓️-after="--name" [data-⚓️-after^="--"] { inset-inline-start: anchor(attr(data-⚓️-after type()) self-end) } ### data-⚓️-side Outside edge as one attribute: which side of the anchor the target sits on. Same as the boolean data-⚓️-above/below/before/after, but a state change is a single value swap. Requires data-⚓️-to (no named-anchor form). Works with data-⚓️-try flips and data-⚓️-tween. - data-⚓️-side="above" — Bottom edge on the anchor's top edge. [data-⚓️-side="above"] { inset-block-end: anchor(self-start) } - data-⚓️-side="below" — Top edge on the anchor's bottom edge. [data-⚓️-side="below"] { inset-block-start: anchor(self-end) } - data-⚓️-side="before" — Inline-end edge on the anchor's inline-start edge (left in LTR, right in RTL). [data-⚓️-side="before"] { inset-inline-end: anchor(self-start) } - data-⚓️-side="after" — Inline-start edge on the anchor's inline-end edge (right in LTR, left in RTL). [data-⚓️-side="after"] { inset-inline-start: anchor(self-end) } ### data-⚓️-align-top Flushes the target's top edge to the inside top edge of the anchor. - data-⚓️-align-top [data-⚓️-align-top=""] { inset-block-start: anchor(self-start) } - data-⚓️-align-top="--name" [data-⚓️-align-top^="--"] { inset-block-start: anchor(attr(data-⚓️-align-top type()) self-start) } ### data-⚓️-align-bottom Flushes the target's bottom edge to the inside bottom edge of the anchor. - data-⚓️-align-bottom [data-⚓️-align-bottom=""] { inset-block-end: anchor(self-end) } - data-⚓️-align-bottom="--name" [data-⚓️-align-bottom^="--"] { inset-block-end: anchor(attr(data-⚓️-align-bottom type()) self-end) } ### data-⚓️-align-leading Flushes the target's leading edge to the inside leading edge of the anchor. - data-⚓️-align-leading [data-⚓️-align-leading=""] { inset-inline-start: anchor(self-start) } - data-⚓️-align-leading="--name" [data-⚓️-align-leading^="--"] { inset-inline-start: anchor(attr(data-⚓️-align-leading type()) self-start) } ### data-⚓️-align-trailing Flushes the target's trailing edge to the inside trailing edge of the anchor. - data-⚓️-align-trailing [data-⚓️-align-trailing=""] { inset-inline-end: anchor(self-end) } - data-⚓️-align-trailing="--name" [data-⚓️-align-trailing^="--"] { inset-inline-end: anchor(attr(data-⚓️-align-trailing type()) self-end) } ### data-⚓️-stretch-y Spans the target across the anchor's full block axis. - data-⚓️-stretch-y [data-⚓️-stretch-y=""] { inset-block-start: anchor(self-start); inset-block-end: anchor(self-end) } - data-⚓️-stretch-y="--name" [data-⚓️-stretch-y^="--"] { inset-block-start: anchor(attr(data-⚓️-stretch-y type()) self-start); inset-block-end: anchor(attr(data-⚓️-stretch-y type()) self-end) } ### data-⚓️-stretch-x Spans the target across the anchor's full inline axis. - data-⚓️-stretch-x [data-⚓️-stretch-x=""] { inset-inline-start: anchor(self-start); inset-inline-end: anchor(self-end) } - data-⚓️-stretch-x="--name" [data-⚓️-stretch-x^="--"] { inset-inline-start: anchor(attr(data-⚓️-stretch-x type()) self-start); inset-inline-end: anchor(attr(data-⚓️-stretch-x type()) self-end) } ### data-⚓️-fill Fills the anchor's full box on both axes (`inset: anchor(inside)` plus width/height from `anchor-size()`). Use instead of stretch-x + stretch-y when the target also transitions — it doesn't collapse mid-animation. - data-⚓️-fill [data-⚓️-fill=""] { inset: anchor(inside); width: anchor-size(inline); height: anchor-size(block) } - data-⚓️-fill="--name" [data-⚓️-fill^="--"] { inset: anchor(attr(data-⚓️-fill type()) inside); width: anchor-size(attr(data-⚓️-fill type()) inline); height: anchor-size(attr(data-⚓️-fill type()) block) } ### data-⚓️-tween Opt-in: makes swaps between edge, stretch and center attributes (e.g. below → above, stretch-x → align-x=center) tween instead of snap, via a start inset + anchor-size() + percentage translate. Horizontal writing modes only; don't combine with data-⚓️-try. Use with data-⚓️-transition=morph|glide. - data-⚓️-tween [data-⚓️-tween] { translate: var(--⚓️-tx, 0%) var(--⚓️-ty, 0%) } ## 5. Sizing Synchronization (with Auto Layout Math) ### data-⚓️-sync-width Sizes the target's width as (anchor width × --⚓️-mult-x) + --⚓️-offset-x, via anchor-size(). - data-⚓️-sync-width [data-⚓️-sync-width=""] { width: calc((anchor-size(inline) * var(--⚓️-mult-x, 1)) + var(--⚓️-offset-x, 0px)) } - data-⚓️-sync-width="--name" [data-⚓️-sync-width^="--"] { width: calc((anchor-size(attr(data-⚓️-sync-width type()) inline) * var(--⚓️-mult-x, 1)) + var(--⚓️-offset-x, 0px)) } ### data-⚓️-sync-height Sizes the target's height as (anchor height × --⚓️-mult-y) + --⚓️-offset-y, via anchor-size(). - data-⚓️-sync-height [data-⚓️-sync-height=""] { height: calc((anchor-size(block) * var(--⚓️-mult-y, 1)) + var(--⚓️-offset-y, 0px)) } - data-⚓️-sync-height="--name" [data-⚓️-sync-height^="--"] { height: calc((anchor-size(attr(data-⚓️-sync-height type()) block) * var(--⚓️-mult-y, 1)) + var(--⚓️-offset-y, 0px)) } ## 6. Position Area & Alignment ### data-⚓️-area Coarse anchor-relative placement — sets `position-area`. - data-⚓️-area="top" — position-area: block-start [data-⚓️-area="top"] { position-area: block-start } - data-⚓️-area="bottom" — position-area: block-end [data-⚓️-area="bottom"] { position-area: block-end } - data-⚓️-area="leading" — position-area: inline-start [data-⚓️-area="leading"] { position-area: inline-start } - data-⚓️-area="trailing" — position-area: inline-end [data-⚓️-area="trailing"] { position-area: inline-end } ### data-⚓️-align-x Horizontal self-alignment relative to the anchor. - data-⚓️-align-x="center" — justify-self: anchor-center [data-⚓️-align-x="center"] { justify-self: anchor-center } ### data-⚓️-align-y Vertical self-alignment relative to the anchor. - data-⚓️-align-y="center" — align-self: anchor-center [data-⚓️-align-y="center"] { align-self: anchor-center } ### data-⚓️-center-all Centers the target on the anchor on both axes — sets `place-self: anchor-center`. Overrides stretch-derived sizing on any axis it applies to, the same way align-self overrides align-items:stretch in flex/grid. - data-⚓️-center-all [data-⚓️-center-all] { place-self: anchor-center } ### data-⚓️-align-self Margin-based self-alignment, meant to pair with data-⚓️-stretch-*. - data-⚓️-align-self="center" — margin: auto [data-⚓️-align-self="center"] { margin: auto } - data-⚓️-align-self="start" — margin-inline-end: auto [data-⚓️-align-self="start"] { margin-inline-end: auto } - data-⚓️-align-self="end" — margin-inline-start: auto [data-⚓️-align-self="end"] { margin-inline-start: auto } ## 7. Collision Resolution (@position-try) ### data-⚓️-try Automatic collision-resolution fallback — sets `position-try-fallbacks`. - data-⚓️-try="flip-y" — position-try-fallbacks: flip-block [data-⚓️-try="flip-y"] { position-try-fallbacks: flip-block } - data-⚓️-try="flip-x" — position-try-fallbacks: flip-inline [data-⚓️-try="flip-x"] { position-try-fallbacks: flip-inline } - data-⚓️-try="flip-all" — position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline [data-⚓️-try="flip-all"] { position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline } ### data-⚓️-try-order Sets `position-try-order`, hunting for the quadrant with the most available space. - data-⚓️-try-order="most-y" — position-try-order: most-block-size [data-⚓️-try-order="most-y"] { position-try-order: most-block-size } - data-⚓️-try-order="most-x" — position-try-order: most-inline-size [data-⚓️-try-order="most-x"] { position-try-order: most-inline-size } ## 8. Visibility State ### data-⚓️-visibility Maps to the real `position-visibility` CSS property (default anchor-visible; the deprecated plural anchors-* spellings are declared first as a fallback for older engines). Note: `anchor-valid` (and any value including "valid") needs Chrome 157+ — see SPEC.md's Known Browser Limitations. - data-⚓️-visibility="always" — position-visibility: always [data-⚓️-visibility="always"] { position-visibility: always } - data-⚓️-visibility="valid" — position-visibility: anchor-valid [data-⚓️-visibility="valid"] { position-visibility: anchors-valid; position-visibility: anchor-valid } - data-⚓️-visibility="visible" — position-visibility: anchor-visible (the default) [data-⚓️-visibility="visible"] { position-visibility: anchors-visible; position-visibility: anchor-visible } - data-⚓️-visibility="no-overflow" — position-visibility: no-overflow [data-⚓️-visibility="no-overflow"] { position-visibility: no-overflow } - data-⚓️-visibility="valid-no-overflow" — position-visibility: anchor-valid no-overflow [data-⚓️-visibility="valid-no-overflow"] { position-visibility: anchors-valid no-overflow; position-visibility: anchor-valid no-overflow } - data-⚓️-visibility="visible-no-overflow" — position-visibility: anchor-visible no-overflow [data-⚓️-visibility="visible-no-overflow"] { position-visibility: anchors-visible no-overflow; position-visibility: anchor-visible no-overflow } - data-⚓️-visibility="all" — position-visibility: anchor-valid anchor-visible no-overflow (strictest) [data-⚓️-visibility="all"] { position-visibility: anchors-valid anchors-visible no-overflow; position-visibility: anchor-valid anchor-visible no-overflow } ## 9. AIM (Anchor Interpolated Morphing) ### data-⚓️-transition Transition preset for anchored layout changes. - data-⚓️-transition="morph" — Animates the element in from data-⚓️-morph-from's collapsed starting-style to its resolved layout, and animates later geometry changes (0.5s). [data-⚓️-transition="morph"] { /* Use a nice spatial bezier for the geometry, and ease for opacity/colors */ transition: inset 0.5s cubic-bezier(0.25, 1, 0.5, 1), width 0.5s cubic-bezier(0.25, 1, 0.5, 1), height 0.5s cubic-bezier(0.25, 1, 0.5, 1), position-area 0.5s cubic-bezier(0.25, 1, 0.5, 1), translate 0.5s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.4s ease } @media (prefers-reduced-motion: reduce) [data-⚓️-transition="morph"] { transition: opacity 0.2s ease } - data-⚓️-transition="glide" — Short (0.25s) geometry-only transition for layouts that reflow while visible (grid reflow, re-sorting) — no entry animation, no opacity. [data-⚓️-transition="glide"] { transition: inset 0.25s cubic-bezier(0.25, 1, 0.5, 1), width 0.25s cubic-bezier(0.25, 1, 0.5, 1), height 0.25s cubic-bezier(0.25, 1, 0.5, 1), translate 0.25s cubic-bezier(0.25, 1, 0.5, 1) } @media (prefers-reduced-motion: reduce) [data-⚓️-transition="glide"] { transition: none } ### data-⚓️-morph-from Sets the @starting-style origin for the AIM entry morph — the element starts collapsed inside this anchor's bounding box. Empty value defaults to the element's own data-⚓️-to target. Pair with data-⚓️-transition="morph". - data-⚓️-morph-from @starting-style :is(dialog, [popover])[data-⚓️-morph-from=""] { inset-block-start: anchor(self-start); inset-block-end: anchor(self-end); inset-inline-start: anchor(self-start); inset-inline-end: anchor(self-end); width: anchor-size(inline); height: anchor-size(block); opacity: 0 } @starting-style [data-⚓️-morph-from=""]:not([hidden]) { inset-block-start: anchor(self-start); inset-block-end: anchor(self-end); inset-inline-start: anchor(self-start); inset-inline-end: anchor(self-end); width: anchor-size(inline); height: anchor-size(block); opacity: 0 } - data-⚓️-morph-from="--name" @starting-style :is(dialog, [popover])[data-⚓️-morph-from^="--"] { inset-block-start: anchor(attr(data-⚓️-morph-from type()) self-start); inset-block-end: anchor(attr(data-⚓️-morph-from type()) self-end); inset-inline-start: anchor(attr(data-⚓️-morph-from type()) self-start); inset-inline-end: anchor(attr(data-⚓️-morph-from type()) self-end); width: anchor-size(attr(data-⚓️-morph-from type()) inline); height: anchor-size(attr(data-⚓️-morph-from type()) block); opacity: 0 } @starting-style [data-⚓️-morph-from^="--"]:not([hidden]) { inset-block-start: anchor(attr(data-⚓️-morph-from type()) self-start); inset-block-end: anchor(attr(data-⚓️-morph-from type()) self-end); inset-inline-start: anchor(attr(data-⚓️-morph-from type()) self-start); inset-inline-end: anchor(attr(data-⚓️-morph-from type()) self-end); width: anchor-size(attr(data-⚓️-morph-from type()) inline); height: anchor-size(attr(data-⚓️-morph-from type()) block); opacity: 0 } ## Custom properties - --⚓️-tx (default 0%) — internal, set by data-⚓️-tween; don't author it - --⚓️-ty (default 0%) — internal, set by data-⚓️-tween; don't author it - --⚓️-mult-x (default 1) - --⚓️-offset-x (default 0px) - --⚓️-mult-y (default 1) - --⚓️-offset-y (default 0px)