react 0.5.0
This release published @cookieyes/react@0.5.0, @cookieyes/nextjs@0.5.0, @cookieyes/core@0.4.0, @cookieyes/cli@0.3.1 and @cookieyes/scripts@0.1.0.
Install
Minor changes
Add optional region-based regulation (geo-detection).
- New
regionconfig:detect(return the visitor's region synchronously),map(region → regulation, you own it),honorGpc(default true), andstrictest(defaultGDPR). - Resolution rules: which banner shows is geo only; a detected region maps to your regulation; unknown/failed detection falls back to the strictest (a required banner is never skipped); a manual
regulationalways wins (with a dev warning). - GPC: the browser's "do not sell" signal never changes which banner shows. On a CCPA banner it starts the visitor opted out, non-required categories denied, so gated scripts/iframes don't run, until they explicitly choose otherwise. Applied client-side; set
honorGpc: falseto ignore it. - New
<CookieYesProvider region={…}>(React/Next.js): supplies the regulation per request through context, so a Server Component tree renders the correct banner on the server for each visitor (no post-hydration correction). Optional and additive: without it, the hooks read the runtime as before. Pass the sameregionconfig you giveinitCookieYes. - New
useRegion()hook (React) andconsentStore.getRegion()(core) expose the decision:region,regulation,source("manual" | "detected" | "strictest"),confidence.useRegion()/useRegulation()read the provider when present.useRegulation()is unchanged in shape. region.debug: truelogs the resolved decision to the console at setup; a quick check without writing component code.- Self-hosted: the detected
regionis included on the consent-log payload. - New
regionFromHeaders(headers, { header? })reads the visitor's region from request headers on the server (defaults to the Vercel/Cloudflare headers, or a custom one): feed it toregion.detect. Works with Next.jsheaders()or any framework.
Fully optional and off by default: omit region and nothing changes.
In @cookieyes/core, @cookieyes/nextjs, @cookieyes/react.
Add consent-gated third-party integrations.
- New
@cookieyes/scriptspackage with ready-made presets, Segment, Meta Pixel, and Google (GA4, Ads, and Tag Manager via Consent Mode), plus acustomScripthelper for any other tag. Pass them to theintegrationsconfig:initCookieYes({ integrations: [segment({ writeKey })] }). Google products share onegtag.js/dataLayer, soga4()+googleAds()compose without loading the library twice. - For Google,
@cookieyes/nextjs/serverexports<GoogleConsentMode />: the deny-by-default snippet for the page<head>(also available asgoogleConsentModeSnippet()/bootstrapGoogleConsentMode()for non-Next apps), so a returning visitor's saved choice applies from first paint. - New generic integration engine in core. Each integration declares two things:
load("immediately"|"afterConsent") andonRevoke("keep"|"remove"|"silence"). The runtime loads it once its category is granted (or immediately for Google Consent Mode), and removes or silences it on withdrawal: reconciling on every consent change. - Breaking rename. The old
integrationsfield, built-in vendor stop-handlers such as{ vendor: "meta" }, is renamed tobuiltInIntegrations, because theintegrationsname now takes the new presets. Existingintegrations: [{ vendor: … }]code will no longer work as written; move those entries tobuiltInIntegrations. That field keeps working but is deprecated (logs a warning) and will be removed in a future release. If an old{ vendor }entry is left inintegrations, the SDK skips it with a targeted warning pointing tobuiltInIntegrations, rather than failing silently. - The SDK warns if the same vendor is configured in both
integrationsandbuiltInIntegrations, which would load it twice (e.g. a double-counted Meta pixel).
In @cookieyes/core, @cookieyes/nextjs, @cookieyes/react, @cookieyes/scripts.
Returning visitors no longer see the banner flash before it disappears
The server had no way to know whether a visitor had already chosen, so it sent banner markup to everyone and the client removed it after hydration. A returning visitor watched the banner appear and then vanish, which reads as a bug rather than as a remembered choice.
Three additions let the server know:
readServerConsent(cookieHeader, options?): new in @cookieyes/core. Reads a stored decision
from a request's Cookie header with no document and no browser APIs, so it works in any SSR
framework:
const initialConsent = readServerConsent(
request.headers.get("cookie") ?? "",
config
);<CookieYesProvider initialConsent={…}>: new prop in @cookieyes/react. Given a decision, the
banner is never rendered: absent from the HTML rather than present-then-removed, so there is nothing
to flash.
getServerConsent(options?): new in @cookieyes/nextjs, from the @cookieyes/nextjs/server
subpath. Reads cookies() for you in the App Router:
import { CookieYesProvider } from "@cookieyes/nextjs";
import { getServerConsent } from "@cookieyes/nextjs/server";
export default async function RootLayout({ children }) {
const initialConsent = await getServerConsent({ regulation: "GDPR" });
return (
<CookieYesProvider regulation="GDPR" initialConsent={initialConsent}>
{children}
</CookieYesProvider>
);
}It lives on a separate subpath because it imports next/headers and must stay server-only: the
main @cookieyes/nextjs entry is "use client".
readServerConsent returns null, meaning "show the banner", for a first-time visitor, a cookie
recording no choice yet, a corrupt cookie, or one written against a different category taxonomy. That
last rule mirrors the client's exactly, including the exception that honours a legacy cookie with no
taxonomy stamp on the built-in five categories, so an upgrade never re-prompts existing visitors. If
the two ever disagreed, the banner would flash again.
initialConsent is a provider prop rather than an initCookieYes option deliberately: the consent
runtime is a module-level singleton shared across concurrent server requests, so per-visitor state
stored there would leak between visitors. React context is per-request.
Purely additive: omitting initialConsent leaves rendering byte-for-byte as it was.
In @cookieyes/core, @cookieyes/nextjs, @cookieyes/react.
Add @cookieyes/react/critical.css: the paint-critical banner stylesheet
styles.css is ~25 KB and styles every surface: banner, preferences dialog, opt-out flow,
toggles, revisit widget, reload notice. If your bundler puts it in the critical path: what
an app-root import normally does: the banner is already styled at first paint and you
need nothing new.
For anyone who would rather keep that 25 KB off the critical path, critical.css contains
only the rules needed to render the banner (~1.6 KB gzipped). Inline it in <head> and load
the full sheet without blocking render:
<style>
/* contents of @cookieyes/react/critical.css */
</style>
<link
rel="stylesheet"
href="…/styles.css"
media="print"
onload="this.media='all'"
/>Every rule in it is byte-identical to the same rule in styles.css, enforced by a test, so
the two can never disagree about how the banner looks. It is a supplement, not a
replacement: keep importing styles.css, or the preferences dialog will be unstyled when a
visitor opens it.
Purely additive: styles.css is unchanged and existing setups need no edits.
In @cookieyes/react.
Patch changes
Point the generated Next.js layout at the returning-visitor setup
The scaffolded app/layout.tsx is a Server Component, which is where a returning visitor's stored
consent has to be read. It now carries a short comment explaining that by default the banner is
rendered for everyone and removed after hydration, which returning visitors see as it appearing and
then vanishing, and linking to the @cookieyes/nextjs README section that shows how to avoid it
with getServerConsent().
No change to what the scaffold does; the generated app behaves exactly as before.
In @cookieyes/cli.
A failing third-party tag can no longer strand the consent banner, or stop the SDK mounting
Two ordering/isolation fixes on the consent path. Both address the same failure: side effects that run third-party code were able to throw before the SDK told anyone about the consent decision.
Accepting or rejecting. persist() ran gated-script injection, integration stop handlers and
the Google Consent Mode broadcast before notifying subscribers. Two of those can throw for reasons
outside this SDK's control: script injection touches the DOM, and the Consent Mode broadcast calls
dataLayer.push, which Google Tag Manager replaces with its own function that runs
customer-authored templates. When one threw, notify() never ran: the consent cookie recorded the
visitor's choice, but the banner stayed on screen until the next page load. The click looked like it
had done nothing.
The decision is now committed (cookie written, categories committed) and broadcast to subscribers first, and each side effect afterwards is isolated, so no single failure can strand the banner or prevent the others from running.
Mounting. The same broadcast also runs at load, inside createConsentManager, where it was
likewise unguarded, so a throwing dataLayer.push propagated out of initCookieYes and the SDK
never mounted at all. No banner, no consent prompt, caused by an unrelated broken tag. That call and
the load-time stop-handler pass are now isolated too.
Notes on observable behaviour:
onConsentUpdatestill fires synchronously within the accept/reject call, as before.- The Consent Mode broadcast still happens in the same task as the click, so tags see the update immediately.
- Consent-gated scripts are now injected just after subscribers are notified rather than just
before. If you have a callback that inspected the DOM for an injected
<script>synchronously inside a consent listener, it will no longer find it there. - This is not a latency change. Reordering work inside one task cannot make the browser paint sooner; measured click-to-visible-response is ~11ms either way.
In @cookieyes/core.
Add _clearScriptRegistry(): an internal, test-only reset for the consent-gated script registry, mirroring the existing _clearStopHandlers().
The registry is module-level with no way to empty it, so a script registered in one test stayed registered for the next. It now clears both the registrations and the record of what was injected, removing the injected <script> elements from the document when there is one.
Consumed by the new @cookieyes/test package to guarantee a clean slate per test.
Also add CORE_VERSION: the version this build was produced from, injected at build time from package.json so it cannot drift from the manifest. @cookieyes/test reads it to warn when a project's test double and engine versions don't match, rather than silently testing against rules the project isn't shipping.
Both exports are additive: no existing symbol or behaviour changes.
In @cookieyes/core.
Fix the banner painting unstyled on first load, and replace its slide-in with a fade
The banner now looks right on the very first paint. cookieyes.css referenced
var(--cy-primary), var(--cy-bg), var(--cy-text) and the rest of the --cy-* tokens
without declaring any of them: the values only arrived once useThemeVars ran after
hydration. Until then the server-rendered banner painted with a transparent background,
no border radius and the host page's font. The stylesheet now ships :root defaults (plus
a prefers-color-scheme: dark block, matching the default colorScheme: "system"), so the
banner is correctly styled before any JavaScript runs.
Custom themes are unaffected: useThemeVars still applies your theme config to each
component container via element.style.setProperty, which beats a :root rule, and it
still uses the CSSOM rather than a generated <style> block, so strict style-src CSP
support is unchanged.
The entry animation is now an opacity-only fade. It was cy-slide-up: 0.5s, starting
from opacity: 0 and translateY(40px), which left the banner effectively invisible for
the first half-second after the page painted, and read as content sliding over the page.
It is now cy-fade-in 0.2s ease-out, and the exit animation cy-fade-out no longer
translates either. Neither keyframe set touches transform or any layout property, so
layout shift stays at zero.
If you were targeting @keyframes cy-slide-up or overriding .cy-banner's animation
in your own CSS, update it to cy-fade-in. prefers-reduced-motion: reduce continues to
disable the animation entirely.
In @cookieyes/react.
The banner no longer fades in twice on slower devices
The banner is server-rendered inline (React cannot server-render a portal), then moves into a
<body> portal just after hydration so it can escape any transformed ancestor. That move replaces
its DOM node, and the replacement re-ran the CSS entry animation.
On a fast machine this was invisible: the swap lands inside the 200ms fade, so it reads as one continuous ramp. On a slow device it was not. Measured under 20× CPU throttling, hydration landed around a second in, long after the fade had finished, so the visitor watched a fully visible banner disappear and fade in again. Opacity dropped by 0.73–1.00 at the swap.
Banner.Root now marks the re-parent, and the replacement keeps the banner visible instead of
re-animating. Measured opacity drop after the change: 0.000, on both a fast machine and under 20×
throttling.
Unchanged: the banner still animates when it genuinely appears, including when it reappears after
resetConsent(), and the exit fade still plays on accept/reject.
If you override .cy-banner's animation in your own CSS, note the new
.cy-banner-wrap[data-cy-entered] .cy-banner:not([data-leaving]) rule, which sets
animation: none for the re-parent case only.
In @cookieyes/react.
Minify the shipped stylesheets
dist/styles.css was copied verbatim from source, so consumers downloaded the source comments:
and the source is deliberately heavily commented, because several rules encode non-obvious
reasoning. The build now strips comments and collapses whitespace, taking styles.css from 4.80 KB
to 3.68 KB gzipped.
The transform is deliberately conservative: comment removal and whitespace collapsing only, no
value shortening, no rule merging, no reordering, so it cannot change what the CSS means. Space
after : is even left intact, since collapsing it is only safe inside a declaration and not in a
selector. It is verified by tests asserting every declaration survives, braces stay balanced, and
the constructs this sheet relies on (calc(), color-mix(), quoted font names, :where(),
attribute selectors) come through unchanged.
Net effect of this release on what an existing consumer downloads: 0.87 KB gzipped smaller, with the SSR, first-paint and consent-isolation work included.
In @cookieyes/react.
Source
Read the full diff and commit history for react 0.5.0 on GitHub.