Skip to content
The consent storeBuild your own UI

Build your own UI for JavaScript

Markdown
Loading…

A banner and a preferences dialog in plain JavaScript, drawn from the consent store.

What it does

@cookieyes/core decides what to show and records what the visitor chooses; the markup is yours. This page is the smallest complete banner on top of it: three files, about 60 lines. The pattern is the same whether you render with innerHTML, a Vue template or a Web Component.

The three files

The runtime, a render function that reads the store, and the wiring that connects buttons to actions.

src/consent.ts
import { initCookieYes } from "@cookieyes/core";

export const { consentStore, consentManager } = initCookieYes({
  mode: "cookie-only",
  regulation: "GDPR",
});
src/render.ts
import type { ConsentStoreState } from "@cookieyes/core";
import { consentStore } from "./consent";

// The categories a visitor can decide on. Required ones are always on and never shown.
const optional = consentStore.categories.list.filter((c) => !c.required);

export function render(root: HTMLElement, state: ConsentStoreState): void {
  if (state.hasActed && state.activeUI !== "dialog") {
    root.innerHTML = "";
    return;
  }

  if (state.activeUI === "dialog") {
    root.innerHTML = `
      <div role="dialog" aria-labelledby="cy-title">
        <h2 id="cy-title">Cookie preferences</h2>
        ${optional
          .map(
            (c) => `
          <label>
            <input type="checkbox" data-category="${c.id}" ${state.consents[c.id] ? "checked" : ""} />
            ${c.id}
          </label>`,
          )
          .join("")}
        <button data-action="save">Save</button>
        <button data-action="close">Cancel</button>
      </div>`;
    return;
  }

  root.innerHTML = `
    <div role="dialog" aria-labelledby="cy-title">
      <h2 id="cy-title">We use cookies</h2>
      <p>Required cookies are always on. Analytics and marketing need your yes first.</p>
      <button data-action="accept">Accept all</button>
      <button data-action="reject">Reject all</button>
      <button data-action="customise">Customise</button>
    </div>`;
}
src/main.ts
import { consentManager, consentStore } from "./consent";
import { render } from "./render";

const root = document.getElementById("consent") as HTMLElement;

// Draw on every change, including switches flipped inside the dialog.
consentStore.subscribe((state) => render(root, state));
render(root, consentStore.getState());

// One listener for every button. The manager updates the store, subscribe() redraws.
root.addEventListener("click", (event) => {
  const target = event.target as HTMLElement;
  switch (target.dataset.action) {
    case "accept":
      consentManager.acceptAll("banner");
      break;
    case "reject":
      consentManager.rejectAll("banner");
      break;
    case "customise":
      consentManager.showPreferences();
      break;
    case "save":
      consentManager.savePreferences("preferences");
      consentManager.hidePreferences();
      break;
    case "close":
      consentManager.hidePreferences();
      break;
  }
});

// A checkbox flips the live value; nothing is saved until Save.
root.addEventListener("change", (event) => {
  const input = event.target as HTMLInputElement;
  if (input.dataset.category) consentManager.updateCategory(input.dataset.category, input.checked);
});

Add <div id="consent"></div> to your page and this is a working banner: it shows until the visitor decides, hides after any decision, reopens as a dialog on Customise, and every button goes through the manager, which saves the cookie and runs your integrations.

Good to know

  • Draw from the store, not from your own state. Other things change consent too: a "manage cookies" link calling resetConsent(), a second tab. Rendering inside subscribe() keeps the UI right in every case.
  • Reopen it from anywhere. A footer link that calls consentManager.showPreferences() is enough.
  • Make it behave like the SDK's banner. It is a role="dialog" with aria-modal="true", does not take focus when it appears, keeps Tab inside until the visitor answers, and afterwards moves focus to a control that reopens the choice, so focus is never lost.
  • Accessibility is yours here. Keep the heading the dialog is labelled by, use real <button> elements, and keep 4.5:1 contrast.
  • Say where each decision was made. In mode: "self-hosted" each decision is sent as a consent record. The "banner" and "preferences" arguments in the example set its source; without them it says "api".
  • Other languages. consentStore.getCategoryText(id) gives a category's name in the active language, and consentStore.translations holds the standard strings; pass a catalogue through the i18n option.

Common mistakes

The banner never hides after Accept. Render is wired to on("change"), which does not run for showPreferences(). Draw from subscribe().

Analytics loads when a checkbox is ticked, before Save. It is wired to subscribe(). Load things from consentStore.on("change", …).

The dialog's checkboxes do not follow the clicks. render() reads state.categories (saved) instead of state.consents (live).

Next steps

On this page