Anchored Auto Layout
npm

Hover or focus a box to measure it. Click its tag, or press ] and [, to pin one. Esc unpins.

    Legend
    • Anchor: a box with data-⚓️-name
    • Placed by anchors: out of flow
    • Gap from the anchor's edge, in px
    • Edges of the box you're inspecting

    Every tag, line and number here is itself placed by data-⚓️-* attributes. JS only measures.

    Browser support

    It needs anchor positioning and typed attr(). The last column is your browser, tested as this page loaded.

    What it needs

    FeatureUsed byChromiumHere
    Typed attr()Every attribute that takes a --name. Without it no anchor name resolves.133+…
    Anchor positioningEverything: anchor(), position-anchor, anchor-size().125+…
    position-areadata-⚓️-area129+…
    anchor-centerdata-⚓️-align-x, -align-y, -center-all125+…
    position-try-fallbacksdata-⚓️-try128+…
    position-try-orderdata-⚓️-try-order125+…
    anchor-scopedata-⚓️-scope, -scope-name131+…
    position-visibility: anchor-visibledata-⚓️-visibility="visible" (plural spelling; singular from 157)125+…
    position-visibility: anchor-validdata-⚓️-visibility values that include valid157+…
    Anchored container queriesEvery data-⚓️-to target; lets insides query the chosen fallback143+…
    @starting-styledata-⚓️-morph-from117+…
    interpolate-sizeMorphs to and from auto sizes129+…
    popover="hint"Optional; this site's tooltips133+…

    Other engines

    Typed attr() is the one to watch: it's how a name gets from your markup into CSS, and without it the attributes are inert. Test for it, not for anchoring.

    • Chrome and Edge 133+: supported. The library and this site are built and verified in Chromium.
    • Safari: not in a stable release yet.
    • Firefox: unverified.

    Early signal only: in our audit, Playwright's Firefox 155 and WebKit 26.6 builds pass both required probes and lay this site out with the library. Neither had anchored container queries orinterpolate-size, so fallback-aware styles and morphs to auto sizes don't apply there, and WebKit took a flip-y fallback the popover didn't need. The Here column above tests the browser you're in.

    Fallbacks

    This site's own, which puts the page back into normal flow:

    @supports not (anchor-name: attr(data-x type(<custom-ident>))) {
      /* anchored regions go back into the flow, in source order */
      .shell-region { position: static; }
      .shell-nav { display: none; }        /* the Menu button takes over */
    
      /* popovers: the library's reset cleared the UA centring; restore it */
      [popover][data-⚓️-to] { inset: 0; margin: auto; width: fit-content; height: fit-content; }
    }

    Known limitations

    From SPEC.md, verified in Chromium.

    • Resolved — the "end-only inset" problem was the UA popover stylesheet, not a browser bug. [popover]/<dialog> 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.

    Copied