Cookie Consent Banner
A React cookie consent banner with a preferences dialog: necessary/analytics/marketing category toggles, accept-all and reject-all, a consent-object callback and localStorage persistence — SSR-safe with reduced-motion support.
The compliance component every production site needs. A bottom banner slides in after a short delay with three entry points — accept all, reject all, and manage preferences. "Manage preferences" opens a centered dialog with a switch per category (required categories are locked on); saving hands the consent object to onConsent and remembers it in localStorage, so returning visitors with a stored record are never bothered again. The first render outputs nothing at all, and localStorage is only read after mount, so SSR and hydration stay perfectly consistent.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add cookie-consent-banner.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CookieConsentBanner/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/cookie-consent-banner.en.jsonOr, once registries are configured in components.json, install it as @webberui/cookie-consent-banner.
Usage
import {
CookieConsentBanner,
getStoredConsent,
} from "@/components/ui/cookie-consent-banner";
<CookieConsentBanner
privacyHref="/privacy"
onConsent={(consent, action) => {
// Load analytics/marketing scripts only when consented
if (consent.analytics) {
// initAnalytics();
}
}}
/>;
// The stored record can be read back at any time (e.g. for conditional script loading)
const stored = getStoredConsent(); // StoredConsent | nullProps
| Prop | Type | Default | Description |
|---|---|---|---|
categories | CookieCategory[] | 3 built-in | Cookie category list; the default provides necessary (locked), analytics and marketing with Traditional Chinese copy |
onConsent | (consent: Record<string, boolean>, action: ConsentAction) => void | — | Callback when the user decides: the consent object (category id → consented) and how the decision was made |
storageKey | string | "webberui:cookie-consent" | localStorage key |
persist | boolean | true | Whether to read/write localStorage; with false the banner reappears on every mount (for demos or self-managed storage) |
position | "bottom" | "bottom-left" | "bottom-right" | "bottom" | Where the banner docks: a centered bar or a corner card |
privacyHref | string | — | Privacy policy URL; when provided a link is appended after the message |
message | string | — | Main banner copy (a shortcut for labels.message) |
showRejectAll | boolean | true | Whether to show the reject-all button |
labels | CookieConsentLabels | built-in zh copy | Override the UI copy; omitted fields keep the defaults |
defaultOpen | boolean | — | Force the banner shown or hidden (for showcases); when omitted, visibility follows whether a localStorage record exists |
container | HTMLElement | null | — | Showcase container: when provided, the banner and dialog mount inside it with absolute positioning (the container needs relative); otherwise they mount fixed on document.body |
className | string | — | Forwarded to the banner root element |
CookieCategory
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Category id, used as the key of the consent object |
name | string | — | Display name |
description | string | — | What this category is used for |
required | boolean | false | Required category: its switch is locked on and it is always recorded as consented |
CookieConsentLabels
| Field | Type | Default | Description |
|---|---|---|---|
title | string | "Cookie Usage Instructions" (About cookies) | Banner title |
message | string | built-in copy | Main banner copy |
privacyText | string | "Privacy Policy" (Privacy policy) | Privacy link text |
acceptAll | string | "Accept all" (Accept all) | Accept-all button text |
rejectAll | string | "Reject All" (Reject all) | Reject-all button text |
manage | string | "Manage Preferences" (Manage preferences) | Manage-preferences button text |
save | string | "Store Settings" (Save settings) | Save button text in the dialog |
dialogTitle | string | "Cookie Preferences" (Cookie preferences) | Dialog title |
requiredBadge | string | "Required" (Required) | Badge text on required categories |
close | string | "Turn Off Preferences" (Close preferences) | Accessible name of the dialog close button |
getStoredConsent(storageKey?) and clearStoredConsent(storageKey?) are also named exports, along with the ConsentAction ("accept-all" \| "reject-all" \| "custom"), StoredConsent (the stored shape with consent, action and updatedAt) and CookieConsentPosition types, so the application layer can read, clear and branch on the consent record.
How it works
- The banner waits about 0.6 s before sliding in — the page's main content gets to enter first, and the consent prompt does not steal the first glance
- The first render (including SSR) outputs nothing; only after mount does the component read localStorage to decide visibility, avoiding hydration mismatches
- localStorage is only touched inside effects and event handlers; the stored format is a
StoredConsentJSON (consent object, action, ISO timestamp), and when localStorage is unavailable (e.g. private browsing)onConsentstill fires so the caller can store the result itself - Reject-all and save always record required categories as consented; non-required categories default to off in the dialog, following the "no tracking before consent" principle
containeris an escape hatch for showcases: docs sandboxes and Storybook can trap the banner inside a fixed-height box; production usage does not need it
Accessibility
- The banner is a
role="region"named by its title, and all three actions are nativebuttonelements - The preferences dialog is
role="dialog"witharia-modal="true": focus moves into it on open, Tab cycles within it, and Esc or a backdrop click closes it and returns focus to the manage-preferences button - Category switches use
role="switch"witharia-checked; required categories aredisabled, locked on, and marked with a badge - When the user enables "reduce motion" at the system level, the slide in/out and dialog scale animations become quick cross-fades with no loss of function
Pull To Refresh
A mobile pull-to-refresh container for React: damped drag with a threshold, an arrow that rotates into a spinner, onRefresh returning a Promise, a check on completion, plus a desktop button fallback.
Popover
An anchored popover opened from a trigger: positioned by side and align with automatic collision flipping, it fades, scales and slides in from its side, with full focus handling and outside-click dismissal.