> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cookiechimp.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# JS & Single Page Apps

> Install a consent banner in JavaScript-powered SPAs like Astro, Vue, React Router, SvelteKit, and more.

Single Page Applications (SPAs) update the URL and the visible page **without a full page reload**. Because of this, a script tag added to `<head>` only runs once — on the first load — and the CookieChimp widget can be torn down or lost when the user navigates between routes.

This page covers two things:

1. **How to load the CookieChimp script so it runs on every page**, framework by framework.
2. **How to position the Privacy Trigger** so the floating icon survives client-side navigation.

<Info>
  We have dedicated guides for the most common frameworks — they're more detailed than this overview:
  [Next.js](/installation/nextjs) ·
  [React (Vite/CRA)](/installation/react) ·
  [Remix](/installation/remix) ·
  [Nuxt](/installation/nuxt) ·
  [Angular](/installation/angular) ·
  [Astro](/installation/astro) ·
  [SvelteKit](/installation/sveltekit).
  This page is the catch-all for any SPA that doesn't have its own guide.
</Info>

***

## How do I install CookieChimp in an SPA?

The pattern is the same across all SPA frameworks:

1. **Inject the script** into `<head>` so it loads as early as possible — ideally before the framework hydrates.
2. **Re-initialize** the widget on every client-side route change by listening to the framework's navigation event.

The exact navigation event differs per framework. Pick yours below.

The reinit pattern is the same across frameworks: **remove the existing CookieChimp script tag (if any) and append a fresh one**. Loading the script again triggers CookieChimp to rescan the newly mounted DOM. There's no public `reinitialize()` API — script re-injection is the documented mechanism.

### Astro

Astro's [View Transitions](https://docs.astro.build/en/guides/view-transitions/) emit an `astro:page-load` event after every navigation **including the first**, so this single handler covers both initial load and subsequent transitions — no separate static `<script src>` needed. Use `is:inline` so Astro doesn't bundle the script.

Add this to your root layout — typically `src/layouts/Layout.astro` — inside `<head>`:

```html theme={null}
<script is:inline>
  function runCookieChimp() {
    document.getElementById("cookiechimp-js")?.remove();

    var script = document.createElement("script");
    script.src = "https://cookiechimp.com/widget/YOUR_ACCOUNT_ID.js";
    script.id = "cookiechimp-js";
    document.head.appendChild(script);
  }

  // Fires on first load AND after every Astro view transition.
  document.addEventListener("astro:page-load", runCookieChimp);
</script>
```

<Snippet file="install/replace-account-id.mdx" />

<Tip>
  **Not using View Transitions?** If you haven't enabled `<ViewTransitions />` in your layout, Astro performs a full page reload on each navigation — the standard `<script src="...">` install works without any re-initialization logic.
</Tip>

### Vue (Vue Router)

Add the script tag in your `index.html` `<head>` for the initial load, then re-inject on every subsequent route change from your router config (`src/router/index.js`):

```js theme={null}
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */],
});

let isInitial = true;
router.afterEach(() => {
  if (isInitial) {
    isInitial = false;
    return;
  }

  document.getElementById("cookiechimp-js")?.remove();

  const script = document.createElement("script");
  script.src = "https://cookiechimp.com/widget/YOUR_ACCOUNT_ID.js";
  script.id = "cookiechimp-js";
  document.head.appendChild(script);
});

export default router;
```

### React (React Router v6+)

Add the script tag in your `index.html` `<head>` for the initial load, then mount a tiny component that re-injects on subsequent route changes:

```tsx theme={null}
// src/components/CookieChimpReinit.tsx
import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";

export default function CookieChimpReinit() {
  const { pathname } = useLocation();
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }

    document.getElementById("cookiechimp-js")?.remove();

    const script = document.createElement("script");
    script.src = "https://cookiechimp.com/widget/YOUR_ACCOUNT_ID.js";
    script.id = "cookiechimp-js";
    document.head.appendChild(script);
  }, [pathname]);

  return null;
}
```

```tsx theme={null}
// src/main.tsx
<BrowserRouter>
  <CookieChimpReinit />
  <App />
</BrowserRouter>
```

### SvelteKit

Add the script to `src/app.html` inside `<head>` for the initial load, then re-inject on subsequent navigations from your root layout:

```svelte theme={null}
<!-- src/routes/+layout.svelte -->
<script>
  import { afterNavigate } from "$app/navigation";
  import { browser } from "$app/environment";

  let isInitial = true;

  afterNavigate(() => {
    if (!browser) return;
    if (isInitial) {
      isInitial = false;
      return;
    }

    document.getElementById("cookiechimp-js")?.remove();

    const script = document.createElement("script");
    script.src = "https://cookiechimp.com/widget/YOUR_ACCOUNT_ID.js";
    script.id = "cookiechimp-js";
    document.head.appendChild(script);
  });
</script>

<slot />
```

### Generic SPA (no router events)

If your framework doesn't expose a router event you can hook into, listen for `history.pushState` and `popstate` — this fires on both programmatic navigation and back/forward:

```html theme={null}
<script>
  function runCookieChimp() {
    document.getElementById("cookiechimp-js")?.remove();

    var script = document.createElement("script");
    script.src = "https://cookiechimp.com/widget/YOUR_ACCOUNT_ID.js";
    script.id = "cookiechimp-js";
    document.head.appendChild(script);
  }

  // Patch pushState/replaceState so we get notified on client-side navigation.
  ["pushState", "replaceState"].forEach((method) => {
    var original = history[method];
    history[method] = function () {
      var result = original.apply(this, arguments);
      window.dispatchEvent(new Event("locationchange"));
      return result;
    };
  });
  window.addEventListener("popstate", () => window.dispatchEvent(new Event("locationchange")));
  window.addEventListener("locationchange", runCookieChimp);

  // First load.
  runCookieChimp();
</script>
```

***

## How do I position the Privacy Trigger in an SPA?

The Privacy Trigger is the floating icon users click to update their consent preferences. By default it's appended to `<body>`, which is fine for traditional pages — but in an SPA, you usually want it in a **specific spot in your layout** and you want it to **survive route changes**.

Place a `<div>` with the ID `cookiechimp-container` wherever you want the trigger rendered:

```html theme={null}
<div id="cookiechimp-container"></div>
```

Put this in a part of your layout that **doesn't unmount on navigation** — typically the root layout, just inside `<body>`.

### Persisting across view transitions

Some frameworks tear down and recreate DOM nodes between routes, which would remove the Privacy Trigger. Use the framework's "persistent element" attribute to keep it:

```html theme={null}
<!-- Astro (View Transitions) -->
<div id="cookiechimp-container" transition:persist></div>

<!-- Hotwire Turbo -->
<div id="cookiechimp-container" data-turbo-permanent></div>
```

See the Astro docs on [persisting components](https://docs.astro.build/en/guides/view-transitions/#maintaining-state) and the Turbo docs on [persisting elements across page loads](https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads).

***

## How do I run code based on consent?

The `cc:onConsented` event fires once when the user has made an initial choice (and on subsequent page loads when consent is already stored).

```javascript theme={null}
window.addEventListener("cc:onConsented", function (event) {
  if (CookieChimp.acceptedCategory("analytics")) {
    // "analytics" category enabled
  }

  if (CookieChimp.acceptedService("Google Analytics", "analytics")) {
    // "Google Analytics" service enabled
  }
});
```

The `cc:onUpdate` event fires when the user changes their consent from the preferences modal.

```javascript theme={null}
window.addEventListener("cc:onUpdate", function (event) {
  var detail = event.detail;

  /**
   * detail.cookie
   * detail.changedCategories
   * detail.changedServices
   */

  if (detail.changedCategories.includes("analytics")) {
    if (CookieChimp.acceptedCategory("analytics")) {
      // "analytics" category was just enabled
    } else {
      // "analytics" category was just disabled
    }

    if (detail.changedServices["analytics"].includes("Google Analytics")) {
      if (CookieChimp.acceptedService("Google Analytics", "analytics")) {
        // "Google Analytics" service was just enabled
      } else {
        // "Google Analytics" service was just disabled
      }
    }
  }
});
```

<Snippet file="install/callbacks-events-link.mdx" />

***

## Troubleshooting

* **Banner shows on first load but not after navigation** — your re-initialization listener isn't firing. Double-check the framework-specific event name (`astro:page-load`, `router.afterEach`, `useLocation`, `afterNavigate`).
* **Privacy Trigger disappears after navigation** — add the framework's persistence attribute (`transition:persist`, `data-turbo-permanent`) to your `#cookiechimp-container` div.

<Snippet file="install/spa-troubleshooting-base.mdx" />
