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

@zdenekkurecka/astro-consent

v0.3.2

Published

Astro integration for GDPR/ePrivacy-friendly cookie consent: banner, preferences modal, runtime API, category-based consent state, strict-CSP safe, works with or without View Transitions.

Readme

astro-consent

An Astro integration for GDPR / ePrivacy-friendly cookie consent — banner, preferences modal, runtime API, and typed events. Zero runtime dependencies. Strict-CSP safe. Works with or without <ClientRouter /> / View Transitions.

npm version npm downloads license astro


Table of contents


Why this integration?

Most cookie-consent libraries were written for plain HTML sites and fall over the moment you drop them into a modern Astro app: they inject inline scripts (breaking strict CSPs), they re-initialize incorrectly on View Transitions, or they force you to serialize your tracker callbacks into a JSON config.

astro-consent is built for the way Astro actually works:

  • It is installed as an Astro integration, not a <script> tag.
  • It runs through Astro's Vite / CSS pipeline, so its script and stylesheet are emitted as hashed external assets — compatible with script-src 'self'; style-src 'self' with no 'unsafe-inline'.
  • It re-initializes on astro:page-load, so it behaves correctly on sites that use <ClientRouter /> for View Transitions.
  • You wire your trackers up in regular <script> tags in your layout, using typed document events — no callback serialization, full access to your module scope.

Features

  • Banner + preferences modal out of the box
  • Category-based consent (analytics, marketing, … — whatever you declare), plus an always-on implicit essential category
  • Versioned consent — bump a number to re-prompt every user
  • Typed config, runtime API, and document event map
  • Optional cookie policy link surfaced in both banner and preferences modal
  • Fully configurable UI text with partial overrides and per-locale resolution from <html lang> — every banner / modal string can be translated or customised
  • Accessible modal: role="dialog" / aria-modal, focus trap, focus restoration, Escape to close, click-outside to dismiss
  • Declarative script blocking via type="text/plain" + data-cc-category — gate trackers and embeds without writing glue code
  • Google Consent Mode v2 out of the box: opt-in config that maps your categories to GCM signals, injects the default-denied snippet, and wires gtag('consent', 'update', …) into the consent events
  • Strict-CSP safe: no inline <script>, no inline <style>
  • View Transitions ready: initializes on DOMContentLoaded and astro:page-load, idempotently
  • Zero runtime dependencies

Installation

The recommended way is to use Astro's integration installer, which adds the package and wires it into astro.config.* in one step:

# pnpm
pnpm astro add @zdenekkurecka/astro-consent
# npm
npx astro add @zdenekkurecka/astro-consent
# yarn
yarn astro add @zdenekkurecka/astro-consent

Heads up: the integration requires at least version and categories. astro add inserts a bare integration call with an auto-derived import name (zdenekkureckaconsent()) — open astro.config.* after it runs and pass the required options shown in Quick start. Feel free to rename the import to cookieConsent to match the examples below. You'll get a clear error at build time if you forget to fill in the config.

Or install manually:

# pnpm
pnpm add @zdenekkurecka/astro-consent
# npm
npm install @zdenekkurecka/astro-consent
# yarn
yarn add @zdenekkurecka/astro-consent

Peer dependency: astro@^5 || ^6. Node >=18.17.

Quick start

Add the integration to your astro.config.* and declare whichever categories you need:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import cookieConsent from '@zdenekkurecka/astro-consent';

export default defineConfig({
  integrations: [
    cookieConsent({
      version: 1,
      categories: {
        analytics: {
          label: 'Analytics',
          description: 'Helps us understand how the site is used.',
          default: false,
        },
        marketing: {
          label: 'Marketing',
          description: 'Used to personalize ads.',
          default: false,
        },
      },
    }),
  ],
});

That's it. On the next page load you'll see a banner, the preferences modal is accessible via the "Manage preferences" button, and the current state is persisted under localStorage.

Note: An essential category is always implicit. It appears in the preferences modal as a disabled-on toggle and is always true in the consent state. You do not need to declare it yourself — and you shouldn't.

Configuration

interface ConsentConfig {
  /** Bump this number to invalidate existing consent and re-prompt users. */
  version: number;

  /** The user-visible categories of cookies you want consent for. */
  categories: Record<string, {
    label: string;
    description: string;
    default: boolean;
  }>;

  /**
   * Optional link to your cookie / privacy policy. When provided, a small
   * link is rendered inside the banner and inside the preferences modal.
   * Only `http(s)://` URLs and same-origin paths (`/…`, `#…`, `?…`) are
   * accepted — other schemes (e.g. `javascript:`) are ignored.
   */
  cookiePolicy?: {
    url: string;
    /** Defaults to `"Cookie Policy"`. */
    label?: string;
  };

  /**
   * localStorage key used to persist the consent record. Override this when
   * multiple Astro apps share a single origin (e.g. `example.com/docs` and
   * `example.com/app`) so they don't clobber each other's state.
   *
   * @default "astro-consent"
   */
  storageKey?: string;

  /**
   * Maximum age of a stored consent record, in days. When set, a consent
   * record older than this is treated as missing and the banner is
   * re-shown — useful for complying with GDPR/DPA guidance that
   * recommends re-prompting every 6–12 months.
   *
   * @default undefined (no expiry)
   */
  maxAgeDays?: number;

  /**
   * Enables verbose `console.debug` logging of runtime events (init, banner
   * show, accept/reject/save, event dispatch, storage writes) and exposes
   * `window.astroConsent.debug()` for an on-demand state dump. Gate it
   * behind `import.meta.env.DEV` so it never ships to production.
   *
   * @default false
   */
  debug?: boolean;

  /**
   * Single-language text overrides for the banner and modal. Any field
   * omitted falls back to the built-in English default. Also used as a
   * shared fallback layer under `localeText`.
   */
  text?: ConsentText;

  /**
   * Per-locale text overrides. Keys are BCP 47 language tags matched
   * against `<html lang>` at runtime (e.g. `"en"`, `"cs"`, `"en-US"`).
   *
   * Resolution order: exact match → primary subtag → `text` →
   * built-in defaults.
   */
  localeText?: Record<string, ConsentText>;

  /**
   * Google Consent Mode v2 integration. When set, the integration injects an
   * inline snippet at the top of `<head>` to pre-declare denied defaults, and
   * auto-dispatches `gtag('consent', 'update', …)` on every consent event.
   *
   * Opt-in. Requires `'unsafe-inline'` (or a matching hash) under strict CSP —
   * see [Enable Google Consent Mode v2](#enable-google-consent-mode-v2).
   */
  googleConsentMode?: GoogleConsentModeConfig;
}

interface ConsentText {
  // Banner
  bannerText?: string;
  acceptAll?: string;        // shared by banner + modal
  rejectAll?: string;        // shared by banner + modal
  manage?: string;

  // Modal
  modalTitle?: string;
  closeAriaLabel?: string;
  savePreferences?: string;

  // Essential category
  essentialLabel?: string;
  essentialDescription?: string;

  /** Per-category label/description overrides (key = category key). */
  categories?: Record<string, { label?: string; description?: string }>;
}

Example with a cookie policy link:

cookieConsent({
  version: 1,
  cookiePolicy: {
    url: '/legal/cookies',
    label: 'Cookie Policy',
  },
  categories: {
    analytics: {
      label: 'Analytics',
      description: 'Help us understand how visitors use the site.',
      default: false,
    },
  },
});

Under the hood, the integration:

  1. Registers a Vite plugin exposing this config to the client via a virtual module (virtual:astro-consent/init).
  2. Injects the runtime with injectScript('page', ...), which Astro compiles into a hashed <script type="module" src="…"> — never inline.
  3. Injects the stylesheet with injectScript('page-ssr', ...), which flows through Astro's CSS pipeline and becomes a hashed external <link rel="stylesheet"> — never inline.

How-tos

React to consent changes

The integration dispatches typed CustomEvents on document. Subscribe from a regular <script> tag in your layout — you have full access to the module scope (imports, closures, framework globals), unlike callbacks that would have to be serialized through integration config.

---
// src/layouts/Layout.astro
---
<html>
  <body>
    <slot />
    <script>
      import { loadAnalytics, loadAds } from '../lib/trackers';

      // Fired once per session after consent has been given (or after we
      // detect an existing valid consent record on first page load).
      document.addEventListener('astro-consent:consent', (e) => {
        if (e.detail.categories.analytics) loadAnalytics();
        if (e.detail.categories.marketing) loadAds();
      });

      // Fired whenever the user updates their preferences after the initial
      // consent. Use this to tear down or re-enable trackers.
      document.addEventListener('astro-consent:change', (e) => {
        console.log('consent updated', e.detail);
      });
    </script>
  </body>
</html>

Both events are typed: importing this package augments DocumentEventMap, so e.detail is inferred as ConsentState.

Trigger the UI from your own buttons

Any element with a data-cc attribute is handled by the delegated listener — you don't need to add event listeners yourself:

<button data-cc="accept-all">Accept all</button>
<button data-cc="reject-all">Reject all</button>
<button data-cc="manage">Manage preferences</button>

Open the preferences modal from a footer link

A classic "Cookie settings" link in your footer:

<footer>
  <button type="button" data-cc="manage">Cookie settings</button>
</footer>

Or programmatically, from anywhere in your app:

window.astroConsent?.showPreferences();

Gate third-party scripts (GA, Meta Pixel, …)

There are two ways to gate a tracker: a declarative markup pattern (good for 90% of cases), and the event-based hook (for anything that needs custom logic). They compose — use whichever fits each tracker.

Declarative blocking (recommended)

Mark a <script> with type="text/plain" and a data-cc-category. The browser treats text/plain scripts as inert data, so the tracker stays dormant until the integration unblocks it once the category is granted. Use data-cc-src for external scripts and a plain body for inline ones.

<!-- External — recommended, CSP-safe -->
<script
  is:inline
  type="text/plain"
  data-cc-category="analytics"
  data-cc-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
  async
></script>

<!-- Inline — requires `'unsafe-inline'` (or a nonce) under strict CSP -->
<script is:inline type="text/plain" data-cc-category="analytics">
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>

<!-- iframe embeds work the same way -->
<iframe
  data-cc-category="marketing"
  data-cc-src="https://www.youtube.com/embed/…"
></iframe>

Use is:inline so Astro leaves the placeholder markup untouched — otherwise the compiler may bundle or rewrite the tag and break the type="text/plain" convention.

<ConsentScript> component. A thin Astro component is exported from @zdenekkurecka/astro-consent/components for the common case — it emits the same placeholder markup as above (including is:inline) with a named-prop API and forwards any other <script> attributes through:

---
import { ConsentScript } from '@zdenekkurecka/astro-consent/components';
---
<ConsentScript
  category="analytics"
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
  async
/>

<ConsentScript category="analytics">
  {`window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');`}
</ConsentScript>

How it works:

  • On astro-consent:consent / astro-consent:change, the integration scans for blocked elements whose category is now granted and activates them in place.
  • A MutationObserver catches blocked elements inserted after the initial scan (e.g. via client-side routing or framework islands).
  • All other attributes on the placeholder (async, defer, nonce, integrity, crossorigin, …) are preserved on the activated script.
  • Activated elements are marked with data-cc-activated="true" so repeated scans are a no-op.

Revocation caveat. Once a tracker has executed, the integration cannot unload it — most trackers aren't teardown-safe. If a user later revokes a category, the next full page load will keep those scripts blocked, but the current session will still have them running. Design accordingly, or drive teardown yourself from astro-consent:change.

Event-based hook (advanced / full control)

For trackers that need custom bootstrap logic — dynamic config, manual teardown, integration with window.dataLayer before the script tag lands — listen to the consent events directly:

<script>
  function loadGA() {
    const s = document.createElement('script');
    s.async = true;
    s.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX';
    document.head.appendChild(s);

    window.dataLayer = window.dataLayer || [];
    function gtag(...args) { window.dataLayer.push(args); }
    gtag('js', new Date());
    gtag('config', 'G-XXXXXXX');
  }

  document.addEventListener('astro-consent:consent', (e) => {
    if (e.detail.categories.analytics) loadGA();
  });

  document.addEventListener('astro-consent:change', (e) => {
    // A real implementation would also tear GA down when revoked.
    if (e.detail.categories.analytics) loadGA();
  });
</script>

Enable Google Consent Mode v2

GA4, Google Ads, and every other Google tag need Consent Mode v2 to run legally in the EU. The integration ships first-class support: map your categories to GCM signals and the rest is handled for you.

cookieConsent({
  version: 1,
  categories: {
    analytics: { label: 'Analytics', description: '…', default: false },
    marketing: { label: 'Marketing', description: '…', default: false },
  },
  googleConsentMode: {
    enabled: true,
    mapping: {
      analytics: ['analytics_storage'],
      marketing: ['ad_storage', 'ad_user_data', 'ad_personalization'],
    },
    // Hint to GTM on how long to delay firing tags. Default 500ms.
    waitForUpdate: 500,
    // Optional: regional default overrides. Accepts either a single value
    // (applied to every mapped signal) or a per-signal object.
    regions: {
      US: 'granted',
      BR: { ad_storage: 'denied' },
    },
    // Optional Google flags, forwarded via gtag('set', …).
    adsDataRedaction: true,
    urlPassthrough: false,
  },
});

What the integration does for you:

  1. Injects an inline snippet at the top of <head> that bootstraps window.dataLayer + gtag and calls gtag('consent', 'default', { …, wait_for_update: 500 }) with every mapped signal set to "denied" (unless overridden via defaults / regions).
  2. On every astro-consent:consent or astro-consent:change event, dispatches gtag('consent', 'update', …) with each signal set to "granted" only when every category that maps to it is granted.
  3. Forwards adsDataRedaction / urlPassthrough as gtag('set', …) calls in the default snippet.

Drop your GA4 / Google Ads tag anywhere in your layout (or gate it via data-cc-category — the two compose) and it will pick up the consent state automatically:

<script
  is:inline
  async
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
></script>
<script is:inline>
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>

CSP caveat. The default snippet is inline, so enabling googleConsentMode requires script-src to include 'unsafe-inline' or a matching hash/nonce. If you don't configure googleConsentMode, the integration stays strict-CSP-safe.

interface GoogleConsentModeConfig {
  enabled?: boolean;                                          // default: true
  mapping: Partial<Record<string, GoogleConsentSignal[]>>;
  waitForUpdate?: number;                                     // default: 500
  defaults?: Partial<Record<GoogleConsentSignal, 'granted' | 'denied'>>;
  regions?: Record<string,
    | 'granted' | 'denied'
    | Partial<Record<GoogleConsentSignal, 'granted' | 'denied'>>
  >;
  adsDataRedaction?: boolean;
  urlPassthrough?: boolean;
}

type GoogleConsentSignal =
  | 'ad_storage'
  | 'ad_user_data'
  | 'ad_personalization'
  | 'analytics_storage'
  | 'functionality_storage'
  | 'personalization_storage'
  | 'security_storage';

Recipes (GA4, GTM, Meta Pixel)

Copy-paste wiring for the trackers people ask about most often, with category mappings, GCM configuration where relevant, and tracker-specific gotchas. These live under docs/recipes/:

Re-prompt users after changing categories

If you add, remove, or meaningfully change a category (e.g. splitting marketing into marketing and personalization), bump the version number in your integration config:

cookieConsent({
  version: 2, // was 1
  categories: { /* … */ },
});

Any stored consent with a lower version is treated as missing and the banner re-appears for every user on their next visit.

Customise banner & modal text (and localize it)

Every string in the banner and modal can be overridden. For a single-language site, pass a text object. For a multi-lingual site, pass a localeText map keyed by BCP 47 language tag — the integration reads document.documentElement.lang at runtime and picks the best match.

Resolution order: exact locale matchprimary subtag (e.g. en-GB falls back to en) → shared textbuilt-in English defaults. All fields are optional and layers compose, so you only need to supply the keys you actually want to change.

cookieConsent({
  version: 1,
  categories: {
    analytics: { label: 'Analytics', description: '…', default: false },
  },
  localeText: {
    en: {
      bannerText: 'We use cookies to improve your experience and analyse traffic.',
      acceptAll: 'Accept all',
      rejectAll: 'Reject all',
      manage: 'Manage preferences',
      modalTitle: 'Cookie preferences',
      closeAriaLabel: 'Close preferences',
      savePreferences: 'Save preferences',
      essentialLabel: 'Essential',
      essentialDescription: 'Required for the website to function. Cannot be disabled.',
      categories: {
        analytics: {
          label: 'Analytics',
          description: 'Helps us understand how visitors use the site.',
        },
      },
    },
    cs: {
      bannerText: 'Používáme cookies ke zlepšení vašeho zážitku a analýze návštěvnosti.',
      acceptAll: 'Přijmout vše',
      rejectAll: 'Odmítnout vše',
      manage: 'Spravovat předvolby',
      modalTitle: 'Předvolby cookies',
      closeAriaLabel: 'Zavřít předvolby',
      savePreferences: 'Uložit předvolby',
      essentialLabel: 'Nezbytné',
      essentialDescription: 'Nutné pro fungování webu. Nelze vypnout.',
      categories: {
        analytics: {
          label: 'Analytické',
          description: 'Pomáhají nám pochopit, jak návštěvníci web používají.',
        },
      },
    },
  },
});

Set <html lang="cs"> and the Czech strings render; set <html lang="en-GB"> and the en primary-subtag fallback kicks in. If neither text nor a matching localeText entry is supplied, the built-in English defaults are used.

Per-category label / description pulled from localeText override the ones declared in categories, so you can keep a single category-key definition and translate its user-visible labels per language.

Theme the UI

The base stylesheet is injected automatically via Astro's CSS pipeline as a hashed external <link>. If you'd rather pull it into your own CSS pipeline, it is exposed via a subpath export:

import '@zdenekkurecka/astro-consent/styles';

The styles use CSS custom properties, so you can theme them from your own stylesheet without forking anything:

:root {
  --cc-primary: #7c3aed;
  --cc-primary-hover: #6d28d9;
  --cc-radius: 0.75rem;
  --cc-font-family: 'Inter', sans-serif;
}

Use with a strict Content Security Policy

The integration is compatible with strict CSPs out of the box:

Content-Security-Policy:
  default-src 'self';
  script-src  'self';
  style-src   'self';

No 'unsafe-inline' is required for either scripts or styles, because the integration only uses injectScript('page', ...) (emitted as a hashed external module script) and injectScript('page-ssr', ...) (which flows through Astro's CSS extractor and becomes a hashed external stylesheet).

Debug mode

During local integration work it can be hard to tell what the runtime is doing — which events fired, which locale was resolved, whether the stored version still matches. Set debug: true to opt in to verbose logging. Gate it on import.meta.env.DEV so it never ships to production:

// astro.config.mjs
cookieConsent({
  version: 1,
  debug: import.meta.env.DEV,
  categories: { /* ... */ },
});

With debug on, the runtime emits console.debug('[astro-consent]', …) at every lifecycle point: init (with version, resolved locale, stored consent), banner show, accept/reject/save clicks, event dispatches, and localStorage writes. Because it uses console.debug, you need to include Verbose in your DevTools log level to see the messages.

You also get an on-demand snapshot helper:

window.astroConsent.debug();
// groups and returns:
// {
//   config,          // the full serialized integration config
//   resolvedLocale,  // matched <html lang> key, or null
//   resolvedText,    // fully merged UI text used by the banner/modal
//   storageKey,      // active localStorage key
//   state,           // current ConsentState, or null
//   versionMatch,    // stored version === config version
//   needsConsent,    // true if banner would be shown right now
// }

astroConsent.debug is only attached when debug: true, so production bundles stay clean. Typed as optional in ConsentAPI, so TypeScript will remind you to null-check it.

Runtime API

A global window.astroConsent is exposed (also aliased as window.zdenekkureckaConsent, deprecated):

interface ConsentAPI {
  /** Returns the currently stored consent state, or `null` if none. */
  get(): ConsentState | null;

  /**
   * Merge a partial category map into the current state and persist it.
   *
   * If no consent has been recorded yet (first-time visitor who has not
   * interacted with the banner), this seeds an initial consent record from
   * the config defaults, hides the banner, and dispatches
   * `astro-consent:consent`. Subsequent calls merge into the existing state
   * and dispatch `astro-consent:change` instead.
   *
   * The `essential` category is always forced to `true` and cannot be
   * disabled through this method.
   */
  set(categories: Partial<Record<string, boolean>>): void;

  /** Clear the stored consent and re-show the banner. */
  reset(): void;

  /** Show the consent banner. */
  show(): void;

  /** Open the preferences modal. */
  showPreferences(): void;

  /**
   * Dumps the current config, resolved locale/text, storage key, stored
   * state, and version/needs-consent flags to a `console.group`, and
   * returns the same snapshot. Only attached when the integration is
   * configured with `debug: true`.
   */
  debug?(): ConsentDebugSnapshot;
}

interface ConsentState {
  version: number;
  timestamp: number;
  categories: Record<string, boolean>;
}

Example:

// Read current state
const state = window.astroConsent?.get();

// Programmatically update. Safe to call before the user has interacted
// with the banner — the missing categories fall back to your config
// defaults and an initial consent record is written.
window.astroConsent?.set({ analytics: true });

// Re-open the preferences modal (e.g. from a "Cookie settings" footer link)
window.astroConsent?.showPreferences();

// Clear consent and re-prompt
window.astroConsent?.reset();

Events

| Event | When it fires | e.detail | | ------------------------ | -------------------------------------------------------------------------------------------------- | -------------- | | astro-consent:consent | Once per session, after the user gives consent — or on first page load if valid consent exists. | ConsentState | | astro-consent:change | Whenever the user updates their preferences after initial consent. | ConsentState |

Both are typed CustomEvents on document, so in TypeScript you get full autocompletion on e.detail.categories.

Typed category keys

By default e.detail.categories is typed as Record<string, boolean> — usable, but no autocomplete and typos don't error. To get the category keys you declared in cookieConsent({ categories: … }) to narrow across every listener and window.astroConsent, drop a project-level .d.ts that augments the ConsentKeys marker interface:

// src/astro-consent.d.ts
declare module '@zdenekkurecka/astro-consent' {
  interface ConsentKeys {
    analytics: true;
    marketing: true;
  }
}

export {};

After this, both event listeners and the runtime API narrow:

document.addEventListener('astro-consent:consent', (e) => {
  e.detail.categories.analytics; // boolean
  e.detail.categories.analyitcs; // ❌ TS error — unknown key
});

window.astroConsent?.get()?.categories.marketing; // boolean
window.astroConsent?.set({ analytics: true });    // ✓
window.astroConsent?.set({ analyitcs: true });    // ❌ TS error

This is an opt-in pattern (same shape as Vue Router's RouteMap or Pinia's store augmentation): the library ships with a wide default so nothing breaks if you don't declare it, and the narrow type kicks in the moment you do.

Accessibility

  • Modal has role="dialog" with aria-modal="true" and aria-labelledby pointing at the visible title.
  • Opening the modal saves the previously focused element and moves focus to the first control inside the dialog on the next animation frame.
  • Tab / Shift+Tab is trapped inside the modal while it's open.
  • Escape closes the modal and returns focus to the trigger.
  • Clicking the overlay closes the modal; the overlay itself is aria-hidden.
  • Banner and modal both toggle aria-hidden in lockstep with their visibility, so screen readers don't announce them while they are visually hidden.
  • All buttons have type="button" so they never submit ambient forms.

Repository layout

This repository is a pnpm workspace:

.
├── packages/
│   └── astro-consent/        # the published npm package
│       ├── src/               # integration, runtime, UI, types, styles
│       └── README.md          # the README published to npm
├── playground/                # Astro app used to develop and test locally
├── package.json
├── pnpm-workspace.yaml
└── README.md                  # you are here

Local development

# Install dependencies
pnpm install

# Build the package and start the playground dev server
pnpm dev

# Build the package and the playground
pnpm build

# Build everything in the workspace
pnpm build:all

The playground in playground/ is a small Astro app wired up to the local package. Use it to iterate on the integration, try out new config options, and verify View Transitions behavior.

Contributing

Issues and pull requests are welcome. If you're reporting a bug, a minimal reproduction in the playground app (or a link to a minimal Astro repro) massively speeds things up.

License

MIT © zdenekkurecka