libconsent

The banner

Install the shadcn/ui banner and preferences dialog, or build your own.

libconsent can work with your existing banner. However, if you don't have one, libconsent ships a ready-made banner and preferences dialog that you can drop in!

libconsent's banner is a shadcn/ui registry item — the files are copied into your project and you edit them like your own components.

npx shadcn@latest add https://vantezzen.github.io/libconsent/r/consent.json

That writes components/consent/ — a banner, a preferences dialog, a floating trigger and the strings — and installs the shadcn primitives they use.

app/layout.tsx
import { ConsentProvider } from "@libconsent/core";
import { ConsentBanner } from "@/components/consent";

<ConsentProvider>
  {children}
  <ConsentBanner privacyPolicyUrl="/privacy" />
</ConsentProvider>;

Running the real provider. Answer the banner and watch the right column.

const { categories } = useConsent();
  • essential: true
  • functional: false
  • analytics: false
  • marketing: false
<CookieService id="google-analytics">  <GoogleAnalytics gaId="G-XXXXXXX" /></CookieService>

nothing rendered — no analytics consent

<CookieService id="youtube" fallback={<Poster />}>  <YouTubeEmbed id="dQw4w9WgXcQ" /></CookieService>
Needs marketing cookies

Opening the preferences from elsewhere

<ConsentBanner> renders both the banner and the preferences dialog, so a "Cookie settings" link in your footer needs no state of its own:

function Footer() {
  const { openSettings } = useConsent();
  return <button onClick={openSettings}>Cookie settings</button>;
}

Props

PropDefault
variant"bar""bar" is a corner card; "dialog" opens the full preferences straight away.
position"bottom-right""bottom-left" and "bottom-center" also available.
privacyPolicyUrl—Linked from both layers. Omitted, the link is left out.
messages—Overrides any of the strings.

The service list

The dialog lists the services your app gates. Add a <CookieService id="hotjar"> anywhere and Hotjar appears under Analytics with its own switch; remove it and the row goes away. There is no separate list to keep in sync.

Per-service switches appear when the consent platform accepts them. Keeping consent in the browser does; most platforms take category-level answers only, and the dialog then offers categories alone.

Text and translations

By default, the banner ships with english. However there are more translations available - simply pull them from the shadcn registry:

npx shadcn@latest add https://vantezzen.github.io/libconsent/r/consent-locales.json
import { de } from "@/components/consent/locales";

<ConsentBanner messages={de} />;

Eleven languages are included: ar, de, es, fr, it, ja, ko, nl, pt, ru, zh. Overrides are merged field by field, so a single string is a valid messages object:

<ConsentBanner messages={{ acceptAll: "Sounds good" }} />

Building your own

useConsentForm() holds the unsaved draft and the rules that act on it, so a dialog of your own is markup and event handlers:

function Preferences() {
  const form = useConsentForm();

  return (
    <form onSubmit={(e) => (e.preventDefault(), form.save())}>
      {form.categories.map((row) => (
        <label key={row.category}>
          <input
            type="checkbox"
            checked={row.granted}
            disabled={row.locked}
            onChange={(e) => row.toggle(e.target.checked)}
          />
          {row.category}
        </label>
      ))}
      <button disabled={!form.dirty}>Save</button>
    </form>
  );
}

Mount it inside something that unmounts when the dialog closes and the draft resets itself. Toggling a category also clears the per-service switches under it, so the category control always wins.

useConsentForm() needs a platform that accepts consent from your own UI — the built-in adapter, or Cookiebot. On the others it throws and points you at openSettings(), which opens their dialog instead.

On this page