CSS Anchor Positioning Polyfill

Anchoring Elements in the shadow DOM

Note: We strive to keep the polyfill up-to-date with ongoing changes to the spec, and we welcome code contributions and financial support to make that happen.

Works if anchor and target are both inside the same shadow root

With polyfill applied: Target and Anchor’s right edges line up. Target’s top edge lines up with the bottom edge of the Anchor.

Note: this will not work across shadow root boundaries. See the next demo for an example of how to use the polyfill with adopted stylesheets.

<anchor-web-component>
  <template shadowrootmode="open">
    <style>
    #my-anchor-positioning {
      anchor-name: --my-anchor-positioning;
    }

    #my-target-positioning {
      position: absolute;
      top: anchor(--my-anchor-positioning bottom);
      right: anchor(--my-anchor-positioning right, 50px);
    }
    </style>
    <div style="position: relative">
      <div id="my-target-positioning">Target</div>
      <div id="my-anchor-positioning">Anchor</div>
    </div>
  </template>
</anchor-web-component>
<script>
class AnchorDemo extends HTMLElement {
  connectedCallback() {
    window.ANCHOR_POSITIONING_POLYFILL({ roots: [this.shadowRoot] });
  }
}
customElements.define("anchor-web-component", AnchorDemo);
</script>

Works when the anchor is a pseudo-element inside a shadow root

With polyfill applied: the Target is positioned against the anchor's ::before pseudo-element (its top-left corner meets the pseudo-element's bottom-right corner).

To measure a pseudo-element the polyfill creates a temporary "fake pseudo-element" and a matching <style>. That style is appended to the shadow root, since a <style> in document.head would not apply inside the shadow root.

<anchor-pseudo-element>
  <template shadowrootmode="open">
    <style>
    #shadow-pseudo-anchor::before {
      content: "";
      display: block;
      width: 120px;
      height: 100px;
      anchor-name: --shadow-pseudo-anchor;
    }

    #shadow-pseudo-target {
      position: absolute;
      position-anchor: --shadow-pseudo-anchor;
      top: anchor(bottom);
      left: anchor(right);
    }
    </style>
    <div style="position: relative">
      <span id="shadow-pseudo-anchor"></span>
      <div id="shadow-pseudo-target">Target</div>
    </div>
  </template>
</anchor-pseudo-element>
<script>
class AnchorPseudoElement extends HTMLElement {
  connectedCallback() {
    window.ANCHOR_POSITIONING_POLYFILL({ roots: [this.shadowRoot] });
  }
}
customElements.define("anchor-pseudo-element", AnchorPseudoElement);
</script>

Works with adopted stylesheets (constructed CSSStyleSheet)

With polyfill applied: Target and Anchor's right edges line up. Target's top edge lines up with the bottom edge of the Anchor.

This demo uses new CSSStyleSheet() with replaceSync(), added to the shadow root via adoptedStyleSheets.

<anchor-adopted-styles></anchor-adopted-styles>
<script>
<!-- Load the shadow entrypoint before defining custom elements,
so the replaceSync and adoptedStyleSheets patches are installed
before any connectedCallback runs. -->
import { patchAndPolyfillConstructedStylesheets } from '@oddbird/css-anchor-positioning/fn';
patchAndPolyfillConstructedStylesheets();
class AnchorAdoptedStyles extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: "open" });

    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`
      .anchor {
        anchor-name: --adopted-anchor;
      }
      .target {
        position: absolute;
        position-anchor: --adopted-anchor;
        position-area: bottom span-left;
      }
    `);
    this.shadowRoot.adoptedStyleSheets = [sheet];

    this.shadowRoot.innerHTML = `
      <div class="target">Target</div>
      <div class="anchor">Anchor</div>
    `;
  }
}
</script>

Works when a custom element host has position-anchor

Anchor
Anchor 2
Target Target 2

With polyfill applied: Target is positioned above the Anchor, horizontally centered on it.

There are two anchors and two targets, to demonstrate that each target resolves its own anchor independently, even though they share the same shadow root and the same adopted stylesheet.

This demo uses position-anchor on a custom element host (<position-anchor-on-host>), with anchor() in the shadow root's :host styles.



<div class="anchor" style="anchor-name: --position-anchor-on-host">Anchor</div>
<position-anchor-on-host style="position-anchor: --position-anchor-on-host">Target</position-anchor-on-host>
<script>
<!-- Load the shadow entrypoint before defining custom elements,
so the replaceSync and adoptedStyleSheets patches are installed
before any connectedCallback runs. -->
import { patchAndPolyfillConstructedStylesheets } from '@oddbird/css-anchor-positioning/fn';
patchAndPolyfillConstructedStylesheets();

class PositionAnchorOnHost extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: "open" });

    const sheet = new CSSStyleSheet();
    sheet.replaceSync(`
      :host {
        top: anchor(top);
        left: anchor(center);
        position: absolute;
        translate: -50% -100%;
      }
    `);
    this.shadowRoot.adoptedStyleSheets = [sheet];
    this.shadowRoot.innerHTML = "<slot></slot>";
  }
}
customElements.define("position-anchor-on-host", PositionAnchorOnHost);
</script>

Works with position-area inside a shadow root

With polyfill applied: the Target popover sits directly above the Anchor, horizontally centered on it.

Both the anchor and the target live inside the shadow root, and the target is a popover="manual" opened by the custom element itself with showPopover() rather than by an invoker button.

Like a design system component would, it sets anchor-name and position-anchor from JavaScript through the CSSOM. A browser that needs this polyfill does not support those properties, and the CSSOM drops what it does not know, so the assignments never become CSS declarations and nothing is written to the style attributes the polyfill reads. patchCSSOM() makes those properties settable, at the cost of 'anchorName' in element.style reporting support that isn't there.

So detect native support with CSS.supports(), which the patch leaves alone:

// Not this, `patchCSSOM()` makes it true:
if (!('anchorName' in document.documentElement.style)) {
}

// This:
if (!CSS.supports('anchor-name: --a')) {
  const { default: polyfill, patchCSSOM } = await import(
    '@oddbird/css-anchor-positioning/fn'
  );

  patchCSSOM();
  await polyfill();
}

A popover is promoted to the top layer, where its containing block is the viewport rather than the <polyfill-position-area> wrapper, so this demo is polyfilled with positionAreaContainingBlock: false and positioned directly.

<anchor-position-area></anchor-position-area>
<script>
import polyfill, { patchCSSOM } from '@oddbird/css-anchor-positioning/fn';

<!-- Call before any connectedCallback runs, so the anchor properties
are settable by the time an element wires up its anchor. -->
patchCSSOM();

class AnchorPositionArea extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        #shadow-position-area-target {
          margin: initial;
          position-area: top;
          width: 14em;
        }
      </style>
      <div id="shadow-position-area-anchor">Anchor</div>
      <div id="shadow-position-area-target" popover="manual">Popover (Target)</div>
    `;

    const anchor = this.shadowRoot.getElementById("shadow-position-area-anchor"),
      target = this.shadowRoot.getElementById("shadow-position-area-target");

    // Dropped by the CSSOM without `patchCSSOM()`.
    anchor.style.anchorName = target.style.positionAnchor =
      "--shadow-position-area-anchor";

    target.showPopover();
  }
}
customElements.define("anchor-position-area", AnchorPositionArea);

await polyfill({
  roots: [document.querySelector("anchor-position-area").shadowRoot],
  positionAreaContainingBlock: false,
});
</script>