react 0.8.0

Markdown
Loading…
Sep 10, 2026release
Banner CSS in the first response.

This release published @cookieyes/react@0.8.0, @cookieyes/nextjs@0.6.0 and @cookieyes/cli@0.3.3.

Install

npm i @cookieyes/react@0.8.0
npm i @cookieyes/nextjs@0.6.0
npm i @cookieyes/cli@0.3.3

Minor changes

Fix the stylesheet import for pnpm users: @cookieyes/nextjs now exports its own styles.css and critical.css.

The bug. Every piece of our documentation told a Next.js consumer to write:

import "@cookieyes/react/styles.css";

That resolves under npm and Yarn Classic, which hoist @cookieyes/react to the app's node_modules root. Under pnpm it does not. pnpm's strict layout puts @cookieyes/react in the virtual store, reachable from @cookieyes/nextjs, not from the app, so an app that installed only @cookieyes/nextjs got MODULE_NOT_FOUND, and the fix was not obvious from the error. This is the same resolution rule that already forced registerNetworkBlocker to be re-exported from @cookieyes/react, applied to a stylesheet.

Verified with clean installs of the same tarballs: @cookieyes/react present at the app root under npm, absent under pnpm.

The fix. @cookieyes/nextjs now exports ./styles.css and ./critical.css:

import "@cookieyes/nextjs/styles.css";

Each is a one-line @import of the real sheet in @cookieyes/react, generated by scripts/emit-css-proxies.mjs. The specifier is resolved from inside node_modules/@cookieyes/nextjs, where the sibling is always reachable, so it works under every package manager, and there is still exactly one copy of the CSS on disk and one source of truth for its contents. Confirmed end to end: a pnpm app depending only on @cookieyes/nextjs now builds and serves the full 17 KB sheet.

@cookieyes/react/styles.css still works and is unchanged. React consumers should keep using it; Next.js consumers should switch, and must switch if they use pnpm.

Both packages now declare sideEffects: ["**/\*.css"]instead offalse. A package that ships CSS cannot claim to be entirely side-effect-free: a bundler is entitled to drop a side-effect-only import from such a package, which would unstyle the banner with no error and no build failure. Every JS module in both packages is still pure, so tree-shaking is unaffected: pnpm size reports the same figures either way.

The CLI's Next.js scaffold, the README, the quick-start, the installation page and the styling pages now all emit the nextjs path. Three doc statements were outright wrong for pnpm users and are corrected: installation.mdx previously said the path "is @cookieyes/react/styles.css even in a Next.js app that installed @cookieyes/nextjs".

In @cookieyes/cli, @cookieyes/nextjs, @cookieyes/react.

Put the banner's CSS in the first response: <CookieYesStyles /> and a styles-route handler.

The problem it removes. The banner is server-rendered, so it is in the very first HTML, but the browser cannot paint it until the stylesheet arrives, and the stylesheet is a second round trip discovered only after the HTML has been parsed. On a slow connection that round trip costs more than the whole SDK. Measured on the Lighthouse Mobile profile (150 ms RTT, 4× CPU), cold cache: an empty Next.js page paints at 192 ms; the same page with the SDK and its stylesheet as a <link> paints at 468 ms, of which ~276 ms is waiting for a 4 KB file.

The fix. @cookieyes/nextjs/server now exports <CookieYesStyles />. Render it once in your root layout's <head> and it emits:

  1. an inline <style> holding critical.css, only the rules the banner needs to paint, so the first response is enough to paint the banner styled, and
  2. a <link> to the full stylesheet with media="print", which the browser fetches at low priority without blocking render. Once the SDK mounts, the client runtime switches it to media="all".

@cookieyes/nextjs/styles-route exports a Route Handler GET that serves the full stylesheet, so nothing has to be copied into public/ and there is no build step:

// app/layout.tsx
import { CookieYesStyles } from "@cookieyes/nextjs/server";
<head>
  <CookieYesStyles />
</head>;
// app/cookieyes/styles.css/route.ts
export { GET } from "@cookieyes/nextjs/styles-route";

Then remove import "@cookieyes/nextjs/styles.css": leaving it puts the sheet back on the critical path.

Measured (same profile, same machine, 20 iterations): first paint 468 → 224 ms, banner styled 451 → 183 ms; within 32 ms of the empty page, and within 32 ms of a competitor that ships no stylesheet at all. Neutral on fast desktop, where there is no round trip to save. Bundle bytes are unchanged; this moves when the browser may paint, not how much it downloads.

Opt-in, and the default is untouched. Nothing changes unless you render the component. The one inline element it adds is static, so a strict style-src admits it with a single hash rather than 'unsafe-inline': the value is exported as CRITICAL_CSS_HASH ('sha256-…'), published with every release that changes the stylesheet, and pins exactly that byte sequence and nothing else. If you use nonces instead, pass <CookieYesStyles nonce={nonce} />. There is deliberately no inline <script>, the print → all switch runs from the SDK bundle your policy already admits, so script-src needs nothing. Without a CSP there is nothing to configure.

How the bytes get in. The CSS is injected at build time by a sentinel-replacement plugin, the same way CORE_VERSION is, from @cookieyes/react's built output: the minified bytes a consumer actually ships. A Server Component cannot import a .css file (Turbopack rejects it as a non-ECMAScript asset), and a filesystem read fails under pnpm's layout and on Edge runtimes; a build-time string works everywhere. Tests assert the inlined string is byte-identical to @cookieyes/react/critical.css and that the exported hash is the hash of those bytes; one stray byte would break every consumer's policy silently.

In @cookieyes/react: mountRuntime now calls _activateDeferredStylesheets(), which flips any link[data-cy-full] from media="print" to all. SSR-safe, idempotent, and a no-op on pages that never rendered the component. Costs ~60 bytes gzip on the UI layers; core is untouched. Budgets pass.

In @cookieyes/nextjs, @cookieyes/react.

Take another 0.66 KB of gzip out of the initial download by loading the custom-theme runtime only when a theme is actually configured: banner + preferences + recall goes from 14.92 KB to 14.26 KB over an empty Next.js app, and the banner alone from 14.37 KB to 13.73 KB.

What was shipping, and why it was redundant. useThemeVars called computeThemeVars on every render, for every consumer, and wrote all twelve --cy-* tokens onto each component container through the CSSOM. But cookieyes.css already declares all twelve: the light defaults on :root, and the five that differ in dark mode plus --cy-on-widget-bg in a @media (prefers-color-scheme: dark) block. For the default configuration (no theme, colorScheme: "system") the JavaScript was recomputing, at hydration, values the stylesheet had already applied at first paint. That cost every consumer computeThemeVars, its CSS-value sanitiser, relativeLuminance, contrastRatio and readableTextOn.

What changed. computeThemeVars and the WCAG maths behind it moved to styles/theme-runtime.ts, reached only through an import(). With no theme, nothing loads it and the hook writes nothing at all. With a theme, mountRuntime starts the fetch the moment it sees one, not at the banner's hydration effect, so the load overlaps hydration instead of queueing behind it, and brand colours land when they did before.

An explicit colorScheme: "light" | "dark" is the one case the media query gets wrong, because it follows the device and the developer has said not to. That is now handled by a data-cy-scheme attribute on the container and two matching rules in the stylesheet (+0.03 KB gzip on styles.css), rather than by computing twelve values in JavaScript to override six.

No behaviour change. A themed container ends up with byte-identical values to the synchronous path, verified by asserting the full applied map against computeThemeVars for light, dark, and an explicit scheme overriding the device preference. The CSSOM write is unchanged, so custom theme colours still work under a strict style-src CSP with no unsafe-inline/nonce. First paint is untouched: both stylesheets still carry the defaults, and the banner still renders styled before any JavaScript runs.

total rises 0.35 KB while initial falls 0.66 KB, and the budgets in tools/size/budgets.json were moved to match in this same change. That is the honest shape of a deferral, a second chunk costs its own module wrapper, and it is why the two figures are budgeted separately. Only initial is what a first-time visitor downloads.

Worth recording: a plain if (!theme) return inside useThemeVars would have saved nothing. The static import is what ships the code, so a runtime guard leaves every byte in the bundle. Splitting the module is the only form of this change that moves the number: the same lesson as the integration runner in 0.6.0, and the reason the saving here is quoted from pnpm size rather than from reading the diff.

In @cookieyes/react.

Source

Read the full diff and commit history for react 0.8.0 on GitHub.

On this page