Polyfill for CSS Anchor Positioning
492
stars
1,225
commits
TypeScript
primary language
Sep 3, 2026
updated
The CSS anchor positioning specification defines anchor positioning, "where a positioned element can size and position itself relative to one or more 'anchor elements' elsewhere on the page." This CSS Anchor Positioning Polyfill supports and is based on this specification.
Anchor positioning was added to Chrome, Chrome Android, and Edge in Chromium 125, so the polyfill will not be applied to versions after 124. Some aspects of anchor positioning were shipped later in Chromium, meaning that they are not polyfilled and are not present in those versions.
position-try-fallbacks was added in 128 after being renamed from
position-try-order. Use both -fallbacks and -order or the position-try
shorthand to make sure all versions are covered.position-area was added in 129.anchor-scope was added in 131.To use the polyfill, add this script tag to your document <head>:
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
If you want to manually apply the polyfill, you can instead import the
polyfill function directly from the
@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js file.
For build tools such as Vite, Webpack, and Parcel, that will look like this:
import polyfill from '@oddbird/css-anchor-positioning/fn';
polyfill();
The polyfill function returns a promise that resolves when the polyfill has
been applied.
adoptedStyleSheets)If your custom elements use constructed stylesheets
(via new CSSStyleSheet() + replaceSync() + shadowRoot.adoptedStyleSheets), call patchAndPolyfillConstructedStylesheets() before any of those custom elements are defined:
<script type="module">
if (!('anchorName' in document.documentElement.style)) {
const { patchAndPolyfillConstructedStylesheets } =
await import('https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js');
patchAndPolyfillConstructedStylesheets();
// Define your custom elements after the patch has applied the polyfill.
// You don't need to explicitly call polyfill().
}
</script>
With a bundler:
import { patchAndPolyfillConstructedStylesheets } from '@oddbird/css-anchor-positioning/fn';
patchAndPolyfillConstructedStylesheets();
This patches CSSStyleSheet.prototype.replaceSync to capture stylesheet source
text, and patches the ShadowRoot.prototype.adoptedStyleSheets setter to
automatically run the polyfill for each shadow root, deferred until its host is
connected and its shadow DOM has been populated.
A host that adopts its stylesheet before it is connected is positioned only if
it is a custom element defined after
patchAndPolyfillConstructedStylesheets() is called.
Those automatic runs use the same options as polyfill(),
either passed directly or read from
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS:
patchAndPolyfillConstructedStylesheets({ positionAreaContainingBlock: false });
The roots and elements options are ignored, since each run is scoped to the
shadow root being positioned. The global is read when each run happens, not when
patchAndPolyfillConstructedStylesheets() is called, so it can still be set
afterwards; an explicit argument takes precedence over the global.
You can view a more complete demo here.
The polyfill supports a small number of options. When using the default version
of the polyfill that executes automatically, options can be set by setting the
value of window.ANCHOR_POSITIONING_POLYFILL_OPTIONS.
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS = {
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
};
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
When manually applying the polyfill, options can be set by passing an object as an argument.
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js");
polyfill({
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
});
}
</script>
type: HTMLElements[], default: undefined
If set, the polyfill will only be applied to the specified elements instead of
to all styles. Any specified <link> and <style> elements will be polyfilled.
By default, all inline styles in the document will also be polyfilled, but if
excludeInlineStyles is true, only inline styles on specified elements will be
polyfilled.
type: boolean, default: false
When not defined or set to false, the polyfill will be applied to all elements
that have eligible inline styles, regardless of whether the elements option is
defined. When set to true, elements with eligible inline styles listed in the
elements option will still be polyfilled, but no other elements in the
document will be implicitly polyfilled.
type: (Document | HTMLElement | ShadowRoot)[], default: [document]
By default the polyfill applies to document, but you can configure one or more
shadow roots the polyfill should apply to using this option. See the
shadow DOM examples to learn
more.
type: boolean, default: false
Determines whether anchor calculations should update on every animation
frame (e.g. when the
anchor element is animated using transforms), in addition to always updating
on scroll/resize. While this option is optimized for performance, it should be
used sparingly.
For legacy support, this option can also be set by setting the value of
window.UPDATE_ANCHOR_ON_ANIMATION_FRAME, or, when applying the polyfill
manually, by passing a single boolean with polyfill(true).
type: boolean | 'auto', default: true
Controls how the polyfill emulates the containing block that position-area
natively creates for a target.
true (default): the polyfill wraps each position-area target with an
element that approximates the grid-area containing block, and aligns the
target within it. This matches the native behavior most closely, but the extra
wrapper element can interfere with author CSS that depends on the target's
position in the DOM tree (for example direct-child or sibling combinators, or
flex/grid layout of the target's original parent).
false: the polyfill never adds a wrapper. Instead it computes and applies
inset values directly on the target. This avoids the extra element, but styles
that resolve against the containing block — percentage sizes, auto or
percentage margins, percentage padding, or stretch/anchor-center
self-alignment — will not match the native behavior.
'auto': the polyfill adds the wrapper only for targets whose styles resolve
against the containing block (using the same heuristics listed above), and
positions all other targets directly. This keeps the wrapper's correctness
where it matters while avoiding the extra element for the common case.
Two caveats apply to 'auto':
@position-try fallback block are not detected, so a target that becomes
containing-block dependent only in a fallback may be positioned without the
wrapper it needs.auto margin), the target is not
re-wrapped. Use true when a target's containing-block dependence can
change at runtime.While this polyfill supports many basic use cases, it doesn't (yet) support the following features:
position-try-order. If try-size is specified in position-try
shorthand, it will be parsed, and try-tactics will be applied, but the
try-size will be ignored.flip-start try-tactic is only partially supported. The tactic is
only applied to property names and anchor sides.position-area as a try-tacticposition-anchor.anchor-scope property on pseudo-elementsanchor-name,
anchor-scope) into custom properties, which it makes non-inherited so they
mirror the non-inherited behavior of the properties they stand in for -- via
CSS.registerProperty (Safari 16.4+, Firefox 128+), or, on older engines, a
universal initial reset injected once per polyfilled root. This reaches
every root the polyfill reads these properties from (document and any shadow
roots passed in the roots option).anchor-center value for justify-self, align-self, justify-items, and
align-items propertiesposition-visibility propertyposition-anchor: auto keyword (pending resolution of
https://github.com/whatwg/html/pull/9144)position-area support has a few differences from native behavior:
position-area grid section outside its containing block, where the
implementation would move the target inside the containing block.positionAreaContainingBlock
option), this adds further differences:
It breaks selectors that rely on a direct relationship with the target,
for instance ~ target, + target, > target or using :nth selectors.
For popover targets, the browser promotes the element to the top layer
when it is shown, which makes the viewport (not the wrapper) its
containing block. To work around this, the polyfill strips any non-auto
inset from the target (setting inset: auto) and re-applies it as padding
on the wrapper, so the wrapper continues to drive positioning.
Moving the target into the wrapper disconnects and reconnects it. If the
target is a custom element, its connectedCallback therefore runs more
than once, and any setup that can only happen once must be guarded — for
example, calling attachShadow() a second time throws. This applies to
any custom element the polyfill positions with position-area, including
a host positioned by a position-area in its own :host rule:
class MyElement extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return;
this.attachShadow({ mode: 'open' });
// ...
}
}
Setting positionAreaContainingBlock to
false (or 'auto', for targets that don't need the wrapper) avoids the
wrapper, and with it the reconnection.
auto or percentage margins, percentage padding,
or stretch/anchor-center self-alignment — will not match native
behavior.In addition, JS APIs like CSSPositionTryRule or CSS.supports will not be
polyfilled.
Browsers provide some validation for imperatively setting inline styles.
el.style.color = "foo" and el.style.foo = "bar" do not change the inline
styles of el. This is problematic for this polyfill, as we would like to
support el.style.anchorName = "--foo", but that won't work in browsers that
don't support the anchor-name property.
While el.setAttribute('style', 'anchor-name: --foo') or <div style="anchor-name: --foo" /> both work, developers are often using tools that
generate the DOM. Both React and Vue use methods that remove the unknown inline
style properties at runtime.
If you are using inline styles to set anchor-related properties and the polyfill isn't working, verify that the inline styles are actually showing up in the DOM.
Some types of invalid CSS will cause the polyfill to throw an error. In these cases, the polyfill will report any parse errors encountered in the console as warnings. This will be followed by the error thrown by the polyfill.
The polyfill can't determine which parse error caused the polyfill error, but please resolve any reported parse errors before opening a bug. We also recommend using a CSS linter like Stylelint or @eslint/css.
At OddBird, we love contributing to the languages & tools developers rely on. We're currently working on polyfills for new Popover & Anchor Positioning functionality, as well as CSS specifications for functions, mixins, and responsive typography. Help us keep this work sustainable and centered on your needs as a developer! We display sponsor logos and avatars on our website.
TypeScript
74.8%
HTML
20.1%
CSS
4.1%
Polyfill for CSS Anchor Positioning
492
stars
1,225
commits
TypeScript
primary language
Sep 3, 2026
updated
The CSS anchor positioning specification defines anchor positioning, "where a positioned element can size and position itself relative to one or more 'anchor elements' elsewhere on the page." This CSS Anchor Positioning Polyfill supports and is based on this specification.
Anchor positioning was added to Chrome, Chrome Android, and Edge in Chromium 125, so the polyfill will not be applied to versions after 124. Some aspects of anchor positioning were shipped later in Chromium, meaning that they are not polyfilled and are not present in those versions.
position-try-fallbacks was added in 128 after being renamed from
position-try-order. Use both -fallbacks and -order or the position-try
shorthand to make sure all versions are covered.position-area was added in 129.anchor-scope was added in 131.To use the polyfill, add this script tag to your document <head>:
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
If you want to manually apply the polyfill, you can instead import the
polyfill function directly from the
@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js file.
For build tools such as Vite, Webpack, and Parcel, that will look like this:
import polyfill from '@oddbird/css-anchor-positioning/fn';
polyfill();
The polyfill function returns a promise that resolves when the polyfill has
been applied.
adoptedStyleSheets)If your custom elements use constructed stylesheets
(via new CSSStyleSheet() + replaceSync() + shadowRoot.adoptedStyleSheets), call patchAndPolyfillConstructedStylesheets() before any of those custom elements are defined:
<script type="module">
if (!('anchorName' in document.documentElement.style)) {
const { patchAndPolyfillConstructedStylesheets } =
await import('https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js');
patchAndPolyfillConstructedStylesheets();
// Define your custom elements after the patch has applied the polyfill.
// You don't need to explicitly call polyfill().
}
</script>
With a bundler:
import { patchAndPolyfillConstructedStylesheets } from '@oddbird/css-anchor-positioning/fn';
patchAndPolyfillConstructedStylesheets();
This patches CSSStyleSheet.prototype.replaceSync to capture stylesheet source
text, and patches the ShadowRoot.prototype.adoptedStyleSheets setter to
automatically run the polyfill for each shadow root, deferred until its host is
connected and its shadow DOM has been populated.
A host that adopts its stylesheet before it is connected is positioned only if
it is a custom element defined after
patchAndPolyfillConstructedStylesheets() is called.
Those automatic runs use the same options as polyfill(),
either passed directly or read from
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS:
patchAndPolyfillConstructedStylesheets({ positionAreaContainingBlock: false });
The roots and elements options are ignored, since each run is scoped to the
shadow root being positioned. The global is read when each run happens, not when
patchAndPolyfillConstructedStylesheets() is called, so it can still be set
afterwards; an explicit argument takes precedence over the global.
You can view a more complete demo here.
The polyfill supports a small number of options. When using the default version
of the polyfill that executes automatically, options can be set by setting the
value of window.ANCHOR_POSITIONING_POLYFILL_OPTIONS.
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS = {
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
};
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
When manually applying the polyfill, options can be set by passing an object as an argument.
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js");
polyfill({
elements: undefined,
excludeInlineStyles: false,
positionAreaContainingBlock: true,
roots: [document],
useAnimationFrame: false,
});
}
</script>
type: HTMLElements[], default: undefined
If set, the polyfill will only be applied to the specified elements instead of
to all styles. Any specified <link> and <style> elements will be polyfilled.
By default, all inline styles in the document will also be polyfilled, but if
excludeInlineStyles is true, only inline styles on specified elements will be
polyfilled.
type: boolean, default: false
When not defined or set to false, the polyfill will be applied to all elements
that have eligible inline styles, regardless of whether the elements option is
defined. When set to true, elements with eligible inline styles listed in the
elements option will still be polyfilled, but no other elements in the
document will be implicitly polyfilled.
type: (Document | HTMLElement | ShadowRoot)[], default: [document]
By default the polyfill applies to document, but you can configure one or more
shadow roots the polyfill should apply to using this option. See the
shadow DOM examples to learn
more.
type: boolean, default: false
Determines whether anchor calculations should update on every animation
frame (e.g. when the
anchor element is animated using transforms), in addition to always updating
on scroll/resize. While this option is optimized for performance, it should be
used sparingly.
For legacy support, this option can also be set by setting the value of
window.UPDATE_ANCHOR_ON_ANIMATION_FRAME, or, when applying the polyfill
manually, by passing a single boolean with polyfill(true).
type: boolean | 'auto', default: true
Controls how the polyfill emulates the containing block that position-area
natively creates for a target.
true (default): the polyfill wraps each position-area target with an
element that approximates the grid-area containing block, and aligns the
target within it. This matches the native behavior most closely, but the extra
wrapper element can interfere with author CSS that depends on the target's
position in the DOM tree (for example direct-child or sibling combinators, or
flex/grid layout of the target's original parent).
false: the polyfill never adds a wrapper. Instead it computes and applies
inset values directly on the target. This avoids the extra element, but styles
that resolve against the containing block — percentage sizes, auto or
percentage margins, percentage padding, or stretch/anchor-center
self-alignment — will not match the native behavior.
'auto': the polyfill adds the wrapper only for targets whose styles resolve
against the containing block (using the same heuristics listed above), and
positions all other targets directly. This keeps the wrapper's correctness
where it matters while avoiding the extra element for the common case.
Two caveats apply to 'auto':
@position-try fallback block are not detected, so a target that becomes
containing-block dependent only in a fallback may be positioned without the
wrapper it needs.auto margin), the target is not
re-wrapped. Use true when a target's containing-block dependence can
change at runtime.While this polyfill supports many basic use cases, it doesn't (yet) support the following features:
position-try-order. If try-size is specified in position-try
shorthand, it will be parsed, and try-tactics will be applied, but the
try-size will be ignored.flip-start try-tactic is only partially supported. The tactic is
only applied to property names and anchor sides.position-area as a try-tacticposition-anchor.anchor-scope property on pseudo-elementsanchor-name,
anchor-scope) into custom properties, which it makes non-inherited so they
mirror the non-inherited behavior of the properties they stand in for -- via
CSS.registerProperty (Safari 16.4+, Firefox 128+), or, on older engines, a
universal initial reset injected once per polyfilled root. This reaches
every root the polyfill reads these properties from (document and any shadow
roots passed in the roots option).anchor-center value for justify-self, align-self, justify-items, and
align-items propertiesposition-visibility propertyposition-anchor: auto keyword (pending resolution of
https://github.com/whatwg/html/pull/9144)position-area support has a few differences from native behavior:
position-area grid section outside its containing block, where the
implementation would move the target inside the containing block.positionAreaContainingBlock
option), this adds further differences:
It breaks selectors that rely on a direct relationship with the target,
for instance ~ target, + target, > target or using :nth selectors.
For popover targets, the browser promotes the element to the top layer
when it is shown, which makes the viewport (not the wrapper) its
containing block. To work around this, the polyfill strips any non-auto
inset from the target (setting inset: auto) and re-applies it as padding
on the wrapper, so the wrapper continues to drive positioning.
Moving the target into the wrapper disconnects and reconnects it. If the
target is a custom element, its connectedCallback therefore runs more
than once, and any setup that can only happen once must be guarded — for
example, calling attachShadow() a second time throws. This applies to
any custom element the polyfill positions with position-area, including
a host positioned by a position-area in its own :host rule:
class MyElement extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return;
this.attachShadow({ mode: 'open' });
// ...
}
}
Setting positionAreaContainingBlock to
false (or 'auto', for targets that don't need the wrapper) avoids the
wrapper, and with it the reconnection.
auto or percentage margins, percentage padding,
or stretch/anchor-center self-alignment — will not match native
behavior.In addition, JS APIs like CSSPositionTryRule or CSS.supports will not be
polyfilled.
Browsers provide some validation for imperatively setting inline styles.
el.style.color = "foo" and el.style.foo = "bar" do not change the inline
styles of el. This is problematic for this polyfill, as we would like to
support el.style.anchorName = "--foo", but that won't work in browsers that
don't support the anchor-name property.
While el.setAttribute('style', 'anchor-name: --foo') or <div style="anchor-name: --foo" /> both work, developers are often using tools that
generate the DOM. Both React and Vue use methods that remove the unknown inline
style properties at runtime.
If you are using inline styles to set anchor-related properties and the polyfill isn't working, verify that the inline styles are actually showing up in the DOM.
Some types of invalid CSS will cause the polyfill to throw an error. In these cases, the polyfill will report any parse errors encountered in the console as warnings. This will be followed by the error thrown by the polyfill.
The polyfill can't determine which parse error caused the polyfill error, but please resolve any reported parse errors before opening a bug. We also recommend using a CSS linter like Stylelint or @eslint/css.
At OddBird, we love contributing to the languages & tools developers rely on. We're currently working on polyfills for new Popover & Anchor Positioning functionality, as well as CSS specifications for functions, mixins, and responsive typography. Help us keep this work sustainable and centered on your needs as a developer! We display sponsor logos and avatars on our website.
TypeScript
74.8%
HTML
20.1%
CSS
4.1%