You might think the ad-
prefix in CSS is harmless. It’s just a couple of letters, clear enough for even a junior dev to spot. But those few letters can turn your frontend into a totally blank page.
Even worse? You’ll only hear about the bug from a furious client. In production. Late Friday afternoon. With GA4 quiet as a mouse because not a single pixel fired.
So if you want to keep your UX (and your sanity) intact, keep reading. Today I’ll show you why ad-blockers hunt anything that smells like advertising, where we most often get burned, and how to avoid it, no gimmicky “anti-ad-block” scripts required.
Audio version of the article
Recording length: 18 minutes
Why does this matter?
EasyList the ammo belt behind most ad-blockers packs thousands of rules that fire before the first pixel of your layout lights up. Buried in there are innocent-looking lines like:
##[id^="ad-"]
##[class*=" ads-"]
Note: ##
is ad-block syntax; the regular CSS selector is the part without it.
This regex machete hides every element whose id
starts with ad-
or whose class
contains a stand-alone “ads”.
Network filters work the same way, killing any request to URLs like /ads/
, /banner/
, or the sub-domain ads.example.com
(easylist.to). The file never loads, your JavaScript throws an error, and you’re left wondering why the component is “only broken for some users.”
Heads-up: It’s not just IDs and classes
Blockers now run “cosmetic” filters on text content too :-abp-contains()
will hide <span>Advertisement</span>
purely by its inner text (help.adblockplus.org). Simple? Not even close.
How do blockers hunt your selectors?
- CSS injection – the extension’s manifest injects a stylesheet with global selectors like the ones above.
- URL rewrite & cancel – if the request matches a filter, it dies at the network layer.
- Scriptlet hijacking – uBlock Origin can inject JS that selectively nukes or tweaks the DOM.
Watch out, this is where folks slip up most: If the element isn’t in the DOM yet (lazy render) and the ad-blocker injects first, it gets hidden the moment it appears. Your layout re-flows, and your pixel-perfect grid is toast.
Real-world examples
- A SaaS project for SMBs had a button labeled “Add-ons” (
id="addons_btn"
). Harmless, right? They “improved” it by addingclass="ads-variant"
, conversions dropped 32 %. Why? Users with uBlock never saw the button. Debugging that is a nightmare. - I’ve seen server-side GTM endpoints placed on sub-domains named gtm, analytics, etc. and getting blocked even harder than the default installer. Use random, innocuous names like “bread”, “lightbulb”, or “car” anything not linked to analytics or ads.
- I know a project that stuffed the word “analytics” into its main domain. Clicking any link to that domain threw up a warning. Not great. The project eventually died.
Where do we mess up most often?
- Folder structure:
/static/**ads**/banner.png
- Hashed IDs:
gtm-ad-1234
- WordPress themes:
theme-adwise
,widget_banner
- Alt and title:
alt="Advertisement"
- Data attributes:
data-ad-slot="header"
- Sub-domain:
ads.mydomain.cz
And always the same story: “Works on my machine, 30 % of prod traffic is broken.”
How do you get out of this mess?
1. Name it something else
Use project-specific aliases: instead of ad-slot
go with slotHero
or slotSidemenu
. Inside a word is fine (he*ad*er
, ro*ad*map
) filters usually need a word boundary.
2. Hash your asset paths
If the CDN forces /img/ads/…
, add a reverse-proxy rewrite to /img/a1b2c3/…
. One Nginx rule and you’re golden.
From an analytics guy: Detecting ad-blockers used to be as easy as loading a file named “ads.js”, if it didn’t load you knew the user was blocking ads.
3. Separate semantics from layout
If compliance says you need a visible “Sponsored” label, give the element two classes:
<span class="slotA1 sponsorLabel">Sponsored</span>
When styling, assume sponsorLabel
might disappear, let the parent handle size and padding.
4. Detect & fallback
const slot = document.querySelector('#slotHero'); if (!slot || slot.offsetHeight === 0) { console.warn('Ad-block? Switching to fallback.'); renderFallback(slot); }
No fancy API needed plain DOM does the job.
Start now, future you will thank you.
Pre-release checklist
- Name audit – scan build artifacts (
grep -R --line-number -E '\\b(ad|banner|promo|sponsor)\\b' dist/
). - Local test – install both uBlock Origin and AdGuard; their feeds differ.
- Run Lighthouse + ad-block – watch for new JS errors.
- Check GA4 hits – still tracking with a blocker on? If not, consider server-side GTM.
- E2E test in CI – Cypress + uBlock in headless Chrome.
- Monitor broken layout – simple visual-diff screenshot test.
- Log fallbacks – when
renderFallback
fires, push an event to Sentry. - Set alerts – 5 % fallback spike ⇒ Slack ping.
- Re-audit on every major frontend refactor.
- Document it – so the next dev knows why
slotHero
exists instead ofadHeader
.
Personal tips that saved my career
“Name it ugly first, then beautify”
When I draft component IDs, I deliberately slap in an ugly placeholder like XXX_NOT_AD
. I only refactor once I see the layout on a dev server. Stops me from reflexively typing “adBox”.
“Mock the ad-blocker at the proxy”
Need to debug without the extension? Have your reverse-proxy drop /ads/
requests. Faster iteration, same result.
“Grep the diff constantly”
Pre-commit hook that rejects commits containing \bads?\b
in new files will save you cold sweats later.
Summary
- Ad-blockers don’t read intent, only regex.
- Keywords like
ad
,ads
,banner
,promo
in ID, class, URL, or file names ⇒ element vanishes. - One hidden wrapper can nuke your whole layout.
- The fix is simple: rename and fallback.
- Automate detection or you’ll forget.
What should you do after reading?
- Audit your own site right now.
- Add this rule to your dev docs.
How about you? Got your own horror story with id="adBanner"
, or did you survive a launch unscathed? Share on social media so next time we’re laughing together instead of crying alone.
Short version I dropped into our docs:
Avoid “ad”, “ads”, “advert”, “banner”… in identifiers and paths
Applies to: id
, class
, data-*
attributes, file & folder names, sub-domains, URL params
Rule
- Never use those keywords or any shortened/extended variant (ad, ads, adv, advert, banner, promo, sponsor…)
- …when they’re at the start or after a delimiter (
-
,_
,/
,.
).
- …when they’re at the start or after a delimiter (
- Allowed if the string sits inside another word (header, roadmap).
Why
- Ad-blockers (uBlock Origin, AdBlock Plus, AdGuard…) run off lists like EasyList.
- They include global CSS filters that hide elements via selectors like
##[id^="ad-"]
,##.ads-banner
stackoverflow.com - And network filters that kill requests hitting
/ads/
,/banner/
, or domains likeads.example.com
adblockplus.org
- They include global CSS filters that hide elements via selectors like
- When a rule hits, the element gets
display:none
or the request never fires → layout breaks, code fails with zero clues. - Incidents with IDs like
ad_holder
or themes named “adwise” show how common these silent bugs are.
Consequences of breaking the rule
- Invisible components (buttons, modals, icons…)
- Missing JS/CSS files ⇒ JS errors, broken design
- Hard to debug, many vendors & QA testers don’t run ad-block
Exceptions
Where it’s safe | Why |
---|---|
Visible text (“Advertisement”, “Sponsored”) | Filters only apply cosmetic styles; don’t rely on its dimensions |
HTML/JS comments, internal variables | Blockers don’t parse them |
Examples
❌ Wrong | ✅ Right |
---|---|
<div id="ad_holder"> | <div id="slotHero"> |
/static/ads/logo.svg | /static/assets/logo.svg |
class="banner_top" | class="heroTop" |
Following this rule keeps your elements visible and saves hours of chasing “mysteriously disappearing” components.