NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name YouTube & Twitch Video Dimmer (with Cycling Slider)
// @namespace https://github.com/E-VanDeWoestyne
// @version 1.2
// @description Adjust video brightness on YouTube and Twitch with a slider (Shift+S to open, rebindable), or cycle through it in steps with a hotkey (Shift+D by default, rebindable, capped at 100%). Double-click a slider to reset it to 100%. An Advanced panel adds an optional contrast slider, a configurable cycle step size, custom keybinds, and an Extra brightness toggle for boosting up to 200%.
// @author E-VanDeWoestyne
// @match https://www.youtube.com/*
// @match https://www.twitch.tv/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const STORAGE_KEYS = {
brightness: "yt-video-dimmer-brightness",
contrast: "yt-video-dimmer-contrast",
interval: "yt-video-dimmer-interval",
keybind: "yt-video-dimmer-cycle-keybind",
showKeybind: "yt-video-dimmer-show-keybind",
extraBrightness: "yt-video-dimmer-extra-brightness",
};
const MIN_BRIGHTNESS_PCT = 20;
const DEFAULT_PCT = 100; // used by double-click reset, and as the cycle hotkey's cap
const CYCLE_MAX_PCT = DEFAULT_PCT;
const CONTRAST_MIN_PCT = 50;
const CONTRAST_MAX_PCT = 150;
const INTERVAL_MIN_PCT = 5;
const INTERVAL_MAX_PCT = 70;
let currentBrightness = 1.0; // 0.2 - 1.0 (up to 2.0 if Extra brightness is enabled)
let currentContrast = 1.0; // 0.5 - 1.5
let cycleInterval = 20; // percent, used by the cycle hotkey
let extraBrightnessEnabled = false; // when true, slider range extends to 200%
function getMaxBrightnessPct() {
return extraBrightnessEnabled ? 200 : CYCLE_MAX_PCT;
}
let cycleKeybind = {
key: "d",
shiftKey: true,
ctrlKey: false,
altKey: false,
metaKey: false,
};
let showKeybind = {
key: "s",
shiftKey: true,
ctrlKey: false,
altKey: false,
metaKey: false,
};
let capturingKeybindTarget = null; // null | 'cycle' | 'show'
// ---------- Styles ----------
const style = document.createElement("style");
style.id = "yt-video-dimmer-styles";
style.appendChild(
document.createTextNode(`
video.html5-main-video,
[data-a-target="video-player"] video,
[data-a-target="player-overlay-click-handler"] video,
.video-player__container video {
filter: brightness(var(--yt-dimmer-b, 1.0)) contrast(var(--yt-dimmer-c, 1.0)) !important;
transition: filter 0.15s ease-in-out;
}
#yt-dimmer-slider-panel {
position: absolute;
top: 60px;
right: 20px;
background: rgba(28, 28, 28, 0.92);
color: #f1f1f1;
padding: 8px 12px;
border-radius: 6px;
font-family: Roboto, Arial, sans-serif;
font-size: 12px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 6px;
width: 220px;
box-sizing: border-box;
overflow: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
border: 1px solid rgba(255,255,255,0.1);
}
#yt-dimmer-slider-panel.visible {
opacity: 1;
pointer-events: auto;
}
.yt-dimmer-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
.yt-dimmer-row-label {
flex: 0 0 60px;
color: #ccc;
}
.yt-dimmer-row input[type="range"] {
flex: 1 1 auto;
min-width: 0;
accent-color: #3ea6ff;
cursor: pointer;
}
.yt-dimmer-row input[type="number"] {
width: 50px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 3px;
color: #f1f1f1;
padding: 2px 4px;
font-size: 12px;
box-sizing: border-box;
}
.yt-dimmer-slider-value {
flex: 0 0 32px;
text-align: right;
font-weight: bold;
}
.yt-dimmer-row input[type="checkbox"] {
flex: 0 0 auto;
accent-color: #3ea6ff;
cursor: pointer;
}
.yt-dimmer-hint {
flex: 1;
color: #999;
font-size: 10.5px;
line-height: 1.3;
}
#yt-dimmer-advanced-toggle {
background: none;
border: none;
color: #9ab8d6;
font-size: 11px;
cursor: pointer;
padding: 2px 0;
text-align: left;
opacity: 0.85;
}
#yt-dimmer-advanced-toggle:hover {
opacity: 1;
}
#yt-dimmer-advanced-content {
display: none;
flex-direction: column;
gap: 6px;
padding-top: 2px;
}
#yt-dimmer-advanced-content.expanded {
display: flex;
}
#yt-dimmer-keybind-display, .yt-dimmer-keybind-display {
flex: 1;
font-family: monospace;
background: rgba(255,255,255,0.08);
border-radius: 3px;
padding: 2px 6px;
}
#yt-dimmer-keybind-change, .yt-dimmer-keybind-change {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: #f1f1f1;
border-radius: 3px;
font-size: 11px;
padding: 3px 8px;
cursor: pointer;
}
#yt-dimmer-keybind-change:hover, .yt-dimmer-keybind-change:hover {
background: rgba(255,255,255,0.18);
}
`),
);
const injectStyles = () => {
if (document.head) {
document.head.appendChild(style);
} else {
const observer = new MutationObserver(() => {
if (document.head) {
document.head.appendChild(style);
observer.disconnect();
}
});
observer.observe(document.documentElement, { childList: true });
}
};
injectStyles();
function saveToStorage(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
/* ignore storage errors */
}
}
function getPlayer() {
// YouTube's player container
const ytPlayer = document.querySelector("#movie_player");
if (ytPlayer) return ytPlayer;
// Twitch's player container (class names are hashed/unstable, so we
// anchor off the video element itself and walk up to a positioned ancestor)
const twitchVideo = document.querySelector(
'[data-a-target="video-player"] video, [data-a-target="player-overlay-click-handler"] video, .video-player__container video',
);
if (twitchVideo) {
const container = twitchVideo.closest(
'[data-a-target="player-overlay-click-handler"], .video-player__container, [data-a-target="video-player"]',
);
if (container) return container;
}
return document.body;
}
// ---------- Apply brightness / contrast ----------
function applyBrightness(brightness, save = true) {
currentBrightness = brightness;
document.documentElement.style.setProperty(
"--yt-dimmer-b",
brightness.toFixed(2),
);
if (save) {
saveToStorage(STORAGE_KEYS.brightness, brightness.toFixed(2));
}
if (sliderInput) {
sliderInput.value = String(Math.round(brightness * 100));
sliderValueLabel.textContent = `${sliderInput.value}%`;
}
}
function applyContrast(contrast, save = true) {
currentContrast = contrast;
document.documentElement.style.setProperty(
"--yt-dimmer-c",
contrast.toFixed(2),
);
if (save) {
saveToStorage(STORAGE_KEYS.contrast, contrast.toFixed(2));
}
if (contrastInput) {
contrastInput.value = String(Math.round(contrast * 100));
contrastValueLabel.textContent = `${contrastInput.value}%`;
}
}
// Steps brightness down by cycleInterval%, wrapping back to 100% once it
// would drop below the minimum.
function cycleBrightness() {
let pct = Math.round(currentBrightness * 100);
if (pct > CYCLE_MAX_PCT) {
pct = CYCLE_MAX_PCT; // snap down to the cap first, even with Extra brightness on
} else {
pct -= cycleInterval;
if (pct < MIN_BRIGHTNESS_PCT) pct = CYCLE_MAX_PCT;
}
applyBrightness(pct / 100);
showSliderPanel();
}
// Wires up double-click-to-reset, live dragging, and save-on-release for a
// percentage range input. applyFn must accept (fraction, save = true).
function setupRangeControl(input, valueLabel, applyFn, storageKey) {
input.addEventListener("dblclick", () => {
applyFn(DEFAULT_PCT / 100);
resetSliderHideTimer();
});
input.addEventListener("input", () => {
const pct = parseInt(input.value, 10);
valueLabel.textContent = `${pct}%`;
applyFn(pct / 100, false);
resetSliderHideTimer();
});
input.addEventListener("change", () => {
saveToStorage(storageKey, (parseInt(input.value, 10) / 100).toFixed(2));
});
}
// ---------- Keybind helpers ----------
function formatKeybind(binding) {
const parts = [];
if (binding.ctrlKey) parts.push("Ctrl");
if (binding.altKey) parts.push("Alt");
if (binding.shiftKey) parts.push("Shift");
if (binding.metaKey) parts.push("Meta");
let keyLabel = binding.key;
if (keyLabel.length === 1) keyLabel = keyLabel.toUpperCase();
parts.push(keyLabel);
return parts.join("+");
}
function matchesKeybind(e, binding) {
if (!binding) return false;
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
return (
key === binding.key &&
e.shiftKey === !!binding.shiftKey &&
e.ctrlKey === !!binding.ctrlKey &&
e.altKey === !!binding.altKey &&
e.metaKey === !!binding.metaKey
);
}
function updateKeybindUI() {
if (keybindDisplay)
keybindDisplay.textContent = formatKeybind(cycleKeybind);
if (keybindChangeBtn) keybindChangeBtn.textContent = "Change";
if (showKeybindDisplay)
showKeybindDisplay.textContent = formatKeybind(showKeybind);
if (showKeybindChangeBtn) showKeybindChangeBtn.textContent = "Change";
resetSliderHideTimer();
}
// ---------- Slider panel ----------
let sliderPanel, sliderInput, sliderValueLabel, sliderHideTimeout;
let contrastInput, contrastValueLabel;
let intervalInput;
let keybindDisplay, keybindChangeBtn;
let showKeybindDisplay, showKeybindChangeBtn;
let advancedContent;
function buildSliderPanel() {
const player = getPlayer();
sliderPanel = document.createElement("div");
sliderPanel.id = "yt-dimmer-slider-panel";
// --- Brightness row ---
const brightnessRow = document.createElement("div");
brightnessRow.className = "yt-dimmer-row";
sliderInput = document.createElement("input");
sliderInput.type = "range";
sliderInput.min = String(MIN_BRIGHTNESS_PCT);
sliderInput.max = String(getMaxBrightnessPct());
sliderInput.step = "1";
sliderInput.value = String(Math.round(currentBrightness * 100));
sliderValueLabel = document.createElement("span");
sliderValueLabel.className = "yt-dimmer-slider-value";
sliderValueLabel.textContent = `${sliderInput.value}%`;
setupRangeControl(
sliderInput,
sliderValueLabel,
applyBrightness,
STORAGE_KEYS.brightness,
);
brightnessRow.append(sliderInput, sliderValueLabel);
// --- Advanced toggle ---
const advancedToggle = document.createElement("button");
advancedToggle.type = "button";
advancedToggle.id = "yt-dimmer-advanced-toggle";
advancedToggle.textContent = "Advanced \u25be";
advancedContent = document.createElement("div");
advancedContent.id = "yt-dimmer-advanced-content";
advancedToggle.addEventListener("click", () => {
const expanded = advancedContent.classList.toggle("expanded");
advancedToggle.textContent = expanded
? "Advanced \u25b4"
: "Advanced \u25be";
resetSliderHideTimer();
});
// --- Contrast row ---
const contrastRow = document.createElement("div");
contrastRow.className = "yt-dimmer-row";
const contrastLabel = document.createElement("span");
contrastLabel.className = "yt-dimmer-row-label";
contrastLabel.textContent = "Contrast";
contrastInput = document.createElement("input");
contrastInput.type = "range";
contrastInput.min = String(CONTRAST_MIN_PCT);
contrastInput.max = String(CONTRAST_MAX_PCT);
contrastInput.step = "1";
contrastInput.value = String(Math.round(currentContrast * 100));
contrastValueLabel = document.createElement("span");
contrastValueLabel.className = "yt-dimmer-slider-value";
contrastValueLabel.textContent = `${contrastInput.value}%`;
setupRangeControl(
contrastInput,
contrastValueLabel,
applyContrast,
STORAGE_KEYS.contrast,
);
contrastRow.append(contrastLabel, contrastInput, contrastValueLabel);
// --- Cycle step row ---
const intervalRow = document.createElement("div");
intervalRow.className = "yt-dimmer-row";
const intervalLabel = document.createElement("span");
intervalLabel.className = "yt-dimmer-row-label";
intervalLabel.textContent = "Cycle step";
intervalInput = document.createElement("input");
intervalInput.type = "number";
intervalInput.min = String(INTERVAL_MIN_PCT);
intervalInput.max = String(INTERVAL_MAX_PCT);
intervalInput.step = "5";
intervalInput.value = String(cycleInterval);
const intervalUnit = document.createElement("span");
intervalUnit.className = "yt-dimmer-slider-value";
intervalUnit.textContent = "%";
intervalInput.addEventListener("change", () => {
let val = parseInt(intervalInput.value, 10);
if (isNaN(val) || val < INTERVAL_MIN_PCT) val = INTERVAL_MIN_PCT;
if (val > INTERVAL_MAX_PCT) val = INTERVAL_MAX_PCT;
intervalInput.value = String(val);
cycleInterval = val;
saveToStorage(STORAGE_KEYS.interval, String(val));
resetSliderHideTimer();
});
intervalRow.append(intervalLabel, intervalInput, intervalUnit);
// --- Keybind row ---
const keybindRow = document.createElement("div");
keybindRow.className = "yt-dimmer-row";
const keybindLabel = document.createElement("span");
keybindLabel.className = "yt-dimmer-row-label";
keybindLabel.textContent = "Cycle key";
keybindDisplay = document.createElement("span");
keybindDisplay.id = "yt-dimmer-keybind-display";
keybindDisplay.textContent = formatKeybind(cycleKeybind);
keybindChangeBtn = document.createElement("button");
keybindChangeBtn.type = "button";
keybindChangeBtn.id = "yt-dimmer-keybind-change";
keybindChangeBtn.textContent = "Change";
keybindChangeBtn.addEventListener("click", () => {
capturingKeybindTarget = "cycle";
keybindChangeBtn.textContent = "Press a key\u2026";
keybindDisplay.textContent = "\u2026";
if (sliderHideTimeout) clearTimeout(sliderHideTimeout);
});
keybindRow.append(keybindLabel, keybindDisplay, keybindChangeBtn);
// --- Show key row ---
const showKeybindRow = document.createElement("div");
showKeybindRow.className = "yt-dimmer-row";
const showKeybindLabel = document.createElement("span");
showKeybindLabel.className = "yt-dimmer-row-label";
showKeybindLabel.textContent = "Show key";
showKeybindDisplay = document.createElement("span");
showKeybindDisplay.id = "yt-dimmer-show-keybind-display";
showKeybindDisplay.className = "yt-dimmer-keybind-display";
showKeybindDisplay.textContent = formatKeybind(showKeybind);
showKeybindChangeBtn = document.createElement("button");
showKeybindChangeBtn.type = "button";
showKeybindChangeBtn.className = "yt-dimmer-keybind-change";
showKeybindChangeBtn.textContent = "Change";
showKeybindChangeBtn.addEventListener("click", () => {
capturingKeybindTarget = "show";
showKeybindChangeBtn.textContent = "Press a key\u2026";
showKeybindDisplay.textContent = "\u2026";
if (sliderHideTimeout) clearTimeout(sliderHideTimeout);
});
showKeybindRow.append(
showKeybindLabel,
showKeybindDisplay,
showKeybindChangeBtn,
);
// --- Extra brightness toggle row ---
const extraBrightnessRow = document.createElement("div");
extraBrightnessRow.className = "yt-dimmer-row";
const extraBrightnessLabel = document.createElement("span");
extraBrightnessLabel.className = "yt-dimmer-row-label";
extraBrightnessLabel.textContent = "Extra bright";
const extraBrightnessToggle = document.createElement("input");
extraBrightnessToggle.type = "checkbox";
extraBrightnessToggle.id = "yt-dimmer-extra-brightness-toggle";
extraBrightnessToggle.checked = extraBrightnessEnabled;
const extraBrightnessHint = document.createElement("span");
extraBrightnessHint.className = "yt-dimmer-hint";
extraBrightnessHint.textContent = "up to 200%";
extraBrightnessToggle.addEventListener("change", () => {
extraBrightnessEnabled = extraBrightnessToggle.checked;
try {
localStorage.setItem(
STORAGE_KEYS.extraBrightness,
extraBrightnessEnabled ? "1" : "0",
);
} catch (e) {
/* ignore */
}
if (sliderInput) {
sliderInput.max = String(getMaxBrightnessPct());
if (!extraBrightnessEnabled && currentBrightness > 1.0) {
applyBrightness(1.0);
}
}
resetSliderHideTimer();
});
extraBrightnessRow.append(
extraBrightnessLabel,
extraBrightnessToggle,
extraBrightnessHint,
);
advancedContent.append(
contrastRow,
intervalRow,
keybindRow,
showKeybindRow,
extraBrightnessRow,
);
sliderPanel.append(brightnessRow, advancedToggle, advancedContent);
player.appendChild(sliderPanel);
}
function resetSliderHideTimer() {
if (sliderHideTimeout) clearTimeout(sliderHideTimeout);
if (capturingKeybindTarget) return; // don't hide mid key-capture
if (advancedContent && advancedContent.classList.contains("expanded"))
return; // keep open while tweaking advanced settings
sliderHideTimeout = setTimeout(hideSliderPanel, 4000);
}
function showSliderPanel() {
if (!sliderPanel) buildSliderPanel();
const player = getPlayer();
if (sliderPanel.parentElement !== player) player.appendChild(sliderPanel);
sliderPanel.classList.add("visible");
resetSliderHideTimer();
}
function hideSliderPanel() {
if (sliderPanel) sliderPanel.classList.remove("visible");
}
function toggleSliderPanel() {
if (sliderPanel && sliderPanel.classList.contains("visible")) {
hideSliderPanel();
} else {
showSliderPanel();
}
}
// ---------- Restore saved settings on load ----------
(function restoreSaved() {
try {
const savedExtraBrightness = localStorage.getItem(
STORAGE_KEYS.extraBrightness,
);
if (savedExtraBrightness !== null) {
extraBrightnessEnabled = savedExtraBrightness === "1";
}
const savedBrightness = localStorage.getItem(STORAGE_KEYS.brightness);
if (savedBrightness !== null) {
const val = parseFloat(savedBrightness);
if (!isNaN(val)) {
const maxAllowed = getMaxBrightnessPct() / 100;
const clamped = Math.min(
maxAllowed,
Math.max(MIN_BRIGHTNESS_PCT / 100, val),
);
currentBrightness = clamped;
document.documentElement.style.setProperty(
"--yt-dimmer-b",
clamped.toFixed(2),
);
}
}
const savedContrast = localStorage.getItem(STORAGE_KEYS.contrast);
if (savedContrast !== null) {
const val = parseFloat(savedContrast);
if (!isNaN(val) && val >= 0.5 && val <= 1.5) {
currentContrast = val;
document.documentElement.style.setProperty(
"--yt-dimmer-c",
val.toFixed(2),
);
}
}
const savedInterval = localStorage.getItem(STORAGE_KEYS.interval);
if (savedInterval !== null) {
const val = parseInt(savedInterval, 10);
if (!isNaN(val) && val >= 5 && val <= 70) cycleInterval = val;
}
const savedKeybind = localStorage.getItem(STORAGE_KEYS.keybind);
if (savedKeybind !== null) {
const parsed = JSON.parse(savedKeybind);
if (parsed && typeof parsed.key === "string") {
cycleKeybind = {
key: parsed.key,
shiftKey: !!parsed.shiftKey,
ctrlKey: !!parsed.ctrlKey,
altKey: !!parsed.altKey,
metaKey: !!parsed.metaKey,
};
}
}
const savedShowKeybind = localStorage.getItem(STORAGE_KEYS.showKeybind);
if (savedShowKeybind !== null) {
const parsed = JSON.parse(savedShowKeybind);
if (parsed && typeof parsed.key === "string") {
showKeybind = {
key: parsed.key,
shiftKey: !!parsed.shiftKey,
ctrlKey: !!parsed.ctrlKey,
altKey: !!parsed.altKey,
metaKey: !!parsed.metaKey,
};
}
}
} catch (e) {
/* ignore */
}
})();
// ---------- Hotkeys ----------
window.addEventListener(
"keydown",
(e) => {
// Capturing a new keybind after the user clicked "Change" on either row
if (capturingKeybindTarget) {
if (["Shift", "Control", "Alt", "Meta"].includes(e.key)) return; // wait for a real key
e.preventDefault();
e.stopPropagation();
const binding = {
key: e.key.length === 1 ? e.key.toLowerCase() : e.key,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
metaKey: e.metaKey,
};
if (capturingKeybindTarget === "cycle") {
cycleKeybind = binding;
try {
localStorage.setItem(STORAGE_KEYS.keybind, JSON.stringify(binding));
} catch (err) {
/* ignore */
}
} else if (capturingKeybindTarget === "show") {
showKeybind = binding;
try {
localStorage.setItem(
STORAGE_KEYS.showKeybind,
JSON.stringify(binding),
);
} catch (err) {
/* ignore */
}
}
capturingKeybindTarget = null;
updateKeybindUI();
return;
}
const active = document.activeElement;
if (
active &&
(active.tagName === "INPUT" ||
active.tagName === "TEXTAREA" ||
active.isContentEditable)
) {
return;
}
if (matchesKeybind(e, cycleKeybind)) {
e.preventDefault();
e.stopPropagation();
cycleBrightness();
return;
}
if (matchesKeybind(e, showKeybind)) {
e.preventDefault();
e.stopPropagation();
toggleSliderPanel();
}
},
true,
);
})();