Many publishers track how to detect adblock or ship an adblock detection script so they can show a message, swap creatives, or measure loss. Client-side checks are always heuristic: filter lists evolve (EasyList, uAssets), users can bypass probes, and unrelated CSS or privacy features can hide your bait. This page fixes common copy-paste bugs (wrong DOM APIs), shows a small bait probe you can run in the browser, explains the popular BlockAdBlock / blockadblock js guard, and uses Node to print the branch labels the same logic would emit when style/size inputs are stubbed (real detection still needs verification in Chrome, Firefox, or Safari).
Tested on: Node.js v20.18.2 for §3–§4; §2 is browser-only (
{run=false}). A short note after each block describes the expected console output.
Quick reference
Use this table for javascript detect adblock heuristics and limits.
| Approach | What you observe |
|---|---|
| Bait DOM | getComputedStyle, offsetWidth / offsetHeight after layout |
| Missing script global | typeof wantedGlobal === "undefined" (heuristic) |
| Server-side | Track 404/blocked rates for known ad hosts (out of scope here) |
1. How “bait” detection works
Most detect adblocker snippets insert a harmless element whose class or id resembles real ad markup (ads, adsbox, adsbygoogle, …). If an extension hides or collapses that node, getComputedStyle may report display: none, or offsetWidth / offsetHeight drop to zero after layout—the get width of an element in JavaScript guide explains how those layout reads differ from CSS width strings. You normally schedule the read after paint (requestAnimationFrame or a short setTimeout) so measurements are stable.
Treat the result as a signal, not proof: responsive CSS, contain, or transform can also zero out metrics.
2. Minimal HTML + checker.js (browser)
Use querySelector (or document.getElementsByClassName('ads')[0]—see JavaScript DOM selectors for getElementsByClassName vs querySelector). There is no getElementByClassName.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ad-block probe</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="bait" aria-hidden="true">
<div class="ads"></div>
</div>
<script src="checker.js" defer></script>
</body>
</html>style.css
.ads {
width: 1px;
height: 1px;
position: absolute;
left: -9999px;
}checker.js (runs in the browser; log in DevTools)
function classifyBait(el) {
if (!el) return "missing-bait";
const { offsetWidth, offsetHeight } = el;
const { display, visibility } = getComputedStyle(el);
const d = display.toLowerCase();
const v = visibility.toLowerCase();
if (d === "none" || d === "contents" || v === "hidden") {
return "hidden (common when an extension applies cosmetic filters)";
}
if (offsetWidth === 0 && offsetHeight === 0) {
return "zero box (collapsed bait or removed from layout)";
}
return "visible bait (no strong blocklist signal from this probe alone)";
}
const bait = document.querySelector("#bait .ads");
function run() {
console.log(classifyBait(bait));
}
requestAnimationFrame(run);Open DevTools → Console to see which branch you get with and without an extension. The same classifyBait rules are what we exercise next in Node using plain objects instead of a real DOM node.
3. Same decision branches in Node (stubbed measurements)
Headless DOM libraries often mis-report offsetWidth, so the transcript below does not pretend to be a browser. It only proves the string labels your checker.js would emit for a few stubbed measurements (for example after you map a real element’s getComputedStyle into { display, visibility }).
function classifyBait({ offsetWidth, offsetHeight, display, visibility }) {
const d = (display || "").toLowerCase();
const v = (visibility || "").toLowerCase();
if (d === "none" || d === "contents" || v === "hidden") {
return "hidden (common when an extension applies cosmetic filters)";
}
if (offsetWidth === 0 && offsetHeight === 0) {
return "zero box (collapsed bait or removed from layout)";
}
return "visible bait (no strong blocklist signal from this probe alone)";
}
console.log(
classifyBait({
offsetWidth: 1,
offsetHeight: 1,
display: "block",
visibility: "visible",
}),
);
console.log(
classifyBait({
offsetWidth: 0,
offsetHeight: 0,
display: "block",
visibility: "visible",
}),
);
console.log(
classifyBait({
offsetWidth: 120,
offsetHeight: 50,
display: "none",
visibility: "visible",
}),
);Three lines in order: the “visible bait” label, the “zero box” label, then the “hidden (cosmetic filters)” label—matching the three stubbed measurement objects.
4. blockadblock js and other detect adblock script bundles
BlockAdBlock exposes a global constructor when its file executes. If the script URL or inline payload is blocked, the global never appears—similar to javascript detect adblock checks that watch for missing ad network libraries.
const outcome =
typeof blockAdBlock === "undefined"
? "blockAdBlock is undefined (script did not run or name was stripped)"
: "blockAdBlock is defined";
console.log(outcome);In Node there is no blockAdBlock global, so typeof blockAdBlock === "undefined" and you should see the message that the script did not run or the name was stripped.
In a real page you would then call blockAdBlock.onDetected / onNotDetected only in the branch where it is defined. Remember false positives: CSP, adblock lists targeting blockadblock.js by filename, or offline caching can all leave the global undefined without a user “confessing” to anything.
5. Practical limits (what outranking content should admit)
- Filter lists react to popular bait strings; rotate names responsibly and expect breakage.
- Cosmetic filters differ between uBlock Origin, AdGuard, Brave, and mobile WebView.
- Regulations and platform rules may restrict how you respond to detection—get legal/product review for your jurisdiction and ad contracts.
- Prefer transparent monetization (fewer, faster ads; direct support) over endless escalation.
Summary
- Bait elements plus layout/style reads are common but never definitive; schedule reads after paint.
typeof blockAdBlock === "undefined"only means the script did not run or was stripped—many false positives.- Prefer transparent monetization and legal/product review over aggressive escalation.
References
MDN APIs and community references for detect adblock javascript patterns.
- MDN:
Window.getComputedStyle() - MDN:
HTMLElement.offsetWidth - Stack Overflow: How to detect AdBlock on my website?
- BlockAdBlock (GitHub)
Frequently Asked Questions
How to detect adblock in JavaScript reliably?
There is no fully reliable client-side signal. Common heuristics inject a bait element with a class or id that filter lists target, then read getComputedStyle and box metrics after layout. Third-party scripts such as BlockAdBlock can be blocked by name, so typeof checks are also heuristic.
Why is my adblock detection script wrong sometimes?
EasyList and similar lists change constantly; bait class names get neutralized or spoofed. Custom CSS, shadow DOM, privacy modes, and network errors can hide or collapse your probe without an ad blocker.
What was wrong with getElementByClassName in older tutorials?
The DOM API is getElementsByClassName (plural, returns a live collection) or, more simply, document.querySelector('.ads'). There is no getElementByClassName.
Does BlockAdBlock still work as a detect adblock script?
It may, but many blockers treat blockadblock.js as suspicious. If the script never executes, the global blockAdBlock is undefined—which looks like detection even when the failure was CSP, rename, or a network error.
Is blocking users who use ad blockers a good idea?
Hard paywalls and aggressive anti-adblock often hurt trust. Many sites prefer a polite message, lighter ads, subscriptions, or donations while respecting that lists exist to improve user experience.
Should bait markup be exposed to assistive technologies?
Decorative bait should be hidden from accessibility APIs, for example with aria-hidden="true" on a container that is also visually unobtrusive.
