Cookie Consent React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React cookie consent banner for GDPR: a bottom bar or card with Accept, Reject and Settings, per-category switches, and a gate for embeds.
The bar rises from the bottom edge once the page has painted, only for visitors who have not chosen yet. Accept all, Reject all and Settings are equal buttons (on a phone three full-width 48px buttons with Accept on top); any choice slides the bar away, saves the record for 180 days and is announced to screen readers. Settings opens a dialog on desktop and a full-height bottom sheet on phones: flip a switch per category (Strictly necessary is locked on), tap Show cookies for the list behind it, then Save choices or Accept all; closing it any other way discards the draft. Keyboard: Tab reaches the buttons and the policy link, Enter or Space activates, Escape closes the settings without saving. With blocking the page dims, focus starts on Reject all, Tab stays inside and Escape does nothing until a choice; afterwards focus returns to what had it before the bar. A footer link (CookieSettingsLink) reopens the settings from anywhere; raising version brings the bar back.
Cookie Consent is a Pro React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add cookie-consent, together with useConsent, Button, Switch, Dialog, Collapsible and 6 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add cookie-consentCookie Consent is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import CookieConsent from @/components/ui/cookie-consent.
import type { ReactNode } from "react"
import { ConsentGate, CookieConsent, CookieSettingsLink } from "@/components/ui/cookie-consent"
export function SiteShell({ children }: { children: ReactNode }) {
return (
<>
{children}
<ConsentGate category="marketing" className="aspect-video">
<iframe title="Acme CRM overview" src="https://www.youtube-nocookie.com/embed/abc123" className="aspect-video w-full rounded-2xl" />
</ConsentGate>
<footer>
<CookieSettingsLink />
</footer>
<CookieConsent
policyHref="/cookie-policy"
delay={500}
onDecide={(record) => {
if (record.choices.analytics) console.log("load analytics")
}}
/>
</>
)
}Props
titledefault""ReactNodeThe heading. Empty uses labels.title ("We use cookies" by default).
descriptiondefault""ReactNodeThe text under the heading. Empty uses labels.description; the policy link follows it.
policyHrefdefault""stringThe cookie policy URL, shown as an underlined link after the description. Empty hides the link.
iconReactNode | falseA cookie in a 40px tinted tile by default; pass your own icon, or false to hide it.
categoriesConsentCategoryConfig[]{ id, label, description?, required?, cookies?: { name, provider?, duration?, purpose? }[] }[]. Display order; necessary (or any required) is locked on. Default DEFAULT_CONSENT_CATEGORY_CONFIG (English), or CONSENT_CATEGORY_CONFIG_RO.
labelsPartial<CookieConsentLabels>Every string: region, title, description, policy, acceptAll, rejectAll, settings, settingsTitle, settingsDescription, save, alwaysOn, showCookies(count), hideCookies, cookieName, provider, duration, purpose, saved, gateTitle(category), gateDescription, gateAllow, close. English by default; pass COOKIE_CONSENT_LABELS_RO for the ready Romanian copy.
variantdefault"bar""bar" | "card"bar: a full-width glass bar at the bottom (a card-like sheet on phones). card: a 400px floating card.
positiondefault"bottom""bottom" | "bottom-start" | "bottom-end"Where the card floats (centered, start or end corner); for the bar, where its content column sits on wide screens. Unset, the bar uses bottom and the card bottom-start.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The Accept button, the switches and the icon tile. Reject keeps the neutral soft look at the same size, so both choices weigh the same.
colordefault""stringAny CSS color; replaces the tone on the Accept button, the switches and the icon tile.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape of the buttons, scaled per size like the button's (the 48px phone buttons take the lg step).
showRejectdefaulttruebooleanReject all next to Accept all, as easy as accepting. Keep it on in the EU.
showSettingsdefaulttruebooleanThe Settings button that opens the per-category panel.
defaultOpendefaultfalsebooleanShows the bar on mount even when the visitor already chose. Unset, the bar shows exactly while the visitor has not decided. Controlled with open / onOpenChange.
openbooleanControls the bar's visibility (previews, a consent page). onOpenChange(false) fires when a choice hides it.
onOpenChange(open: boolean) => voidCalled when a choice hides the bar.
defaultSettingsOpendefaultfalsebooleanOpens the settings panel on mount. Controlled with settingsOpen / onSettingsOpenChange; openConsentSettings() opens it from anywhere.
settingsOpenbooleanControls the settings panel.
onSettingsOpenChange(open: boolean) => voidCalled when the settings panel opens or closes.
blockingdefaultfalsebooleanDims the page and traps focus until a choice (role dialog, focus on Reject all, Escape does nothing). Non-blocking is the default and the usual advice; use this only where legally advised.
delaydefault0numberMilliseconds after hydration before the bar shows, so the page paints first. The bar is fixed, so it never shifts the layout.
storageKeydefault"ui-consent"stringCookie / storage key of the record, passed to useConsent. Every consent hook, gate and link with the same key shares it.
versiondefault1numberThe policy version. Bump it when the policy or the categories change: older choices count as undecided and the bar returns.
storagedefault"cookie""cookie" | "local" | "session"Where the record lives. A cookie can be read on the server too. When storage is blocked the choice lives in memory for the session.
maxAgeDaysdefault180numberHow long a cookie choice is kept before the visitor is asked again.
responsivedefaulttruebooleanSettings as a full-height bottom sheet below 768px (a dialog otherwise).
portaldefaulttruebooleanRenders the region as the first child of document.body (or container), so keyboard and screen reader users reach it before the page. false renders it in place with absolute instead of fixed (previews, a phone frame, docs).
containerHTMLElement | nullThe portal target instead of document.body. A container with contain: layout or a transform keeps the fixed bar inside it.
onDecide(record: ConsentRecord) => voidAfter every decision (accept, reject, save): { version, choices, decidedAt, method }. Load scripts here, or use onConsentChange from use-consent.
classNamesPartial<Record<"overlay" | "inner" | "icon" | "content" | "title" | "description" | "policy" | "actions" | "button" | "settings" | "category" | "cookies" | "cookie", string>>Extra classes for the inner parts; settings is the dialog / sheet content (the data-slot cookie-consent-settings marks the category list inside it).
classNamestringExtra classes for the bar or card itself, merged last.
Parts
ConsentGate
componentRenders children only when the category is allowed, nothing during hydration, and otherwise a placeholder tile with labels.gateTitle and an Allow and load button that turns that category on. Props: { category; children; fallback?; storageKey?; version?; storage?; maxAgeDays?; categories?; icon?; tone?; color?; radius?; labels? }.
CookieSettingsLink
componentA link-styled button for the footer that calls openConsentSettings(storageKey). Children default to labels.settingsTitle. Props: ButtonProps & { storageKey?; labels? }.
COOKIE_CONSENT_LABELS_RO
constantReady Romanian copy. Type: CookieConsentLabels.
CONSENT_CATEGORY_CONFIG_RO
constantReady Romanian categories (necessary, preferences, analytics, marketing). Type: ConsentCategoryConfig[].
DEFAULT_COOKIE_CONSENT_LABELS
constantThe English default copy, to extend. Type: CookieConsentLabels.
DEFAULT_CONSENT_CATEGORY_CONFIG
constantThe English default categories, to extend or reorder. Type: ConsentCategoryConfig[].
ConsentCategoryConfig
typeOne category of categories: { id, label, description?, required?, cookies? }.
Examples
Card, bottom-start
A 400px floating card for sites where a full-width bar feels too loud.
Settings with cookie lists
Opened from a link anywhere on the page. A draft per category; each one lists its cookies.
ConsentGate around a YouTube embed
The video loads only once marketing is allowed; the placeholder asks for it in place.
Blocking before checkout
Dims the page, keeps focus inside, and Escape does nothing until a choice. Use only where advised.
Romanian copy
COOKIE_CONSENT_LABELS_RO and CONSENT_CATEGORY_CONFIG_RO: the ready Romanian wording, with no strings to write.
Policy version bump
A new version makes old choices undecided, so the bar comes back.
Dependencies
Files
- components/ui/cookie-consent.tsx
FAQ
Is the React Cookie Consent component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Cookie Consent?
Run npx wingo-ui@latest add cookie-consent. It also adds the Wingo UI items it builds on (useConsent, Button, Switch, Dialog, Collapsible and 6 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/cookie-consent.
How does Cookie Consent behave on phones?
Settings as a full-height bottom sheet below 768px (a dialog otherwise). The responsive prop controls this and is on by default.
See also
Used in
More React feedback components
Articles using this component
- Nextjs Cookie Consent: GDPR Banner and Consent Mode v2
Next.js and Tailwind CSS12 min read