Cookie consent
FreeAsks once and plainly: reject and accept carry the same weight, choices explain what they are for, and the preferences can be reopened later.
pnpm dlx shadcn@latest add https://caliperui.site/r/cookie-consent.jsonHalden journal
Reading continues behind the banner: it never covers the page or traps focus.
No choice stored yet.
Cookies on this site
We use necessary cookies to make the site work. With your permission we would also like to measure visits and remember your settings. Cookie policy
When to use #
Use it for
- Sites that set analytics or marketing cookies.
- A cookie settings link in the footer.
Skip it when
- Only necessary cookies are used; no banner is needed, mention them in the policy.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/cookie-consent.json
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from CookieConsent and CookiePreferences.
import { useState } from "react" import { CookieConsent, CookiePreferences, type CookieCategory, type CookieChoice } from "@/components/ui/cookie-consent" const cookieCategories: CookieCategory[] = [ { id: "necessary", label: "Necessary", description: "Keeps you signed in and remembers this choice. The site does not work without them.", required: true }, { id: "preferences", label: "Preferences", description: "Remembers your language and theme between visits." }, { id: "analytics", label: "Analytics", description: "Counts visits and which pages are read, without identifying you." }, { id: "marketing", label: "Marketing", description: "Measures which campaigns bring people here." }, ]; export function CookieConsentDemo() { const [choice, setChoice] = useState<CookieChoice | null>(null); const [how, setHow] = useState(""); const [settings, setSettings] = useState(false); const save = (next: CookieChoice, decision: string) => { setChoice(next); setHow(decision); }; const on = choice ? cookieCategories.filter((category) => choice[category.id]).map((category) => category.label) : []; return ( <div className="relative grid min-h-[380px] w-full max-w-2xl content-between gap-6 overflow-hidden rounded-[14px] border border-border p-5"> <div className="grid gap-2"> <p className="text-sm font-semibold">Halden journal</p> <p className="max-w-[56ch] text-sm leading-6 text-muted-foreground">Reading continues behind the banner: it never covers the page or traps focus.</p> <p role="status" className="text-[13px]"> {choice ? `Saved (${how}): ${on.join(", ")}.` : "No choice stored yet."} </p> </div> <div className="grid gap-3"> <CookieConsent open={!choice} position="inline" categories={cookieCategories} onDecide={save} policyHref="#cookie-policy" /> <footer className="flex flex-wrap items-center gap-3 border-t border-border pt-3 text-[13px] text-muted-foreground"> <span>© 2026 Halden</span> <button type="button" className="underline underline-offset-2" onClick={() => setSettings(true)}>Cookie settings</button> {choice && <button type="button" className="underline underline-offset-2" onClick={() => setChoice(null)}>Forget my choice</button>} </footer> </div> <CookiePreferences open={settings} onOpenChange={setSettings} categories={cookieCategories} value={choice ?? undefined} onSave={save} policyHref="#cookie-policy" /> </div> ); }
API reference #
CookieConsent
component
CookiePreferences
component
Accessibility #
It is built on native elements and keeps aria-describedby, aria-labelledby and aria-hidden in step with state.
Keyboard and focus handling comes from Radix dialog: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.