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.tsxAbout
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.
<CookieConsent
defaultOpen={false}
policyHref="/privacy"
onSave={(accepted) => persist(accepted)}
/>Examples
Categories open
<CookieConsent defaultOpen onSave={persist} policyHref="/privacy" />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'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | 'Cookies' | — |
| description | ReactNode | 'We use cookies to run the site, and — with your permission — to understand how it is used.' | — |
| categories | ConsentCategory[] | DEFAULT_CATEGORIES | Nothing 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 | — | — |
| acceptAllLabel | string | 'Accept all' | — |
| rejectAllLabel | string | 'Reject all' | — |
| customiseLabel | string | 'Customise' | — |
| saveLabel | string | 'Save choices' | — |
| policyHref | string | — | Links the privacy policy inline with the description. |
| policyLabel | string | 'Privacy policy' | — |
| defaultOpen | boolean | false | Start with the categories expanded. |
| fixed | boolean | false | Fixed to the bottom of the viewport, as it would be in production. |
Functions
| Function | Signature | Description |
|---|---|---|
| DEFAULT_CATEGORIES | ConsentCategory[] | — |
Types
ConsentCategory
A consent banner with the categories laid out and reject as easy as accept.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| label | ReactNode | — |
| description? | ReactNode | — |
| required? | boolean | Locked on, and not part of consent. |
| defaultEnabled? | boolean | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| reject | equal weight | Not 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 nothing | onSave | Storage 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 modal | aria-modal=false | Announced and reachable, but focus is not trapped — a banner people cannot escape is its own accessibility failure. |