Under GDPR and the ePrivacy Directive, tracking scripts can't execute until the user has actively given consent for the relevant cookie category. Showing a cookie banner isn't enough. You need to technically prevent scripts from running until consent is granted.
CookieBeam blocks scripts two ways. The automatic blocking engine works from your site's scan results: the scanner discovers scripts, you categorize them in the dashboard, publish, and the runtime handles everything. No HTML changes needed. For inline scripts or edge cases where you need direct control, manual HTML tagging with data-category attributes gives you that option.
Most sites only need the automatic path. This guide covers both.
Using Google Tag Manager?
If all your tracking scripts are loaded through Google Tag Manager and you've enabled 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 Script Blocking (Recommended)
The fastest way to block scripts is to let CookieBeam's scanner and runtime handle it. You don't touch any HTML. Here's how it works:
The Automatic Blocking Workflow
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.
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.
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.
The runtime blocks automatically
On every page load, the CookieBeam runtime intercepts 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.
No code changes required
The automatic blocking engine intercepts scripts at the browser API level. You don't need to add type="text/plain" or data-category attributes to your HTML. The runtime identifies scripts by their URL and content, matching them against the published script map.
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
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.
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.
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.
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.
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 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:
| API | What Happens When Blocked |
|---|---|
| <code>fetch()</code> | Returns a Response with status 204 and an <code>X-CookieBeam-Blocked</code> header. No network request is made. |
| <code>XMLHttpRequest</code> | Fires an error event without making the request. |
| <code>navigator.sendBeacon()</code> | Returns <code>false</code>. The beacon is never sent. |
| <code>WebSocket</code> | Returns a stub that fires error and close events. No connection is opened. |
| <code>EventSource</code> | 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.
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
Page loads with dormant scripts
Your script tags have type="text/plain" and data-category="...". The browser sees them but doesn't execute them.
CookieBeam initializes
The CookieBeam runtime scans the DOM and finds all <script data-category="..."> elements, recording which category each belongs to.
User interacts with the cookie banner
The user clicks "Accept All" or selects specific categories in the preferences modal.
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:
| 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:
1 <!-- Before: this script runs immediately on page load --> 2 <script> 3 console.log('Tracking user...'); 4 </script> 5 6 <!-- After: blocked until user accepts "analytics" --> 7 <script type="text/plain" data-category="analytics"> 8 console.log('Tracking user...'); 9 </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.
1 <!-- Before: browser downloads and executes immediately --> 2 <script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script> 3 4 <!-- After: not even downloaded until consent --> 5 <script type="text/plain" data-category="analytics" 6 data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"> 7 </script>
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:
| 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)
1 <!-- Google Analytics GA4 --> 2 <script type="text/plain" data-category="analytics" 3 data-service="Google Analytics" 4 data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"> 5 </script> 6 <script type="text/plain" data-category="analytics" 7 data-service="Google Analytics"> 8 window.dataLayer = window.dataLayer || []; 9 function gtag(){dataLayer.push(arguments);} 10 gtag('js', new Date()); 11 gtag('config', 'G-XXXXXXX'); 12 </script>
Facebook Pixel
1 <!-- Facebook Pixel --> 2 <script type="text/plain" data-category="marketing" 3 data-service="Facebook Pixel"> 4 !function(f,b,e,v,n,t,s) 5 {if(f.fbq)return;n=f.fbq=function(){n.callMethod? 6 n.callMethod.apply(n,arguments):n.queue.push(arguments)}; 7 if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; 8 n.queue=[];t=b.createElement(e);t.async=!0; 9 t.src=v;s=b.getElementsByTagName(e)[0]; 10 s.parentNode.insertBefore(t,s)}(window, document,'script', 11 'https://connect.facebook.net/en_US/fbevents.js'); 12 fbq('init', 'YOUR_PIXEL_ID'); 13 fbq('track', 'PageView'); 14 </script>
Hotjar
1 <!-- Hotjar --> 2 <script type="text/plain" data-category="analytics" 3 data-service="Hotjar"> 4 (function(h,o,t,j,a,r){ 5 h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)}; 6 h._hjSettings={hjid:YOUR_HJID,hjsv:6}; 7 a=o.getElementsByTagName('head')[0]; 8 r=o.createElement('script');r.async=1; 9 r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv; 10 a.appendChild(r); 11 })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); 12 </script>
LinkedIn Insight Tag
1 <!-- LinkedIn Insight Tag --> 2 <script type="text/plain" data-category="marketing" 3 data-service="LinkedIn Insight"> 4 _linkedin_partner_id = "YOUR_PARTNER_ID"; 5 window._linkedin_data_partner_ids = 6 window._linkedin_data_partner_ids || []; 7 window._linkedin_data_partner_ids.push(_linkedin_partner_id); 8 </script> 9 <script type="text/plain" data-category="marketing" 10 data-service="LinkedIn Insight" 11 data-src="https://snap.licdn.com/li.lms-analytics/insight.min.js"> 12 </script>
TikTok Pixel
1 <!-- TikTok Pixel --> 2 <script type="text/plain" data-category="marketing" 3 data-service="TikTok Pixel"> 4 !function (w, d, t) { 5 w.TiktokAnalyticsObject=t; 6 var ttq=w[t]=w[t]||[]; 7 ttq.methods=["page","track","identify","instances", 8 "debug","on","off","once","ready","alias", 9 "group","enableCookie","disableCookie"]; 10 ttq.setAndDefer=function(t,e){ 11 t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}; 12 for(var i=0;i<ttq.methods.length;i++) 13 ttq.setAndDefer(ttq,ttq.methods[i]); 14 ttq.instance=function(t){ 15 for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++) 16 ttq.setAndDefer(e,ttq.methods[n]);return e}; 17 ttq.load=function(e,n){ 18 var i="https://analytics.tiktok.com/i18n/pixel/events.js"; 19 ttq._i=ttq._i||{};ttq._i[e]=[];ttq._i[e]._u=i; 20 ttq._t=ttq._t||{};ttq._t[e]=+new Date; 21 ttq._o=ttq._o||{};ttq._o[e]=n||{}; 22 var o=document.createElement("script"); 23 o.type="text/javascript";o.async=!0;o.src=i+"?sdkid="+e+"&lib="+t; 24 var a=document.getElementsByTagName("script")[0]; 25 a.parentNode.insertBefore(o,a)}; 26 ttq.load('YOUR_PIXEL_ID'); 27 ttq.page(); 28 }(window, document, 'ttq'); 29 </script>
Google Ads Conversion Tracking
1 <!-- Google Ads Conversion Tracking --> 2 <script type="text/plain" data-category="marketing" 3 data-service="Google Ads" 4 data-src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXX"> 5 </script> 6 <script type="text/plain" data-category="marketing" 7 data-service="Google Ads"> 8 window.dataLayer = window.dataLayer || []; 9 function gtag(){dataLayer.push(arguments);} 10 gtag('js', new Date()); 11 gtag('config', 'AW-XXXXXXX'); 12 </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.
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 !:
1 <!-- This runs when user ACCEPTS analytics --> 2 <script type="text/plain" data-category="analytics"> 3 gtag('config', 'G-XXXXXXX'); 4 </script> 5 6 <!-- This runs when user DISABLES analytics --> 7 <script type="text/plain" data-category="!analytics"> 8 window['ga-disable-G-XXXXXXX'] = true; 9 document.cookie = '_ga=; Max-Age=0; path=/;'; 10 document.cookie = '_ga_XXXXXXX=; Max-Age=0; path=/;'; 11 </script>
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?
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)
Make sure the CookieBeam runtime script loads before any other scripts in your <head>. The interception engine needs to install its hooks before third-party scripts execute. 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 <head> so the interception engine installs its hooks early
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