NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Auto Scroll Down
// @description Toggles continuous auto-scrolling of the page or Gmail preview pane when Alt+S is pressed or using the floating button
// @author Rami S Ejailat
// @copyright RamiEjailat (https://openuserjs.org/users/RamiEjailat)
// @updateURL https://openuserjs.org/meta/RamiEjailat/Auto_Scroll_Down.meta.js
// @downloadURL https://openuserjs.org/install/RamiEjailat/Auto_Scroll_Down.user.js
// @license MIT
// @version 20260727
// @namespace http://tampermonkey.net/
// @match *://*/*
// @exclude *://challenges.cloudflare.com/*
// @grant none
// @icon https://cdn-icons-png.flaticon.com/64/11606/11606886.png
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const stepSize = 1;
const fastSpeed = 20;
const slowSpeed = 50;
let scrolling = false;
let scrollInterval;
let currentSpeed;
let dynamicContentTimeout;
let pausedByFocus = false;
// Universal Target Selector
function getScrollTarget() {
// Special handling if on Gmail
if (window.location.hostname === 'mail.google.com') {
const mainWrapper = document.querySelector('[role="main"]')?.lastElementChild?.lastElementChild;
if (mainWrapper) {
// If the wrapper itself handles the scrollbar
if (mainWrapper.scrollHeight > mainWrapper.clientHeight) {
return mainWrapper;
}
// Look for the internal descendant that holds the active scroll overflow
const internalPane = Array.from(mainWrapper.querySelectorAll('*')).find(el => {
const style = window.getComputedStyle(el);
const isScrollableStyle = style.overflowY === 'auto' || style.overflowY === 'scroll';
return isScrollableStyle && el.scrollHeight > el.clientHeight;
});
if (internalPane) return internalPane;
}
}
// Universal fallback for all other sites (and Gmail main list view if window scrolls)
return window;
}
// Helper to extract dimensions seamlessly whether target is Window or an Element
function getScrollMetrics(target) {
if (target === window) {
return {
scrollTop: window.scrollY,
scrollHeight: document.documentElement.scrollHeight,
clientHeight: window.innerHeight
};
} else {
return {
scrollTop: target.scrollTop,
scrollHeight: target.scrollHeight,
clientHeight: target.clientHeight
};
}
}
const btnStyle = `
all: initial;
position: fixed;
bottom: 20px;
left: 20px;
height: 32px;
width: 32px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.7);
border: none;
outline: none;
font-size: x-large;
border-radius: 5px;
cursor: pointer;
z-index: 2147483646;
display: none;
`;
const button = document.createElement('button');
button.textContent = '▶️';
button.style.cssText = btnStyle;
button.classList.add('autoscrolldown');
document.body.appendChild(button);
const topButton = document.createElement('button');
topButton.textContent = '⬆️';
topButton.style.cssText = btnStyle;
topButton.style.left = '80px';
topButton.classList.add('autoscrolldown');
document.body.appendChild(topButton);
const progressBar = document.createElement('div');
progressBar.classList.add('autoscrolldown');
Object.assign(progressBar.style, {
all: 'initial',
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '2px',
zIndex: '2147483647',
display: 'none',
pointerEvents: 'none',
backgroundColor: 'rgba(0, 0, 0, 0.08)',
});
progressBar.style.setProperty('direction', 'ltr', 'important');
const progressFill = document.createElement('div');
progressFill.classList.add('autoscrolldown-fill');
Object.assign(progressFill.style, {
height: '100%',
width: '0%',
backgroundColor: '#4a90d9',
transition: 'width 0.1s linear',
});
progressFill.style.setProperty('direction', 'ltr', 'important');
progressBar.appendChild(progressFill);
document.body.appendChild(progressBar);
function updateProgressBar() {
const target = getScrollTarget();
const metrics = getScrollMetrics(target);
const scrollableHeight = metrics.scrollHeight - metrics.clientHeight;
const progress = scrollableHeight > 0 ? (metrics.scrollTop / scrollableHeight) * 100 : 0;
progressFill.style.width = progress + '%';
}
function updateButtonVisibility() {
const target = getScrollTarget();
const metrics = getScrollMetrics(target);
const isScrollable = metrics.scrollHeight - metrics.clientHeight > 5;
button.style.display = isScrollable ? 'block' : 'none';
topButton.style.display = (isScrollable && metrics.scrollTop > 0) ? 'block' : 'none';
progressBar.style.display = isScrollable ? 'block' : 'none';
if (isScrollable) updateProgressBar();
}
function startScrolling(speed) {
if (!scrolling) {
scrolling = true;
button.textContent = '🐰';
}
currentSpeed = speed;
clearInterval(scrollInterval);
scrollInterval = setInterval(() => {
const target = getScrollTarget();
const metrics = getScrollMetrics(target);
const distanceToBottom = metrics.scrollHeight - metrics.scrollTop - metrics.clientHeight;
if (distanceToBottom >= stepSize) {
target.scrollBy(0, stepSize);
if (dynamicContentTimeout) clearTimeout(dynamicContentTimeout);
dynamicContentTimeout = setTimeout(stopIfNoDynamicContent, 1000);
} else {
stopScrolling();
}
}, speed);
button.blur();
}
function stopScrolling() {
clearInterval(scrollInterval);
scrolling = false;
button.textContent = '▶️';
}
function stopIfNoDynamicContent() {
const target = getScrollTarget();
const metrics = getScrollMetrics(target);
const distanceToBottom = metrics.scrollHeight - metrics.scrollTop - metrics.clientHeight;
if (distanceToBottom <= stepSize) {
stopScrolling();
}
}
function toggleSpeed() {
if (button.textContent === '🐰') {
button.textContent = '🐢';
startScrolling(slowSpeed);
} else {
button.textContent = '🐰';
startScrolling(fastSpeed);
}
}
function pauseForFocus() {
if (scrolling && !pausedByFocus) {
pausedByFocus = true;
clearInterval(scrollInterval);
}
}
function resumeAfterFocus() {
if (pausedByFocus) {
pausedByFocus = false;
if (scrolling) {
startScrolling(currentSpeed);
}
}
}
// When switching to another tab in the same window
document.addEventListener('visibilitychange', () => {
if (document.hidden) pauseForFocus();
else resumeAfterFocus();
});
// When switching focus to another window or app while the tab stays visible
window.addEventListener('blur', pauseForFocus);
window.addEventListener('focus', resumeAfterFocus);
document.addEventListener('keydown', (event) => {
if (event.altKey && event.code === 'KeyS') {
if (scrolling) toggleSpeed();
else startScrolling(fastSpeed);
event.preventDefault();
} else if (event.code === 'Escape' && scrolling) {
stopScrolling();
event.preventDefault();
}
});
button.addEventListener('click', () => {
if (!scrolling) startScrolling(fastSpeed);
else toggleSpeed();
});
button.addEventListener('contextmenu', (event) => {
stopScrolling();
event.preventDefault();
});
topButton.addEventListener('click', () => {
const target = getScrollTarget();
target.scrollTo({ top: 0 });
});
// Monitor for changes on the page (essential for single page applications like Gmail)
new MutationObserver(() => {
updateButtonVisibility();
}).observe(document.body, { childList: true, subtree: true });
// Use capture phase (true) to intercept internal pane scrolls as well as window scrolls
document.addEventListener('scroll', (event) => {
const target = getScrollTarget();
// Fire updates if scrolling the window, or if scrolling our internal targeted element
if (target === window || event.target === target) {
updateButtonVisibility();
updateProgressBar();
}
}, true);
document.addEventListener('mouseover', (event) => {
if (scrolling && event.target.closest('a')) {
clearInterval(scrollInterval);
}
});
document.addEventListener('mouseout', (event) => {
if (scrolling && event.target.closest('a')) {
startScrolling(currentSpeed);
}
});
window.addEventListener('load', () => {
setTimeout(updateButtonVisibility, 300);
});
})();