npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vitreajs/vitrea-web

v0.27.0

Published

Framework-agnostic browser host for vitrea: mount a glass root from any framework, or none. DOM host registration, plane management, backdrop proxies, CSS-tier renderer, WebGPU lifecycle.

Readme

vitrea-web

The browser host for vitrea — mount a glass root from any framework, or none.

vitrea replicates Apple's Liquid Glass material on the web: real-time size-parameterized lensing, per-element backdrop adaptation, container-scoped sampling, and shape-to-shape morphing. @vitreajs/vitrea is the pure runtime and holds no DOM code by design. This package is the half that touches the browser — element registration, the plane sandwich, per-group backdrop proxies, the conformance probe, the CSS-tier renderer, and the WebGPU lifecycle — and it is what turns the runtime into pixels.

If you are writing React, install @vitreajs/vitrea-react instead; it depends on this package and gives you components. Reach for this one when you are writing plain JavaScript, or an adapter for Vue, Svelte, Angular or Web Components. It is the same createGlassRoot the React bindings themselves are built on: there is no privileged path.

Designing with the material. Which layer a thing belongs to, how a size family, curvature, colour and motion are decided, and how each decision maps onto this host's API (the vanilla path included) is the materialist guideline in this repository, skills/materialist/SKILL.md, with the measured optics in its references/optics.md and the cookbook in references/vitrea.md. It stands on its own, and it is also a skill the designer Claude Code plugin loads for an agent building with vitrea.


Install

npm install @vitreajs/vitrea-web

@vitreajs/vitrea is this package's one declared dependency and is installed for you. There are no others — the geometry kernel, the motion kernel and the WebGPU renderer are internal and bundled in at publish time.

TypeScript. The published declarations resolve on their own, with no types entry and nothing extra installed, including with skipLibCheck: false. The two WebGPU names the surface uses are declared inside the artifact and merge with your own WebGPU types (TypeScript 6's DOM lib, @types/web, or @webgpu/types) rather than competing with them.


Quickstart

import { createGlassRoot, GLASS_CHANNEL_PROPERTIES } from "@vitreajs/vitrea-web";

// 1. The root builds the plane sandwich and starts a frame loop.
const root = createGlassRoot();

// 2. A sampling group: one backdrop read, shared by every surface in it.
root.registerGroup({ id: "controls" });

// 3. Your element, placed by you. vitrea never creates or moves your DOM.
const button = document.createElement("button");
button.textContent = "Share";
button.style.cssText = "position:absolute;left:120px;top:220px;width:160px;height:44px";
root.plane("base").hostLayer.append(button);

// 4. Bind it to the scene. The handle is how you patch it afterwards.
const handle = root.registerHost({
  host: button,
  groupId: "controls",
  shapeFamily: "fixed-rounded-rect",
  radii: [14, 14, 14, 14],
});

// 5. Per-frame work joins the loop the root already runs.
const stop = root.subscribe(({ deltaMs }) => {
  // advance your own springs, write the material's channels
  button.style.setProperty(GLASS_CHANNEL_PROPERTIES.press, "0");
});

// Teardown, in the order things were built.
stop();
handle.release();
root.removeGroup("controls");
root.destroy();

A complete, runnable version of exactly this — with interaction wired up — is e2e/fixtures/vanilla.ts. It is executed against Chromium, Firefox and WebKit on every run of this package's suite, so the snippet above is a tested path rather than a promise.


What you get, and what stays yours

Yours. Every element. registerHost binds an element you created and placed; it never wraps, replaces or reparents it. That is why a glass button is a real <button> — focusable, IME-capable, announced as a button — with the material drawn on canvases above and below it rather than instead of it. The one property vitrea takes ownership of on a registered host is transform, and it says so (host-inline-transform fires in dev mode if you had one).

Ours. The plane sandwich (root.plane(...) hands you the layer to append into), the per-group backdrop proxies, the batched layout read — the steady state performs no layout reads at all — the tier decision, and everything the material writes.

Material presence without a framework

const surface = root.registerHost({ host: button, groupId: "controls", present: true });
surface.update({ present: false }); // Animate the glass to identity; the button stays.
surface.update({ present: true });  // Reverse from the current material value.

present defaults to true and is independent of hover, press, focus and disabled state. An initially absent surface starts at identity. Changes use the motion kernel's monotonic 220 ms ease; Reduced Motion steps to the destination on both tiers. That ease is the kernel's built-in default and this root takes no motion profile of its own, so it is the same ramp for every app; retuning it means publishing the channel yourself, below. release() still tears down synchronously, so animate out by changing presence before deciding when to remove your content.

The root publishes --vitrea-materialization in [0, 1] on the host, consumed by both tiers. A custom motion binding can publish that channel directly instead of using present. It scales optical material terms, not element opacity: at 0 the glass is absent while your element, geometry, semantics and published foreground tokens remain. Never fade the host or its ancestors as a substitute — doing so cuts off backdrop sampling. Content visibility, pointer handling and contrast over the uncovered page remain the app's responsibility. The timing is authored and has no measured native reference; the tier-specific optical limits are recorded in claims §5.132.

Tint without a framework

import { glassTint } from "@vitreajs/vitrea";

// A group's seed is core's parsed tint, not a string, and rides the group's
// material profile — so the profile's `variant` comes with it.
root.registerGroup({
  id: "controls",
  material: { variant: "regular", tint: glassTint([1, 0.584, 0]) },
});

// A surface takes any CSS colour the engine parses, and overrides its group.
const handle = root.registerHost({ host: button, groupId: "controls", tint: "#ff9500" });

handle.update({ tint: "rgb(255 149 0 / 50%)" }); // the colour's alpha IS the strength
handle.update({ tint: null });                   // untinted, even under a tinted group
handle.update({ tint: undefined });              // drop the override, inherit again

A surface that declares no tint inherits the group's seed; null clears an inherited one, the way Glass.tint(nil) does. In a patch the distinction is the key's presence, as it is for the other overrides on update(): { tint: undefined } removes this surface's override, while an absent tint key leaves it alone. Strength 0 is not a tint — the material is byte-identical to an untinted one.

The surface path takes a string because that is what an app already has; the group path takes glassTint([r, g, b], strength), sRGB channels in 0..1. To hand a group a CSS colour instead, parse it the way the React binding does — createTintParser(document) once per document, then resolveTintDeclaration(colour, parse, groupId, root.diagnostics). A colour neither parser resolves leaves the surface untinted and reports tint-unparseable rather than guessing at it; outside a browser (jsdom, SSR) only the numeric syntaxes resolve, so write #rrggbb, #rrggbbaa, rgb() or rgba() there.

A group is one optics pass carrying one seed, so a group tint plus a member that overrides it is two: dev mode reports tint-mixing, and the tiers disagree there — WebGPU paints them all with the first surface's colour while the CSS tier honours each. Give a second colour its own group. The tint is a seed the material tone-maps against the backdrop it is already sampling, not a fill, and it never moves the material's own opacity; the foreground tokens below are published against the tinted material, so the ink follows the colour without the app deciding it again.

Foreground tokens and ownership without a framework

Every registered host publishes --vitrea-foreground and its -secondary, -tertiary and -quaternary levels. The primary is Apple's automatic macOS label ink through the vibrant operator; secondary is raised where the primary can hold WCAG 4.5, while tertiary and quaternary deliberately carry no body-text floor. Use the tokens directly when the app owns its content.

Set vibrant: true on registerHost or handle.update() when vitrea owns the label. This does not change the token value. It raises the runtime's color rule from the zero-specificity :where([data-vitrea-node]) path to [data-vitrea-vibrant], so it survives a reset such as button { color: … }; set it back to false to return to the token path. An application rule that names the element still wins. Presence 0 removes the vibrant marker so identity leaves content exactly as the app wrote it.

The pieces

| What | Where | | --- | --- | | mounting, frames, registration | createGlassRoot, GlassRoot | | per-element handle | registerHost → GlassHostHandle (update, promoteTo, setOwnedTransform, release) | | planes | root.plane(plane) → PlaneLayers; GLASS_PLANES | | interaction channels | GLASS_CHANNEL_PROPERTIES — write 0..1, the material reads | | findings | root.diagnostics, consoleDiagnosticSink(), VitreaDiagnostic | | capability answers | root.capabilities(groupId), root.accessibility, root.webgpu, root.colorScheme, root.windowActivation | | how far apart two groups must sit | samplingPaddingFor({ members, material, profile?, cssTierMapping? }) — omit profile for the default document, pass it as undefined for an endpoint that has none | | which measured material draws | materialProfileDocument, macos27MaterialProfileDocument, macos27Glass025MaterialProfileDocument, macos26MaterialProfileDocument, root.material (.glassTintAmount names the slider position) | | colour scheme | colorScheme: "light" \| "dark" \| "auto", root.setColorScheme | | window activation | windowActivation: "auto" \| "active" \| "inactive", root.setWindowActivation, setWindowActivation(root, value), recededMaterialProfile | | WebGPU | renderer: "webgpu", root.ready(), root.replaceDevice(device) |

Everything else this package exports is exported because the React bindings and the WebGPU renderer compose against it directly, and because a test should be able to reach the decision that failed rather than the whole runtime. Those are public but not the path an app takes.


Colour scheme

The material is measured per colour scheme, so a dark page needs the dark material rather than the light one dimmed. Ask for it at the root:

const root = createGlassRoot({ colorScheme: "auto" }); // "light" | "dark" | "auto"

"light" is the default. The scheme picks one endpoint out of the material document the root selected — under the default document that is the macOS 27 light or dark patch, and under macos26MaterialProfileDocument it is the macOS 26.5 pair, whose dark half is the exported darkMaterialProfile and whose light half is the renderer's own constants. "auto" follows prefers-color-scheme and re-derives both tiers when the system flips. root.colorScheme reports which of the two is actually drawing, and root.setColorScheme(...) changes it on a live root — a scheme change is a material change, not a reason to tear a root down.

An app that resolves its own scheme somewhere vitrea cannot see can name the endpoint directly (materialProfile: macos27DarkMaterialProfile), and an app that wants a scheme's material with a tuning of its own can do both: the scheme selects the base and materialProfile merges over it, leaf by leaf. Take the endpoint from the document the root is drawing — a macOS 26.5 patch over the macOS 27 base is neither measured material.

Which macOS the material is measured against, and how to choose

Every optical number in this package is measured against Apple's own material on a Mac, and macOS 27 changed that material under every app — the body's level over a dark backdrop, the rim's amplitude and width, the outer shadow, how much of a backdrop's structure survives, and what a surface becomes when its window loses focus. From 0.19.0 a page draws the macOS 27 material by default, measured at the Glass appearance slider's system default, 0.5, which is what a Mac nobody has adjusted draws.

macOS 27 also gives its user a Glass appearance slider (NSGlassTintAmount), and the material moves with it. Two positions are measured and shipped: macos27MaterialProfileDocument, the default, at 0.5, and macos27Glass025MaterialProfileDocument at 0.25, where Apple's glass is clearer. They are two fixed settings, not a range. There is no continuous slider and no document for any other position, because a position between or beyond them would draw numbers nobody measured.

That default is a selection, not a rewritten constant. The renderer's own DEFAULT_MATERIAL_PROFILE still holds the macOS 26.5 light material, and every shipped material is a patch over it, so both macOS 26.5 documents keep the fingerprints they were recorded with while what a page draws moves.

import {
  createGlassRoot,
  macos26MaterialProfileDocument,
  macos27Glass025MaterialProfileDocument,
} from "@vitreajs/vitrea-web";

createGlassRoot({ container });                                       // macOS 27, slider 0.5
createGlassRoot({ container, materialProfileDocument: macos27Glass025MaterialProfileDocument });
createGlassRoot({ container, materialProfileDocument: macos26MaterialProfileDocument });

@vitreajs/vitrea-react takes the same value as a prop: <GlassRoot materialProfileDocument={macos27Glass025MaterialProfileDocument}>.

A root reads its document once, at construction, so switching material on a live page means destroying the root and creating another (in React, remounting <GlassRoot>). That is how a page has always moved between macOS 26.5 and 27.

A document is one value, and it carries four patches and a mapping. That is what makes the option worth having: a material lands on two tiers and in two window poses, and the parts have to travel together.

| what a document holds | why it is in there | | --- | --- | | the active patch, per colour scheme | the material is measured per scheme, and only one scheme's numbers can be the renderer's defaults | | the receded difference, per colour scheme | an unfocused window's material is its own measurement — its rim collapses, its tint keeps its shade and loses its chroma, and its body carries its own backdrop tone response. On neither generation does a receded surface cast an exterior shadow: from 0.22.0 the macOS 27 endpoints' six occlusion anchors, liftAmplitude and reducedTransparencyOcclusion are 0, as the macOS 26.5 endpoints' always were (claims §5.168 §4) | | cssTierMapping | what that same material costs as one backdrop-filter plus an rgba() overlay — macOS 27 sets blurSigmaScale to 2.2 against the module default of 1, which is the CSS tier's whole share of the 27 diffusion refit |

The three shipped documents and the calibration documents they are generated from:

| document | profile documents | | --- | --- | | macos27MaterialProfileDocument (the default) | packages/calibration/profiles/apple-macos-27.0-1x-{light,dark}-standard-glass0.5{,-receded}.json | | macos27Glass025MaterialProfileDocument | …/apple-macos-27.0-1x-{light,dark}-standard-glass0.25{,-receded}.json | | macos26MaterialProfileDocument | …/apple-macos-26.5-1x-light-standard.json (the identity with the renderer's own constants), …/apple-macos-26.5-1x-dark-standard.json, and the receded endpoints fitted into src/receded-profile.ts |

The individual patches are exported by name as well — macos27LightMaterialProfile, macos27DarkMaterialProfile, macos27RecededMaterialProfile, macos27CssTierMapping, their macos27Glass025… counterparts, darkMaterialProfile, recededMaterialProfile — for an app composing a material of its own over one of them. materialProfile and cssTierMapping still take a patch each and merge over whatever the document selected, which is the shape to reach for when tuning one leaf rather than choosing a reference.

What drew is a readout, not an assumption. root.material and every group's resolved state (GlassGroupState.materialDocument) name the endpoint that actually drew — its profile key, its resolvedMaterialSha256, the slider position its document was measured at, and whether an app merged a patch of its own over it:

root.material;
// { name: "apple-macos-27.0-glass0.5", platform: "macOS 27.0", glassTintAmount: 0.5,
//   profileKey: "apple-macos-27.0-1x-dark-standard-glass0.5-receded",
//   resolvedMaterialSha256: "7c454858a3cbad5b", tuned: false }

glassTintAmount is 0.5 or 0.25 under the two macOS 27 documents, and absent, not 0.5, under macos26MaterialProfileDocument: macOS 26.5 had no slider, so its material says nothing about a position.

The four digests moved again at 0.22.0, and this time the shadow's EXTERIOR did (claims §5.168). spreadPx — the distance the shadow's silhouette is grown by before it is blurred — was 3.10 CSS px inherited from the macOS 26.5 default and had never been fitted on the macOS 27 bed; it ships at 0.50 on the light document and 1.80 on the dark one, which are the two beds' own readings, with offsetPx unmoved at 7.95 because a rendered coordinate step over Apple's own interval moves the fit's objective by 4 × 10⁻⁷. The six occlusion anchors are re-solved against what the shadow does between 3 and 48 CSS px, and the dark document's σ slope moves inside B1's window. And the two RECEDED documents' amplitude is 0: Apple's unfocused window removes no light at all from 3 CSS px outward, on 121 of 121 non-holdout inactive cells and 235 of 235 on the frozen macOS 26.5 bed, where vitrea had been drawing the active shadow leaf for leaf. So a window that loses focus drops its shadow and outerShadowReachPx returns 0 in that pose — fading out on the CSS tier, where the box-shadow carries a transition, and in one frame on the WebGPU tier, whose two poses are fixed endpoints rather than an interpolation. W32's four digests were 40a6dec2dc34c748 (light), bd1814fac34f9b30 (dark), f34dcc03e2774db3 (light receded) and 6b6237b7ae241638 (dark receded).

W33 G1b's lift stand-down and thick-anchor compensation (claims §5.172) move the active digests to dcbccbd9feac9881 (light) and e59f9106bcd7c966 (dark). The receded digests above and both frozen macOS 26.5 digests are unchanged; the prior readings remain here as history.

0.24.0 carries W36's black-only seal (claims §5.179–§5.180): be13dae45098fc89 (light active), 2a4323f33df8d799 (dark active), b0d0d8dacc6a03af (light receded) and 7c454858a3cbad5b (dark receded). A separate identity-gated response below encoded input 0.003 closes the measured black fallback without moving the old middle or impulse response. Thick black is extrapolated from thin; the grey/chroma and rim gaps remain. The two frozen macOS 26.5 digests stay unchanged. CSS derives the same response, with a measured +1/+2-code active-black boundary residual and exact receded black. The separate receded colour-formula trial was declined on level-growth and structure stops; footprint locality is retained. L1 now gates fixed-native-mask mean linear level error ≤ 0.055 and growth ≤ 0.005 against W33 on 140 standard WebGPU calibration/validation rows: 136 measured, four UNMEASURED, two named 0.066 misses. That is not a deep-body or an all-cell-pass claim (claims §5.180).

0.27.0 refits the clearer glass's light body at Retina scale (claims §5.205–§5.207). The two light -glass0.25 documents now read 3741b22934f17f4d (active, from 50430fa62c1120bd) and c4ca0e1cd6791bde (receded, from 5d8680980b7aeb55). The 0.25 dark pair (b074fc6913a91c66 / 280f0fddf014e0f6), the four 0.5 digests and both macOS 26.5 digests are unchanged, and so is every 1x surface. The WebGPU tier now grades the second heavy blur tap's share by the surface's span (sizeHeavySecondShareFar2x, 0 in every other document), and moves the 2x floor and span top with it. Against Apple's 2x render, the fine-checker texture error falls from 0.6462 to 0.2190. Eleven cells whose error grew, and the receded photo's structure, ship named rather than gated. The CSS tier keeps that document's 2x floor and span top at 0.26.0's values (0.6 and 256; CSS_DECLINED_SIZE_GLASS025_LIGHT). It draws no second tap, so the two leaves alone would wash its coarse checkers out; an app's own value for either leaf still reaches both tiers.

0.23.0 carries that declaration, not a new rim model (claims §5.173). liftAmplitude is 0 on all four macOS 27 endpoints; the frozen 26.5 active material keeps 0.01 / 0.0051. “Zero over black” for the leaf means a uniformly black source: its 40 px blurred input can lift locally black pixels beside light. The active thickOcclusionAt96/128/160 anchors compensate at 0.0870 / 0.1650 / 0.2518 light and 0.1038 / 0.2188 / 0.3443 dark; the six receded anchors remain zero. C1 passes on all twelve strata, with a near/far trade rather than unchanged 3–6 bands. X1 now gates the WebGPU black floor at zero on 218 non-holdout standard single-shape cells, both poses; the CSS tier and composites are not claimed by that row. Apple's contour term is present but unidentified here and remains undrawn under W33 Decision Log 3; §5.171 records the native capture needed to separate its geometry and colour.

That digest moved at 0.21.0, and one of the two reasons is not a rendering change. The four macOS 27 documents were refitted — 3dc24a74f17fd87e (light), 8a43f54162606db4 (dark), ab3ed65aa02869b1 (light receded) and e1f42c5656ef392f (dark receded) — and, separately, the fingerprint's own definition changed. From 0.21.0 a leaf whose resolved value equals its declared inert identity is dropped before hashing, so adding an operator at its identity moves no document's digest at all. The visible consequence is that the two frozen macOS 26.5 documents report the numbers they were sealed at — b2b570e4adcea8fb and 874be66ea501621b — instead of the numbers an inert leaf had pushed them to at 0.20.0, while drawing exactly what they drew. An app comparing a digest against a literal recorded under 0.19.0 or 0.20.0 has to re-record it; a digest is a name for a material and this release renamed some of them without repainting them.

The -glass0.5 in a macOS 27 key is that release's appearance slider, NSGlassTintAmount, at the position a Mac ships with; the material was measured there. The documents themselves stay in the repository rather than in the published tarball — a published package that loaded a calibration file would be shipping a data dependency for numbers that never move between releases — and packages/platform-web/src/macos27-profile.ts is generated from them by scripts/generate-macos27-profile.mjs, pinned leaf for leaf by packages/calibration/test/macos27-profile-export.test.ts. A -glass0.25 key is the same slider at 0.25; src/macos27-glass025-profile.ts is generated from those four documents by scripts/generate-macos27-glass025-profile.mjs and pinned by the same test.

Upgrading to 0.19.0 changes what your page looks like. That is the point of the release, and it is the one thing to know before taking it: surfaces over dark backdrops are no longer nearly invisible, the rim and the outer shadow are different, and CSS-tier visitors get a wider blur. Pin macos26MaterialProfileDocument to keep exactly what 0.18.0 drew.

0.20.0 moves the outer shadow again, and this time by the size of the surface. On macOS 27 a wider surface casts a wider-blurred shadow; on macOS 26.5 it did not, which is a measurement and not an absence. So the macOS 27 documents carry a line in the casting span rather than one width, and the effect is two-sided: a 44 px control's blur falls from 22 CSS px to 4.3 and a 160 px panel's rises from 22 to 34.7. What a layout feels is the sampling geometry moving with it — a group's shadow reach falls about a third at a small caster and rises about a third at a large one, and keeps rising above the bed — so a control packed against a neighbour reads as further from it and a large panel near a viewport edge reaches further. macos26MaterialProfileDocument is unaffected: that material's σ is span-invariant and its three new leaves are zero, which is where its own measurement puts them.

0.21.0 gives the body the backdrop's colour, on the WebGPU tier. Over a photograph, a gradient or any coloured backdrop, a surface used to render a grey of about the right lightness: the body is a neutral plate composited over the blurred backdrop, so what a backdrop's hues survived at was 1 − alpha — about half of them in the light material and a tenth in the dark one — against a reference that keeps 0.71 to 0.83 and 0.90 to 0.97 of its own. Now the composited colour's chromaticity is restored toward the blurred backdrop's by a fitted fraction per colour scheme and per window pose.

The after, in the same statistic as the before, because they were not the same one (2026-09-21, review closure; claims §5.165 §9, finding R5). The reference figures above are a measured MEAN — the body's per-pixel chroma over the backdrop's — and the light material's "about half" is not: it is 1 − alpha, the fraction the plate transmits, where the same measurement read 0.24 to 0.33 before the fit. Measured the same way after it, the body keeps 0.51 to 0.57 of the backdrop's chroma on a focused light window and 0.58 to 0.66 on an unfocused one, 0.35 to 0.37 and 0.22 to 0.23 on the dark scheme's, against the reference's 0.71–0.83 and 0.90–0.97. What the fit was judged on is a different statistic again — the chroma-to-structure SPREAD ratio, in which the blur cancels and which is what the eye reads as colour in the body — so these means are the record's other column and not the bound.

The level does not move: both ends of that mix carry the same linear luminance by construction, and gamut is taken by scaling chroma toward the neutral at a held luma rather than by clipping a channel. An author's tint still displaces the result exactly as it did, because the tint's shade law reads a luminance the restoration preserves. Over a NEUTRAL backdrop nothing changes, to the bit — restoring toward a chromaticity that is not there is the identity.

Three things to know before taking it.

  • The CSS tier carries none of it, and that is a measurement rather than an omission. The mirror was written — a gain on the one saturate() that tier already has, at the alpha it actually solves — rendered on the measured bed and taken back out. On the dark scheme it bought nothing, unchanged to four decimals even at the largest retention the leaf can hold, because that tier's converted alpha leaves no backdrop underneath for a saturation to act on; on the light scheme it bought a great deal and broke the material's own level and structure stops, because saturate() is a matrix on sRGB-encoded channels and stops preserving luminance the moment one clips. So a CSS-tier visitor draws the body they drew at 0.20.0, and BODY_CHROMA_RETENTION is exported beside the tier's other mirrored constants at 0 — the constant that re-opens the decline if it ever leaves that value.
  • Under an accessibility OCCLUSION lift the retention stands down entirely. A lift sends the plate's alpha to alpha + lift*(1 - alpha), so the plate covers more of the backdrop and restoring the nominal fraction of its chromaticity gives back what the preference asked to have covered up. Reduce Transparency raises that occlusion and stands the operator down; those pages draw what 0.20.0 drew, to the byte. Increase Contrast alone does not — it raises no occlusion of its own, and macOS 27 decoupled the two switches — so a page under Increase Contrast by itself draws the retention at full value. That is not a regression against 0.20.0, which had no operator; it is a combination the wave's bed does not measure, and it is recorded as an open gap rather than claimed either way.
  • The fidelity target is the WebGPU tier, which is where the fit was judged and where the two adopted gate rows are stated. A page that resolves to the CSS tier gets the same geometry, the same level and the same shadow, and a colourless body.

0.22.0 fits the shadow's exterior as a whole, and takes it off a receded window. Until this release the shadow had one fitted length — the blur's σ, graded by the casting span since 0.20.0 — and its other two were the macOS 26.5 default's, carried unexamined onto the macOS 27 bed. Measured against Apple's own render, vitrea's exterior was 2.66 to 3.77 CSS px too wide at every thick span on every bed, and a green σ bound was consistent with that, because the bound compares a blur leaf in closed form to a FIT of Apple's render and the fit absorbs whatever else Apple's shadow is besides a blur. So the exterior is fitted against what it actually draws: the transmission it applies at each distance from the contour, per band and per direction, inside each capture's own clearance.

What moves, and what a page sees. The outset falls 3.10 → 0.50 CSS px on the light material and → 1.80 on the dark one — Apple's own outset measures about half a pixel — and the offset stays at 7.95. The six occlusion anchors are re-solved against the 3–48 CSS px window, and the dark material's σ slope moves inside its bound, which re-derives its thin knee: a 44 px control on the dark material now blurs its shadow at a σ of 2.72 CSS px where 0.21.0 drew 2.07. The shadow's transmission tracks Apple's to a mean of 0.0009 to 0.0039 of a unit of backdrop light at the thick spans against 0.0039 to 0.0089 before, and to 0.00004 to 0.00023 at the thin ones. The rendered blur is still 1.27 to 2.77 CSS px wider than Apple's fitted blur — halved and not closed, and recorded rather than claimed. macos26MaterialProfileDocument is untouched by all of it.

And a receded window draws no outer shadow. Apple's unfocused window removes no light at all from 3 CSS px outward: on 121 of 121 non-holdout inactive cells the native transmission reads exactly 1.000000 in every band and the capture is byte-identical to the backdrop from 2 device px out, and the same holds on all 235 frozen macOS 26.5 cells. vitrea had been drawing the ACTIVE shadow there, because both receded documents carried their active document's anchors leaf for leaf. Their amplitudes are 0 from 0.22.0, so a window that loses focus drops its shadow and a group's clip shrinks with it in that pose — fading out on the CSS tier, where the box-shadow carries a transition, and in one frame on the WebGPU tier, whose two poses are fixed endpoints rather than an interpolation. What Apple's recede does have is one device pixel of dark stroke at the contour, a rim term vitrea does not draw and which is named as an open gap.

The demo site draws macOS 27 at the default 0.5, and at 0.25 when it is loaded with ?glass=0.25; each page's selector reloads it. A document is selected at construction — a page drawing one has surfaces measured against it — so a single root cannot present two materials at once, and each page has one root. The macOS 26.5 document is not offered there.

A backdrop hint and the colour scheme are different things. A group's backdrop: { tone, luminance } declaration (React's hint prop) states the tone of what is BEHIND the surface, which is what the adaptation and the foreground decision read; the scheme states which material the surface is made of. A dark page can legitimately hand a light hint to a surface sitting over a white card.

Ordinary page content uses the profile material too. A WebGPU group over DOM content resolves samplingBackend: "css-backdrop": its proxy supplies the browser's blur and the canvas draws the profile's body, tint shade, rim and shadow at the group's known backdrop tone, with each member's own size law. This is approximate refraction, not texture sampling or a lens. A real backdrop: { tone: "dark", luminance: measuredLevel } on registerGroup (React's hint prop) can supply the tone; without a hint or another measured tone, vitrea does not guess what arbitrary page content looks like, so tone response and dark-backdrop collapse remain unavailable. A scalar level cannot describe a page's local colour or texture either. Registered image, canvas and video backdrops retain the sampled path.

The page's own background is still the page's: vitrea does not write your tokens, so an app offering "follow the system" reads prefers-color-scheme for its own colours as well as passing "auto" here.

The receded differences the pose below selects come out of the root's material document, one per colour scheme, and they are exported by name as well so an app can reach an endpoint directly — a preview that is never a window, a harness that captures the receded appearance. macos27RecededMaterialProfile.light and .dark are what a default root applies; macos27Glass025RecededMaterialProfile holds the 0.25 document's pair; recededMaterialProfile.light and .dark are the macOS 26.5 endpoints, which macos26MaterialProfileDocument selects. Reaching for one by hand means merging it over that scheme's material yourself; through the runtime it is just createGlassRoot({ colorScheme: "dark", windowActivation: "inactive" }).


Window activation

Apple's Liquid Glass recedes when its window loses focus, and vitrea models that as a pose of the root: a window is active or it is not, once per document, so no individual surface carries the answer and it is not a seventh interaction state.

const root = createGlassRoot({ windowActivation: "auto" }); // "auto" | "active" | "inactive"

"auto" is the default and follows the window's own focus. "active" and "inactive" pin the pose and win over what the window is doing, which is what a preview pane, a screenshot or a capture harness needs. root.setWindowActivation(value) moves a live root and takes effect on its next frame; setWindowActivation(root, value) is the same call as a free function, for a caller holding a root rather than writing against its methods. root.windowActivation reports which pose is actually drawing — "active" or "inactive", with "auto" already folded.

What feeds "auto" is document.hasFocus() on the window the root was created for, re-read whenever that window fires focus or blur. Visibility is a separate fact: a document behind another window can remain "visible" while unfocused, so visibilitychange does not select this pose. A blur event dispatched by page script cannot invent the answer either: the event says a reading is stale, and document.hasFocus() supplies it. Unfocused test documents (including jsdom) recede under "auto"; deterministic material tests should explicitly select their pose.

The pose is a material change and travels the same path every other one does — the receded difference for the resolved colour scheme, merged and applied through applyMaterialProfile, so the CSS tier re-derives its declarations and the WebGPU tier takes new uniforms. Two frozen endpoints, with no intermediate profile document: the existing CSS transitions carry the visual transit, while the GPU takes the selected endpoint on its next frame. It is merged over an app's own materialProfile, so where the two name the same constant the recede wins for as long as it is on.

The endpoints remove the outer shadow and the bright rim and retain an author's tint strength as an achromatic shade. The native reference is macOS 26.5, with 1x-only light accessibility evidence, and it is a fitted endpoint rather than a pixel-match guarantee: photo chroma, intermediate dark levels and large-surface scattering remain measured gaps (claims §5.130, §5.145–§5.147). In particular a full-strength neutral tint loses background colour that the native inactive material can retain.


Frames

The root owns a cadence: five scene phases per frame, driven by requestAnimationFrame. autoStart: false turns that off and root.runFrame(t) steps it by hand, which is what the calibration harness and every test in here do.

root.subscribe(listener) joins that loop and returns its unsubscribe. Listeners run after the frame, so they observe a settled scene and may register, patch and measure like any other caller. Use it rather than starting a second requestAnimationFrame of your own: two loops mean two wake-ups per frame and no declared ordering between them. deltaMs is 0 on the first frame and should be capped by whatever integrates it — a backgrounded tab delivers an arbitrarily large first step on return.

A listener that throws is reported as frame-listener-failed and unsubscribed. One adapter's bad frame must not stop the material drawing.


Diagnostics

Dev mode is on by default and findings go to the console. They are written to name what to do, not just what is wrong — a host placed outside its plane, a sampling padding below 3σ of its own blur, glass nested inside glass, an engine version with a recorded defect and its workarounds.

const root = createGlassRoot({
  devMode: process.env.NODE_ENV !== "production",
  diagnosticSink: ({ origin, diagnostic }) => report(origin, diagnostic.code, diagnostic.message),
});

Findings are deduplicated by code and subject, so a condition that persists across frames is reported once. Structural mistakes — an unknown id, a duplicate id, a still-referenced source — are not diagnostics: those throw, because continuing past them would leave a half-built scene.


License

Apache-2.0. See LICENSE and NOTICE.