# Cookie Consent

> 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.

- Type: React component (Feedback)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add cookie-consent`
- Docs: https://wingo-ui.com/components/cookie-consent

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add cookie-consent
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/cookie-consent` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/cookie-consent.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [useConsent](https://wingo-ui.com/components/use-consent) ([Markdown](https://wingo-ui.com/components/use-consent.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Collapsible](https://wingo-ui.com/components/collapsible) ([Markdown](https://wingo-ui.com/components/collapsible.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title?` | `ReactNode` | `""` | The heading. Empty uses labels.title ("We use cookies" by default). |
| `description?` | `ReactNode` | `""` | The text under the heading. Empty uses labels.description; the policy link follows it. |
| `policyHref?` | `string` | `""` | The cookie policy URL, shown as an underlined link after the description. Empty hides the link. |
| `icon?` | `ReactNode \| false` |  | A cookie in a 40px tinted tile by default; pass your own icon, or false to hide it. |
| `categories?` | `ConsentCategoryConfig[]` |  | { 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. |
| `labels?` | `Partial<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. |
| `variant?` | `"bar" \| "card"` | `"bar"` | bar: a full-width glass bar at the bottom (a card-like sheet on phones). card: a 400px floating card. |
| `position?` | `"bottom" \| "bottom-start" \| "bottom-end"` | `"bottom"` | 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. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | 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. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone on the Accept button, the switches and the icon tile. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of the buttons, scaled per size like the button's (the 48px phone buttons take the lg step). |
| `showReject?` | `boolean` | `true` | Reject all next to Accept all, as easy as accepting. Keep it on in the EU. |
| `showSettings?` | `boolean` | `true` | The Settings button that opens the per-category panel. |
| `defaultOpen?` | `boolean` | `false` | Shows 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. |
| `open?` | `boolean` |  | Controls the bar's visibility (previews, a consent page). onOpenChange(false) fires when a choice hides it. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when a choice hides the bar. |
| `defaultSettingsOpen?` | `boolean` | `false` | Opens the settings panel on mount. Controlled with settingsOpen / onSettingsOpenChange; openConsentSettings() opens it from anywhere. |
| `settingsOpen?` | `boolean` |  | Controls the settings panel. |
| `onSettingsOpenChange?` | `(open: boolean) => void` |  | Called when the settings panel opens or closes. |
| `blocking?` | `boolean` | `false` | Dims 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. |
| `delay?` | `number` | `0` | Milliseconds after hydration before the bar shows, so the page paints first. The bar is fixed, so it never shifts the layout. |
| `storageKey?` | `string` | `"ui-consent"` | Cookie / storage key of the record, passed to useConsent. Every consent hook, gate and link with the same key shares it. |
| `version?` | `number` | `1` | The policy version. Bump it when the policy or the categories change: older choices count as undecided and the bar returns. |
| `storage?` | `"cookie" \| "local" \| "session"` | `"cookie"` | 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. |
| `maxAgeDays?` | `number` | `180` | How long a cookie choice is kept before the visitor is asked again. |
| `responsive?` | `boolean` | `true` | Settings as a full-height bottom sheet below 768px (a dialog otherwise). |
| `portal?` | `boolean` | `true` | Renders 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). |
| `container?` | `HTMLElement \| null` |  | The portal target instead of document.body. A container with contain: layout or a transform keeps the fixed bar inside it. |
| `onDecide?` | `(record: ConsentRecord) => void` |  | After every decision (accept, reject, save): { version, choices, decidedAt, method }. Load scripts here, or use onConsentChange from use-consent. |
| `classNames?` | `Partial<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). |
| `className?` | `string` |  | Extra classes for the bar or card itself, merged last. |

## Parts

### ConsentGate (component)

Renders 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 (component)

A link-styled button for the footer that calls openConsentSettings(storageKey). Children default to labels.settingsTitle. Props: ButtonProps & { storageKey?; labels? }.

### COOKIE_CONSENT_LABELS_RO (constant)

Ready Romanian copy. Type: CookieConsentLabels.

### CONSENT_CATEGORY_CONFIG_RO (constant)

Ready Romanian categories (necessary, preferences, analytics, marketing). Type: ConsentCategoryConfig[].

### DEFAULT_COOKIE_CONSENT_LABELS (constant)

The English default copy, to extend. Type: CookieConsentLabels.

### DEFAULT_CONSENT_CATEGORY_CONFIG (constant)

The English default categories, to extend or reorder. Type: ConsentCategoryConfig[].

### ConsentCategoryConfig (type)

One 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.

## See also

- [useConsent](https://wingo-ui.com/components/use-consent) ([Markdown](https://wingo-ui.com/components/use-consent.md))
- [Banner](https://wingo-ui.com/components/banner) ([Markdown](https://wingo-ui.com/components/banner.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))

---

Canonical: https://wingo-ui.com/components/cookie-consent
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
