Fix sites w/white scroll bars on a light theme, vice versa!
// ==UserScript==
// @name Scrollbar Customizer
// @namespace https://greasyfork.org/en/users/922168-mark-zinzow
// @version 11.8
// @description Fix sites w/white scroll bars on a light theme, vice versa!
// @author Mark Zinzow
// @match *://*/*
// @exclude *://chromewebstore.google.com/*
// @exclude chrome://*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-start
// @license MIT
// ==/UserScript==
/* jshint esversion: 9 */
/* eslint-disable no-multi-spaces */
// Let's put the version number in our dialog title next time.
(function() {
'use strict';
// ====================================================================
// 1. GLOBAL SETTINGS & SITE CONFIG
// ====================================================================
const DEFAULT_CONFIG = {
enabled: false,
matchFavicon: false,
width: 18, // Also update 'w: 18' in presets and Panic Buttons if changing this!
outlineWidth: 2,
outlineColor: '#00FF00',
thumbColor: 'transparent',
trackColor: 'transparent'
};
const currentDomain = window.location.hostname;
let storage = GM_getValue('scrollbarConfig_v11', {});
let siteConfig = storage[currentDomain] || { ...DEFAULT_CONFIG };
if (typeof siteConfig.matchFavicon === 'undefined') siteConfig.matchFavicon = false;
let globalTheme = GM_getValue('globalUITheme', 'dark');
// Tracks the currently applied settings (handles live UI previews before saving)
window._usbActiveConfig = { ...siteConfig };
// ====================================================================
// 2. CSS ENGINE (Scoped)
// ====================================================================
const cssSkeleton = `
:root {
--usb-width: 16px;
--usb-outline-width: 2px;
--usb-outline-color: #00FF00;
--usb-thumb-color: transparent;
--usb-track-color: transparent;
}
/* Target the HTML root explicitly as well as all children */
html[data-usb-enabled="true"],
html[data-usb-enabled="true"] * {
scrollbar-width: auto !important;
scrollbar-color: var(--usb-outline-color) var(--usb-track-color) !important;
}
html[data-usb-enabled="true"]::-webkit-scrollbar,
html[data-usb-enabled="true"] *::-webkit-scrollbar {
width: var(--usb-width) !important;
height: var(--usb-width) !important;
background: var(--usb-track-color) !important;
}
html[data-usb-enabled="true"]::-webkit-scrollbar-track,
html[data-usb-enabled="true"] *::-webkit-scrollbar-track {
background: var(--usb-track-color) !important;
}
html[data-usb-enabled="true"]::-webkit-scrollbar-thumb,
html[data-usb-enabled="true"] *::-webkit-scrollbar-thumb {
background-color: var(--usb-thumb-color) !important;
background-clip: padding-box !important;
border: var(--usb-outline-width) solid var(--usb-outline-color) !important;
border-radius: 0px !important;
}
html[data-usb-enabled="true"]::-webkit-scrollbar-thumb:hover,
html[data-usb-enabled="true"] *::-webkit-scrollbar-thumb:hover {
background-color: var(--usb-outline-color) !important;
border-color: var(--usb-outline-color) !important;
}
html[data-usb-enabled="true"]::-webkit-scrollbar-corner,
html[data-usb-enabled="true"] *::-webkit-scrollbar-corner {
background: var(--usb-track-color) !important;
}
`;
GM_addStyle(cssSkeleton);
// --- FAVICON LOGIC ---
let faviconTimeout;
function updateFavicon(color, enable) {
let link = document.querySelector("link[rel~='icon']");
if (!enable) {
if (window._originalFaviconHref && link) {
link.href = window._originalFaviconHref;
}
return;
}
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
if (!window._originalFaviconHref) {
window._originalFaviconHref = link.href || '/favicon.ico';
}
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
const drawSolidFallback = () => {
ctx.fillStyle = color;
ctx.fillRect(0, 0, 32, 32);
link.href = canvas.toDataURL('image/png');
};
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
ctx.fillStyle = color;
ctx.fillRect(0, 0, 32, 32);
try {
ctx.drawImage(img, 2, 2, 28, 28);
link.href = canvas.toDataURL('image/png');
} catch (e) {
drawSolidFallback();
}
};
img.onerror = drawSolidFallback;
img.src = window._originalFaviconHref;
}
function debouncedFaviconUpdate(color, enable) {
clearTimeout(faviconTimeout);
faviconTimeout = setTimeout(() => {
updateFavicon(color, enable);
}, 150);
}
// --- MUTATION OBSERVER ---
const headObserver = new MutationObserver((mutations) => {
if (!window._usbActiveConfig.enabled || !window._usbActiveConfig.matchFavicon) return;
let shouldUpdate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.tagName === 'LINK' && (node.rel.includes('icon'))) {
if (!node.href.startsWith('data:image')) {
window._originalFaviconHref = node.href;
shouldUpdate = true;
}
}
});
} else if (mutation.type === 'attributes' && mutation.target.tagName === 'LINK' && mutation.target.rel.includes('icon')) {
if (mutation.attributeName === 'href' && !mutation.target.href.startsWith('data:image')) {
window._originalFaviconHref = mutation.target.href;
shouldUpdate = true;
}
}
}
if (shouldUpdate) {
debouncedFaviconUpdate(window._usbActiveConfig.outlineColor, true);
}
});
function initObserver() {
if (document.head) {
headObserver.observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
} else {
document.addEventListener('DOMContentLoaded', () => {
headObserver.observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
});
}
}
initObserver();
function updatePageVariables(config) {
window._usbActiveConfig = config;
const root = document.documentElement;
if (config.enabled) {
root.setAttribute('data-usb-enabled', 'true');
} else {
root.removeAttribute('data-usb-enabled');
debouncedFaviconUpdate(null, false);
return;
}
root.style.setProperty('--usb-width', config.width + 'px');
root.style.setProperty('--usb-outline-width', config.outlineWidth + 'px');
root.style.setProperty('--usb-outline-color', config.outlineColor);
root.style.setProperty('--usb-thumb-color', config.thumbColor);
root.style.setProperty('--usb-track-color', config.trackColor);
if (config.matchFavicon) {
debouncedFaviconUpdate(config.outlineColor, true);
} else {
debouncedFaviconUpdate(null, false);
}
}
updatePageVariables(siteConfig);
window.addEventListener('DOMContentLoaded', () => {
if (siteConfig.enabled && siteConfig.matchFavicon) {
updatePageVariables(siteConfig);
}
});
function saveSiteConfig(newConfig) {
siteConfig = newConfig;
storage[currentDomain] = siteConfig;
GM_setValue('scrollbarConfig_v11', storage);
updatePageVariables(siteConfig);
}
// ====================================================================
// 3. MENU "PANIC BUTTONS"
// ====================================================================
GM_registerMenuCommand("⚡ Force: Black on White", () => {
saveSiteConfig({
enabled: true, matchFavicon: siteConfig.matchFavicon,
width: 18, outlineWidth: 0, outlineColor: '#000000', thumbColor: '#000000', trackColor: '#FFFFFF'
});
});
GM_registerMenuCommand("⚡ Force: White on Black", () => {
saveSiteConfig({
enabled: true, matchFavicon: siteConfig.matchFavicon,
width: 18, outlineWidth: 0, outlineColor: '#FFFFFF', thumbColor: '#FFFFFF', trackColor: '#000000'
});
});
GM_registerMenuCommand("⚙️ Customize Scrollbars (UI)", createUI);
// ====================================================================
// 4. UI BUILDER
// ====================================================================
function createEl(tag, styles = {}, parent = null) {
const el = document.createElement(tag);
for (const [key, value] of Object.entries(styles)) {
el.style[key] = value;
}
if (parent) parent.appendChild(el);
return el;
}
const PALETTE_COLORS = ['#000000', '#FFFFFF', '#808080', '#C0C0C0', '#FF0000', '#FF8800', '#00FF00', '#00FFFF', '#0000FF', '#FF00FF'];
function createControlGroup(labelText, value, parent, isColor = false, isTransparentSupported = false) {
const wrapper = createEl('div', { marginBottom: '12px', paddingBottom: '5px', borderBottom: '1px solid var(--border)' }, parent);
const header = createEl('div', { display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }, wrapper);
createEl('span', { fontSize: '12px', color: 'var(--text-dim)' }, header).textContent = labelText;
let input, chkTransparent;
if (isColor) {
const controls = createEl('div', { display: 'flex', gap: '8px', alignItems: 'center' }, header);
input = createEl('input', { width: '40px', height: '25px', border: '1px solid #777', cursor: 'pointer', padding: '0', background: 'none' }, controls);
input.type = 'color';
input.title = "Open System Picker";
input.value = (value === 'transparent') ? '#000000' : value;
if (isTransparentSupported) {
const lbl = createEl('label', { fontSize: '10px', display: 'flex', alignItems: 'center', gap: '3px', color: 'var(--text)' }, controls);
chkTransparent = createEl('input', {}, lbl);
chkTransparent.type = 'checkbox';
chkTransparent.checked = (value === 'transparent');
createEl('span', {}, lbl).textContent = 'None';
const toggle = () => {
input.disabled = chkTransparent.checked;
input.style.opacity = chkTransparent.checked ? '0.3' : '1';
};
chkTransparent.addEventListener('change', toggle);
toggle();
}
const palContainer = createEl('div', { display: 'flex', gap: '4px', marginBottom: '8px', flexWrap: 'wrap' }, wrapper);
PALETTE_COLORS.forEach(color => {
const swatch = createEl('div', { width: '18px', height: '18px', background: color, border: '1px solid #555', cursor: 'pointer' }, palContainer);
swatch.onclick = () => {
input.value = color;
if (chkTransparent) { chkTransparent.checked = false; input.disabled = false; input.style.opacity = '1'; }
input.dispatchEvent(new Event('input', { bubbles: true }));
};
});
} else {
input = createEl('input', { width: '50px', background: 'var(--input-bg)', color: 'var(--text)', border: '1px solid #777', padding: '2px' }, header);
input.type = 'number';
input.value = value;
}
return { input, chkTransparent };
}
let uiContainer = null;
function createUI() {
if (uiContainer) { uiContainer.remove(); uiContainer = null; return; }
const host = createEl('div', { position: 'fixed', top: '10px', right: '10px', zIndex: '2147483647', fontFamily: 'sans-serif' });
document.body.appendChild(host);
uiContainer = host;
const shadow = host.attachShadow({ mode: 'open' });
const themeStyles = globalTheme === 'dark'
? { bg: '#1a1a1a', text: '#ffffff', textDim: '#cccccc', inputBg: '#333', border: '#444' }
: { bg: '#f4f4f4', text: '#000000', textDim: '#333333', inputBg: '#ffffff', border: '#cccccc' };
const panel = createEl('div', {
background: themeStyles.bg, color: themeStyles.text,
padding: '15px', border: `2px solid ${themeStyles.border}`, borderRadius: '8px', width: '310px',
boxShadow: '0 10px 40px rgba(0,0,0,0.5)',
'--text': themeStyles.text, '--text-dim': themeStyles.textDim, '--input-bg': themeStyles.inputBg, '--border': themeStyles.border
}, shadow);
const headRow = createEl('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px', borderBottom: `1px solid ${themeStyles.border}`, paddingBottom: '5px' }, panel);
createEl('h3', { margin: '0', fontSize: '14px', textTransform: 'uppercase', color: 'var(--text-dim)' }, headRow).textContent = 'Scrollbar Config';
const topBtns = createEl('div', { display: 'flex', gap: '8px' }, headRow);
const btnTheme = createEl('button', { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px' }, topBtns);
btnTheme.textContent = globalTheme === 'dark' ? '☀️' : '🌙';
btnTheme.title = "Switch Panel Theme";
btnTheme.onclick = () => {
globalTheme = globalTheme === 'dark' ? 'light' : 'dark';
GM_setValue('globalUITheme', globalTheme);
host.remove(); uiContainer = null; createUI();
};
const btnClose = createEl('button', { background: 'none', border: 'none', cursor: 'pointer', fontSize: '18px', color: 'var(--text-dim)', fontWeight: 'bold' }, topBtns);
btnClose.textContent = '✕';
btnClose.title = "Close (Keeps changes on current tab, but does not save)";
btnClose.onclick = () => {
host.remove(); uiContainer = null;
};
const presetGrid = createEl('div', { display: 'flex', gap: '4px', marginBottom: '15px', height: '35px' }, panel);
const presets = [
{ name: 'Outline', w: 18, out: 2, outC: '#00FF00', thC: 'transparent', trC: 'transparent' },
{ name: 'Classic', w: 18, out: 1, outC: '#808080', thC: '#C0C0C0', trC: '#FFFFFF' },
{ name: 'BnW', w: 18, out: 0, outC: '#000000', thC: '#000000', trC: '#FFFFFF' },
{ name: 'WnB', w: 18, out: 0, outC: '#FFFFFF', thC: '#FFFFFF', trC: '#000000' },
{ name: 'Neon Y', w: 18, out: 0, outC: '#FFFF00', thC: '#FFFF00', trC: '#000000' },
{ name: 'Neon P', w: 18, out: 0, outC: '#FF00FF', thC: '#FF00FF', trC: '#000000' },
{ name: 'Neon C', w: 18, out: 0, outC: '#00FFFF', thC: '#00FFFF', trC: '#000000' },
{ name: 'Neon R', w: 18, out: 0, outC: '#FF0000', thC: '#FF0000', trC: '#000000' },
{ name: 'Neon O', w: 18, out: 0, outC: '#FF8800', thC: '#FF8800', trC: '#000000' },
{ name: 'Neon G', w: 18, out: 0, outC: '#00FF00', thC: '#00FF00', trC: '#000000' }
];
presets.forEach(p => {
const btn = createEl('button', { flex: '1', padding: '0', background: '#333', border: '1px solid #555', cursor: 'pointer', borderRadius: '4px', position: 'relative', overflow: 'hidden' }, presetGrid);
btn.title = p.name;
createEl('div', { width: '100%', height: '100%', background: '#444' }, btn);
const track = createEl('div', { position: 'absolute', top: '2px', bottom: '2px', right: '30%', left: '30%', background: (p.trC === 'transparent') ? 'none' : p.trC, border: (p.trC === 'transparent') ? '1px dashed #666' : 'none' }, btn);
createEl('div', { position: 'absolute', top: '20%', height: '40%', left: '0', right: '0', backgroundColor: (p.thC === 'transparent') ? 'transparent' : p.thC, border: `${p.out}px solid ${p.outC}`, boxSizing: 'border-box' }, track);
btn.onclick = () => {
inpEnable.checked = true;
ctrlWidth.input.value = p.w;
ctrlOutlineW.input.value = p.out;
ctrlOutlineC.input.value = p.outC;
if (p.thC === 'transparent') { ctrlThumbC.chkTransparent.checked = true; ctrlThumbC.input.disabled = true; }
else { ctrlThumbC.chkTransparent.checked = false; ctrlThumbC.input.disabled = false; ctrlThumbC.input.value = p.thC; }
if (p.trC === 'transparent') { ctrlTrackC.chkTransparent.checked = true; ctrlTrackC.input.disabled = true; }
else { ctrlTrackC.chkTransparent.checked = false; ctrlTrackC.input.disabled = false; ctrlTrackC.input.value = p.trC; }
updateLive();
};
});
const rowEnable = createEl('div', { marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--text)' }, panel);
const inpEnable = createEl('input', { transform: 'scale(1.2)' }, rowEnable);
inpEnable.type = 'checkbox';
// Initialize UI from the active unsaved state to prevent visual jumps
inpEnable.checked = window._usbActiveConfig.enabled;
createEl('span', { fontWeight: 'bold' }, rowEnable).textContent = 'Enable Custom Scrollbar';
const rowFavicon = createEl('div', { marginBottom: '15px', display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--text)' }, panel);
const inpFavicon = createEl('input', { transform: 'scale(1.2)' }, rowFavicon);
inpFavicon.type = 'checkbox';
inpFavicon.checked = window._usbActiveConfig.matchFavicon;
createEl('span', {}, rowFavicon).textContent = 'Match Favicon Background';
const ctrlWidth = createControlGroup('Total Width (px)', window._usbActiveConfig.width, panel);
const ctrlOutlineW = createControlGroup('Outline Thickness (px)', window._usbActiveConfig.outlineWidth, panel);
const ctrlOutlineC = createControlGroup('Outline Color', window._usbActiveConfig.outlineColor, panel, true, false);
const ctrlThumbC = createControlGroup('Thumb Fill', window._usbActiveConfig.thumbColor, panel, true, true);
const ctrlTrackC = createControlGroup('Track Background', window._usbActiveConfig.trackColor, panel, true, true);
function getValues() {
return {
enabled: inpEnable.checked,
matchFavicon: inpFavicon.checked,
width: ctrlWidth.input.value,
outlineWidth: ctrlOutlineW.input.value,
outlineColor: ctrlOutlineC.input.value,
thumbColor: ctrlThumbC.chkTransparent.checked ? 'transparent' : ctrlThumbC.input.value,
trackColor: ctrlTrackC.chkTransparent.checked ? 'transparent' : ctrlTrackC.input.value
};
}
function updateLive() { updatePageVariables(getValues()); }
inpEnable.addEventListener('change', () => {
if (!inpEnable.checked) {
inpFavicon.checked = false;
}
updateLive();
});
inpFavicon.addEventListener('change', updateLive);
panel.addEventListener('input', updateLive);
// Action Buttons Row (Bottom)
const btnRow = createEl('div', { display: 'flex', gap: '8px', marginTop: '15px' }, panel);
const btnSave = createEl('button', { flex: '1', padding: '8px', background: '#00d26a', border: 'none', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer', color: '#000' }, btnRow);
btnSave.textContent = 'Save';
btnSave.title = "Save settings permanently for this site";
btnSave.addEventListener('click', () => {
saveSiteConfig(getValues());
const origText = btnSave.textContent;
btnSave.textContent = 'Saved!';
setTimeout(() => btnSave.textContent = origText, 1000);
});
const btnUndo = createEl('button', { flex: '1', padding: '8px', background: '#e0a800', border: 'none', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer', color: '#000' }, btnRow);
btnUndo.textContent = 'Undo';
btnUndo.title = "Revert to last saved settings";
btnUndo.addEventListener('click', () => {
inpEnable.checked = siteConfig.enabled;
inpFavicon.checked = siteConfig.matchFavicon;
ctrlWidth.input.value = siteConfig.width;
ctrlOutlineW.input.value = siteConfig.outlineWidth;
ctrlOutlineC.input.value = siteConfig.outlineColor;
if (siteConfig.thumbColor === 'transparent') { ctrlThumbC.chkTransparent.checked = true; ctrlThumbC.input.disabled = true; }
else { ctrlThumbC.chkTransparent.checked = false; ctrlThumbC.input.disabled = false; ctrlThumbC.input.value = siteConfig.thumbColor; }
if (siteConfig.trackColor === 'transparent') { ctrlTrackC.chkTransparent.checked = true; ctrlTrackC.input.disabled = true; }
else { ctrlTrackC.chkTransparent.checked = false; ctrlTrackC.input.disabled = false; ctrlTrackC.input.value = siteConfig.trackColor; }
updatePageVariables(siteConfig);
});
const btnExit = createEl('button', { flex: '1', padding: '8px', background: '#dc3545', border: 'none', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer', color: '#fff' }, btnRow);
btnExit.textContent = 'Exit';
btnExit.title = "Close (Keeps changes on current tab, but does not save)";
btnExit.addEventListener('click', () => {
host.remove(); uiContainer = null;
});
}
})();