Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c777369
Add a shadow DOM demo for anchors wired up through the CSSOM
jpzwarte Jul 25, 2026
38e3646
Add `patchCSSOM`
jpzwarte Jul 25, 2026
b35753d
Update README
jpzwarte Jul 25, 2026
8d2e506
Revert feature check doc change
jpzwarte Jul 27, 2026
98a98b0
Tweaks to docs
jpzwarte Jul 27, 2026
35979ae
Remove SVGElement
jpzwarte Jul 28, 2026
45be448
Add doc about performance hit
jpzwarte Jul 28, 2026
82b6875
Shift CSSOM properties to CSS variables
jpzwarte Jul 28, 2026
40caad4
Merge branch 'main' into fix/445-cssom-not-working
jpzwarte Aug 15, 2026
5b315b0
Support all properties instead of just anchorName and positionAnchor
jpzwarte Aug 15, 2026
9eca642
Be less verbose in comments
jpzwarte Aug 15, 2026
56be5d6
Expand the isDeclaration util function to include an optional property
jpzwarte Aug 15, 2026
c14913f
Revert regexp back to simpler solution
jpzwarte Aug 15, 2026
3ac1917
Add example of patchCSSOM to index.html
jpzwarte Aug 15, 2026
e1bdda2
Merge branch 'main' of https://github.com/oddbird/css-anchor-position…
jamesnw Aug 17, 2026
ca4e81d
Merge branch 'main' of https://github.com/oddbird/css-anchor-position…
jamesnw Aug 21, 2026
f888c37
Let a CSSOM-set value win over the literal it supersedes
jpzwarte Aug 25, 2026
708bc07
Position a shadow root that adopts a stylesheet again
jpzwarte Aug 25, 2026
6e352a8
Keep every CSSOM write that lands during a run
jpzwarte Aug 25, 2026
fdaa15e
Match CSSOM property names case-insensitively
jpzwarte Aug 25, 2026
d2dc6a6
Patch getPropertyPriority for the CSSOM properties
jpzwarte Aug 25, 2026
cd84abc
Re-read the source sheet a polyfill-owned copy came from
jpzwarte Aug 25, 2026
abf409c
Position a plain element that adopts before being connected
jpzwarte Aug 25, 2026
386aa53
Leave the declaration standing for a blank CSSOM value
jpzwarte Aug 25, 2026
39d38f7
Restore connectedCallback when define() throws
jpzwarte Aug 25, 2026
ec0fdc3
Make a CSSOM property with nowhere to be stored a compile error
jpzwarte Aug 25, 2026
19ce3e3
Resolve a CSSOM property through one lookup
jpzwarte Aug 25, 2026
a319a06
Cover case and whitespace in the inline style match
jpzwarte Aug 25, 2026
191c586
Slice the inline styles when there is no rule to split out
jpzwarte Aug 25, 2026
2cf51ff
Pin how inline styles are collected across roots
jpzwarte Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,79 @@ afterwards; an explicit argument takes precedence over the global.
You can view a more complete demo
[here](https://anchor-positioning.oddbird.net/shadow-dom.html).

### Setting anchor properties from JavaScript

Assigning an anchor positioning property through the CSSOM does nothing in a
browser without native anchor positioning. The CSSOM drops properties it doesn't
know, so no CSS declaration is produced and nothing is written to the `style`
attribute the polyfill reads — while reading the value back still returns it,
which makes the assignment look like it worked:

```js
el.style.anchorName = '--foo';
el.style.anchorName; // '--foo'
el.getAttribute('style'); // null
```

If your components wire up their anchor positioning at runtime, call
`patchCSSOM()` to make those properties settable. Like the constructed
stylesheet patch, call it before any `connectedCallback` runs:

```js
import {
patchAndPolyfillConstructedStylesheets,
patchCSSOM,
} from '@oddbird/css-anchor-positioning/fn';

patchCSSOM();
patchAndPolyfillConstructedStylesheets();

// Define your custom elements after the patches are installed.
```

If your components don't adopt constructed stylesheets, apply the polyfill
yourself once the elements are defined, passing their shadow roots as
[`roots`](#roots).

It covers every anchor positioning property the polyfill supports, defining each
on `CSSStyleDeclaration` and storing what you set in the custom property the
polyfill shifts that declaration into internally:

| Property | CSSOM name |
| ------------------------ | ---------------------- |
| `anchor-name` | `anchorName` |
| `anchor-scope` | `anchorScope` |
| `position-anchor` | `positionAnchor` |
| `position-area` | `positionArea` |
| `position-try` | `positionTry` |
| `position-try-fallbacks` | `positionTryFallbacks` |
| `position-try-order` | `positionTryOrder` |

`setProperty()`, `getPropertyValue()` and `removeProperty()` accept the dashed
names as well. `position-visibility` is left out, as the polyfill does not
support it.

Four things to know:

- Once it has run, `'anchorName' in element.style` returns `true`. Feature
detection happens before the polyfill is loaded, so that check is unaffected,
but anything detecting support later on can use
`CSS.supports('anchor-name: --a')`, which the patch leaves alone.
- The `style` attribute holds the polyfill's custom property rather than the
property you assigned, so devtools shows `--anchor-name-<id>: --foo` instead
of `anchor-name: --foo`. Reading the value back through the CSSOM returns what
you set.
- It covers properties, not values. An `anchor()` value assigned to
`el.style.top` is dropped just the same, for being a value the browser does
not understand.
- Any CSSOM write re-serializes the whole declaration block from what the
browser parsed, so it also drops `anchor()` values already in the `style`
attribute. Keep those in a stylesheet if the element's inline styles are
written to from JavaScript.

Values set before `patchCSSOM()` runs are not picked up, since the assignments
that were dropped left nothing behind.

## Configuration

The polyfill supports a small number of options. When using the default version
Expand Down Expand Up @@ -310,6 +383,8 @@ Browsers provide some validation for imperatively setting inline styles.
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.
[`patchCSSOM()`](#setting-anchor-properties-from-javascript) makes the anchor
positioning properties settable that way.

While `el.setAttribute('style', 'anchor-name: --foo')` or `<div
style="anchor-name: --foo" />` both work, developers are often using tools that
Expand Down
80 changes: 79 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<link rel="stylesheet" href="/anchor-media-query.css" />
<link rel="stylesheet" href="/anchor-scope.css" />
<link rel="stylesheet" href="/position-area.css" />
<link rel="stylesheet" href="/cssom.css" />
<link rel="stylesheet" href="/anchor-inside-outside.css" />
<link rel="stylesheet" href="/import-has-import.css" />
<link rel="stylesheet" href="/invalid-anchor.css" />
Expand All @@ -69,7 +70,7 @@
}
</style>
<script type="module">
import polyfill from '/src/index-fn.ts';
import polyfill, { patchCSSOM } from '/src/index-fn.ts';

const SUPPORTS_ANCHOR_POSITIONING =
'anchorName' in document.documentElement.style;
Expand All @@ -91,6 +92,20 @@
'anchor-positioning is supported in this browser; polyfill skipped.',
);
}
// Wire up the `#cssom` demo from JavaScript, the way a design system
// component would. `patchCSSOM()` goes after the feature check above:
// defining these properties makes `'anchorName' in element.style` true.
patchCSSOM();
document.getElementById('my-cssom-anchor').style.anchorName =
'--my-cssom-anchor';
const cssomTarget = document.getElementById('my-cssom-target');
cssomTarget.style.positionAnchor = '--my-cssom-anchor';
const cssomPositionAreaTarget = document.getElementById(
'my-cssom-position-area-target',
);
cssomPositionAreaTarget.style.positionAnchor = '--my-cssom-anchor';
cssomPositionAreaTarget.style.positionArea = 'top';

// Vite errors while trying to parse the end script tag, but interpolating
// it avoids the error.
const polyfillCDN = `
Expand Down Expand Up @@ -1617,6 +1632,69 @@ <h2>
#imports .anchor {
color: var(--brand-orange);
}</code></pre>
</section>
<section id="cssom" class="demo-item">
<h2>
<a href="#cssom" aria-hidden="true">🔗</a>
Setting properties from JavaScript [<code>patchCSSOM()</code>]
</h2>
<div style="position: relative" class="demo-elements">
<div id="my-cssom-position-area-target" class="target">
position-area Target
</div>
<div id="my-cssom-anchor" class="anchor">Anchor</div>
<div id="my-cssom-target" class="target">anchor() Target</div>
</div>
<p class="note">
With polyfill applied: the position-area Target sits above the Anchor,
and the anchor() Target at its bottom right corner. Neither
<code>anchor-name</code>, <code>position-anchor</code> nor
<code>position-area</code> is declared in CSS here — all three are
assigned through the CSSOM, which a browser without native anchor
positioning drops on the floor unless
<a
href="https://github.com/oddbird/css-anchor-positioning#setting-anchor-properties-from-javascript"
><code>patchCSSOM()</code></a
>
has been called. Note that it makes
<code>'anchorName' in element.style</code> true, so detect native
support with <code>CSS.supports('anchor-name: --a')</code> instead.
</p>
<pre><code class="language-html"
>&lt;div id="my-cssom-position-area-target" class="target"&gt;position-area Target&lt;/div&gt;
&lt;div id="my-cssom-anchor" class="anchor"&gt;Anchor&lt;/div&gt;
&lt;div id="my-cssom-target" class="target"&gt;anchor() Target&lt;/div&gt;</code>

<code class="language-css"
>/* A CSSOM write re-serializes the declaration block, dropping the
`anchor()` values the browser does not understand, so those stay
in the stylesheet. */
#my-cssom-target {
position: absolute;
top: anchor(bottom);
left: anchor(right);
}

#my-cssom-position-area-target {
position: absolute;
}</code>

<code class="language-js"
>import polyfill, { patchCSSOM } from '@oddbird/css-anchor-positioning/fn';

// Call before anything assigns these properties.
patchCSSOM();

document.getElementById('my-cssom-anchor').style.anchorName = '--my-cssom-anchor';

const target = document.getElementById('my-cssom-target');
target.style.positionAnchor = '--my-cssom-anchor';

const positionAreaTarget = document.getElementById('my-cssom-position-area-target');
positionAreaTarget.style.positionAnchor = '--my-cssom-anchor';
positionAreaTarget.style.positionArea = 'top';

if (!CSS.supports('anchor-name: --a')) await polyfill();</code></pre>
</section>
<section id="shadow-dom">
<h2>
Expand Down
20 changes: 20 additions & 0 deletions public/cssom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* `anchor-name`, `position-anchor` and `position-area` are set from JavaScript
for this demo (see `patchCSSOM()` in index.html), so they are deliberately
absent here. Only what a CSSOM write cannot carry lives in this file: a write
re-serializes the declaration block from what the browser parsed, which drops
the `anchor()` values it does not understand. */

#cssom .anchor {
inline-size: fit-content;
margin: 3em auto;
}

#my-cssom-target {
position: absolute;
top: anchor(bottom);
left: anchor(right);
}

#my-cssom-position-area-target {
position: absolute;
}
Loading