Cookie Consent
A GDPR-ready consent surface in two coordinated pieces over one shared state: a non-blocking banner and a preferences dialog with a toggle per cookie category. The parts are the real DS components.
Installation#
Usage#
import { ShieldCheck, ChartBar } from "@phosphor-icons/react"
import {
CookieConsent,
CookieBanner,
CookieBannerContent,
CookieBannerTitle,
CookieBannerDescription,
CookieBannerActions,
CookieAcceptAllButton,
CookieRejectAllButton,
CookieCustomizeButton,
CookiePreferences,
CookiePreferencesContent,
CookiePreferencesHeader,
CookiePreferencesTitle,
CookieCategoryList,
CookiePreferencesFooter,
CookieSavePreferencesButton,
type CookieCategoryDef,
} from "@/components/ui/cookie-consent"
const categories: CookieCategoryDef[] = [
{ id: "necessary", label: "Strictly necessary", required: true, icon: ShieldCheck, description: "Essential for the site to work." },
{ id: "analytics", label: "Analytics", icon: ChartBar, description: "Help us understand how the site is used." },
]
export function Example() {
return (
<CookieConsent categories={categories} onSave={(prefs) => savePreferences(prefs)}>
<CookieBanner position="bottom-right">
<CookieBannerContent>
<CookieBannerTitle>We value your privacy</CookieBannerTitle>
<CookieBannerDescription>We use cookies to improve your experience.</CookieBannerDescription>
</CookieBannerContent>
<CookieBannerActions>
<CookieCustomizeButton size="sm" />
<CookieRejectAllButton size="sm" />
<CookieAcceptAllButton size="sm" />
</CookieBannerActions>
</CookieBanner>
<CookiePreferences>
<CookiePreferencesContent>
<CookiePreferencesHeader>
<CookiePreferencesTitle>Cookie preferences</CookiePreferencesTitle>
</CookiePreferencesHeader>
<CookieCategoryList />
<CookiePreferencesFooter>
<CookieRejectAllButton variant="ghost" />
<CookieSavePreferencesButton />
</CookiePreferencesFooter>
</CookiePreferencesContent>
</CookiePreferences>
</CookieConsent>
)
}Details on hover#
CookieBannerDetails keeps the banner clean: it stays folded away and unrolls above the copy on hover or keyboard focus, so the buttons never move under the pointer. CookieDetailList fills it from your categories, listing the cookies of each one with its purpose, provider and duration. Touch screens have no hover, so CookieBannerDetailsTrigger shows there, and only there. Escape folds it again.
Essential cookies only#
A site that stores only what it needs to work (a sign-in, a saved theme) needs no consent, so there is nothing to accept or reject, only something to know. layout="launcher" rests the banner as its icon alone in a small circle. On hover, focus or a tap, the circle grows into the card and the icon slides to its seat beside the title, with the reason and the full list above. This is the notice koalaui.com runs.
Remember the choice#
Pass storageKey and the root keeps the answer in localStorage: the banner shows only until the visitor answers, the preferences come back on the next visit, and every open tab follows. The record is dated and versioned. Bump version when your categories change and everyone is asked again; after maxAgeDays (six months by default) the choice expires and the banner returns. Nothing optional is on until the visitor says so, so gate each script on the preferences map.
<CookieConsent categories={categories} storageKey="cookie-consent" version={2} maxAgeDays={180}>
…
<Analytics />
</CookieConsent>
// Stored as: { "version": 2, "date": "2026-09-27T10:04:12.000Z", "preferences": { "necessary": true, "analytics": false } }
function Analytics() {
const { prefs } = useCookieConsent()
// Off until the visitor accepts, and off again the moment they withdraw.
if (!prefs.analytics) return null
return <Script src="https://plausible.io/js/script.js" data-domain="example.com" />
}Preferences dialog#
The configurator is a DS Dialog bound to the same shared state. CookieCategoryList maps your categories into rows, each with a leading icon, a description, and a Switch. Required categories render locked-on with an Always on badge. Open it from the banner's CookieCustomizeButton, or from a standalone CookiePreferencesTrigger anywhere on the page.
Position#
position anchors the banner: bottom is a full-bleed bar that rows its content out on wide screens, while bottom-left and bottom-right are compact floating cards. Each slides in from its own edge. layout arranges the parts inside: stack (the default) is a column, inline one slim row, and launcher (corners only) the icon alone until the banner is expanded.
API reference#
CookieConsent
The root and single source of truth. Owns the category booleans and the coordinated actions; both surfaces read from it.
categories: the cookie categories to manage (CookieCategoryDef[]): each has anid,label, optionaldescription,icon,required(locks the toggle on), andcookies(name,purpose,provider,duration) for the detail list.storageKey/version(default1) /maxAgeDays(default180): remember the answer in localStorage, dated and versioned, and ask again when the version changes or it expires.value/defaultValue/onValueChange: the preferences map ({ [id]: boolean }), controlled or uncontrolled. Required categories are always clamped on.onAcceptAll/onRejectAll/onSave: fire on each coordinated action with the resulting map. Persist it (cookie / localStorage) here.open/defaultOpen/onOpenChange: the preferences dialog open state.defaultConsented: start with the banner hidden (passtruefor returning visitors).density:comfortable(default) orcompact.
CookieBanner
The non-blocking consent region (role="region"), hidden once a choice is made. Takes position (bottom / bottom-left / bottom-right) and layout (stack / inline / launcher). Compose CookieBannerIcon, CookieBannerContent, CookieBannerTitle, CookieBannerDescription, CookieBannerDetails (the layer that opens on hover, focus or CookieBannerDetailsTrigger), and CookieBannerActions inside. The banner stamps data-expanded while the layer is open. CookieDetailList lists every category's cookies, in the layer or anywhere inside the root.
CookiePreferences
The dialog family: CookiePreferences (root, bound to the shared open state), CookiePreferencesTrigger (asChild), CookiePreferencesContent, CookiePreferencesHeader / Title / Description / Footer, and CookieCategoryList / CookieCategory for the rows.
Action buttons & hook
Pre-wired DS Buttons: CookieAcceptAllButton, CookieRejectAllButton, CookieCustomizeButton, CookieSavePreferencesButton. Each forwards every Button prop. For a custom surface, read the state directly with the useCookieConsent() hook.