Engineering4 min read
How to block third-party scripts until consent
A developer's guide to blocking third-party scripts until consent: inert script tags, activation code, tag managers, embeds and how to test that nothing leaks.
To block third-party scripts until consent, ship them in a form the browser will not execute, such as a <script> tag with type="text/plain", and activate them only after the visitor opts into the matching category. Apply the same rule to inline snippets, iframes, tracking pixels and tag manager tags, then verify it in a clean browser before clicking anything.
Why do third-party scripts need to wait for consent?
Because most of them store or read information on the visitor’s device the moment they run, and in the EU that requires prior consent. Article 5(3) of the ePrivacy Directive requires consent for storing or accessing information on a device, unless it is strictly necessary for a service the user explicitly asked for.
Analytics, advertising, social and many embed scripts set cookies, read browser storage or collect device characteristics as soon as they execute. In Planet49 (C-673/17), the Court of Justice of the EU confirmed that the rule applies whether or not the information is personal data, and that pre-ticked boxes are not valid consent. A banner that appears while trackers are already running does not meet that standard.
Scripts that are genuinely needed for the service, such as payment, fraud prevention or the consent tool itself, can load normally; see what counts as a strictly necessary cookie.
How do I block scripts before consent?
Change their type. Browsers only download and execute scripts whose type is a JavaScript type or module; a script marked text/plain is treated as inert data. Keep the original attributes and add a category so your consent code knows what to activate.
<!-- Inert: the browser neither downloads nor runs these -->
<script type="text/plain" data-consent="analytics"
src="https://analytics.example.com/script.js"></script>
<script type="text/plain" data-consent="marketing">
window.adPixel = window.adPixel || [];
window.adPixel.push(['init', 'PIXEL_ID']);
</script>
<!-- Embeds: keep the real URL in data-src until consent -->
<iframe data-consent="marketing"
data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
title="Product demo" width="560" height="315"></iframe>
Remove the <noscript> image fallbacks that many vendor snippets include. They fire a request when JavaScript is disabled, which is exactly when your banner cannot appear, so there is no way to gate them.
How do I activate blocked scripts after consent?
Replace each inert script with a fresh <script> element. Changing the type attribute on an existing element does not make the browser run it, and scripts added through innerHTML never execute, so you need a new element created with document.createElement.
function activate(category) {
document
.querySelectorAll(`script[type="text/plain"][data-consent="${category}"]`)
.forEach((inert) => {
const live = document.createElement('script');
for (const { name, value } of inert.attributes) {
if (name !== 'type') live.setAttribute(name, value);
}
if (!inert.hasAttribute('async')) live.async = false; // keep insertion order
live.text = inert.text; // inline code, if any
inert.replaceWith(live);
});
document
.querySelectorAll(`iframe[data-consent="${category}"][data-src]`)
.forEach((frame) => { frame.src = frame.dataset.src; });
}
// Apply the stored choice on load; call activate() again from your banner's handlers.
for (const [category, granted] of Object.entries(readStoredConsent())) {
if (granted) activate(category);
}
Three details trip people up:
- Order. External scripts inserted with JavaScript run asynchronously by default, in whatever order they finish downloading. Setting
async = falsemakes them run in insertion order, which matters when one library depends on another. - Inline snippets run immediately. An inline script executes as soon as it is inserted, without waiting for external scripts. Most vendor snippets queue their calls until the library loads, but check any custom code that assumes a library is already there.
document.write(). Old snippets that call it will not work once the page has loaded. Replace them with the vendor’s asynchronous snippet.
Which blocking method should I use?
Most sites combine several methods, chosen by where each tag lives.
| Method | Best for | Watch out for |
|---|---|---|
Inert text/plain tags |
Tags hard-coded in templates | Every tag must be edited; execution order |
| Consent-aware loader in your own code | SPAs and SDKs installed from npm | SDKs that initialize as soon as they are imported |
| Tag manager consent settings | Teams that manage tags in a tag manager | Each tag needs the correct consent requirement |
| Automatic blocking | Large legacy sites with many hard-coded tags | Must load synchronously first; can miss or break scripts |
| Iframe placeholders | Video, maps and social embeds | The placeholder must explain what loading it means |
Libraries installed from npm never appear as script tags, so the gate is your own code. Call the SDK’s init() only after consent, and load it with dynamic import() so it is not even downloaded before. That pattern, and handling choices that change mid-session, is covered in handling consent in single-page apps.
Google tags have an extra option. With Google Consent Mode, tags can load before consent and adjust their behavior to the consent signals instead of being blocked. Whether that setup fits your risk appetite is a separate decision; see basic vs. advanced consent mode.
How do I verify that blocking works?
Test like a first-time visitor, and check what actually happens on the network and in storage:
- Open the site in a fresh private window from a region where your banner appears.
- Before clicking anything, open DevTools and check the Network panel. There should be no requests to analytics or advertising domains, unless you deliberately chose a setup that sends them.
- In the Application panel, check cookies, local storage and session storage. Only strictly necessary entries should exist.
- Click “Reject all” and confirm that nothing new loads or gets stored.
- In a new session, accept one category at a time and confirm that only its scripts and embeds load.
- Repeat after every release that adds a tag, and run a periodic cookie audit to catch tags added outside your release process.
Bottom line
Blocking is about execution, not just cookies: once a non-essential script runs, it can store or read data before anyone has agreed. Ship tags inert, activate them per category after an opt-in, gate embeds and SDKs the same way, and test in a clean browser. Tools like Cookiesh can automate the activation step, running scripts tagged type="text/plain" data-cookiesh="analytics" only after the visitor opts into that category.
This article is general information, not legal advice. For decisions about your specific situation, consult a qualified privacy professional.
Frequently asked questions
Is it enough to block cookies but still load the script?
Usually not. The EU rule covers storing and accessing any information on the device, not only cookies, and a running script can use local storage, read device characteristics or send identifiers. Not executing the script at all is the reliable approach.
Can I load Google Tag Manager before consent?
Many sites do, and use the container's consent settings so individual tags fire only when the relevant consent is granted. Check what the container and every tag actually do before consent in a fresh browser profile rather than assuming the configuration is right.
Do I need to block scripts for visitors outside the EU?
It depends on the law that applies to them. Opt-in rules such as the EU's and the UK's require blocking before consent. Under California's CCPA and CPRA, the duty is mainly opt-out: you must stop selling or sharing personal information when a visitor opts out, including through a Global Privacy Control signal.
Sources
Cookiesh Editorial Team
Privacy engineers and compliance specialists writing practical guidance on consent, cookies and data protection for product teams.