Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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