Cosmetic Filtering
Some ads survive network blocking because the site serves them inline from its
own origin — YouTube feed and masthead ads, Twitch display banners, X promoted
entries. declarativeNetRequest cannot touch those, so Very Good AdBlock hides
them in the page with cosmetic filtering.
Cosmetic filtering is on by default. It was previously deferred because the first attempt used broad, generic selectors that risked breaking modern video and social layouts. The current implementation ships with the guardrails that deferral asked for.
How It Works
- The content script runs at
document_startand injects the default stylesheet synchronously, before the page parses, so ads never paint. Once settings load it reconciles — removing the sheet if the extension is off, the site is allowlisted, or cosmetic filtering is disabled, and adding aggressive selectors when enabled. - The stylesheet sets
display: none !importantper selector. CSS hides matching elements immediately, including ones YouTube's SPA adds later, so no per-element mutation race is needed. - Each selector gets its own rule. In a comma-joined selector list a single
invalid or unsupported selector (for example
:has()on an old engine) makes the browser discard the entire rule; per-selector rules fail in isolation, so one bad selector never disables the rest of the hiding. - A throttled mutation sweep then tags each hidden node (
data-adblock-hidden) and attributes it to the selector that matched, feeding the blocked count and per-page diagnostics.
Guardrails
- Site-specific selectors. Selectors target dedicated ad-only custom
elements (
ytd-ad-slot-renderer,ytd-display-ad-renderer,#masthead-ad), not broad[class*="ad"]matches. Real videos, comments, Shorts, and recommendations are never targeted. - X promoted tweets are pruned at the source. A MAIN-world content script
(
content/x-inpage.ts) wrapsfetchand removes entries carryingcontent.itemContent.promotedMetadatafrom X's GraphQL timeline responses before the app renders them — the same source-level approach uBlock Origin uses. It is locale-independent, leaves no flash, and never guesses at DOM nodes. It talks to the isolated content script overwindow.postMessage: it receives an enable flag (honoring the off switch and allowlist) and reports how many ads it removed for stats. A DOM label check (xPromotedLabelsinconstants.ts) stays as a fallback for stragglers — matched only against standalone label spans, never the[data-testid="placementTracking"]media container X reuses on ordinary tweets. - Never the player. Instream video ads are handled by skip automation.
Player-region containers (
.video-ads,.ytp-ad-module) are intentionally not hidden, so hiding an ad can never hide the skip control or the video. - Detection, not hiding, for stream ads. Twitch now stitches video ads into
the stream server-side (SSAI), so the legacy display-ad selectors
(
.stream-display-ad__container,sad-overlay,video-ad-banner) are gone from the maintained filter lists and were dropped. What remains is hiding the ad-only affordances (Leave feedback for this Ad/Learn more about this adbuttons) and the anti-adblock nag overlay. The in-stream ad markers (.commercial-break-in-progress,[data-a-target="video-ad-label"], countdown) stay visible and are only counted — the ad is the live stream, so hiding the notice would remove feedback while the ad keeps playing. - Global + per-site kill switches.
cosmeticFilteringdisables all hiding without touching network blocking; the per-site YouTube/Twitch toggles and the allowlist scope it further. - Opt-in cookie-consent hiding.
cookieConsentFiltering(off by default) adds a group targeting the common consent-management platforms by their dedicated container ids/classes only (OneTrust, Didomi, Cookiebot, Usercentrics, Quantcast, Sourcepoint, and more) — never a generic[class*="cookie"]. When a banner is hidden the content script also restores the page scroll these overlays lock. - Narrow default, opt-in aggressive. A high-confidence set ships on;
aggressiveCosmeticadds broader matches for people who accept a small risk of over-hiding. - Diagnostics. The dashboard's Cosmetic activity panel lists which selectors acted on the active tab and how many placements each removed.
Selectors
The full registry lives in src/shared/cosmetic.ts, grouped by surface
(generic, YouTube, Twitch, X) and tier (default vs aggressive).
activeCosmeticGroups() resolves the set for the current page from the settings,
which keeps the selector policy testable without a browser.
Tests
test/youtube-content.test.tsloads a YouTube-like page in Bun WebView and asserts masthead, feed, and display ads are hidden while the real feed video, comments,<video>, and the skip button stay visible and playback works.test/twitch-content.test.tsasserts the ad-only feedback button and the anti-adblock nag overlay are hidden, the dropped legacy display-ad container is left visible, and the in-stream video-ad markers stay visible for detection.test/x-content.test.tsasserts a promoted tweet cell is hidden by the DOM fallback while an ordinary tweet's photo and video (in the sameplacementTrackingcontainer) stay visible.test/x-prune.test.tsasserts the GraphQL pruner removes promoted entries (bypromotedMetadataandpromoted-ids, including promoted module items) while leaving organic entries and unrelatedentriesarrays untouched.scripts/smoke-extension.tsscreenshots the built extension against YouTube, Twitch, popup, options, and marketing surfaces.