Streaming apps often lay a dark gradient over the picture so titles and controls stay readable. I wanted that layer gone, not the movie. The result is ClearView, a small Chrome extension with a slider from the original shade to fully removed.
Apart from Netflix almost all major streaming service has this annoying blur on hover that takes too long to disappear for my liking.
The extension is still a WIP but can be used by cloning the repo for now,
Tested on HBO Max, And Apple TV
Apple TV is a bit of an hassle to get right...for now
At 100% reduction, tv.apple.com went black. The extension had found a dark element the size of the player and set its opacity to 0. That element was not the shade. It was a parent of the video. Fading it faded the picture too, and the page behind the player is black.
Two details in Apple's player make this easy to get wrong:
- The
<video>sits in an open shadow root, sodocument.querySelector("video")never sees it. - The shade is a full-size
::aftergradient on an ancestor.elementsFromPoint()does not return pseudo-elements, andNode.contains()does not cross the shadow boundary, so a parent can look like a harmless overlay.
Kiril Ivanov makes the case for a persistent content script when a page keeps mutating, rather than a one-shot chrome.scripting injection. Yuan Zhou is blunter about the cost: on a site you do not control, you only see the DOM, the markup will change, and a content script that assumes the wrong shape can break the host page. Both showed up while I was deciding not to hard-code one streaming service.
The rule that fixed it
Never change the opacity of an element that owns the video, including ownership that crosses a shadow root. Fade the gradient pseudo-element instead.
Finding the video means walking open shadow roots:
function collectVideos(root, found) {
if (!root?.querySelectorAll || found.length > 12) return;
root.querySelectorAll("video").forEach((video) => found.push(video));
root.querySelectorAll("*").forEach((element) => {
if (element.shadowRoot) collectVideos(element.shadowRoot, found);
});
}
Ownership has to follow the same path. element.contains(video) is false for a light-DOM ancestor of a shadow-hosted video.
function ownsMedia(element, video) {
let node = video;
const seen = new Set();
while (node && !seen.has(node)) {
seen.add(node);
if (node === element) return true;
if (node.parentElement) {
node = node.parentElement;
continue;
}
const root = node.getRootNode?.();
node = root instanceof ShadowRoot ? root.host : null;
}
return false;
}
You cannot set a pseudo-element's opacity from element.style. A content-script stylesheet can, and the slider only changes a custom property:
[data-clearview-pseudo~="after"]::after {
opacity: var(--clearview-remaining) !important;
}
The script marks an ancestor only when ::before or ::after is absolutely positioned, covers most of that ancestor, and paints a dark translucent color or a dark gradient. Separate overlay elements that do not contain the video still use normal opacity. Interactive controls are left alone. There is a click-to-hide picker for the ones you do want gone.
On the I, Robot title page, the gradient's computed opacity went to 0 while the video and its black wrapper stayed at 1. The artwork stayed on screen.
What I would not do again
I would not treat "dark and about the size of the video" as enough. A player shell is often exactly that, and its background is the letterbox, not the scrim. I would also not observe attribute mutations on a busy player. Kouta hit an infinite callback loop in Gmail by watching attributes. Child-list changes, debounced, were enough here too.
Settings stay in chrome.storage.local, per site. Nothing about the page is sent anywhere.
The source is here: HumayounBaig/overlay-blocker-chrome-ext
Top comments (2)
The "never fade the element that owns the video" rule is a great invariant — it turns a gnarly debugging session into one guard check. I'm curious about the shadow-root walk though: some players use closed shadow roots, where
element.shadowRootreturns null, socollectVideoswould stop short. Did you hit any sites like that, and did you need a fallback such aselementsFromPointheuristics? Also hard agree on avoiding attribute mutations on busy players — that's practically an invitation to a callback loop.@nehznah haven't had any sites like that yet, still testing it out on other video sites extensively.
feel free to let me know if you come across any sites that have the issue still