# Cookie Banner

> Cookie consent banner: accept all, reject all, or customize per-category preferences.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/cookie-banner
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/cookie-banner.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/cookie-banner-demo.tsx"
"use client"

import * as React from "react"

import { CookieBanner, type CookiePreferences } from "@/components/cookie-banner"

export default function CookieBannerDemo() {
  const [open, setOpen] = React.useState(true)
  const [result, setResult] = React.useState<CookiePreferences | null>(null)

  return (
    <div className="relative h-[360px] w-full max-w-2xl overflow-hidden rounded-2xl border bg-card">
      <div className="grid gap-3 p-6">
        <div className="h-4 w-1/3 rounded bg-muted" />
        <div className="h-3 w-full rounded bg-muted/70" />
        <div className="h-3 w-5/6 rounded bg-muted/70" />
        <div className="h-3 w-2/3 rounded bg-muted/70" />
        {result && (
          <p className="text-xs text-muted-foreground">
            Saved — analytics: <b>{String(result.analytics)}</b>, marketing: <b>{String(result.marketing)}</b>
          </p>
        )}
        {!open && (
          <button type="button" onClick={() => setOpen(true)} className="w-fit text-xs font-medium underline underline-offset-4">
            Reopen cookie settings
          </button>
        )}
      </div>
      <CookieBanner open={open} onOpenChange={setOpen} onDecision={setResult} />
    </div>
  )
}
```

## Source

```tsx title="components/cookie-banner.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export type CookiePreferences = { analytics: boolean; marketing: boolean }

export interface CookieBannerProps extends Omit<React.ComponentProps<"div">, "title"> {
  open: boolean
  onOpenChange: (open: boolean) => void
  onDecision?: (preferences: CookiePreferences) => void
  title?: string
  description?: string
}

function Toggle({ label, hint, checked, disabled, onChange }: { label: string; hint: string; checked: boolean; disabled?: boolean; onChange?: (value: boolean) => void }) {
  return (
    <label className={cn("flex items-center justify-between gap-4 rounded-lg border p-3", disabled ? "opacity-70" : "cursor-pointer")}>
      <span className="grid gap-0.5">
        <span className="text-sm font-medium">{label}</span>
        <span className="text-xs text-muted-foreground">{hint}</span>
      </span>
      <input type="checkbox" role="switch" checked={checked} disabled={disabled} onChange={(event) => onChange?.(event.target.checked)} className="peer sr-only" />
      <span
        aria-hidden
        className="relative h-6 w-10 shrink-0 rounded-full bg-input transition-colors after:absolute after:top-0.5 after:left-0.5 after:size-5 after:rounded-full after:bg-white after:shadow after:transition-transform peer-checked:bg-primary peer-checked:after:translate-x-4 peer-focus-visible:ring-2 peer-focus-visible:ring-ring"
      />
    </label>
  )
}

const btn = "inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"

// Positioned absolutely; place it in a relative container or override with className ("fixed bottom-4 left-4 ...").
export function CookieBanner({
  open,
  onOpenChange,
  onDecision,
  title = "We value your privacy",
  description = "We use cookies to keep the site working and, with your consent, to understand how it is used.",
  className,
  ...props
}: CookieBannerProps) {
  const [customizing, setCustomizing] = React.useState(false)
  const [prefs, setPrefs] = React.useState<CookiePreferences>({ analytics: false, marketing: false })

  if (!open) return null

  const decide = (value: CookiePreferences) => {
    onDecision?.(value)
    setCustomizing(false)
    onOpenChange(false)
  }

  return (
    <div
      role="region"
      aria-label="Cookie consent"
      className={cn(
        "absolute right-4 bottom-4 left-4 z-30 mx-auto max-w-lg animate-in rounded-2xl border bg-background p-5 text-foreground shadow-2xl duration-300 fade-in slide-in-from-bottom-4",
        className
      )}
      {...props}
    >
      <h2 className="text-base font-semibold">{title}</h2>
      <p className="mt-1 text-sm text-muted-foreground">{description}</p>
      {customizing && (
        <div className="mt-4 grid gap-2">
          <Toggle label="Necessary" hint="Required for the site to work" checked disabled />
          <Toggle label="Analytics" hint="Anonymous usage statistics" checked={prefs.analytics} onChange={(analytics) => setPrefs({ ...prefs, analytics })} />
          <Toggle label="Marketing" hint="Personalised content and ads" checked={prefs.marketing} onChange={(marketing) => setPrefs({ ...prefs, marketing })} />
        </div>
      )}
      <div className="mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
        <button type="button" onClick={() => decide({ analytics: false, marketing: false })} className={cn(btn, "border bg-background hover:bg-accent")}>
          Reject all
        </button>
        {customizing ? (
          <button type="button" onClick={() => decide(prefs)} className={cn(btn, "border bg-background hover:bg-accent")}>
            Save choices
          </button>
        ) : (
          <button type="button" onClick={() => setCustomizing(true)} className={cn(btn, "border bg-background hover:bg-accent")}>
            Customize
          </button>
        )}
        <button type="button" onClick={() => decide({ analytics: true, marketing: true })} className={cn(btn, "bg-primary text-primary-foreground hover:opacity-90")}>
          Accept all
        </button>
      </div>
    </div>
  )
}
```
