Build your cookie banner on the tarteaucitron service catalog

zeste loads the 247 services that tarteaucitron.js supports, such as Google Analytics, YouTube, Matomo and Hotjar, only after the visitor accepts them. You write the banner with your own components in Svelte, React or Vue. On a site without a framework, one <script> tag adds a ready-made banner.

The banner texts exist in 36 languages. The tests check that no request reaches a third party before consent.

npx @zeste/cli initRead the docsAvailable on npm (v0.1.0)
A visitor refuses everything, allows YouTube from the video placeholder, then withdraws it in the preferences. The demo site is in French.

Why not keep tarteaucitron’s interface

tarteaucitron.js does the hard work. It knows how to load hundreds of services after consent, and its maintainers keep that list up to date. Its interface is harder to adapt. It generates its own HTML, so making the banner match a site means overriding its CSS, often with !important.

The site after moving to zeste, with its own banner and a video placeholder in its colorsThe site with tarteaucitron, whose banner has yellow buttons and a typeface foreign to the sitetarteaucitronzeste

On the left, an example site with tarteaucitron’s banner and placeholders.

On the right, the same site after moving to zeste. It uses its own banner, buttons and dialog, and loads the same services.

How zeste is built

The engine draws nothing

zeste stores the visitor’s choice in a cookie, loads each script once it is accepted and updates Google Consent Mode. Your components read its state and call its actions.

One module per service

The 247 services come from tarteaucitron.js. You import only the ones you use, and the browser downloads a service’s code when the visitor accepts it.

The components belong to you

npx @zeste/cli add writes a banner and a preferences dialog into your project, with your framework and your UI library. You edit them like any other file.

Bits UIRadixReka UIArk UIshadcn-svelteshadcn/uishadcn-vueyour design system

Set up zeste in four steps

Your framework

Declare your services

npx @zeste/cli init reads the project and declares the services it already uses. It recognizes a tarteaucitron configuration and the usual Google Analytics, Matomo and Hotjar snippets and YouTube embeds. Its report gives the file and line of each script to remove.

lib/consent.ts
import { gtag, youtube, matomocloud } from '@zeste/services';

export const services = () => [
  gtag({ gtagUa: 'G-XXXXXXX' }),
  youtube(),
  matomocloud({ matomoId: 1, matomoHost: 'https://example.matomo.cloud/' }),
];

Read the choice on the server

The server reads the consent cookie, so the first render already shows the right state. A visitor who has already chosen does not see the banner appear and disappear.

src/routes/+layout.server.ts
import { parseConsentCookie } from '@zeste/svelte';

export const load = ({ cookies }) => ({
  consent: parseConsentCookie(cookies.get('tarteaucitron')),
});

Generate the interface

The CLI detects your framework and UI library. It offers two styles, zeste by default and pelure.

npx @zeste/cli add                  # banner, preferences and placeholder
npx @zeste/cli add --style pelure   # the flat style with thick outlines

Add zeste to your layout

src/routes/+layout.svelte
<script lang="ts">
  import { createZeste, ZesteProvider } from '@zeste/svelte';
  import { messages } from '@zeste/services/i18n/en';
  import { CookieBanner, CookiePreferences } from '$lib/components/zeste';
  import { services } from '$lib/consent';

  let { data, children } = $props();
  const zeste = createZeste({ services: services(), initial: data.consent, messages });
</script>

<ZesteProvider {zeste}>
  {@render children()}
  <CookieBanner />
  <CookiePreferences />
  <!-- a permanent link to change the choice -->
  <button {@attach zeste.actions.openPreferences}>Cookie settings</button>
</ZesteProvider>

The docs cover each framework step by step.

Try the demo

The demo below runs zeste with the components that zeste add generates for Bits UI, Ark UI and shadcn-svelte. The services are simulated. Instead of contacting Google or YouTube, they write what they would have loaded in the log. The demo runs in Svelte, and the React and Vue components pass the same tests. The demo site is in French.

UI library
Style
Theme
Guard
maison-exemple.fr
Preview of the demo site with the zeste banner

Loading the demo…

Open “Personnaliser” to choose service by service, or click the button on the video placeholder. “Gestion des services” at the bottom of the site reopens the preferences. “Break the banner” shrinks the “Tout refuser” button, and the guard reports it in the log.

Videos and maps wait for consent

<ConsentGate> shows your placeholder until the visitor accepts the service, then shows the video or the map. The server renders the placeholder. Adding a video to the page does not reload the videos already playing.

<ConsentGate service="youtube" embed={{ videoID: 'dQw4w9WgXcQ' }}>
  {#snippet placeholder({ service, allow })}
    <CookiePlaceholder {service} {allow} />
  {/snippet}
</ConsentGate>

What zeste enforces

  • zeste sends no request to a third party before consent. The tests check this at the browser’s network level for all 247 services. tarteaucitron’s cookieless modes do contact a third party before consent, for example with Matomo Cloud or advanced Consent Mode. They stay off unless you turn them on.
  • In the generated banners, “Deny all” and “Accept all” have the same style.
  • Nothing is pre-checked in the preferences. A service the visitor did not choose stays refused.
  • Visitors can withdraw their consent at any time. zeste then deletes the service’s cookies, puts videos back behind their placeholder and offers to reload the page for scripts already running.
  • Google Consent Mode v2 takes one line, googleConsentMode(). The script version turns it on by default.
  • Consent lasts 13 months. zeste asks again when you change the list of services.
  • The GDPR asks you to be able to show that a visitor consented. zeste gives your code a record of each choice, with the date, the state of each service, your policy version and a random identifier stored in the cookie. You decide where to send it, and zeste sends nothing itself.

Warnings while you develop

In development, zeste checks how your banner actually renders and warns in the console when it breaks a measurable rule. This works with any design system.

A banner whose Deny all button was shrunk, with the guard’s warning in a console panel
Console (dev)
⚠ [zeste/consent] "Deny all" is much smaller than "Accept all" (area 24%, text 9px vs 14px). Both choices must be equally prominent.

The guard also warns when “Deny all” is missing, hidden or hard to read, and when nothing lets the visitor reopen the preferences. These checks do not replace a legal review.

Keeping a record of each choice

createZeste({
  services,
  policyVersion: '2026-10',
  onRecord: (record) =>
    navigator.sendBeacon('/consent', JSON.stringify(record)),
});
// record = {
//   id: '9f2c…', date: '2026-10-09T14:03:12Z',
//   action: 'denyAll', policyVersion: '2026-10',
//   choices: { gtag: false, youtube: false },
// }

The identifier is random and tied to one browser. When a returning visitor’s choice is read from the cookie, nothing is sent. Without a framework, put the same option in window.zesteConfig.

Moving a site from tarteaucitron

We moved an example site with its own design system, built on Ark UI, from tarteaucitron to zeste. Its visitors kept their choices, because zeste reads the same cookie as tarteaucitron.

On a site without a framework, replace tarteaucitron’s <script> tag with zeste’s, in the same place. Keep tarteaucitron.init, tarteaucitron.user and tarteaucitron.job as they are. Links to #tarteaucitron and the youtube_player markup keep working.

  1. Remove tarteaucitron and declare the same services

    The site loses two <script> tags. The configuration, such as gtagUa, moves to TypeScript. The old .youtube_player markup keeps working.

  2. Run npx @zeste/cli add

    The generated banner and preferences work right away.

  3. Use the design system’s components

    The site’s Button, Modal and Toggle replace the generated elements.

    Watch for button components that do not pass their props to the HTML element. Clicks then do nothing, and only the guard reports it.
  4. Put videos behind <ConsentGate>

    The server renders the placeholder, in the site’s style.

The cookie preferences in the site’s own dialog, with its switches
The preferences in the site’s dialog, with its own switches.

Read the full migration guide

How zeste is tested

The project runs 625 tests. Playwright drives Chromium on the built example apps, and nothing is mocked in the page. Behavior is tested once per UI library, and appearance once per style and theme.

What is testedHowTests
The 247 catalog servicesWith the network intercepted, no request leaves before consent, and each service has a visible effect once accepted.247
GA4, YouTube, Matomo CloudOn the real network, with the real scripts. The _ga cookies appear on acceptance and disappear on withdrawal.6
The generated componentsSvelte, React and Vue with 3 UI libraries each. Keyboard use, preferences and withdrawal once per library. Accessibility with axe, theme and equal buttons in both styles, light and dark. The guard once per framework.174
The same app six timesSvelteKit, Next.js, Nuxt, and each framework without a server, including React 18. Navigation, withdrawal and the guard everywhere, server rendering in the three apps that have it.81
The migrated siteChoices made under tarteaucitron are kept and the design system still works.15
The script versionA plain HTML site. Accessibility, keyboard use, withdrawal, the guard, a tarteaucitron configuration used as is, downloads limited to the declared services, a consent record on each choice.13
Automatic languageA browser in German, in Norwegian, and in a language without a translation. Only one language is downloaded.5
Engine, catalog, bindings, CLIUnit tests, including @zeste/cli init on an old PHP site and a check that the examples match what @zeste/cli add generates.84

Project status

The library works, is tested and published on npm.

Done

  • The engine, the full catalog and the texts in 36 languages.
  • Bindings for Svelte 5, React 18 and 19, and Vue 3, with or without server rendering (SvelteKit, Next.js, Nuxt).
  • A CLI that generates components for 3 frameworks and 3 UI libraries in two styles, and @zeste/cli init, which finds the services a project already uses.
  • A script version for sites without a framework, which replaces tarteaucitron.js and keeps its configuration.
  • Automatic language, from the page then the browser, among 36.
  • A record of each choice, ready to send to your server as proof of consent.
  • The compliance guard in development.
  • Migration from tarteaucitron without losing visitors’ choices.
  • Packages and CLI published on npm under the @zeste scope.

Next

  • Banner buttons that work without JavaScript, for very slow connections.
  • Tests on Firefox and Safari.

Questions

Do I still need to load tarteaucitron.js?

No. zeste reuses its service catalog and its translations, and replaces its engine and its interface. The tarteaucitron script is never loaded.

Will my visitors see the banner again after the move?

No. zeste reads and writes the same cookie as tarteaucitron, so the choices already made stay valid.

My design system is not Bits UI, Ark UI or shadcn. Can I use zeste?

Yes. Start from the closest generated variant and replace the imports with your components. zeste’s actions work on any button that passes its props to its HTML element: {@attach zeste.actions.denyAll} in Svelte, {...zeste.actions.denyAll} in React and v-bind="zeste.actions.denyAll" in Vue. A plain click handler on zeste.denyAll also works, but the guard cannot check that button.

Does zeste make my site compliant?

Not on its own, and no tool can. zeste applies strict defaults and checks what can be measured, such as requests before consent, button visibility and the link to change one’s choice. Your texts, your privacy policy and the purpose of each cookie still need a review.

How does it compare with tarteaucitron Pro?

tarteaucitron Pro is a hosted service with a dashboard and a consent log. zeste is a library that runs inside your project. For a consent log, zeste passes each choice to your code through onRecord, and you send it to your own server.

My site has no framework. Can I use zeste?

Yes. One <script> tag adds a banner, preferences and placeholders, in two styles. On a site that already uses tarteaucitron, replace its tag and zeste reads the existing configuration.

Does it work with React and Vue?

Yes. @zeste/react supports React 18 and 19 and Next.js, and @zeste/vue supports Vue 3 and Nuxt. Both have the same API as @zeste/svelte, and zeste add generates components for Radix, Reka UI, Ark UI and shadcn. For other frameworks, the engine (@zeste/core) and the catalog work without one.

What is the license?

MIT, like tarteaucitron.js. The service catalog and the translations come from tarteaucitron.js, Copyright AmauriC.