npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alexpricedev/billet-cookie-consent

v0.2.0

Published

Standalone, GDPR-friendly cookie consent banner. Zero dependencies, vanilla DOM, scoped CSS tokens. Designed for Billet but framework-agnostic.

Readme

@alexpricedev/billet-cookie-consent

GDPR-friendly cookie consent banner. Vanilla DOM, zero dependencies, scoped CSS tokens. Framework-agnostic — designed to drop into Billet but works anywhere.

Copy this to your coding agent

Paste this into your agent (Claude Code, Cursor, etc.):

Install and wire up @alexpricedev/billet-cookie-consent in this repo.

1. Read the package README first:
   node_modules/@alexpricedev/billet-cookie-consent/README.md
   (or https://github.com/alexpricedev/billet-cookie-consent#readme).
   Follow its "Wire up in 3 steps" section. Adapt snippets to this
   repo's controller / template signatures where they differ — don't
   paste blindly.

2. If this repo has a CLAUDE.md or AGENTS.md, read it and respect its
   conventions (filenames, lint rules, where tests go). Those override
   anything generic in the package README.

3. Use three categories: necessary (required), analytics, marketing.
   Set policyUrl to "/privacy" — and add a /privacy stub page if one
   doesn't exist, with a co-located test.

4. Add a "Manage cookies" footer button as shown in the README's
   step 2. GDPR requires users be able to withdraw consent.

5. Apply the README's Theming block: map this repo's existing
   --color-* / --font-* (or equivalent) tokens to --cc-* on
   [data-cc-root]. Otherwise the banner will ship in the package's
   default look instead of matching the site.

6. Verify with the repo's check/test commands, plus the /browse skill
   (or equivalent headless browser tool): banner renders → reject →
   reload-no-banner → manage-cookies-reopens-modal → /privacy returns
   200. Do not scaffold a new browser-test harness — use whatever's
   already wired in.

7. Open a PR.
   - Title: "Add GDPR cookie consent" — but check `git log` first
     and adapt to the repo's commit convention if it uses one
     (e.g. conventional commits → `feat(consent): add GDPR cookie
     consent`). Commit conventions usually live in history, not in
     CLAUDE.md.
   - Body: short summary of what was wired + the verification
     steps you performed.
   - Screenshot: `gh pr create --body` can't embed local images.
     Capture the banner with the browser tool and attach it as a
     follow-up PR comment (`gh pr comment <n> --body-file …`) or
     via the GitHub web UI.

Out of scope: real analytics scripts, real privacy copy, translations,
and server-side parseConsent wiring (defer until a real analytics
script is added).

For AI agents

  • Package: @alexpricedev/billet-cookie-consent
  • Install: bun add @alexpricedev/billet-cookie-consent
  • Peer deps: none
  • Bundle side effects: appends one <div data-cc-root> to <body> on init(); writes one cookie (default name cc_consent)
  • Server runtime: any (./server export is a pure function over a Cookie header string)
  • Wiring: 3 client steps + 1 optional server step — see Wire up in 3 steps
  • Verify install: bun test in the consumer should still pass; document.querySelector('[data-cc-root]') returns a node after init

Install

bun add @alexpricedev/billet-cookie-consent
# or: npm install / pnpm add / yarn add

Wire up in 3 steps

1. Import the CSS once

In your global stylesheet (e.g. src/client/style.css):

@import "@alexpricedev/billet-cookie-consent/styles.css";

2. Initialize the banner on the client

Create a file that runs on every page (e.g. src/client/pages/consent.ts for Billet, or your existing client entry):

import { CookieConsent } from "@alexpricedev/billet-cookie-consent";

const consent = CookieConsent.init({
  categories: [
    { id: "necessary", label: "Strictly necessary", required: true,
      description: "Required for the site to function. Always on." },
    { id: "analytics", label: "Analytics",
      description: "Helps us understand how the site is used." },
    { id: "marketing", label: "Marketing",
      description: "Used to personalize ads and content." },
  ],
  policyUrl: "/privacy",
});

if (consent.has("analytics")) loadAnalytics();
document.addEventListener("cc:consent-granted", (e) => {
  if (e.detail.category === "analytics") loadAnalytics();
});

// "Manage cookies" footer trigger. Use a data attribute on the server-
// rendered button — never inline onclick — so this fits a server-JSX +
// islands pattern (e.g. Billet).
for (const el of document.querySelectorAll<HTMLElement>("[data-cc-open-prefs]")) {
  el.addEventListener("click", () => CookieConsent.current()?.show());
}

function loadAnalytics(): void {
  // Replace with your analytics loader. Idempotent: only run once.
}

In your footer template, render the trigger button:

<button type="button" data-cc-open-prefs>Manage cookies</button>

Wire the init file into your client entry. In Billet, register it in src/client/main.ts:

import "./pages/consent";

3. (Optional) Server-side gating

To avoid sending analytics <script> tags to users who haven't opted in, read the cookie in your request handler and pass the consent state to your template.

import { parseConsent } from "@alexpricedev/billet-cookie-consent/server";

const consent = parseConsent(req.headers.get("cookie"));
// consent?.state.analytics === true | false

In a Billet view controller, thread it into the template:

import { parseConsent } from "@alexpricedev/billet-cookie-consent/server";

export const home = {
  index(req: Request): Response {
    const consent = parseConsent(req.headers.get("cookie"));
    return render(<Home analytics={consent?.state.analytics === true} />);
  },
};

Then in Layout (or the page template):

{props.analytics && <script src="https://plausible.io/js/script.js" defer />}

Decision table

When wiring this up, you'll need to pick values. Sensible defaults are listed; change only when the listed condition applies.

| Option | Default | Change when | |---|---|---| | cookieName | cc_consent | Your app already uses this name for something else | | cookieMaxAgeDays | 365 | Your jurisdiction requires a shorter re-prompt window | | cookieSameSite | Lax | You serve this widget on a third-party origin (use None + secure: true) | | cookieSecure | true on HTTPS, else false | You need to test on plain HTTP and want to force one or the other | | position | bottom | Host UI has a fixed bottom bar — use top, bottom-left, or bottom-right | | policyUrl | none | You have a privacy policy page (recommended for GDPR compliance) | | categories | required, no default | Always supply. Minimum: [{ id: "necessary", label: "Necessary", required: true }] | | texts | English defaults | You need a different language or wording | | autoShow | true | You want to gate rendering on additional logic before showing the banner |

Category fields

Each entry in categories accepts:

| Field | Type | Notes | |---|---|---| | id | string (required) | Stable identifier used in the cookie and in has(id). Must be unique. | | label | string (required) | Shown in the preferences modal. | | description | string | Optional helper text under the label. | | required | boolean | Forces the category on and disables its toggle. Use for strictly-necessary cookies. | | default | boolean | Pre-ticks the category on first visit while leaving the toggle editable. Counts as granted if the user saves without touching it. |

default vs required. required is non-negotiable — always on, toggle disabled. default is a starting position the user can change — pre-ticked but editable, and "Reject all" turns it off. required wins: a category with both is always on. default only affects the first visit (and reset()); a returning user's saved choice is never overridden.

Compliance caveat. Under the GDPR/ePrivacy Directive, consent for non-essential cookies must be a deliberate opt-in. Pre-ticked boxes do not constitute valid consent (CJEU, Planet49, C-673/17). Only use default: true where you have a lawful basis and have considered the regulatory risk; in most cases consumers should opt in deliberately.

Theming

All design tokens are CSS custom properties scoped to [data-cc-root]. To theme, override them on the same selector (or any ancestor):

[data-cc-root] {
  --cc-bg:        var(--color-surface);
  --cc-fg:        var(--color-text);
  --cc-primary:   var(--color-primary);
  --cc-primary-fg:#ffffff;
  --cc-font:      var(--font-main);
  --cc-radius:    8px;
}

The package never reads from host token names (--color-* etc) — you opt in by mapping your tokens to ours. Nothing leaks out of [data-cc-root].

Available tokens: --cc-bg, --cc-fg, --cc-muted, --cc-primary, --cc-primary-fg, --cc-surface, --cc-border, --cc-radius, --cc-radius-sm, --cc-font, --cc-shadow, --cc-z, --cc-gap, --cc-pad, --cc-max-w.

API reference

CookieConsent.init(config): ConsentController

Renders the banner if no valid cookie is present, otherwise stays silent. Throws if called twice — call controller.destroy() first if you need to re-init.

Side effects: appends <div data-cc-root> to <body>; reads document.cookie.

CookieConsent.current(): ConsentController | null

Returns the active controller (or null if init hasn't run yet). Use this from code that doesn't have a direct reference to the controller — e.g. a global "Manage cookies" click handler.

controller.has(category): boolean

True iff the category is currently granted. Required categories are always true once a record exists.

controller.get(): ConsentMap

Returns a copy of the current consent map: { [categoryId]: boolean }.

controller.record(): ConsentRecord | null

Returns the full stored record ({ v, ts, cats }) or null if the user hasn't decided yet.

controller.acceptAll() / controller.rejectAll()

Persist all-on / all-off (required categories stay on). Dismisses banner and modal. Fires events.

controller.save(state: ConsentMap)

Persist a specific consent map. Required categories are forced on. Fires events.

controller.show()

Open the preferences modal. Use this for "Manage cookies" links in your footer (see step 2 above for the data-attribute pattern).

controller.reset()

Clear the cookie and re-show the banner. For testing or "withdraw consent" flows.

controller.destroy()

Remove DOM, allow init() to be called again. Useful for HMR.

parseConsent(cookieHeaderOrJar, opts?) (server)

import { parseConsent } from "@alexpricedev/billet-cookie-consent/server";

parseConsent(req.headers.get("cookie"));         // → { state, record } | null
parseConsent({ cc_consent: "..." });             // accepts pre-parsed jar
parseConsent(header, { cookieName: "custom" }); // override cookie name

Pure function. Returns null on missing/malformed/wrong-version cookie.

hasConsent(cookieHeaderOrJar, category, opts?) (server)

Convenience boolean check: hasConsent(req.headers.get("cookie"), "analytics").

Events

Dispatched on document as CustomEvent:

| Event | Detail | |---|---| | cc:consent-changed | { state: ConsentMap, record: ConsentRecord } — fires on every save | | cc:consent-granted | { category: string } — fires once per newly-granted category | | cc:consent-revoked | { category: string } — fires once per newly-revoked category |

document.addEventListener("cc:consent-granted", (e) => {
  if (e.detail.category === "analytics") loadAnalytics();
});

Cookie format

The cookie value is URL-encoded JSON:

{ "v": 1, "ts": 1736899200000, "cats": { "necessary": true, "analytics": false } }
  • v is the schema version. Future versions invalidate older cookies (user is re-prompted).
  • ts is the decision timestamp in unix ms.
  • cats maps every configured category id to a boolean.

If the configured category set ever drifts from the stored set (you add a new category), the cookie is treated as stale and the banner re-prompts.

Common pitfalls

  • Do not set HttpOnly on the consent cookie. The client must read it to know whether to load gated scripts.
  • Do not call CookieConsent.init() more than once per page — it throws. Use controller.destroy() first if you must.
  • Do not use inline onclick= for the "Manage cookies" button in a server-JSX codebase. Use the data-attribute + client-side listener pattern shown in step 2.
  • Do not import parseConsent "for discoverability" without using it — strict TypeScript / lint setups (noUnusedLocals) will reject it. Add a code comment instead, and import only when you have a real script to gate.
  • Do not assume parseConsent returning null means "no consent". It means "no decision yet" — treat it the same as all-off but consider whether to re-prompt.
  • The package never sets analytics cookies for you. It only stores the user's choice. You are responsible for conditionally loading your own analytics/marketing scripts.
  • Required categories are forced on at write time. If a user toggles necessary off in dev tools, the next save re-enables it.

Verifying the install

After wiring up, run in your host project:

bun test            # your existing tests still pass
bun run check       # your existing lint/typecheck still passes

Then in the browser (or via a headless browser tool):

  1. Hard-reload — banner appears.
  2. Click "Reject all" — banner disappears; document.cookie contains cc_consent=….
  3. Reload — no banner.
  4. Run document.querySelector('[data-cc-root]') — returns a node.
  5. Open preferences via your footer link — modal appears, ESC closes it.

Development

bun install
bun test            # 60+ tests, ~99% line coverage
bun run typecheck   # tsc --noEmit, strict mode

License

MIT © Alex Price