caliper uidocs

Cookie consent

Free

Asks once and plainly: reject and accept carry the same weight, choices explain what they are for, and the preferences can be reopened later.

Installs components/ui/cookie-consent.tsxDepends on @radix-ui/react-dialog, @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/cookie-consent.json

Halden 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

© 2026 Halden

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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
open *boolean—Show the banner, usually while no choice is stored.
categories *readonly CookieCategory[]—
valueCookieChoice—
onDecide *(choice: CookieChoice, decision: CookieDecision) => void—
titlestring"Cookies on this site"
descriptionstring"We use necessary cookies to make the site work. With your permission we would also like to measure visits and remember your settings."
policyHrefstring—
position"inline" | "fixed""fixed"`fixed` sits on the bottom of the viewport; `inline` stays where it is placed, e.g. in a preview.
classNamestring—

CookiePreferences

component
PropTypeDefaultDescription
open *boolean—
onOpenChange *(open: boolean) => void—
categories *readonly CookieCategory[]—
valueCookieChoice—The choice saved last time, if any.
onSave *(choice: CookieChoice, decision: CookieDecision) => void—
policyHrefstring—

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.