Files
agentic-dev/docs/work/epics/dsr-consent-and-cookie-banner/09-cookie-consent-banner/_story.md

4.5 KiB

id, epic, title, type, status, feature, depends-on, blocks, created, updated
id epic title type status feature depends-on blocks created updated
09-cookie-consent-banner dsr-consent-and-cookie-banner CookieConsentBanner component in core-ui user-story done core-ui
05-core-consent-react
08-core-ui-scaffold
11-documentation
2026-05-19T12:00:00Z 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

  • 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.