Skip to content
goodtabs UI
BrowseBrowse components

Cookie notice

A small notice with two real choices of equal weight and no dark patterns.

npx shadcn@latest add https://ui.goodtabs.co/r/cookie-notice.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/cookie-notice

Cookies on this site

We use cookies to remember your theme and count visits. Nothing is sold or shared.

Plain words and two buttons of the same variant and size.

Bar

Your choice on cookies

Analytics cookies tell us which pages get read. Turn them down and the site works the same.

Text and choices in one row from the sm breakpoint up.

With a link

Cookies on this site

One analytics cookie, no ads. Details are in the privacy policy.

Custom labels and a policy link inside the description.

Usage

import { CookieNotice } from "@/components/ui/cookie-notice"

<CookieNotice
  className="fixed right-4 bottom-4"
  description="We use one cookie to count visits."
  onAccept={() => enableAnalytics()}
/>

Props

PropTypeDefaultDescription
descriptionReactNodeWhat is collected and why, in plain words.
titlestring"Cookies on this site"The heading.
acceptLabelstring"Accept all"Label of the accepting choice.
rejectLabelstring"Reject all"Label of the declining choice.
layout"stacked" | "bar""stacked"stacked is a small card; bar is a row from sm up.
onAccept() => voidRuns when they accept, then the notice closes.
onReject() => voidRuns when they decline, then the notice closes.

Source

cookie-notice/cookie-notice.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

type CookieNoticeProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** The heading, as a short sentence. */
  title?: string;
  /** What is collected and why, in plain words. */
  description: React.ReactNode;
  /** Label of the accepting choice. */
  acceptLabel?: string;
  /** Label of the declining choice. Same size and weight as accept. */
  rejectLabel?: string;
  /** "stacked" is a small card; "bar" lays the text and choices in a row from sm up. */
  layout?: "stacked" | "bar";
  /** Runs when they accept. The notice then closes. */
  onAccept?: () => void;
  /** Runs when they decline. The notice then closes. */
  onReject?: () => void;
};

/**
 * A cookie notice with two real choices of equal weight.
 * Both buttons share one variant and one size, so neither is nudged. Closing needs a choice.
 * It renders in flow; place it with className (for example fixed bottom-4 right-4).
 */
export function CookieNotice({
  title = "Cookies on this site",
  description,
  acceptLabel = "Accept all",
  rejectLabel = "Reject all",
  layout = "stacked",
  onAccept,
  onReject,
  className,
  ...props
}: CookieNoticeProps) {
  const [open, setOpen] = React.useState(true);
  const headingId = React.useId();
  if (!open) return null;

  const choose = (cb?: () => void) => () => {
    cb?.();
    setOpen(false);
  };

  return (
    <section
      aria-labelledby={headingId}
      className={cn(
        "flex w-full max-w-sm flex-col gap-3 rounded-lg border bg-card p-4 text-card-foreground",
        layout === "bar" && "max-w-2xl sm:flex-row sm:items-center sm:gap-6",
        className,
      )}
      {...props}
    >
      <div className="flex min-w-0 flex-1 flex-col gap-1">
        <h2 id={headingId} className="m-0 text-sm font-medium">
          {title}
        </h2>
        <p className="m-0 text-sm text-muted-foreground">{description}</p>
      </div>
      <div className="flex shrink-0 gap-2">
        <Button variant="secondary" className="flex-1 sm:flex-none" onClick={choose(onReject)}>
          {rejectLabel}
        </Button>
        <Button variant="secondary" className="flex-1 sm:flex-none" onClick={choose(onAccept)}>
          {acceptLabel}
        </Button>
      </div>
    </section>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/cookie-notice/prompt.md, or the Copy AI prompt button above.

When to use

  • Sites that set non-essential cookies and need a consent choice
  • Anywhere declining must be as easy as accepting

When not to use

  • Granular consent by category; this offers two choices only
  • Sites that set only essential cookies, which need no notice

Agent notes

  • Both buttons use variant secondary and the same size on purpose; do not restyle one to lead
  • Persist the choice yourself in onAccept and onReject, and only mount the notice when no choice is stored
  • It renders in flow; position it with className, for example fixed right-4 bottom-4
  • There is no close button, so dismissal always records a choice