Detect ad blockers in JavaScript: bait DOM, heuristics, and what not to break

How to detect adblock in JavaScript: bait DOM patterns, getComputedStyle and offsetWidth checks, BlockAdBlock (blockadblock js) global guard, common API mistakes, limitations and false positives—with example branch logic you can adapt.

Published

Updated

Read time 5 min read

Reviewed byDeepak Prasad

Detect ad blockers in JavaScript: bait DOM, heuristics, and what not to break

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

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

css
.ads {
  width: 1px;
  height: 1px;
  position: absolute;
  left: -9999px;
}

checker.js (runs in the browser; log in DevTools)

javascript
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 }).

javascript
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",
  }),
);
Output

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.

javascript
const outcome =
  typeof blockAdBlock === "undefined"
    ? "blockAdBlock is undefined (script did not run or name was stripped)"
    : "blockAdBlock is defined";

console.log(outcome);
Output

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.


Frequently Asked Questions

1. 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.

2. 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.

3. 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.

4. 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.

5. 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.

6. 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.
Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs …