Skip to content

Instantly share code, notes, and snippets.

@franzalex
Last active March 10, 2026 11:34
Show Gist options
  • Select an option

  • Save franzalex/63af4ded297cc6538ae3b30a453a1baa to your computer and use it in GitHub Desktop.

Select an option

Save franzalex/63af4ded297cc6538ae3b30a453a1baa to your computer and use it in GitHub Desktop.
Dim Background Toggle (Night Mode) – Userscript
// ==UserScript==
// @name Dim Background Toggle (Night Mode)
// @namespace https://github.com/franzalex/
// @description Automatically dims backgrounds at night with an Always Dim mode. Restores original colors when disabled.
// @author franzalex
// @version 1.10.5
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @icon https://raw.githubusercontent.com/twitter/twemoji/master/assets/72x72/1f31a.png
// @updateURL https://gist.githubusercontent.com/franzalex/63af4ded297cc6538ae3b30a453a1baa/raw/franzalex.background-dimmer.user.js
// @downloadURL https://gist.githubusercontent.com/franzalex/63af4ded297cc6538ae3b30a453a1baa/raw/franzalex.background-dimmer.user.js
// ==/UserScript==
(function() {
'use strict';
let enabled = GM_getValue("enabled", true); // scheduled mode
let alwaysDark = GM_getValue("alwaysDark", false); // persistent override
let isCurrentlyDimmed = false; // track dimmed status of the page
let menuIdAlways, menuIdToggle;
const TOAST_MESSAGES = {
alwaysOnEnabled: "Always Dim ON<br>(Ctrl+Shift+Alt+D)",
alwaysOnDisabled: "Always Dim OFF<br>(Ctrl+Shift+Alt+D)",
autoDimEnabled: "Auto Dim ON<br>(Ctrl+Alt+D)",
autoDimDisabled: "Auto Dim OFF<br>(Ctrl+Alt+D)"
};
// storage for original colors
let originalBodyColor = null;
const originalElementColors = new Map(); // Map is iterable; WeakMap is not
// --- Time functions ---
function isNightTime() {
const now = new Date();
const hour = now.getHours();
return (hour >= 22 || hour < 6);
}
function scheduleNextAutoSwitch() {
const now = new Date();
const next = new Date(now);
// If current time is daytime (6 < hour < 22), schedule for 22:00 today
if (now.getHours() >= 6 && now.getHours() < 22) {
next.setHours(22, 0, 0, 0);
} else {
// Otherwise, schedule for 06:00 next day
next.setDate(next.getDate() + (now.getHours() >= 22 ? 1 : 0));
next.setHours(6, 0, 0, 0);
}
const delay = next.getTime() - now.getTime();
setTimeout(() => {
refreshState(); // refresh state
debounceAdjustBackground(); // run dim/restore logic
scheduleNextAutoSwitch(); // reschedule the next boundary
}, delay);
}
// --- RGB & HSV functions ---
function parseRgb(bgColor) {
const match = bgColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10)
};
}
return null;
}
function rgbToHsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const d = max - min;
let h, s, v = max;
if (d === 0) {
h = 0;
} else if (max === r) {
h = ((g - b) / d) % 6;
} else if (max === g) {
h = (b - r) / d + 2;
} else {
h = (r - g) / d + 4;
}
h = Math.round(h * 60);
if (h < 0) h += 360;
s = max === 0 ? 0 : d / max;
return { h: h, s: s * 100, v: v * 100 };
}
function hsvToRgb(h, s, v) {
s /= 100;
v /= 100;
const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
return `rgb(${r}, ${g}, ${b})`;
}
// --- element adjustment functions ---
function adjustElementBackgrounds() {
const elements = document.querySelectorAll("div, section, article, main, aside, header");
elements.forEach(el => {
const style = window.getComputedStyle(el);
const bgColor = style.backgroundColor;
// Skip transparent backgrounds
if (bgColor === "transparent" || (bgColor.startsWith("rgba") && bgColor.endsWith(", 0)"))) return;
// Avoid reprocessing already dimmed elements
if (el.dataset.dimmed === "true") return;
const rgb = parseRgb(bgColor);
if (rgb) {
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
if (hsv.v > 75) {
hsv.v = 75;
const newColor = hsvToRgb(hsv.h, hsv.s, hsv.v);
// Save original element color once
if (!originalElementColors.has(el)) {
originalElementColors.set(el, el.style.backgroundColor || bgColor);
}
el.style.backgroundColor = newColor;
el.dataset.dimmed = "true";
}
}
});
}
function adjustBackground() {
// break early if toggle is off and not in alwaysDark mode
if (!enabled && !alwaysDark) return;
if (isNightTime() || alwaysDark) {
const body = document.body;
const style = window.getComputedStyle(body);
const bgColor = style.backgroundColor;
const rgb = parseRgb(bgColor);
if (rgb) {
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
if (hsv.v > 75) {
hsv.v = 75;
const newColor = hsvToRgb(hsv.h, hsv.s, hsv.v);
// Save original body color once
if (originalBodyColor === null) {
originalBodyColor = body.style.backgroundColor || bgColor;
}
body.style.backgroundColor = newColor;
body.dataset.dimmed = "true";
adjustElementBackgrounds();
isCurrentlyDimmed = true; // dimming process completed
}
}
} else {
// daytime and not alwaysDark → restore if currently dimmed
if (isCurrentlyDimmed) {
restoreOriginalColors();
isCurrentlyDimmed = false; // no longer dimmed
}
}
// manage observer state
manageObserver();
}
function restoreOriginalColors() {
// Restore body
if (originalBodyColor !== null) {
document.body.style.backgroundColor = originalBodyColor;
originalBodyColor = null;
}
document.body.dataset.dimmed = "false";
// Restore elements
originalElementColors.forEach((color, el) => {
// Element may have been removed from DOM; guard access
if (el && el.style) {
el.style.backgroundColor = color;
el.dataset.dimmed = "false";
}
});
originalElementColors.clear();
}
// --- menu & interaction ---
function showToast(message) {
const toast = document.createElement("div");
toast.innerHTML = message;
toast.style.position = "fixed";
toast.style.bottom = "20px";
toast.style.left = "50%";
toast.style.transform = "translateX(-50%)";
toast.style.background = "rgba(40, 40, 40, 0.8)";
toast.style.color = "#fff";
toast.style.padding = "10px 20px";
toast.style.borderRadius = "5px";
toast.style.fontSize = "14px";
toast.style.zIndex = "9999";
toast.style.opacity = "0";
toast.style.transition = "opacity 0.5s ease";
toast.style.textAlign = "center";
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = "1";
});
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 500);
}, 2500);
}
function updateMenus() {
if (menuIdAlways) GM_unregisterMenuCommand(menuIdAlways);
if (menuIdToggle) GM_unregisterMenuCommand(menuIdToggle);
const labelAlways = alwaysDark
? "Always Dim: ON (click to disable)"
: "Always Dim: OFF (click to enable)";
menuIdAlways = GM_registerMenuCommand(labelAlways, () => {
alwaysDark = !alwaysDark;
GM_setValue("alwaysDark", alwaysDark);
refreshState();
if (alwaysDark) {
adjustBackground();
showToast(TOAST_MESSAGES.alwaysOnEnabled);
} else {
restoreOriginalColors();
showToast(TOAST_MESSAGES.alwaysOnDisabled);
}
updateMenus();
});
const labelToggle = enabled
? "Auto Dim: ON (click to disable)"
: "Auto Dim: OFF (click to enable)";
menuIdToggle = GM_registerMenuCommand(labelToggle, () => {
enabled = !enabled;
GM_setValue("enabled", enabled); // persist state
refreshState();
if (enabled) {
adjustBackground();
showToast(TOAST_MESSAGES.autoDimEnabled);
} else {
restoreOriginalColors();
showToast(TOAST_MESSAGES.autoDimDisabled);
}
updateMenus();
});
}
function refreshState() {
/* refreshes the state of the variables to match persistence;
* useful if state was changed while on a different tab, or the state was
* changed between the two scheduled checkpoints.
*/
enabled = GM_getValue("enabled", true);
alwaysDark = GM_getValue("alwaysDark", false);
}
// --- rate limiters ---
function debounceRateLimiter(fn, delay) {
// debounce: Wait until changes stop for a period, then run
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttleRateLimiter(fn, limit) {
// throttle: Run only once in the time limit specified
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
function hybridRateLimiter(fn, debounceDelay = 250, throttleLimit = 750) {
// --- Hybrid: two debounces then throttle ---
const debounced = debounceRateLimiter(fn, debounceDelay);
const throttled = throttleRateLimiter(fn, throttleLimit);
let debounceRuns = 0;
let useThrottle = false;
return function(...args) {
if (!useThrottle) {
debounced.apply(this, args);
debounceRuns++;
if (debounceRuns >= 2) {
useThrottle = true;
}
} else {
throttled.apply(this, args);
}
};
}
function manageObserver() {
if (!isNightTime() && !alwaysDark) {
// daytime: no need to observe DOM for changes
observer.disconnect();
} else {
// night or alwaysDark: ensure observer is active
observer.observe(document.body, {attributes: true, childList: true, subtree: true});
}
}
// debounced events: debounces duplicate triggers that happen in a short space of time
const debounceAdjustBackground = debounceRateLimiter(adjustBackground, 200);
// Observe DOM changes with hybrid approach
const rateLimitedObserver = hybridRateLimiter(adjustBackground, 250, 750);
const observer = new MutationObserver(() => {
rateLimitedObserver();
});
observer.observe(document.body, { attributes: true, childList: true, subtree: true });
// --- initial run ---
refreshState();
adjustBackground();
scheduleNextAutoSwitch();
updateMenus();
// --- event listeners ---
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.altKey && !e.shiftKey && e.key.toLowerCase() === "d") {
enabled = !enabled;
GM_setValue("enabled", enabled);
refreshState();
if (enabled) {
adjustBackground();
showToast(TOAST_MESSAGES.autoDimEnabled);
} else {
restoreOriginalColors();
showToast(TOAST_MESSAGES.autoDimDisabled);
}
}
});
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.altKey && e.shiftKey && e.key.toLowerCase() === "d") {
alwaysDark = !alwaysDark;
GM_setValue("alwaysDark", alwaysDark);
refreshState();
if (alwaysDark) {
adjustBackground();
showToast(TOAST_MESSAGES.alwaysOnEnabled);
} else {
restoreOriginalColors();
showToast(TOAST_MESSAGES.alwaysOnDisabled);
}
updateMenus();
}
});
document.addEventListener("visibilitychange", () => {
// rerun auto-dark when tab becomes active again
if (!document.hidden) {
refreshState();
debounceAdjustBackground();
}
});
window.addEventListener("focus", () => {
// rerun auto-dark when window regains focus (after minimize/restore)
refreshState();
debounceAdjustBackground();
});
})();
@franzalex
Copy link
Author

franzalex commented Mar 10, 2026

Dim Background Toggle (Night Mode) – Userscript

Install Userscript View Source

A lightweight userscript that dims bright backgrounds at night or on demand.

Features include:

  • Auto Dim scheduling (22:00–06:00)
  • Always Dim mode for persistent override
  • Color restoration when dimming is disabled
  • Keyboard shortcuts:
    • Ctrl+Alt+D → Toggle Auto Dim
    • Ctrl+Shift+Alt+D → Toggle Always Dim

📖 Usage

  • By default, the script automatically dims backgrounds during night hours (22:00–06:00).
  • Use the menu commands in Tampermonkey/Greasemonkey to toggle Auto Dim or Always Dim mode.
  • Keyboard shortcuts provide quick control:
    • Ctrl+Alt+D → Enable/disable Auto Dim scheduling
    • Ctrl+Shift+Alt+D → Enable/disable Always Dim mode
  • When dimming is disabled, the script restores the original background colors seamlessly.

📝 Changelog

  • v1.10.5

    • Added @updateURL and @downloadURL metadata for seamless auto‑updates via Tampermonkey
  • v1.10.4 – First public release

    • Initial publication of the userscript on GitHub Gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment