Cookie consent

A consent bar that morphs into preferences and folds into a reopen button.

pnpm dlx shadcn@latest add @uiarc/cookie-consent
Live · keyboard ready
  • Asking for cookie or tracking consent on a marketing site or app.
  • Sites that need per-category choices with a way to reopen them later.
  • Use announcement-bar for a site-wide promotion or notice that needs no choice.
  • Use toast for a one-off message that dismisses itself.
  • Use dialog when the page must block until the visitor decides.

Installation

Add Cookie consent with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/cookie-consent

Adds the component and its local dependencies, and installs motion, lucide-react. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { useRef } from "react";import { CookieConsent, type CookieConsentHandle } from "@/components/arc/cookie-consent/cookie-consent"; export function Consent() {  const consent = useRef<CookieConsentHandle>(null);  return (    <>      <CookieConsent        ref={consent}        storageKey="acme-consent"        policy={{ label: "Cookie policy", href: "/cookies" }}        onChange={value => { if (value?.analytics) loadAnalytics(); }}      />      <button type="button" onClick={() => consent.current?.openPreferences()}>Cookie settings</button>    </>  );}

API reference

3 parts. The first is the root.

CookieConsent

A consent bar that morphs into a preferences panel and then folds into a small reopen button, with optional localStorage memory.

PropTypeDefaultDescription
categoriesCookieCategory[]defaultCookieCategoriesGroups: { id, label, description?, required?, defaultEnabled? }. Required ones are always on.
valueCookieConsentValue | null–Controlled saved choice (category id to allowed). null means the visitor has not decided.
defaultValueCookieConsentValue | nullnullStarting choice when uncontrolled.
onChange(value: CookieConsentValue | null, detail: { action: CookieConsentAction; updatedAt: Date }) => void–Called on accept-all, reject-all, save, or reset. Load optional scripts from here.
storageKeystring–Remembers the choice in localStorage under this key. Read it elsewhere with readCookieConsent.
versionstring"1"Bump when categories or policy change; a stored choice for another version is ignored and the bar asks again.
preferencesOpenboolean–Controlled open state of the preferences panel.
defaultPreferencesOpenbooleanfalseOpens the panel on first render when uncontrolled.
onPreferencesOpenChange(open: boolean) => void–Called when the panel opens or closes.
policy{ label: string; href?: string; onClick?: () => void }–Link to the full policy, shown in the bar and panel. Renders a button when href is missing.
position"bottom" | "bottom-left" | "bottom-right""bottom"Where the bar sits along the bottom edge.
strategy"fixed" | "absolute""fixed"fixed pins to the viewport; absolute fills the nearest positioned ancestor, for previews.
showReopenButtonbooleantrueKeeps a 44px cookie button after a choice so visitors can change their mind.
reopenSide"left" | "right"–Corner for the reopen button. Defaults to the bar's side, or left when centered.
labelsPartial<CookieConsentLabels>–Overrides for message, customize, acceptAll, rejectAll, save, preferencesTitle, preferencesDescription, reopen, alwaysOn, and close.
classNamestring–Class on the layer.
refRef<CookieConsentHandle>–Imperative handle: openPreferences(), closePreferences(), reset().

readCookieConsent

Reads a saved choice as { value, version, updatedAt }, or null when there is none or it was saved for another version. Safe on the server.

PropTypeDefaultDescription
storageKeyRequiredstring–The same key passed to CookieConsent.
versionstring"1"The version the choice must match.

defaultCookieCategories

The default categories: Strictly necessary (required), Preferences, Analytics, and Marketing.

No props.

Tab
Moves through the bar or panel buttons and switches.
Space
Toggles the focused category switch.
Escape
Closes the preferences panel without saving and returns focus to the bar or reopen button.
  • The bar and panel are non-modal dialogs; the bar is described by its message and the panel is labelled by its heading.
  • Opening the panel focuses its heading; saving focuses the reopen button, which has aria-haspopup="dialog" and the reopen label.
  • Each switch is labelled by its category and described by its hint; required categories show Always on text instead of a switch.
  • Save is described by a hidden count such as 2 of 3 optional categories allowed.
  • Reject all sits beside Accept all at the same weight, so refusing is as easy as accepting.
  • One surface springs its position, width, height, and radius between the bar, the panel, and the 44px reopen button; faces rise from a short blur.
  • Growing uses a slightly bouncy spring and shrinking a flat one, so the surface never overshoots when it folds away.
  • The surface enters rising 24px and scaling from 0.98.
  • Reduced motion jumps the surface to each shape and swaps faces with a plain fade.
  • The bar is at most 660px and the panel 420px wide, both clamped to the layer, which sits 16px from the viewport edges.
  • Below a 560px bar width the message stacks above the actions; panel actions go on one row at a 340px container width.
  • The layer ignores pointer input, so only the surface blocks clicks on the page behind it.
  • Nothing renders until hydration and the stored choice are read, so a decided visitor never sees the bar flash.
  • One ResizeObserver tracks the layer width and one tracks the current face; motion runs only when the shape changes.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Mount once near the root layout. Gate optional scripts on onChange or on readCookieConsent(storageKey, version) at startup.
  • Bump version whenever categories or the policy change to ask everyone again.
  • Wire a footer Cookie settings link to ref.openPreferences(). Set showReopenButton={false} if that link is the only way back.
  • Use strategy="absolute" inside a positioned preview frame, not for production.

The full library index for assistants is at /llms.txt.