Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Polyfills

Eunomia relies on two fairly recent platform APIs: the Popover API for every floating panel (Select, Date picker, Tooltip, the dropdown menu, the drawer, the modal…), and the CSS Anchor Positioning API to keep those panels attached to whatever opened them. Only one of the two already degrades gracefully inside Eunomia's own components on its own: you don't need to install anything just to get <eun-select> or <eun-tooltip> correctly positioned on a browser that lacks native anchor positioning. The other one, the Popover API itself, Eunomia assumes is there, so it's worth knowing exactly when that assumption needs help. This page covers both, the two situations where reaching for a real polyfill is worth it, and, since "import it somewhere" means something different in every setup, exactly where that import belongs in React, Next.js, Vue, Angular, and plain JavaScript. Pick a framework in the toolbar above the page and every code sample below adapts to it.

What Eunomia already handles for you

CSS Anchor Positioning: every Eunomia component that positions a panel against another element (Select, Date picker, Time picker, Tooltip, Search, the dropdown menu…) checks CSS.supports("anchor-name: --a") once, and automatically falls back to a getBoundingClientRect()-based positioning path, with the same flip-to-the-opposite-side behavior, everywhere the native API isn't available. That's why the tooltip below already opens in the right place, with nothing installed, on a browser without native support:

No polyfill installed: this uses the built-in JS fallback wherever native anchor positioning isn't available.

Native anchor positioning itself has shipped in every major engine (Chrome/Edge 125+, Firefox 147+, Safari 26+), but plenty of real traffic still runs older versions of each, which is exactly the gap this fallback closes. There's nothing to configure either way: the check runs on its own, and Eunomia's components use whichever path the browser qualifies for.

Popover API: this one Eunomia does not soften for you. Select's listbox, Tooltip's panel, Date picker's calendar, and the dropdown menu all call .showPopover() / .hidePopover() directly on their panel element. On a browser new enough to matter today (Safari 17+, Firefox 125+, Chrome/Edge 114+) that's the real, native method: nothing to do. On something genuinely old, that call throws (showPopover is not a function), which is exactly the gap the polyfill below closes.

Neither of these is new JS you have to load: it already ships inside @eunomia/elements today.

When you actually need a polyfill

Situation What to do
You only use Eunomia components (eun-select, eun-tooltip, eun-date-picker, …) and support current browsers Nothing, already handled above
You use popover, anchor-name/position-anchor, or anchor() yourself, in your own markup, outside any Eunomia component, and need it on browsers older than Chrome 125 / Firefox 147 / Safari 26 @oddbird/css-anchor-positioning
You use a native <button command commandfor> yourself, outside eun-button/eun-fab, targeting Modal, Drawer, or your own commandfor-aware element, and need it on browsers older than Chrome 135 / Firefox 144 / Safari 26.2 invokers-polyfill
You need Eunomia's popover-driven components to keep working on browsers without the Popover API at all (older than Safari 17 / Firefox 125 / Chrome 114) @oddbird/popover-polyfill

Two of the three below, CSS Anchor Positioning and Popover, are maintained by Oddbird, the team that also helped author the underlying specs, and both ship their own TypeScript types: nothing extra to install for either. The other, Invoker Commands, is unrelated to Eunomia itself: eun-button, eun-fab, eun-modal, and eun-drawer implement command/commandfor in JavaScript directly rather than depending on the browser (see the Invoker Commands API guide), so it only matters for a native <button> you write yourself.

CSS Anchor Positioning polyfill

This one is for your own anchor-positioned UI, not for Eunomia components: Eunomia's own mixin decides whether to use the native path or its JS fallback by asking CSS.supports("anchor-name: --a") directly, which reports the browser's real support and is unaffected by whether this polyfill is loaded. So installing it changes nothing for <eun-select> or <eun-tooltip>, and only helps if you're writing anchor-name / position-anchor / anchor() yourself, elsewhere in your app.

npm install @oddbird/css-anchor-positioning
import polyfill from "@oddbird/css-anchor-positioning/fn";

polyfill();

When and how to call it

polyfill() reads layout from the real DOM (document, and, via roots, any shadow roots you pass it), so unlike the Popover polyfill above, which only patches a prototype and is safe to import unconditionally at the very top of anything, this one needs to run after the elements it's scanning actually exist, and only in the browser. Pick a framework in the toolbar above:

Call it once your anchored markup is in the DOM: right after import is enough for markup that's part of the initial page. Guard it with the same CSS.supports check Eunomia's own components use, so browsers that already support anchor positioning natively skip the scan entirely:

import polyfill from "@oddbird/css-anchor-positioning/fn";

if (!CSS.supports("anchor-name: --a")) {
  polyfill();
}
  • It only scans once, and doesn't track DOM changes. The polyfill doesn't (yet) support anchors or targets added or removed after that initial call: call polyfill() again yourself once the DOM settles if you're positioning something added dynamically (see the Next.js and React tabs above for where that second call belongs in practice).
  • Anchor and target must share a shadow root. An anchor inside one custom element's Shadow DOM can't be linked to a target in a different one, the same constraint the native API itself has, though Eunomia's own AnchorPositionMixin works around it internally wherever it would otherwise apply. Pass your own shadow roots via polyfill({ roots: [someElement.shadowRoot] }) if your anchors live inside one.

Configuring what it scans

polyfill() takes an options object if the defaults (scan the whole document, recalculate on scroll/resize) aren't right for your case:

Option Type Default What it does
roots (Document | ShadowRoot)[] [document] Which root(s) to scan. Pass your own shadow roots here for the shared-shadow-root constraint above.
elements Element[] undefined Scan only these elements instead of the whole root.
excludeInlineStyles boolean false With elements set, also stop implicitly polyfilling inline style="anchor-name: ..." on elements outside that list.
useAnimationFrame boolean false Recalculate every animation frame instead of only on scroll/resize (costs more CPU), so only turn it on for anchors that move via JS/animation outside normal layout flow.
positionAreaContainingBlock boolean | "auto" true Whether the polyfill wraps a target in an extra element to emulate the containing block position-area creates natively.
import polyfill from "@oddbird/css-anchor-positioning/fn";

polyfill({ roots: [someElement.shadowRoot], useAnimationFrame: true });

TypeScript

Like the Popover polyfill, this package ships its own .d.ts files for both the default import and /fn: no extra install, no hand-written ambient declarations needed for the polyfill() call or its options object.

See Tooltip or Select for a live, already-anchored panel: both already run their own fallback wherever native anchor positioning isn't available, without this polyfill.

Invoker Commands API polyfill

This one isn't for Eunomia either. eun-button, eun-fab, eun-modal, and eun-drawer all implement command/commandfor themselves in JavaScript, so they already work with each other on every browser Eunomia supports, with nothing installed. What doesn't, on an older browser, is a plain native <button command commandfor>, written outside any of those four, targeting one of them (or any other element you've wired up to listen for command yourself). See the Invoker Commands API guide for how the attributes work.

npm install invokers-polyfill

Import it once, as early as possible, exactly like the Popover polyfill above: it checks for native support itself and only patches what's missing, so it's always safe to import unconditionally.

import "invokers-polyfill";

Where to import it

The same "as early as possible" rule as the Popover polyfill above applies here too. Pick a framework in the toolbar above:

Put the import at the very top of whatever module boots your app, before it, or anything it imports, ever renders a native invoker button:

import "invokers-polyfill";
// ...the rest of your app's entry point
A native browser implementation keeps aria-expanded on the invoker in sync with a toggle-popover target's open state automatically. This polyfill doesn't manage that attribute for you, so set it yourself if you're relying on it for a raw native invoker on an older browser.

TypeScript

Ships its own .d.ts files for both the default import and the /fn entry point: nothing extra to install. The default import (what every example above uses) patches unconditionally and gives you nothing to type beyond "this module has side effects." To decide yourself whether to apply the patch instead, use /fn:

import { isSupported, apply } from "invokers-polyfill/fn";

if (!isSupported()) {
  apply();
}

Popover API polyfill

npm install @oddbird/popover-polyfill

Import it once, as early as possible (before any Eunomia component that opens a popover gets used): it checks "showPopover" in HTMLElement.prototype itself and only patches the prototype when that's missing, so it's always safe to import unconditionally:

import "@oddbird/popover-polyfill";

Where to import it

"As early as possible" means something different depending on how your app boots. Pick a framework in the toolbar above:

Put the import at the very top of whatever module boots your app, before it, or anything it imports, ever calls a Eunomia component that opens a popover:

import "@oddbird/popover-polyfill";
// ...the rest of your app's entry point

The button below is plain native HTML (no Eunomia component, no JS of ours at all), using the browser's own popovertarget attribute, the exact same showPopover()/hidePopover() machinery Select, Tooltip, Date picker, and the dropdown menu call directly on their own panels:

Plain <button popovertarget> + <div popover>: the browser wires up show/hide entirely on its own.

On a browser that already supports the Popover API natively, this already works with nothing installed. On one old enough to lack it, the import above is what makes this exact markup keep working instead of throwing.

  • :popover-open becomes a real class (.\:popover-open) under the hood rather than a true pseudo-class, which only matters if you query for it yourself from JS, since element.matches(":popover-open") still works through the polyfill's own patched method.
  • There's no real ::backdrop: the polyfill ships a workaround, but a custom backdrop style targeting ::backdrop directly won't apply.
  • It injects a <style> tag into the document (or into each Shadow Root that needs it): harmless, but worth knowing if something in your build inspects injected styles.

TypeScript

Both the default import and the /fn entry point ship their own .d.ts files: nothing to add to a global declaration file, no @types/... package to install. The default import (what every example above uses) patches the prototype unconditionally and gives you nothing to type beyond "this module has side effects." If you'd rather decide yourself whether to apply the patch (say, to log which visitors actually needed it), use the /fn entry point instead:

import { apply, isPolyfilled, isSupported } from "@oddbird/popover-polyfill/fn";

if (!isSupported()) {
  apply();
}

isSupported() mirrors the check the default import already runs internally ("showPopover" in HTMLElement.prototype), while isPolyfilled() tells you afterward whether apply() actually did anything.

See also