Overlay Custom Scrollbar

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
    }
})();