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.

We value your privacy

We use cookies to enhance your experience and analyse traffic. Read our Cookie Policy.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add cookie-consent

Usage#

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

We value your privacy

We use cookies to run the site and, if you agree, to measure it and show relevant ads.

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.

Only essential cookies

No analytics, no ads, no tracking.

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.

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

Cookies on this site

A full-width bar lays its copy and actions out in a row on wide screens.

We value your privacy

A compact corner card that stacks its content and stretches its actions.

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 an id, label, optional description, icon, required (locks the toggle on), and cookies (name, purpose, provider, duration) for the detail list.
  • storageKey / version (default 1) / maxAgeDays (default 180): 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 (pass true for returning visitors).
  • density: comfortable (default) or compact.

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.

FAQ#

Use Cookie Consent when you need a GDPR/ePrivacy consent flow specifically: it pairs a non-blocking CookieBanner with a CookiePreferences dialog over one shared state, so toggling a category and clicking Accept all mutate the same source of truth. A plain Dialog gives you a modal but none of the category booleans, the always-on clamping, or the coordinated accept/reject/save actions.