Components
Cookie Banner
BetaA fixed-position bottom banner for cookie consent. Simple mode shows a single "Accept all" action. Expanded mode lists individual cookie categories with toggles. Persists consent to localStorage.
We use cookies to improve your experience and personalise the content and ads you see. By clicking "Accept all" you consent to our use of cookies. Cookie policy
Purpose
The CookieBanner collects and persists user consent for cookie categories as required by GDPR and the UK PECR. It renders as a fixed bottom-0 overlay so it is always visible on page load until the user interacts.
Two modes:
- Simple (
expanded={false}) — a single message with "Manage preferences" and "Accept all". Use this as the initial state for most users. - Expanded (
expanded={true}) — a category-by-category breakdown with Toggle switches. Shown when the user clicks "Manage preferences" in simple mode.
Consent is persisted to localStorage under the key kuoni_cookie_consent. The banner does not render on subsequent page loads once the user has consented. localStorage is accessed only inside useEffect to remain SSR-safe.
Note: CookieBanner uses
position: fixedand renders as a full-viewport-width overlay. The static mockups on this page represent the component's appearance. In a live Next.js app, mount it once at the layout level (e.g.app/layout.jsx) rather than in individual pages.
Usage
Reach for CookieBanner specifically for cookie consent — it is purpose-built for GDPR/PECR category consent and localStorage persistence. For any other site-wide announcement (a promotion, a service notice, a policy change) use the general-purpose Banner instead; it does not manage consent state.
CookieBanner mounts once at the application layout level and appears at the bottom of the viewport on first visit, ahead of any other page content, until the user makes a consent choice.
Anatomy
Simple mode
We use cookies to improve your experience and personalise the content and ads you see. By clicking "Accept all" you consent to our use of cookies. Cookie policy
- 1Banner surface — Dark (bg-kuoni-dark) full-width bar at the bottom of the viewport. z-toast.
- 2Consent message — Short statement explaining cookie use with a link to the Cookie policy page.
- 3"Manage preferences" button — Ghost button. Opens the expanded category view.
- 4"Accept all" button — Kuoni Yellow CTA. Saves all categories as accepted and hides the banner.
Expanded mode
Cookie preferences
Manage your cookie settings. Necessary cookies cannot be disabled.
- 1Header — "Cookie preferences" heading with explanatory sub-text.
- 2Category row — Category name, description, and a Toggle switch. Necessary category is always checked and disabled.
- 3"Reject all" button — Ghost button. Sets analytics and marketing to false and saves.
- 4"Save preferences" button — Yellow CTA. Saves the current toggle state and hides the banner.
Variants
Simple (initial state)
We use cookies to improve your experience and personalise the content and ads you see. By clicking "Accept all" you consent to our use of cookies. Cookie policy
Expanded (preferences detail)
Shown after clicking "Manage preferences". Toggles for analytics and marketing; necessary is locked on.
Cookie preferences
Manage your cookie settings. Necessary cookies cannot be disabled.
States
| State | Behaviour |
|---|---|
| First visit | Banner visible. Simple mode. |
| "Manage preferences" | Switches to expanded mode. |
| "Accept all" | Saves all categories as true. Banner hides. |
| "Reject all" | Saves optional categories as false. Banner hides. |
| "Save preferences" | Saves current toggle values. Banner hides. |
| Subsequent visits | localStorage key present → banner does not render. |
| Toggle (necessary) | Checked and disabled — cannot be unchecked. |
| Toggle (optional) | Checked/unchecked based on user preference. |
Best practices
Mount CookieBanner once at the layout level so it is consistent across pages.
Mounting CookieBanner once in app/layout.jsx ensures it appears on every page at the same position, and the localStorage check prevents it from re-appearing after consent.
Mount CookieBanner inside individual page components.
Adding CookieBanner to individual page components risks showing it on some pages but not others, or displaying it multiple times on pages with multiple instances.
Keep the Necessary cookie category checked and disabled. It is non-negotiable under PECR.
The Necessary category is required for the site to function — locking it prevents users from entering an invalid consent state.
Add a toggle to the Necessary category that allows users to disable it.
Allowing users to disable necessary cookies makes the site non-functional and creates false expectations about what they can reject.
Gate third-party scripts (analytics, advertising pixels) on the corresponding consent category.
Only firing analytics scripts after the user has accepted analytics cookies ensures GDPR compliance from the first page load.
Load optional scripts before checking or receiving consent.
Loading Google Analytics on the first page load and then asking for consent makes the initial page load non-compliant.
Props
CookieBanner
| Prop | Type | Default | Description |
|---|---|---|---|
| categories | Array<CookieCategory> | Necessary, Analytics, Marketing | Cookie categories to display in expanded mode. The first category is assumed to be required=true (locked on). |
| policyHref | string | '#' | URL for the Cookie policy link in the simple mode message. |
| onConsent | (consent: Record<string, boolean>) => void | — | Called when the user saves preferences or accepts all. Receives a map of category value → boolean. |
| storageKey | string | 'kuoni_cookie_consent' | localStorage key used to persist consent. Change to namespace multiple sites sharing the same domain. |
| className | string | — | Additional Tailwind classes on the root banner element. |
CookieCategory shape
| Property | Type | Description |
|---|---|---|
value | string | Unique identifier used as the consent key in localStorage. |
label | string | Display name shown in the category row. |
description | string | One-sentence explanation of what the cookie category does. |
required | boolean | When true, the toggle is checked and disabled. |
defaultChecked | boolean | Initial checked state for optional categories. |
Accessibility
Dialog role
The banner has role="dialog" aria-modal="true" aria-label="Cookie preferences". This signals to assistive technology that the banner is a modal-like region requiring attention:
<div role="dialog" aria-modal="true" aria-label="Cookie preferences">
…
</div>
Focus management
When the banner becomes visible, focus should move to the first interactive element ("Manage preferences" or the first Toggle in expanded mode). This ensures keyboard users are immediately aware of the banner rather than needing to Tab to it from the top of the page.
Toggle labels
Each Toggle in expanded mode uses the Toggle component's label prop, which renders as sr-only text inside the switch. Screen readers announce the toggle as "Necessary toggle, on, dimmed" or "Analytics toggle, off" — communicating the category name, state, and disabled status.
Keyboard interaction
| Key | Action |
|---|---|
Tab | Move focus through buttons / toggles |
Enter / Space | Activate the focused button or toggle |
Escape | No action — banner requires an explicit consent choice |