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:
1 2 | ##[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:
1 | <span class="slotA1 sponsorLabel">Sponsored</span> |
When styling, assume sponsorLabel might disappear, let the parent handle size and padding.
4. Detect & fallback
1 | 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
renderFallbackfires, 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
slotHeroexists 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,promoin 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-bannerstackoverflow.com - And network filters that kill requests hitting
/ads/,/banner/, or domains likeads.example.comadblockplus.org
- They include global CSS filters that hide elements via selectors like
- When a rule hits, the element gets
display:noneor the request never fires → layout breaks, code fails with zero clues. - Incidents with IDs like
ad_holderor 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.
