Webclat logoWebclat . | OneTrust Solutions
Ad-blocker detection

What's a reliable, current method to detect that a visitor is running an ad blocker?

Short answer

Ad blockers work by pattern-matching request URLs and DOM elements against filter lists like EasyList, so the reliable detection method is a "bait" element or request built from the same patterns those lists target, checked after a short delay to see whether it was hidden or blocked - not user-agent sniffing or feature detection, since blockers don't touch either. No single check is 100% reliable across every blocker and browser combination, so combine a cosmetic-filter bait with a network-request bait rather than relying on one.

Why this happens

Ad blockers use two independent mechanisms, and a detection script has to account for both. Cosmetic filtering hides DOM elements whose class, id, or attributes match a crowd-maintained selector list (EasyList and similar), purely by string pattern - the element is still in the DOM, just given display:none by an injected stylesheet rule. Network filtering blocks the request itself before it leaves the browser, based on the URL matching a known ad/tracker pattern - the request never gets a response at all.

This is why testing right after inserting a bait element often gives a false negative: most extensions apply their cosmetic rules on a short delay (a mutation observer batch, or a scheduled pass), not synchronously the instant the element is added to the DOM. A check that runs in the same tick as the insertion will see the element as still visible even when a blocker is active and about to hide it.

It's also why no method reaches 100% reliability. Filter lists differ by blocker, by list subscription, and by how aggressively the user has configured it, so a bait pattern that one blocker's default list flags might not be on another's. Treat detection as a probabilistic signal you can measure and react to, not a certainty you can branch application logic on.

Fix it

  1. 1

    Build a cosmetic-filter bait element

    Create an off-screen element with class names that commonly appear in filter-list selectors (ad, ads, ad-banner, adsbox), append it to the DOM, then check its computed visibility after a delay - not immediately.

    const bait = document.createElement("div");
    bait.className = "ad-banner ads adsbox ad-placement";
    bait.style.cssText = "height:1px;width:1px;position:absolute;left:-9999px;top:-9999px;";
    document.body.appendChild(bait);
    
    setTimeout(() => {
      const blockedByCosmeticFilter =
        bait.offsetParent === null ||
        bait.offsetHeight === 0 ||
        getComputedStyle(bait).display === "none";
      document.body.removeChild(bait);
      // blockedByCosmeticFilter === true means an element-hiding rule fired
    }, 300);
  2. 2

    Build a network-request bait

    Request a resource whose path matches a known ad-network pattern (or a filename like ads.js) and treat a caught failure or a zero-length response as a network-level block, distinct from a real 404 or CORS error.

    fetch("https://pagead2.googlesyndication.com/pagead/id", { mode: "no-cors" })
      .then(() => { /* request reached the network - not blocked here */ })
      .catch(() => { /* request never left the browser - network filter fired */ });
  3. 3

    Combine both signals before treating the visitor as blocked

    Require the cosmetic check and the network check to agree, or accept either alone only if you understand the false-positive/false-negative tradeoff each carries on its own - a single method flags fewer true blockers but with more confidence, two methods catch more but can disagree.

  4. 4

    Always wait for the blocker to act before reading state

    Give any bait at least 100-300ms before checking it. A same-tick check after DOM insertion will misreport blockers that apply cosmetic rules on a batched pass rather than synchronously.

  5. 5

    Decide what the detection result changes, deliberately

    Detection alone is neutral - what you do with a positive result (show a message, log the block rate, adjust something) is a separate design decision. Don't let the detection code silently start altering page behavior as a side effect.

How to verify it worked

Test with a real blocker installed (uBlock Origin and AdBlock Plus behave differently) and confirm the boolean flips to true; then disable the blocker (or use a clean profile) and confirm it flips to false on the same page.

Open DevTools > Elements while the blocker is active and inspect the bait element's computed style panel - a passing result shows an injected display: none !important rule attributed to the extension, not your own stylesheet.

Open DevTools > Network while the blocker is active and confirm the bait request shows a blocked status (Chrome labels it (blocked:other) or similar) rather than a normal response code.

Related

Still stuck, or want this checked against your specific setup?

We scope every engagement in discovery, before implementation - no assumptions about your stack.

Request a Free OneTrust Audit