Cookie Consent

A consent banner with reject as easy as accept, nothing pre-ticked, and no cookie written by the component itself — the arrangement regulators have repeatedly ruled is the compliant one.

src/components/ui/cookie-consent.tsx

About

From the component's own source, where it was written.

A consent banner with the categories laid out and reject as easy as accept.

**Reject is a button of equal weight, and that is not a style choice.** Under the GDPR consent must be freely given, and both the EDPB's dark-pattern guidelines and a series of national DPA rulings have found that hiding refusal behind an extra click — or greying it out beside a bright "Accept all" — makes the consent invalid. A banner with only "Accept" and "Manage" is the single most-fined pattern on the web. This component is built so the compliant arrangement is the default one.

**Nothing is on by default except what is strictly necessary.** Pre-ticked boxes are explicitly not consent, so necessary is locked on and everything else starts off. Do not "fix" this by defaulting analytics to true.

**It does not write a cookie, and it does not load anything.** Storage and script loading are the caller's job through onSave — because where consent is recorded (cookie, backend, consent-management platform) and what it gates are product decisions, and a component that quietly sets its own cookie is doing the exact thing it is meant to be asking about.

It is role="dialog" with aria-modal="false": announced and reachable, but it does not trap focus, because a banner people cannot escape is its own accessibility failure.

Install

npx astralyx-ui add cookie-consent

Usage

import { CookieConsent } from '@/components/ui/cookie-consent'

<CookieConsent onSave={(accepted) => persist(accepted)} policyHref="/privacy" />

Composer

Change the props and the source below follows.

Props
tsx
<CookieConsent
  defaultOpen={false}
  policyHref="/privacy"
  onSave={(accepted) => persist(accepted)}
/>

Examples

Categories open

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

CookieConsent

Extends Omit<ComponentProps<'div'>, 'onChange' | 'title'>.

PropTypeDefaultDescription
titleReactNode'Cookies'—
descriptionReactNode'We use cookies to run the site, and — with your permission — to understand how it is used.'—
categoriesConsentCategory[]DEFAULT_CATEGORIESNothing optional starts on: pre-ticked boxes are explicitly not consent. necessary is locked on and excluded from the choice.
onSave(accepted: string[]) => void—Called with the ids that were consented to.
onAcceptAll(accepted: string[]) => void——
onRejectAll(accepted: string[]) => void——
acceptAllLabelstring'Accept all'—
rejectAllLabelstring'Reject all'—
customiseLabelstring'Customise'—
saveLabelstring'Save choices'—
policyHrefstring—Links the privacy policy inline with the description.
policyLabelstring'Privacy policy'—
defaultOpenbooleanfalseStart with the categories expanded.
fixedbooleanfalseFixed to the bottom of the viewport, as it would be in production.

Functions

FunctionSignatureDescription
DEFAULT_CATEGORIESConsentCategory[]—

Types

ConsentCategory

A consent banner with the categories laid out and reject as easy as accept.

FieldTypeDescription
idstring—
labelReactNode—
description?ReactNode—
required?booleanLocked on, and not part of consent.
defaultEnabled?boolean—

Behaviour

TopicDetailDescription
rejectequal weightNot a style choice. Consent must be freely given, and hiding refusal behind an extra click — or greying it beside a bright "Accept all" — has been found to invalidate it. "Accept" plus "Manage" is the most-fined pattern on the web.
writes nothingonSaveStorage and script loading are the caller’s job. A component that quietly sets its own cookie is doing the exact thing it is meant to be asking about.
not modalaria-modal=falseAnnounced and reachable, but focus is not trapped — a banner people cannot escape is its own accessibility failure.