Overlay Custom Scrollbar

Custom X and Y scrollbars with drag-to-seek, visible on hover and scroll, no width impact, cross-browser support

// ==UserScript==
// @name Overlay Custom Scrollbar
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Custom X and Y scrollbars with drag-to-seek, visible on hover and scroll, no width impact, cross-browser support
// @license MIT
// @author Grok and Claude
// @match *://*/*
// @grant none
// ==/UserScript==

(function() {
    'use strict';

    // Add CSS styles
    const style = document.createElement('style');
    style.textContent = `
        /* Hide native scrollbars */
        html {
            -ms-overflow-style: none !important; /* Edge/IE */
            scrollbar-width: none !important; /* Firefox */
            overflow: scroll; /* Enable X and Y scrolling */
        }

        html::-webkit-scrollbar, body::-webkit-scrollbar {
            display: none !important; /* Chrome/Edge/Safari */
            width: 0px !important;
            height: 0px !important;
        }

        /* Y-axis custom scrollbar container */
        .custom-scrollbar-y {
            position: fixed;
            top: 0;
            right: 0;
            width: 8px;
            height: 100%;
            opacity: 0;
            transition: opacity 0.2s ease;
            z-index: 9999;
            background: rgba(0, 0, 0, 0.05);
        }

        /* X-axis custom scrollbar container */
        .custom-scrollbar-x {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 8px;
            opacity: 0;
            transition: opacity 0.2s ease;
            z-index: 9999;
            background: rgba(0, 0, 0, 0.05);
        }

        /* Custom scrollbar thumb */
        .custom-scrollbar-thumb {
            position: absolute;
            background: #888;
            border-radius: 4px;
            opacity: 1; /* Always visible */
            cursor: pointer;
        }

        .custom-scrollbar-y .custom-scrollbar-thumb {
            width: 100%;
        }

        .custom-scrollbar-x .custom-scrollbar-thumb {
            height: 100%;
        }

        .custom-scrollbar-thumb:hover {
            background: #555;
        }

        /* Show during scrolling or dragging */
        html.scrolling .custom-scrollbar-y,
        html.scrolling .custom-scrollbar-x {
            opacity: 1;
        }

        /* Show when hovering over scrollbar */
        .custom-scrollbar-y:hover,
        .custom-scrollbar-x:hover {
            opacity: 1;
        }

        /* Always show when dragging */
        .dragging {
            opacity: 1 !important;
        }
    `;
    document.head.appendChild(style);

    // Create Y-axis scrollbar
    const scrollbarY = document.createElement('div');
    scrollbarY.className = 'custom-scrollbar-y';
    const thumbY = document.createElement('div');
    thumbY.className = 'custom-scrollbar-thumb';
    scrollbarY.appendChild(thumbY);
    document.body.appendChild(scrollbarY);

    // Create X-axis scrollbar
    const scrollbarX = document.createElement('div');
    scrollbarX.className = 'custom-scrollbar-x';
    const thumbX = document.createElement('div');
    thumbX.className = 'custom-scrollbar-thumb';
    scrollbarX.appendChild(thumbX);
    document.body.appendChild(scrollbarX);

    // Global variables
    let isDraggingY = false;
    let isDraggingX = false;
    let startY, startX, startScrollTop, startScrollLeft;
    let scrollTimeout;
    const html = document.documentElement;

    function getDocumentDimensions() {
        return {
            clientWidth: window.innerWidth || html.clientWidth || document.body.clientWidth,
            clientHeight: window.innerHeight || html.clientHeight || document.body.clientHeight,
            scrollWidth: Math.max(
                document.body.scrollWidth, html.scrollWidth,
                document.body.offsetWidth, html.offsetWidth,
                document.body.clientWidth, html.clientWidth
            ),
            scrollHeight: Math.max(
                document.body.scrollHeight, html.scrollHeight,
                document.body.offsetHeight, html.offsetHeight,
                document.body.clientHeight, html.clientHeight
            ),
            scrollLeft: window.pageXOffset || html.scrollLeft || document.body.scrollLeft,
            scrollTop: window.pageYOffset || html.scrollTop || document.body.scrollTop
        };
    }

    function updateScrollbars() {
        const dims = getDocumentDimensions();

        const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
        const maxScrollTop = dims.scrollHeight - dims.clientHeight;

        const isScrollableY = dims.scrollHeight > dims.clientHeight;
        scrollbarY.style.display = isScrollableY ? 'block' : 'none';

        if (isScrollableY) {
            const thumbHeightRatio = dims.clientHeight / dims.scrollHeight;
            const thumbHeight = Math.max(thumbHeightRatio * dims.clientHeight, 20);
            const scrollRatioY = maxScrollTop > 0 ? dims.scrollTop / maxScrollTop : 0;
            const thumbTop = scrollRatioY * (dims.clientHeight - thumbHeight);

            thumbY.style.height = `${thumbHeight}px`;
            thumbY.style.top = `${thumbTop}px`;
        }

        const isScrollableX = dims.scrollWidth > dims.clientWidth;
        scrollbarX.style.display = isScrollableX ? 'block' : 'none';

        if (isScrollableX) {
            const thumbWidthRatio = dims.clientWidth / dims.scrollWidth;
            const thumbWidth = Math.max(thumbWidthRatio * dims.clientWidth, 20);
            const scrollRatioX = maxScrollLeft > 0 ? dims.scrollLeft / maxScrollLeft : 0;
            const thumbLeft = scrollRatioX * (dims.clientWidth - thumbWidth);

            thumbX.style.width = `${thumbWidth}px`;
            thumbX.style.left = `${thumbLeft}px`;
        }

        if (!isDraggingX && !isDraggingY) {
            html.classList.add('scrolling');
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(() => {
                html.classList.remove('scrolling');
            }, 500);
        }
    }

    thumbY.addEventListener('mousedown', (e) => {
        e.preventDefault();
        e.stopPropagation();

        const dims = getDocumentDimensions();
        isDraggingY = true;
        startY = e.clientY;
        startScrollTop = dims.scrollTop;

        scrollbarY.classList.add('dragging');
        html.classList.add('scrolling');
        document.body.style.userSelect = 'none';
    });

    thumbX.addEventListener('mousedown', (e) => {
        e.preventDefault();
        e.stopPropagation();

        const dims = getDocumentDimensions();
        isDraggingX = true;
        startX = e.clientX;
        startScrollLeft = dims.scrollLeft;

        scrollbarX.classList.add('dragging');
        html.classList.add('scrolling');
        document.body.style.userSelect = 'none';
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDraggingY && !isDraggingX) return;

        const dims = getDocumentDimensions();

        if (isDraggingY) {
            const deltaY = e.clientY - startY;
            const thumbHeight = parseFloat(thumbY.style.height) || 20;
            const trackHeight = dims.clientHeight;
            const maxScrollTop = dims.scrollHeight - dims.clientHeight;

            const scrollRatio = maxScrollTop / (trackHeight - thumbHeight);
            const newScrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + (deltaY * scrollRatio)));

            window.scrollTo(dims.scrollLeft, newScrollTop);
        }

        if (isDraggingX) {
            const deltaX = e.clientX - startX;
            const thumbWidth = parseFloat(thumbX.style.width) || 20;
            const trackWidth = dims.clientWidth;
            const maxScrollLeft = dims.scrollWidth - dims.clientWidth;

            const scrollRatio = maxScrollLeft / (trackWidth - thumbWidth);
            const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + (deltaX * scrollRatio)));

            document.documentElement.scrollLeft = newScrollLeft;
            document.body.scrollLeft = newScrollLeft;
            window.scrollTo(newScrollLeft, dims.scrollTop);
        }

        updateScrollbars();
    });

    scrollbarY.addEventListener('mousedown', (e) => {
        if (e.target !== scrollbarY) return;

        const dims = getDocumentDimensions();
        const thumbHeight = parseFloat(thumbY.style.height) || 20;
        const clickPos = e.clientY;
        const thumbHalf = thumbHeight / 2;

        const trackHeight = dims.clientHeight;
        const maxScrollTop = dims.scrollHeight - dims.clientHeight;
        const clickRatio = (clickPos - thumbHalf) / (trackHeight - thumbHeight);
        const newScrollTop = Math.max(0, Math.min(maxScrollTop, clickRatio * maxScrollTop));

        window.scrollTo({
            top: newScrollTop,
            left: dims.scrollLeft,
            behavior: 'smooth'
        });
    });

    scrollbarX.addEventListener('mousedown', (e) => {
        if (e.target !== scrollbarX) return;

        const dims = getDocumentDimensions();
        const thumbWidth = parseFloat(thumbX.style.width) || 20;
        const clickPos = e.clientX;
        const thumbHalf = thumbWidth / 2;

        const trackWidth = dims.clientWidth;
        const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
        const clickRatio = (clickPos - thumbHalf) / (trackWidth - thumbWidth);
        const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, clickRatio * maxScrollLeft));

        window.scrollTo({
            top: dims.scrollTop,
            left: newScrollLeft,
            behavior: 'smooth'
        });
    });

    function endDragging() {
        if (isDraggingY) {
            isDraggingY = false;
            scrollbarY.classList.remove('dragging');
        }

        if (isDraggingX) {
            isDraggingX = false;
            scrollbarX.classList.remove('dragging');
        }

        document.body.style.userSelect = '';

        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
            html.classList.remove('scrolling');
        }, 500);
    }

    document.addEventListener('mouseup', endDragging);
    document.addEventListener('mouseleave', endDragging);

    window.addEventListener('scroll', updateScrollbars, { passive: true });
    window.addEventListener('resize', updateScrollbars, { passive: true });

    function initialize() {
        updateScrollbars();
        setTimeout(updateScrollbars, 100);
        setTimeout(updateScrollbars, 500);
    }

    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize);
    }
})();