# Expandable Consent Banner

> Cookie consent banner with accept and reject actions plus a Customize control that expands per-category consent switches inline.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/cookie-consent-5).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/cookie-consent-5
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/cookie-consent-5-light-2eb04f7a2b55e791.png
- Registry item: https://7ovr.com/r/cookie-consent-5.json
- Categories: cookie-consent
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/cookie-consent-5.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `button`, `separator`, `switch`

## Files

### `registry/blocks/cookie-consent/5/cookie-consent-block.tsx`

```tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

const categories = [
  {
    id: "necessary",
    label: "Strictly necessary",
    description: "Required for the site to function. Always on.",
    required: true,
  },
  {
    id: "analytics",
    label: "Analytics",
    description: "Help us understand how the product is used.",
    required: false,
  },
  {
    id: "marketing",
    label: "Marketing",
    description: "Used to personalize ads and measure campaigns.",
    required: false,
  },
]

export default function CookieConsentBlock() {
  const [open, setOpen] = useState(true)
  const [expanded, setExpanded] = useState(false)
  const [prefs, setPrefs] = useState({ analytics: true, marketing: false })

  if (!open) return null

  return (
    <section className="flex min-h-svh w-full items-end justify-center bg-muted/30 px-4 py-6 sm:items-center">
      <div className="w-full max-w-lg rounded-lg border border-border bg-background shadow-lg">
        <div className="flex items-start justify-between gap-4 p-5">
          <div className="flex flex-col gap-1.5">
            <h2 className="font-heading text-sm font-semibold tracking-tight">
              We value your privacy
            </h2>
            <p className="text-sm text-muted-foreground">
              We use cookies to improve your experience, analyze traffic, and
              personalize content. Choose which cookies to allow.
            </p>
          </div>
          <button
            type="button"
            onClick={() => setOpen(false)}
            aria-label="Dismiss"
            className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
          >
            <IconPlaceholder
              lucide="X"
              tabler="IconX"
              hugeicons="Cancel01Icon"
              phosphor="X"
              remixicon="RiCloseLine"
              className="size-5"
              aria-hidden="true"
            />
          </button>
        </div>

        {expanded && (
          <>
            <Separator />
            <div className="flex flex-col gap-4 p-5">
              {categories.map((category) => (
                <div
                  key={category.id}
                  className="flex items-start justify-between gap-4"
                >
                  <div className="flex flex-col gap-0.5">
                    <span className="text-sm font-medium">
                      {category.label}
                    </span>
                    <span className="text-xs text-muted-foreground">
                      {category.description}
                    </span>
                  </div>
                  {category.required ? (
                    <Switch checked disabled aria-label={category.label} />
                  ) : (
                    <Switch
                      checked={prefs[category.id as keyof typeof prefs]}
                      onCheckedChange={(checked) =>
                        setPrefs((prev) => ({
                          ...prev,
                          [category.id]: checked,
                        }))
                      }
                      aria-label={category.label}
                    />
                  )}
                </div>
              ))}
            </div>
          </>
        )}

        <Separator />
        <div className="flex flex-col gap-2 p-5 sm:flex-row sm:justify-end">
          {!expanded && (
            <Button
              variant="ghost"
              onClick={() => setExpanded(true)}
              className="sm:mr-auto"
            >
              Customize
            </Button>
          )}
          <Button variant="outline" onClick={() => setOpen(false)}>
            Reject all
          </Button>
          <Button onClick={() => setOpen(false)}>
            {expanded ? "Save preferences" : "Accept all"}
          </Button>
        </div>
      </div>
    </section>
  )
}
```
