Removes YouTube's 2025 saturated hover effects.
当前为
// ==UserScript==
// @name YouTube No Saturated Hover
// @namespace https://greasyfork.org/users/1476331-jon78
// @version 1.2
// @description Removes YouTube's 2025 saturated hover effects.
// @author jon78
// @license cc0
// @match *://*.youtube.com/*
// @icon https://www.youtube.com/favicon.ico
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
"use strict";
const ID = "no-saturated-hover";
let styleEl = null;
let dark = undefined;
let rafPending = false;
let panelObserver = null;
/* ---------------------------------------------------------
Detect YouTube Light/Dark mode
- Uses attributes/classes first, then falls back to measuring
--yt-spec-base-background brightness.
--------------------------------------------------------- */
const detectDark = () => {
const html = document.documentElement;
// Explicit theme attributes / classes
if (html.hasAttribute("dark") || html.classList.contains("dark-theme")) return true;
if (html.hasAttribute("light") || html.classList.contains("light-theme")) return false;
// Fallback: measure --yt-spec-base-background brightness (best-effort)
try {
const cs = getComputedStyle(html);
const bg = (cs && cs.getPropertyValue("--yt-spec-base-background") || "").trim();
if (bg.startsWith("rgb(") || bg.startsWith("rgba(")) {
const nums = bg.match(/\d+/g);
if (nums && nums.length >= 3) {
const r = Number(nums[0]), g = Number(nums[1]), b = Number(nums[2]);
// simple luminance-ish average check
return ((r + g + b) / 3) < 60;
}
}
} catch (e) {
// ignore and fall through
}
return false;
};
/* ---------------------------------------------------------
Core CSS — parameterized by dark (boolean)
--------------------------------------------------------- */
const buildCss = d => {
return (`
html {
/* Custom theme base variables */
--ytc-base-background:${d ? "#0f0f0f" : "#fff"};
--ytc-additive-background:${d ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
--ytc-text-primary:${d ? "#f1f1f1" : "#0f0f0f"};
--ytc-text-secondary:${d ? "#aaa" : "#606060"};
/* Mirror YT native variables if missing */
--yt-spec-base-background:var(--yt-spec-base-background,var(--ytc-base-background));
--yt-spec-additive-background:var(--yt-spec-additive-background,var(--ytc-additive-background));
--yt-spec-text-primary:var(--yt-spec-text-primary,var(--ytc-text-primary));
--yt-spec-text-secondary:var(--yt-spec-text-secondary,var(--ytc-text-secondary));
/* Playlist panel overrides */
--yt-active-playlist-panel-background-color: var(--yt-spec-additive-background);
--yt-lightsource-primary-title-color: var(--ytc-text-primary);
--yt-lightsource-secondary-title-color: var(--ytc-text-secondary);
--iron-icon-fill-color: var(--yt-lightsource-primary-title-color);
}
/* Disable saturated hover feedback UI */
.yt-spec-touch-feedback-shape__hover-effect,
.yt-spec-touch-feedback-shape__stroke,
.yt-spec-touch-feedback-shape__fill {
display: none !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Remove highlight from promoted videos */
ytd-rich-item-renderer.ytd-rich-item-renderer-highlight {
background: transparent !important;
box-shadow: none !important;
--yt-spec-outline: transparent !important;
}
/* Primary title colors */
ytd-rich-grid-renderer #video-title,
.yt-lockup-metadata-view-model__title,
.yt-lockup-metadata-view-model__title a {
color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}
/* Metadata / secondary text colors */
.yt-lockup-metadata-view-model__metadata,
.yt-lockup-metadata-view-model__metadata span,
#metadata-line span {
color: var(--yt-spec-text-secondary, var(--ytc-text-secondary)) !important;
}
/* Watch page text colors */
ytd-watch-metadata #description,
ytd-video-secondary-info-renderer #description,
ytd-watch-info-text,
#metadata.ytd-watch-info-text,
#metadata-line.ytd-video-primary-info-renderer span,
#snippet-text,
#snippet-text *,
#attributed-snippet-text,
#attributed-snippet-text * {
color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}
#snippet-text:hover,
#snippet-text *:hover,
#attributed-snippet-text:hover,
#attributed-snippet-text *:hover,
ytd-watch-info-text *:hover {
color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
filter: none !important;
opacity: 1 !important;
}
/* Highlighted links */
.yt-core-attributed-string--highlight-text-decorator a.yt-core-attributed-string__link--call-to-action-color,
.yt-core-attributed-string--link-inherit-color .yt-core-attributed-string--highlight-text-decorator a.yt-core-attributed-string__link--call-to-action-color {
color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}
/* CTA links (hashtags, blue links) */
ytd-watch-metadata :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color,
#snippet-text :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color,
#attributed-snippet-text :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color {
color: var(--yt-spec-call-to-action, #3ea6ff) !important;
}
/* Reset saturation variables on watch page */
ytd-watch-metadata, .ytd-watch-metadata {
--yt-saturated-base-background: var(--ytc-base-background);
--yt-saturated-raised-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
--yt-saturated-additive-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
--yt-saturated-text-primary: var(--yt-spec-text-primary,var(--ytc-text-primary));
--yt-saturated-text-secondary: var(--yt-spec-text-secondary,var(--ytc-text-secondary));
--yt-saturated-overlay-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
--yt-spec-overlay-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
--yt-spec-static-overlay-background-light: var(--yt-spec-additive-background,var(--ytc-additive-background));
/* Playlist overrides here too */
--yt-active-playlist-panel-background-color: var(--yt-spec-additive-background);
--yt-lightsource-primary-title-color: var(--ytc-text-primary);
--yt-lightsource-secondary-title-color: var(--ytc-text-secondary);
--iron-icon-fill-color: var(--yt-lightsource-primary-title-color);
}
/* Highlighted text background cleanup */
.yt-core-attributed-string--highlight-text-decorator {
background-color: var(--yt-spec-static-overlay-background-light, ${d ? "rgba(255,255,255,0.102)" : "rgba(0,0,0,0.051)"}) !important;
border-radius: 8px !important;
padding-bottom: 1px !important;
}
`).trim();
};
// Precompute both CSS variants so we avoid rebuilding strings repeatedly
const CSS_CACHE = {
dark: buildCss(true),
light: buildCss(false)
};
/* ---------------------------------------------------------
Insert or update the style element
- Only replace when theme (dark/light) actually changes
--------------------------------------------------------- */
const applyStyle = isDark => {
if (!styleEl) {
styleEl = document.getElementById(ID);
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = ID;
// add as late as possible but safe: if head not ready, append to documentElement
const target = document.head || document.documentElement;
target.appendChild(styleEl);
}
}
// choose cached css
const newCss = isDark ? CSS_CACHE.dark : CSS_CACHE.light;
if (styleEl.textContent !== newCss) {
styleEl.textContent = newCss;
}
};
/* ---------------------------------------------------------
Playlist panel override: apply variables directly to panel(s)
- fast and localized; does not clobber global styles
--------------------------------------------------------- */
const updatePlaylistColors = () => {
document.querySelectorAll("ytd-playlist-panel-renderer").forEach(panel => {
try {
panel.style.setProperty("--yt-active-playlist-panel-background-color", "var(--yt-spec-additive-background)");
panel.style.setProperty("--yt-lightsource-primary-title-color", "var(--ytc-text-primary)");
panel.style.setProperty("--yt-lightsource-secondary-title-color", "var(--ytc-text-secondary)");
panel.style.setProperty("--iron-icon-fill-color", "var(--yt-lightsource-primary-title-color)");
} catch (e) {
// defensive: some nodes may be in closed shadow or removed quickly
}
});
};
const scheduleUpdate = () => {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
updatePlaylistColors();
});
};
const waitForPlaylistPanelAndObserve = () => {
const tryFind = () => {
const panel = document.querySelector("ytd-playlist-panel-renderer");
if (panel) {
// initial update
scheduleUpdate();
// If there's already an observer attached, disconnect first
if (panelObserver) {
try { panelObserver.disconnect(); } catch (e) {}
panelObserver = null;
}
// Observe only the playlist panel for child changes (efficient)
panelObserver = new MutationObserver(scheduleUpdate);
try {
panelObserver.observe(panel, { childList: true, subtree: true });
} catch (e) {
// if observe fails (rare), fall back to a single scheduled update
scheduleUpdate();
}
} else {
// not found yet — try again in next frame (cheap)
requestAnimationFrame(tryFind);
}
};
tryFind();
};
/* ---------------------------------------------------------
Refresh: apply styles if theme changed and ensure playlist observer
--------------------------------------------------------- */
const refresh = () => {
const isDark = detectDark();
if (isDark !== dark) {
dark = isDark;
applyStyle(dark);
}
// ensure playlist panel observation/updates are active
waitForPlaylistPanelAndObserve();
// also schedule an immediate update
scheduleUpdate();
};
/* ---------------------------------------------------------
Init: run at DOM ready / document.head present
--------------------------------------------------------- */
const init = () => {
dark = detectDark();
applyStyle(dark);
waitForPlaylistPanelAndObserve();
// SPA navigation / theme events - kept minimal to avoid duplicates
addEventListener("yt-navigate-finish", refresh, { passive: true });
addEventListener("yt-dark-mode-toggled", refresh, { passive: true });
// Cleanup when page is being unloaded / navigated away from
addEventListener("pagehide", () => {
if (panelObserver) {
try { panelObserver.disconnect(); } catch (e) {}
panelObserver = null;
}
}, { once: true });
};
if (document.documentElement && (document.head || document.readyState === "complete")) {
init();
} else {
addEventListener("DOMContentLoaded", init, { once: true });
}
})();