---
id: 09-cookie-consent-banner
epic: dsr-consent-and-cookie-banner
title: CookieConsentBanner component in core-ui
type: user-story
status: done
feature: core-ui
depends-on: [05-core-consent-react, 08-core-ui-scaffold]
blocks: [11-documentation]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T21:38:48.887Z
---

## Goal

Ship `<CookieConsentBanner>` in `@repo/core-ui` — a headless component with a EU-compliant default UI (equal-prominence Reject All / Accept All), granular category toggles, render-prop overrides for branding, `__consent_state` cookie management for the anonymous pre-signup flow, and `useConsent()` integration for authenticated state.

## Why

Downstream consumers need a drop-in cookie consent banner that satisfies CNIL guidance and EDPB Art. 7 interpretation out of the box (equal visual weight for Reject All / Accept All, no pre-ticked boxes, equal tab order). Building the component as a headless default with render-prop overrides lets consumers brand the visuals without forking the compliance logic. The Storybook story doubles as the human-readable compliance review surface.

## Done when

- `<CookieConsentBanner variant="modal">` renders with Reject All / Accept All as equal-size, equal-weight side-by-side buttons; tab order treats them equally; ARIA labels mirror; focus-trapped inside modal; ESC = "Reject All" (explicit legal choice, not silent dismiss).
- `<CookieConsentBanner variant="banner">` renders fixed to the bottom of the viewport, full-width.
- Default categories: `essential` (always enabled, non-toggleable), `functional`, `analytics`, `marketing`.
- Render-prop overrides: `renderHeader`, `renderCategoryRow`, `renderActions` — default UI works out-of-box; consumer surgically overrides.
- `__consent_state` cookie: SameSite=Lax, Secure, 1-year max-age, versioned with `_v: 1`; component reads/writes/clears.
- `onConsentChange` callback fires with updated `UserConsentState`.
- When `<ConsentProvider>` is present (authenticated context), the banner reads and writes via `useConsent()` from `@repo/core-consent/react`; when absent, the banner manages state via the cookie only (anonymous flow).
- Storybook story covers: modal variant, banner variant, render-prop override example, a11y tab-order demo.
- axe-core a11y test in Storybook passes (WCAG 2.2 AA color contrast, no violations).
- RTL behavioral tests assert: click "Reject All" → `onConsentChange` fires with all non-essential categories `granted: false`; toggle analytics → click "Save Selected" → `onConsentChange` fires with `analytics.granted: true`; ESC in modal → Reject All semantics; tab order visits Reject All before Accept All.
- Modal focus-trap test: focus does not escape the modal while it is open.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.

## In scope

- `packages/core-ui/src/cookie-consent-banner/` — component, types, cookie helpers.
- `__consent_state` cookie read/write/clear (versioned `_v: 1` shape; migrate older versions on read).
- Storybook story in `packages/core-ui/src/cookie-consent-banner/cookie-consent-banner.stories.tsx`.
- axe-core a11y integration in Storybook + RTL behavioral tests.
- Export from `packages/core-ui/src/index.ts` barrel.
- SSR-safe pattern: component is client-only; ship a `<CookieConsentBannerLoader>` SSR placeholder that dynamic-imports the actual banner client-side (see consent.md, Q3 of grill).

## Out of scope

- Per-framework router auto-wiring on consent toggle — banner emits `onConsentChange`; consumer wires (e.g., re-initialize analytics SDK).
- Cookie versioning migration policy documentation (Story 11).
- Strict-mode `ConsentCategory` declaration merging — string-literal-union escape hatch is sufficient.

## Tasks

- [x] Add `<CookieConsentBanner>` headless component in `packages/core-ui/src/cookie-consent-banner/` with `variant: "modal" | "banner"` prop, granular category toggles (essential non-toggleable), equal-prominence Reject All / Accept All buttons (CNIL compliance baked into default visual treatment), render-prop overrides (`renderHeader`, `renderCategoryRow`, `renderActions`), `__consent_state` cookie management (SameSite=Lax, Secure, 1-year, versioned `_v: 1`), `onConsentChange` callback, `useConsent()` integration for authenticated context, `<CookieConsentBannerLoader>` SSR-safe wrapper; Storybook story covering both variants + render-prop example; axe-core a11y pass; RTL behavioral tests (Reject All, Save Selected with toggle, ESC = Reject All, tab order, modal focus-trap); export from `core-ui` index; all gates pass.
