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

@bulut5/ck

v0.6.0

Published

Lightweight cookie-consent banner with Google Consent Mode v2 + Microsoft UET signaling.

Readme

ck

A lightweight, dependency-free cookie-consent banner for React that signals Google Consent Mode v2 and Microsoft UET. Default-deny, granular categories, SSR-safe, themeable with Tailwind.

Install

npm i @bulut5/ck

Usage

import { ConsentProvider, CookieConsent } from "@bulut5/ck";

const config = {
  brand: "example",
  locale: "en-GB",
  privacyPolicyUrl: "/privacy",
  copy: { /* … see BrandConfig */ },
};

export default function App() {
  return (
    <ConsentProvider config={config}>
      {/* your app */}
      <CookieConsent />
    </ConsentProvider>
  );
}

Prompt timing and appearance

By default the card opens on mount for an undecided visitor. Two options change when and how it appears, without changing what it asks or how the decision is stored.

export const consentConfig: BrandConfig = {
  // ...
  prompt: "onInteraction",   // "immediate" (default) | "onInteraction" | "manual"
};

<CookieConsent appearance="strip" />   // "card" (default) | "strip"

prompt

  • immediate — on mount, as before.
  • onInteraction — 400 ms after the visitor's first pointer, key or scroll input, so it never lands under the finger that just tapped. Consent Mode stays at its default until the decision.
  • manual — never opens by itself. The page calls reopen() from useConsent() when it wants the prompt.

In every mode the floating badge is rendered whenever the prompt is not showing, so settings are always one tap away.

appearance

  • card — the bottom-right card.
  • strip — one full-width bar along the bottom: body, policy link, Accept, Reject and Select. Reject keeps the same size and weight as Accept. Select opens the card for the category view.

Sharing one decision across subdomains

By default the decision is stored in localStorage, which is scoped to the origin — so www.example.com and lp.example.com never share it, and a visitor moving between them is asked twice. When one brand spans several hosts, set storage.cookieDomain and the record moves to a cookie scoped to that domain:

export const consentConfig: BrandConfig = {
  brand: "example",
  locale: "en-GB",
  privacyPolicyUrl: "https://www.example.com/privacy-policy",
  storage: { cookieDomain: ".example.com" },   // leading dot optional
  copy: copyForLocale("en-GB"),
};

Cookie attributes: Path=/, SameSite=Lax, Max-Age from storage.cookieMaxAgeDays (default 180), and Secure on https (omitted on http so localhost still works). A cookie is the only mechanism browsers offer here — the old iframe + postMessage trick is broken by Safari ITP and Chrome's storage partitioning.

Two things to know:

  • cookieDomain must be a domain the page can set a cookie for — a parent of the current host, and not a public suffix. Browsers silently drop cookies that violate this, so a wrong value degrades to "no consent is ever remembered" rather than to an error. Verify in DevTools → Application → Cookies after the first deploy.
  • Switching an existing site to a cookie re-prompts its current visitors once. There is deliberately no localStorage→cookie migration: a stale per-origin record would defeat the point of sharing. Every property on the shared domain should be switched together.

The consent record is itself strictly necessary, so storing it needs no consent.

Locale presets

copy can come from a bundled preset instead of being written out per site. Presets ship for en-GB and sv-SE. They are deliberately generic — plain, voiceless boilerplate meant as a lawful starting point, not finished copy. Expect to override them for your own voice:

import { copyForLocale, SV_SE_COPY, type BrandConfig } from "@bulut5/ck";

export const consentConfig: BrandConfig = {
  brand: "example",
  locale: "sv-SE",
  privacyPolicyUrl: "https://example.com/integritetspolicy/",
  copy: SV_SE_COPY,               // or: copyForLocale("sv-SE")
};

copyForLocale falls back to the bare language (sv-FI → sv-SE) and then to en-GB, so an unknown tag still renders a banner. Override wholesale, or spread over a preset to change only some strings: copy: { ...SV_SE_COPY, body: "…your wording…" }.

Whatever you override, keep reject worded exactly as plainly as accept — ICO/PECR and IMY both require equal prominence.

The host page should set the Google Consent Mode default to denied inline in <head> before the bundle loads; the banner flips signals to granted on consent. Tailwind consumers should add ./node_modules/@bulut5/ck/dist/**/*.{js,mjs} to their content so the banner classes compile.

MIT licensed.