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

@ngrithms/cookie-consent

v0.6.0

Published

Modern Angular cookie consent — standalone, signals, provideCookieConsent(), SSR-safe, zero dependencies.

Readme

@ngrithms/cookie-consent

npm License: MIT

Modern Angular cookie consent — standalone components, signal-based state, provideCookieConsent() functional setup, SSR-safe, zero runtime dependencies.

A from-scratch replacement for the abandoned NgModule-era consent libraries. Designed for Angular 17+ (peer range >=17.2.0 <22.0.0).

Project home: ngrithms.aalbadra.workers.dev/cookie-consent · part of the @ngrithms family of modern Angular utilities.

Features

  • Standalone components, no NgModule, no forRoot()
  • Signal-based reactive consent state (with RxJS observable bridges)
  • Two-level data model: Category (visual group) → CookieItem (toggle) → CookieDetail (informational)
  • *ngrIfConsent="'item-key'" structural directive
  • Preset category constants (ANALYTICS_PRESET, MARKETING_PRESET, …) — spread them in or use as templates
  • First-class Google Consent Mode v2 adapter
  • Built-in i18n (en, fr) + custom-language API with icon path + fallback
  • Fully customizable copy via translation keys — no markup forks needed
  • Optional CSS theme presets — or go headless and style it yourself
  • SSR-safe out of the box
  • Zero runtime dependencies

Install

npm install @ngrithms/cookie-consent

Quick start

Three files — matches what ng new scaffolds.

// src/app/app.config.ts — register the provider
import { ApplicationConfig } from '@angular/core';
import { provideCookieConsent, ANALYTICS_PRESET, MARKETING_PRESET } from '@ngrithms/cookie-consent';

export const appConfig: ApplicationConfig = {
  providers: [
    provideCookieConsent({
      privacyPolicyUrl: '/privacy',
      categories: [ANALYTICS_PRESET, MARKETING_PRESET],
    }),
  ],
};
// src/app/app.component.ts — import the standalone components & directive
import { Component } from '@angular/core';
import { ConsentBannerComponent, ConsentBadgeComponent, IfConsentDirective } from '@ngrithms/cookie-consent';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ConsentBannerComponent, ConsentBadgeComponent, IfConsentDirective],
  templateUrl: './app.component.html',
})
export class AppComponent {}
<!-- src/app/app.component.html -->
<ngr-consent-banner></ngr-consent-banner>
<ngr-consent-badge></ngr-consent-badge>

<div *ngrIfConsent="'google_analytics'">
  <!-- Only rendered if the user consented to Google Analytics. -->
</div>
/* src/styles.css — pick a theme, or skip this and theme it yourself */
@import '@ngrithms/cookie-consent/themes/default.css';

How it works

The library is a thin state machine. It exposes a set of CookieItem.key strings (the toggles in the UI) and tracks which ones the user has granted. It does not load any third-party SDKs or set any third-party cookies on its own — the only cookie it writes is its own preferences cookie (ngrithms_consent_*).

Your job, as the consumer, is to wire each CookieItem.key to the actual side effect — loading gtag.js, mounting a YouTube <iframe>, calling Hotjar's init function, etc. The library gives you four patterns for that wiring:

| Pattern | Use it for | API | |---|---|---| | Structural directive | Iframes, embeds, components that should mount/unmount with consent | *ngrIfConsent="'<key>'" | | Script loader | Third-party SDKs loaded via <script> tag (GA, Hotjar, Intercom, FB Pixel) | ScriptLoaderService.load({ itemKey, src, ... }) | | Reactive state | Imperative code in your own services | ConsentService.isGranted('<key>') signal / item$('<key>') observable | | Google Consent Mode v2 | GA4 / Google Ads (keep gtag.js loaded always, gate behavior via gtag('consent', 'update', ...)) | applyGoogleConsentMode(consent, { mapping, defaults }) |

The CookieItem.key is the join column — it appears in your category config, in the UI as a toggle, and in every wiring pattern above. See Integration patterns below for working examples of each.

Consent data model

Category   (visual group, e.g. "Analytics")
  └─ items: CookieItem[]   (toggleable — what *ngrIfConsent checks)
       └─ cookies: CookieDetail[]  (informational rows in the details view)

Each CookieItem.key is the value you pass to *ngrIfConsent="'<key>'" and to ConsentService.isGranted(...). Keep keys stable across releases — if you need to rename one later, see Migrating persisted state.

Building your categories

The categories you pass to provideCookieConsent({ categories: [...] }) are the toggle groups in the consent UI. There are three ways to assemble one:

  1. Drop in a preset — fastest for prototypes.
  2. Build a category from items with makeCategory(...) — recommended for real apps, where your toggle set rarely matches a preset 1:1.
  3. Spread a preset and extend it — when you want a preset's items plus a custom one.
import {
  provideCookieConsent,
  makeCategory,
  // presets (bundles of items under one heading)
  ANALYTICS_PRESET, FUNCTIONAL_PRESET,
  // individual items — pick exactly what you integrated
  GOOGLE_ADS, META_PIXEL,
} from '@ngrithms/cookie-consent';

provideCookieConsent({
  categories: [
    // 1. Drop in a preset as-is.
    ANALYTICS_PRESET,

    // 2. Hand-build a category from items — group them how YOU think about them.
    //    Useful when the preset taxonomy doesn't match your real toggle set.
    makeCategory({
      key: 'tracking',
      name: { en: 'Tracking & Ads', fr: 'Suivi & publicités' },
      description: { en: 'Conversion tracking and remarketing.' },
      items: [GOOGLE_ADS, META_PIXEL],
    }),

    // 3. Spread a preset and append your own one-off CookieItem.
    { ...FUNCTIONAL_PRESET, items: [...FUNCTIONAL_PRESET.items, myCustomItem] },
  ],
});

Available items

| Family | Items | |---|---| | Analytics | GOOGLE_ANALYTICS, HOTJAR, MIXPANEL | | Marketing | GOOGLE_TAG_MANAGER, META_PIXEL, LINKEDIN_INSIGHT | | Advertising | GOOGLE_ADS, MICROSOFT_ADS | | Functional | PREFERENCES, LIVE_CHAT | | Social embeds | YOUTUBE, TWITTER, VIMEO |

The keys these items carry (google_analytics, google_ads, etc.) are what *ngrIfConsent="'<key>'" and the rest of the integration patterns check. They are part of the public contract and stable across patch releases.

Available presets

ANALYTICS_PRESET, MARKETING_PRESET, ADVERTISING_PRESET, FUNCTIONAL_PRESET, SOCIAL_PRESET — each bundles the items shown in the table above under one category heading. Use them for quick-start; switch to makeCategory when your real toggle set diverges (which it usually will).

makeCategory(spec)

Typed builder for hand-assembled categories with light validation:

  • Throws if spec.key === 'essential' (reserved — customise the essential category via config.essential).
  • Throws on duplicate item keys within the same spec.
import { makeCategory, type CategorySpec } from '@ngrithms/cookie-consent';

const tracking = makeCategory({
  key: 'tracking',
  name: { en: 'Tracking & Ads' },
  description: { en: 'Conversion tracking and remarketing.' }, // optional
  items: [GOOGLE_ANALYTICS, GOOGLE_ADS],
});

Need a one-off CookieItem? Define it inline in the items array — it just has to satisfy the CookieItem type (key, name, description, optional cookies, etc.).

Integration patterns

Gating content with *ngrIfConsent

<!-- Only render when consent for this CookieItem is granted -->
<div *ngrIfConsent="'google_analytics'">
  <iframe src="https://www.googletagmanager.com/..."></iframe>
</div>

<!-- Optional fallback template when consent is missing -->
<div *ngrIfConsent="'google_analytics'; else placeholder">
  <iframe src="..."></iframe>
</div>
<ng-template #placeholder>
  Enable analytics in <a href="#" (click)="consent.open()">cookie preferences</a> to see this content.
</ng-template>

When the user toggles consent off, the directive destroys the contained view — iframes are unmounted, components are torn down. The next toggle-on re-creates them.

Deferring <script> tags with ScriptLoaderService

ScriptLoaderService injects a <script> element when consent for an item is granted, removes it when consent is revoked, and re-injects on re-grant. SSR-safe (no-op on the server).

Removing the element does not undo side effects the script already had on window. Analytics SDKs that installed globals stay installed until page reload — for those, pair this with applyGoogleConsentMode (below) to gate runtime behavior on top of script presence.

import { inject } from '@angular/core';
import { ScriptLoaderService } from '@ngrithms/cookie-consent';

const loader = inject(ScriptLoaderService);

loader.load({
  itemKey: 'google_analytics',
  src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX',
  attrs: { async: true },
  onLoad: () => {
    (window as any).dataLayer ??= [];
    (window as any).gtag = function () { (window as any).dataLayer.push(arguments); };
    (window as any).gtag('js', new Date());
    (window as any).gtag('config', 'G-XXXXX');
  },
});

| Option | Type | Notes | |---|---|---| | itemKey | string | CookieItem.key that must be granted before injection | | src | string | External script URL | | inline | string | Inline script body (used when src is omitted) | | attrs | Record<string, string \| boolean> | <script> attributes; true sets the attribute with no value, false omits | | onLoad | () => void | Fires once the external script reports load |

Reading state in your own code

import { inject, effect } from '@angular/core';
import { ConsentService } from '@ngrithms/cookie-consent';

const consent = inject(ConsentService);

// Signal — re-runs effects when consent changes
const isGa = consent.isGranted('google_analytics');
effect(() => {
  if (isGa()) initHotjar();
});

// Observable — for RxJS pipelines. Callable from anywhere
// (no active injection context required).
consent.item$('google_analytics').subscribe(on => console.log('GA granted?', on));

// Imperative control
consent.acceptAll();
consent.denyAll();
consent.accept(['google_analytics']);   // accept a specific subset
consent.open();         // open banner
consent.openModal();    // open preferences modal
consent.reset();        // clear stored cookie + re-prompt

Google Consent Mode v2

GA4 has its own consent protocol — you keep gtag.js loaded always, but call gtag('consent', 'update', {...}) whenever the user changes settings. Use this instead of removing the GA script.

import { applyGoogleConsentMode, ConsentService } from '@ngrithms/cookie-consent';
import { inject } from '@angular/core';

const consent = inject(ConsentService);

applyGoogleConsentMode(consent, {
  mapping: {
    google_analytics: 'analytics_storage',
    google_ads: ['ad_storage', 'ad_user_data', 'ad_personalization'],
  },
  defaults: {
    analytics_storage: 'denied',
    ad_storage: 'denied',
  },
});

Customizing text

Every visible string in the UI is overridable. There are two surfaces — per-item text (set inline on your categories) and chrome text (banner heading, button labels, etc., overridden via translation keys).

Per-item and per-category labels

Category.name, Category.description, CookieItem.name, CookieItem.description, CookieItem.privacyPolicyUrl, CookieDetail.purpose, and CookieDetail.duration all accept string | TranslatableString:

provideCookieConsent({
  categories: [{
    key: 'analytics',
    name: 'Analytics',
    description: 'Helps us understand how visitors use the site.',
    items: [{
      key: 'google_analytics',
      name: 'Google Analytics',
      description: 'Anonymous traffic measurement.',
      privacyPolicyUrl: 'https://policies.google.com/privacy',
      cookies: [
        { name: '_ga', provider: 'Google', purpose: 'Distinguishes users', duration: '2 years' },
      ],
    }],
  }],
});

For multilingual sites use the TranslatableString shape — { en: 'Analytics', fr: 'Statistiques' } — and it resolves against the active language.

Banner, modal, and button copy

All chrome text is keyed by a translation string ID. Override the built-in en pack (or any other) to change the wording without forking the components:

| Key | What it controls | |---|---| | banner.title | Banner heading | | banner.description | Banner body text | | banner.accept_all | "Accept all" button | | banner.deny_all | "Reject all" button | | banner.customize | "Customize" / preferences button | | banner.save_preferences | "Save preferences" button (inside modal) | | banner.show_details | Per-item "Show details" toggle | | banner.hide_details | Per-item "Hide details" toggle | | badge.open | Floating re-open badge label / aria-label | | footer.privacy_policy | Privacy policy link text | | footer.imprint | Imprint link text | | modal.locked | Tooltip on the always-on essential toggle | | modal.cookie.name | "Name" column header in the details table | | modal.cookie.provider | "Provider" column header | | modal.cookie.purpose | "Purpose" column header | | modal.cookie.duration | "Duration" column header |

Override pattern:

provideCookieConsent({
  categories: [...],
  customLanguages: {
    en: {
      languageKey: 'en',
      languageName: 'English',
      translations: {
        'banner.title': 'We use cookies on Acme',
        'banner.description': 'Pick what you want to allow. You can change this any time from the badge in the corner.',
        'banner.accept_all': 'Allow everything',
        'banner.deny_all': 'Reject',
        'banner.customize': 'Choose',
        'footer.privacy_policy': 'Our privacy policy',
      },
    },
  },
});

Only keys you specify are overridden — everything else falls back to the bundled en strings.

Adding a language

provideCookieConsent({
  categories: [...],
  defaultLanguage: 'en',
  availableLanguages: ['en', 'sw'],
  customLanguages: {
    sw: {
      languageKey: 'sw',
      languageName: 'Kiswahili',
      iconPath: '/assets/flags/sw.svg',
      fallback: 'en',                      // missing keys fall back to English
      translations: {
        'banner.title': 'Tunatumia vidakuzi',
        'banner.accept_all': 'Kubali zote',
        // ...
      },
    },
  },
});

The language switcher appears automatically when availableLanguages.length > 1 (override with showLanguageSwitcher).

Per-language links

provideCookieConsent({
  // Single URL for every language:
  privacyPolicyUrl: '/privacy',

  // …or a per-language URL via TranslatableString:
  privacyPolicyUrl: { en: '/privacy', fr: '/fr/confidentialite' },

  // Same for imprintUrl:
  imprintUrl: { en: '/legal', fr: '/fr/mentions-legales' },
});

Recipes

Hide the "Reject all" button

provideCookieConsent({
  categories: [...],
  hideDeny: true,
});

Only Customize and Accept all remain in the banner action row. Note: hiding the reject button is legally contentious under GDPR — many DPAs (notably France's CNIL) hold that "Reject all" must be at least as visually prominent as "Accept all." Use with care.

Style the "Customize" button as an outline button

By default Customize renders as a borderless link-style "ghost" button. Give it a border so it visually matches Reject all:

:root {
  --ngrithms-btn-ghost-bg: transparent;
  --ngrithms-btn-ghost-fg: #1f2937;
  --ngrithms-btn-ghost-border: rgba(0, 0, 0, 0.16);
  --ngrithms-btn-ghost-bg-hover: rgba(0, 0, 0, 0.04);
  --ngrithms-btn-ghost-border-hover: rgba(0, 0, 0, 0.24);
  --ngrithms-btn-ghost-text-decoration-hover: none;
}

The full ghost-button surface: --ngrithms-btn-ghost-bg, -fg, -border, -bg-hover, -fg-hover, -border-hover, -padding-inline, -text-decoration-hover. All default to the current borderless link styling, so adding these only takes effect if you set them.

Drive the banner language from your app's own switcher

If your app already has a language switcher in its navbar, you can suppress the banner's built-in switcher and let your nav drive the banner instead. LanguageService.setLanguage(code) is public — call it from your nav's change handler:

import { Component, inject } from '@angular/core';
import { LanguageService } from '@ngrithms/cookie-consent';

@Component({
  selector: 'app-nav',
  template: `
    <select (change)="onLangChange($any($event.target).value)">
      <option value="en">English</option>
      <option value="fr">Français</option>
    </select>
  `,
})
export class NavComponent {
  private readonly i18n = inject(LanguageService);

  onLangChange(code: string): void {
    this.i18n.setLanguage(code);
    // ...plus whatever else your app does on language change.
  }
}

Then hide the banner's built-in switcher (the availableLanguages list is still needed so the library knows which packs are valid):

provideCookieConsent({
  categories: [...],
  availableLanguages: ['en', 'fr'],
  showLanguageSwitcher: false,
});

LanguageService also exposes currentLanguage() (signal) and currentLanguage$ (observable) if your app needs to react to language changes the other way — e.g., to sync the nav back to a language change made elsewhere.

Themes

Pick one and import in your global stylesheet:

@import '@ngrithms/cookie-consent/themes/default.css';
@import '@ngrithms/cookie-consent/themes/dark.css';
@import '@ngrithms/cookie-consent/themes/minimal.css';
@import '@ngrithms/cookie-consent/themes/rounded.css';

Or override individual CSS custom properties yourself — every visual aspect is themable:

:root {
  --ngrithms-btn-primary-bg: #4f46e5;
  --ngrithms-banner-radius: 16px;
  --ngrithms-switch-on: #16a34a;
}

For full headless control set theme: 'none' and style the semantic class names (.ngr-consent-banner, .ngr-consent-modal__switch, etc.) yourself.

Configuration reference

| Option | Type | Default | Notes | |---|---|---|---| | categories | Category[] | required | The consent categories shown in the UI | | essential | Partial<Category> | implicit | Override label/items of the always-granted essential category | | privacyPolicyUrl / imprintUrl | string \| TranslatableString | — | Linked in the footer | | defaultLanguage | string | 'en' | | | availableLanguages | string[] | ['en'] | | | showLanguageSwitcher | boolean | auto | True when >1 language | | customLanguages | Record<string, LanguagePack> | {} | BYO translations with optional flag icon (see Customizing text) | | position | ConsentPosition | 'bottom-bar' | bottom-bar, top-bar, corners, modal | | theme | 'default'\|'dark'\|'minimal'\|'rounded'\|'none' | 'default' | 'none' ships no CSS (headless mode) | | showBadgeOpener | boolean | true | Floating re-open button | | badgePosition | BadgePosition | 'left-bottom' | | | cookiePrefix | string | 'ngrithms_consent_' | | | cookieExpiryDays | number | 365 | | | showCookieDetails | boolean | true | Show the per-cookie details table | | hideDeny | boolean | false | Hide the "Reject all" button | | hideImprint | boolean | false | Hide the imprint link | | customClass / customOpenerClass | string | — | BYO styling hooks | | excludeRoutes | string[] | [] | Routes on which the banner is suppressed | | version | number | 1 | Bump to force re-prompt without changing storage shape | | schemaVersion | number | 1 | Bump when you rename a CookieItem.key or change persisted-state semantics. Triggers migrate on read | | migrate | (stored: unknown) => ConsentState \| null | — | Called when stored schemaVersionconfig.schemaVersion. Return migrated state or null to re-prompt |

Migrating persisted state across key renames

version forces a re-prompt without touching the stored shape. schemaVersion is for when the shape changes — you renamed a CookieItem.key, restructured granted, or otherwise made old data ambiguous. Bump it and provide a migrate hook to translate forward instead of dropping users' decisions on the floor.

provideCookieConsent({
  categories: [...],
  schemaVersion: 2,
  migrate: (stored) => {
    const old = stored as {
      granted: Record<string, boolean>;
      timestamp: number;
      version: number;
    };
    // v1 used "ga"; v2 standardised on "google_analytics".
    const granted = { ...old.granted };
    granted['google_analytics'] = granted['ga'] === true;
    delete granted['ga'];
    return {
      granted,
      timestamp: old.timestamp,
      version: old.version,
      schemaVersion: 2,
    };
  },
});

Return null from migrate to discard the stored data and re-prompt the user. Stored cookies written before this library introduced schemaVersion (i.e. by 0.1.x) are treated as schema 1 automatically — no migration needed for that upgrade.

SSR

All DOM access is guarded by isPlatformBrowser. The library works under provideServerRendering() and @angular/ssr without configuration. ConsentService.item$() is callable from any context — it captures an Injector internally and runs toObservable inside runInInjectionContext, so calling it from event handlers, lifecycle methods, or arbitrary service code does not require an active injection context.

License

MIT © Aboud Badra