Skip to content

Support matrix

Every @aparte/* package ships at one version, so there is one matrix. Each row gives two numbers, because they answer different questions. The floor is derived from what the code uses — the newest CSS or JS feature it relies on with no fallback — so a break at or above it is a bug you can report. The tested version is what CI runs on every push, and is therefore the only one we have seen work. Nothing in between is run: the floor is a promise read off the feature list, not off a browser farm.

Measured on the release this page ships with. Before 1.0 a floor can move in a minor; the changelog says so when one does.

EngineFloorWhat sets itTested in CI
Chrome, Edge111 (March 2023)color-mix(), oklch()Chromium 149
Safari, iOS Safari16.2 (December 2022)color-mix()WebKit 26.5
Firefox121 (December 2023):has()Firefox 151
Samsung Internet22oklch()

The tested builds are the ones Playwright 1.61.1 pins. pnpm e2e drives the example apps through them — every app on Chromium, five on WebKit, two on Firefox (smoke and the axe audit); the split and the reason for it are in the accessibility guide.

  • color-mix() — Chrome 111, Safari 16.2, Firefox 113. The soft buttons, the alerts and badges, the mark’s tint, the focus ring, the success and error surfaces. Without it the declaration is dropped: the chat works, those tints are missing.
  • oklch() — Chrome 111, Safari 15, Firefox 113. The two theme surfaces mixed in that space; same failure shape as above.
  • :has() — Chrome 105, Safari 15.4, Firefox 121. The mark on a checked choice row, the invalid border on a field group, and the focus ring on a choice row — a keyboard user’s only sign of where they are, which is why it sets a floor rather than counting as a nicety.
  • ES2022 — the JavaScript is emitted as ES2022 and not transpiled below it. The platform APIs it needs — Custom Elements v1, MutationObserver, ResizeObserver, ReadableStream, AbortSignal.timeout() (Safari 16) — are all older than the CSS floors above.
  • Relative colour syntax (oklch(from …)) derives the ink on a fill, so a solid button stays readable on whatever brand colour you set. It sits under @supports: a browser without it (Chrome before 119, Safari before 16.4, Firefox before 128) gets the fixed --aparte-on-intent fallback — readable on the default palette, not recomputed for yours. See the ink on a fill.
  • @container narrows the message padding below 520px. Without it (Chrome before 106, Safari before 16, Firefox before 110) the wide defaults stay.
  • content-visibility is a rendering hint and is ignored where unsupported.

Two browser APIs exist only on https:// and localhost: crypto.randomUUID and navigator.clipboard. Core reaches both through a fallback — uuid() and copyText(), both exported — because http://192.168.1.x with a local model on the LAN box is the deployment this library was written for. The copy buttons use execCommand('copy') there; ids come from a cheap non-cryptographic generator. pnpm check:secure-context keeps every call site on the fallback. Both functions are listed in the pieces core exports.

FloorTested in CI
Node.js18 (engines.node)18 and 24

What runs in Node is the node export condition of @aparte/core — types, the parsers, AparteClient and the transports — which registers no element and touches no DOM at import, so a server-rendered page can import the package safely; @aparte/engine is headless throughout. On 18, CI builds every package, runs the unit suite and enumerates that Node entry (check:node-import); on 24 it runs the full gate. Request, ReadableStream and structuredClone are not the same surface on the two, which is why both run rather than one.

A test runner is Node too, so it takes that same node condition and no <aparte-*> element upgrades under jsdom — point it at @aparte/core/browser instead.

WrapperPeer rangeTested with
@aparte/reactReact 18 or 19typechecked against both; the example runs 19
@aparte/vueVue 3.5+3.5
@aparte/svelteSvelte 4 or 5one example each, both in the browser suite
@aparte/angularAngular 19.2+19.2

Each wrapper declares @aparte/core as a peer at the release it shipped with (the range is written into every wrapper’s peerDependencies at release time): install both from the same release. @aparte/plugin-model-selector carries the same four peers, all optional.

ESM only — no CommonJS build, no UMD. Every package ships an exports map and a sideEffects field that names the CSS and the element registrations, so a bundler keeps customElements.define and drops what you do not import. That is checked on every gate with publint and @arethetypeswrong/cli on its esm-only profile, which also names what is not supported: require() of a package, and TypeScript’s moduleResolution: "node". Use bundler, node16 or nodenext.

Vite, webpack 5, Rollup, esbuild and Parcel 2 all read that map. Without a bundler, the packages load as ES modules from a CDN — the vanilla path in getting started. The .d.ts files are emitted by TypeScript 5.7; the examples compile them from 5.4 up.

At or above a floor, a break is a bug: open an issue with the engine and its version. Below it, the library is not tested and not fixed — but a fallback under @supports that costs nothing above the floor is welcome as a pull request. A floor is not raised for a nicety, and not lowered for a version nobody measured.