---
title: "How to Block Scripts Until Cookie Consent (with Examples) | CookieBeam"
description: "Step-by-step guide to blocking tracking scripts until users give cookie consent. Covers runtime interception, required inert markup for hardcoded scripts, connection-level blocking, and manual tagging examples for Google Analytics, Facebook Pixel, and more."
url: "https://cookiebeam.com/guides/how-to-block-scripts-until-cookie-consent"
lang: "en"
last_updated: "2026-09-06T10:46:49.343Z"
---

# How to Block Scripts Until Cookie Consent

Use runtime interception and inert HTML markup to control scripts and supported network requests before consent. Includes script examples and verification steps.

For scripts that require prior consent, displaying a banner is only part of the setup. Prevent execution until the visitor grants the relevant category, and verify what the page actually loads and stores.

CookieBeam blocks scripts two ways. The **automatic blocking engine** uses scan results to manage scripts created dynamically after the runtime has loaded. For scripts written directly in your HTML, use **manual HTML tagging** with `type="text/plain"`, `data-category`, and `data-src` so the browser cannot fetch or execute them before consent.

This guide explains when to use each path.

## Using Google Tag Manager? (Good News)

If all your tracking scripts are loaded through **Google Tag Manager** and you've enabled [Consent Mode V2](https://cookiebeam.com/learn/google-consent-mode-v2), GTM already gates script execution based on consent signals. You don't need manual script blocking for GTM-managed tags. This guide is for scripts hardcoded directly in your HTML or loaded outside of GTM.

## Automatic Blocking for Dynamically Created Scripts

CookieBeam's scanner and runtime can manage scripts created through JavaScript after the runtime has loaded. Hardcoded non-essential scripts still require the inert markup shown below. Here's how runtime interception works:

## The Automatic Blocking Workflow

### Step 1: Scan your site

Run a scan from the CookieBeam dashboard. The headless browser scanner crawls your pages and records every external script, inline script, and network connection it finds.

### Step 2: Review and categorize

Open the scanner inventory. CookieBeam auto-classifies most scripts using URL/domain pattern matching (140+ known domains) and inline content analysis (23+ vendor patterns for GTM, Meta Pixel, Hotjar, and others). Review the classifications and fix any that need adjustment.

### Step 3: Publish your banner

When you publish, CookieBeam generates a **script map** derived from your scan results. This map tells the runtime which scripts belong to which consent category. It's specific to your site, not a generic block list.

### Step 4: The runtime blocks automatically

On every page load, the CookieBeam runtime intercepts dynamically created scripts that match the script map and holds them until the visitor consents to the relevant category. Blocked scripts are neutralized (not deleted), queued, and revived in their original order when consent is granted.

> **Tip: Hardcoded scripts require inert markup**
>
> Browser API interception applies to scripts created dynamically after the CookieBeam runtime has loaded. It cannot reliably stop a normal `<script src=...>` written directly in HTML because the browser parser may fetch or execute that script first. For deterministic prior-consent blocking, use `type="text/plain"`, `data-category`, and `data-src`.

## The 5-Layer Interception Engine

CookieBeam doesn't rely on a single blocking technique. The runtime uses five layers of interception to catch scripts regardless of how they're injected into the page:

## Interception Layers

### Step 1: createElement override

Intercepts creation of `script`, `iframe`, `img`, and `embed` elements. Proxies the `src` property setter and `setAttribute` calls. If the URL maps to an unconsented category in the script map, the element is neutralized before it can load.

### Step 2: DOM insertion hooks

Wraps `Node.appendChild`, `Node.insertBefore`, and `Node.replaceChild`. This catches scripts being added to the page no matter how they were created, including by other scripts that build elements dynamically.

### Step 3: MutationObserver fallback

Monitors the entire document tree for added nodes and `src` attribute changes. This is the catch-all: if a script somehow bypasses the first two layers, the observer spots it.

### Step 4: Worker blocking

Wraps `Worker` and `SharedWorker` constructors to prevent third-party worker scripts from executing without consent. Workers run in a separate thread with full network access, so they can't be left unblocked.

### Step 5: Anti-tamper protection

Once a script element is neutralized (type set to `text/plain`, src removed), the engine locks the `type` property so it can't be reverted to an executable state. This prevents tracking scripts from detecting they've been blocked and trying to re-enable themselves.

## Learning Mode vs Strict Mode

CookieBeam offers two compliance modes that control how unknown scripts (those not in the script map) are handled:

### Compliance Modes

|  | Aspect | Learning Mode (default) | Strict Mode |
| --- | --- | --- | --- |
| **Unknown scripts** | Allowed to run while building your inventory | Blocked by default (fail-closed) |  |
| **Best for** | Initial setup, when you're still discovering what's on your site | Production, when your inventory is complete and you want maximum compliance |  |
| **Risk** | New unclassified scripts may run before you categorize them | A new script added by your team won't work until you scan and categorize it |  |

Start with Learning Mode while you're setting up. Once your scan inventory is complete and you've verified everything works, switch to Strict Mode for the tightest compliance posture.

## Drift Detection

Your site changes between scans. New scripts get added, third-party tags update themselves, your team deploys new features. CookieBeam's drift detection catches these changes in real time.

The runtime monitors every visitor's browser for three types of drift:

**Drift detection types**

| Drift Type | What It Catches | How It Works |
| --- | --- | --- |
| Script drift | New scripts loading that weren't in the inventory | Compares loaded scripts against the published script map |
| Cookie drift | Unknown cookies appearing between scans | Detects cookies not in the classified inventory |
| Connection drift | New network connections to unknown third parties | Monitors outbound requests against known connection inventory |

This isn't periodic scanning. It's continuous, real-time monitoring running in every visitor's browser. After 5 sightings, a drifted script or connection is auto-promoted into your inventory for review. In Strict Mode, drifted scripts are blocked automatically while you decide how to categorize them.

## Connection-Level Blocking

Script blocking stops scripts from running. But tracking can also happen through API calls made by scripts that are already allowed, or by first-party code that phones home to third-party endpoints. CookieBeam's connection-level blocking is a separate enforcement layer that wraps five browser APIs:

**Blocked connection APIs**

| API | What Happens When Blocked |
| --- | --- |
| `fetch()` | Returns a Response with status 204 and an `X-CookieBeam-Blocked` header. No network request is made. |
| `XMLHttpRequest` | Fires an error event without making the request. |
| `navigator.sendBeacon()` | Returns `false`. The beacon is never sent. |
| `WebSocket` | Returns a stub that fires error and close events. No connection is opened. |
| `EventSource` | Returns a closed stub. No SSE connection is established. |

Connection blocking is category-aware, so it respects the same consent categories as script blocking. Same-site requests are always allowed (your own APIs won't break), and you can configure infrastructure host exclusions and ignore rules for specific endpoints.

The engine also re-patches same-origin iframes so scripts inside them can't extract clean native API references to bypass the wrappers.

> **Tip: Connection blocking is a separate toggle**
>
> Connection-level blocking is independent from script blocking. You can enable one without the other. Script blocking prevents scripts from loading; connection blocking prevents network requests to unconsented third-party endpoints even if the script that makes them is allowed to run.

## Manual Script Tagging

For inline scripts embedded directly in your HTML, the automatic engine can't block them before they run (they're already in the page source). Manual tagging gives you control over these scripts by telling the browser not to execute them until CookieBeam releases them.

You may also prefer manual tagging when you want explicit, visible control over which category a specific script belongs to.

### How Manual Blocking Works

CookieBeam exploits how browsers handle the `type` attribute on `<script>` tags. When a script has `type="text/plain"` instead of `type="text/javascript"`, the browser treats it as a data block and **doesn't execute it**.

CookieBeam scans the page for all script tags marked with a `data-category` attribute. When a user gives consent for that category, CookieBeam clones the dormant script tag, restores its original type and source, and inserts it back into the page, causing the browser to execute it normally.

## The Manual Blocking Lifecycle

### Step 1: Page loads with dormant scripts

Your script tags have `type="text/plain"` and `data-category="..."`. The browser sees them but doesn't execute them.

### Step 2: CookieBeam initializes

The CookieBeam runtime scans the DOM and finds all `<script data-category="...">` elements, recording which category each belongs to.

### Step 3: User interacts with the cookie banner

The user clicks "Accept All" or selects specific categories in the preferences modal.

### Step 4: Consented scripts are activated

CookieBeam replaces each dormant script that belongs to an accepted category with a fresh, executable copy. External scripts are loaded sequentially to maintain dependency order.

### Required Attributes

To manually block a script, you need exactly two attributes:

**Required and optional script blocking attributes**

| Attribute | Required | Description |
| --- | --- | --- |
| type="text/plain" | Yes | Prevents the browser from executing the script. CookieBeam restores the original type when consent is given. |
| data-category="..." | Yes | Tells CookieBeam which cookie category this script belongs to: necessary, analytics, marketing, or preferences. |
| data-src="..." | For external scripts | Use instead of src for external scripts. Prevents the browser from fetching the file before consent. |
| data-service="..." | Optional | Gives the script a human-readable name that appears as a toggle in the preferences modal, letting users control it individually. |
| data-type="..." | Optional | Set a custom script type (e.g. "module") that CookieBeam will restore when the script is activated. |

### Basic Example: Inline Script

Here's the simplest case, an inline script that should only run when the user consents to analytics cookies:

`inline-script-example.html`

```html
<!-- Before: this script runs immediately on page load -->
<script>
  console.log('Tracking user...');
</script>

<!-- After: blocked until user accepts "analytics" -->
<script type="text/plain" data-category="analytics">
  console.log('Tracking user...');
</script>
```

### External Scripts: Use data-src

For external scripts that load a file via the `src` attribute, replace `src` with `data-src`. This prevents the browser from fetching the file before consent is given. Even though `type="text/plain"` stops execution, many browsers still **download** the resource when `src` is present.

`external-script-example.html`

```html
<!-- Before: browser downloads and executes immediately -->
<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>

<!-- After: not even downloaded until consent -->
<script type="text/plain" data-category="analytics"
  data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>
```

> **Warning: Always use data-src for external scripts**
>
> If you keep the `src` attribute, the browser may still download the script file (even though it won't execute it). This can trigger network requests to third-party servers before consent, which some Data Protection Authorities consider a GDPR violation. Always replace `src` with `data-src`.

### Cookie Categories

CookieBeam uses four standard cookie categories. Use the exact lowercase string in the `data-category` attribute:

**Cookie categories and their typical scripts**

| Category | data-category value | Typical scripts | Consent required? |
| --- | --- | --- | --- |
| Necessary | necessary | Login, security tokens, payment processing, reCAPTCHA | No, always active. No tagging needed. |
| Analytics | analytics | Google Analytics, Hotjar, Mixpanel, Plausible, PostHog, Amplitude | Yes |
| Marketing | marketing | Facebook Pixel, Google Ads, LinkedIn Insight, TikTok Pixel, Twitter Pixel, Criteo | Yes |
| Preferences | preferences | Chat widgets (Intercom, Drift), language preferences, UI customisation | Yes |

## Ready-to-Use Examples

Below are copy-paste examples for the most commonly used tracking scripts. These are for manual tagging. Replace the placeholder IDs with your own.

### Google Analytics (GA4)

`google-analytics.html`

```html
<!-- Google Analytics GA4 -->
<script type="text/plain" data-category="analytics"
  data-service="Google Analytics"
  data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>
<script type="text/plain" data-category="analytics"
  data-service="Google Analytics">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>
```

### Facebook Pixel

`facebook-pixel.html`

```html
<!-- Facebook Pixel -->
<script type="text/plain" data-category="marketing"
  data-service="Facebook Pixel">
  !function(f,b,e,v,n,t,s)
  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};
  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
  n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];
  s.parentNode.insertBefore(t,s)}(window, document,'script',
  'https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', 'YOUR_PIXEL_ID');
  fbq('track', 'PageView');
</script>
```

### Hotjar

`hotjar.html`

```html
<!-- Hotjar -->
<script type="text/plain" data-category="analytics"
  data-service="Hotjar">
  (function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:YOUR_HJID,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
  })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
```

### LinkedIn Insight Tag

`linkedin-insight.html`

```html
<!-- LinkedIn Insight Tag -->
<script type="text/plain" data-category="marketing"
  data-service="LinkedIn Insight">
  _linkedin_partner_id = "YOUR_PARTNER_ID";
  window._linkedin_data_partner_ids =
    window._linkedin_data_partner_ids || [];
  window._linkedin_data_partner_ids.push(_linkedin_partner_id);
</script>
<script type="text/plain" data-category="marketing"
  data-service="LinkedIn Insight"
  data-src="https://snap.licdn.com/li.lms-analytics/insight.min.js">
</script>
```

### TikTok Pixel

`tiktok-pixel.html`

```html
<!-- TikTok Pixel -->
<script type="text/plain" data-category="marketing"
  data-service="TikTok Pixel">
  !function (w, d, t) {
    w.TiktokAnalyticsObject=t;
    var ttq=w[t]=w[t]||[];
    ttq.methods=["page","track","identify","instances",
      "debug","on","off","once","ready","alias",
      "group","enableCookie","disableCookie"];
    ttq.setAndDefer=function(t,e){
      t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
    for(var i=0;i<ttq.methods.length;i++)
      ttq.setAndDefer(ttq,ttq.methods[i]);
    ttq.instance=function(t){
      for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)
        ttq.setAndDefer(e,ttq.methods[n]);return e};
    ttq.load=function(e,n){
      var i="https://analytics.tiktok.com/i18n/pixel/events.js";
      ttq._i=ttq._i||{};ttq._i[e]=[];ttq._i[e]._u=i;
      ttq._t=ttq._t||{};ttq._t[e]=+new Date;
      ttq._o=ttq._o||{};ttq._o[e]=n||{};
      var o=document.createElement("script");
      o.type="text/javascript";o.async=!0;o.src=i+"?sdkid="+e+"&lib="+t;
      var a=document.getElementsByTagName("script")[0];
      a.parentNode.insertBefore(o,a)};
    ttq.load('YOUR_PIXEL_ID');
    ttq.page();
  }(window, document, 'ttq');
</script>
```

### Google Ads Conversion Tracking

`google-ads.html`

```html
<!-- Google Ads Conversion Tracking -->
<script type="text/plain" data-category="marketing"
  data-service="Google Ads"
  data-src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXX">
</script>
<script type="text/plain" data-category="marketing"
  data-service="Google Ads">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'AW-XXXXXXX');
</script>
```

## Per-Service Toggles

When you add the `data-service` attribute, CookieBeam automatically generates a toggle in the preferences modal that lets users enable or disable that specific service within its category. For example, a user might accept the "analytics" category overall but disable Hotjar specifically.

All scripts with the same `data-service` name are grouped together. They're activated or deactivated as a unit.

> **Tip: When to use data-service**
>
> Use `data-service` when you have multiple tracking scripts in the same category and want to give users fine-grained control. For example, if you use both Google Analytics and Hotjar (both analytics), adding `data-service` to each lets users keep one while disabling the other.

## Running Cleanup Scripts on Consent Withdrawal

Sometimes you need to run cleanup logic when a user **withdraws** consent for a category, for example, disabling a Google Analytics property or removing event listeners. You can do this by prefixing the category name with `!`:

`cleanup-example.html`

```html
<!-- This runs when user ACCEPTS analytics -->
<script type="text/plain" data-category="analytics">
  gtag('config', 'G-XXXXXXX');
</script>

<!-- This runs when user DISABLES analytics -->
<script type="text/plain" data-category="!analytics">
  window['ga-disable-G-XXXXXXX'] = true;
  document.cookie = '_ga=; Max-Age=0; path=/;';
  document.cookie = '_ga_XXXXXXX=; Max-Age=0; path=/;';
</script>
```

> **Warning: Disable scripts run only once**
>
> A `data-category="!analytics"` script can only fire once, when the user actively switches the category off. If you need logic that runs every time consent changes (e.g. the user toggles analytics on and off multiple times), use the `onChange` callback instead of script tag blocking.

## Script Blocking vs Google Consent Mode

It's important to understand the difference between these two approaches, because they solve the same problem in different ways:

### Script Blocking vs Consent Mode

|  | Aspect | Script Blocking (automatic + manual) | Google Consent Mode V2 |
| --- | --- | --- | --- |
| **How it works** | Prevents the script from executing entirely until consent | Lets the script load but sends consent signals so Google tags self-regulate |  |
| **What scripts support it** | Any script, works with all vendors | Only Google tags (GA4, Google Ads, Floodlight) |  |
| **Data sent before consent** | None. Script doesn't even load. | Cookieless pings are sent (no PII, used for conversion modeling) |  |
| **Best for** | Non-Google scripts (Facebook, Hotjar, TikTok, etc.) | Google ecosystem tags loaded via GTM |  |
| **Setup** | Automatic: scan and categorize in dashboard. Manual: add type and data-category to each script. | Automatic when using CookieBeam GTM template |  |

## Can I use both? (Recommended)

Yes, and you should. Use **Google Consent Mode V2** for your Google tags (loaded through GTM), and use **script blocking** for any non-Google scripts hardcoded in your HTML. This gives you the best of both worlds: Google's cookieless conversion modeling plus strict blocking for everything else.

## Troubleshooting

### Script still fires before consent (automatic blocking)

The automatic engine only intercepts dynamically inserted scripts after its runtime has loaded. Convert hardcoded non-essential scripts to the manual inert markup described above; source order alone is not a reliable before-fetch control. Also verify that you've published your banner after the most recent scan, so the runtime has an up-to-date script map.

### Script still fires before consent (manual tagging)

Check that you have **both** required attributes: `type="text/plain"` and `data-category="..."`. Missing either one means the browser will execute the script normally. Also make sure the CookieBeam runtime loads **before** the blocked scripts in your HTML.

### External script file is still downloaded

Replace `src` with `data-src`. Even with `type="text/plain"`, some browsers still fetch the file from the server if `src` is present. It just won't execute it.

### Script doesn't activate after consent

Verify the `data-category` value matches one of the configured categories exactly (lowercase): `necessary`, `analytics`, `marketing`, or `preferences`. A typo or different casing will cause CookieBeam to ignore the script.

### Multiple scripts depend on each other

CookieBeam loads external scripts **sequentially** in DOM order. If script B depends on script A, make sure script A appears first in your HTML. Inline scripts execute immediately after replacement, before the next external script starts loading.

### Strict mode breaks a new script

When Strict Mode is enabled, any script not in the published script map is blocked by default. If your team adds a new script, it won't work until you run a scan, categorize it, and republish. Switch to Learning Mode temporarily if you need the script to work while you update the inventory.

## Checklist Before Going Live

### Script Blocking Checklist

- Run a full site scan — The scanner needs to discover all scripts and connections before the automatic blocking engine can do its job
- Review and categorize all scripts in the dashboard — Check the scanner inventory for any scripts classified as 'unknown' and assign them to the right category
- Publish your banner with the updated script map — The runtime only blocks scripts that are in the published script map
- Manually tag inline scripts that need blocking — Add type="text/plain" and data-category to any inline scripts hardcoded in your HTML
- External scripts use data-src instead of src (manual tagging) — Prevents the browser from fetching the file before consent
- CookieBeam runtime loads first — Place it before any other scripts in
- Test with a fresh browser (no cookies) — Clear all cookies and localStorage, then reload. No tracking requests should appear in DevTools Network tab.
- Test consent flow end-to-end — Accept cookies and verify blocked scripts activate. Check the Network tab for the expected requests.
- Test reject flow — Reject all cookies and verify no tracking requests are made on subsequent page loads
- Consider enabling Strict Mode for production — Strict Mode blocks unknown scripts by default. It's the safest option once your inventory is complete.
- Verify Google Consent Mode still works — If you use GTM, check that consent default and update commands still fire correctly alongside script blocking

### Related Guides

- [Google Consent Mode V2](https://cookiebeam.com/learn/google-consent-mode-v2): How to implement Consent Mode V2 for Google tags via GTM
- [What Is a CMP?](https://cookiebeam.com/learn/what-is-a-cmp): How a Consent Management Platform helps you comply with GDPR
- [What Is TCF?](https://cookiebeam.com/learn/what-is-tcf): How the Transparency & Consent Framework works with CMPs
