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. 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]). 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)
