Overlay Custom Scrollbar

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

  1. // ==UserScript==
  2. // @name Overlay Custom Scrollbar
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description Custom X and Y scrollbars with drag-to-seek, visible on hover and scroll, no width impact, cross-browser support
  6. // @license MIT
  7. // @author Grok and Claude
  8. // @match *://*/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Add CSS styles
  16. const style = document.createElement('style');
  17. style.textContent = `
  18. /* Hide native scrollbars */
  19. html {
  20. -ms-overflow-style: none !important; /* Edge/IE */
  21. scrollbar-width: none !important; /* Firefox */
  22. overflow: scroll; /* Enable X and Y scrolling */
  23. }
  24.  
  25. html::-webkit-scrollbar, body::-webkit-scrollbar {
  26. display: none !important; /* Chrome/Edge/Safari */
  27. width: 0px !important;
  28. height: 0px !important;
  29. }
  30.  
  31. /* Y-axis custom scrollbar container */
  32. .custom-scrollbar-y {
  33. position: fixed;
  34. top: 0;
  35. right: 0;
  36. width: 8px;
  37. height: 100%;
  38. opacity: 0;
  39. transition: opacity 0.2s ease;
  40. z-index: 9999;
  41. background: rgba(0, 0, 0, 0.05);
  42. }
  43.  
  44. /* X-axis custom scrollbar container */
  45. .custom-scrollbar-x {
  46. position: fixed;
  47. bottom: 0;
  48. left: 0;
  49. width: 100%;
  50. height: 8px;
  51. opacity: 0;
  52. transition: opacity 0.2s ease;
  53. z-index: 9999;
  54. background: rgba(0, 0, 0, 0.05);
  55. }
  56.  
  57. /* Custom scrollbar thumb */
  58. .custom-scrollbar-thumb {
  59. position: absolute;
  60. background: #888;
  61. border-radius: 4px;
  62. opacity: 1; /* Always visible */
  63. cursor: pointer;
  64. }
  65.  
  66. .custom-scrollbar-y .custom-scrollbar-thumb {
  67. width: 100%;
  68. }
  69.  
  70. .custom-scrollbar-x .custom-scrollbar-thumb {
  71. height: 100%;
  72. }
  73.  
  74. .custom-scrollbar-thumb:hover {
  75. background: #555;
  76. }
  77.  
  78. /* Show during scrolling or dragging */
  79. html.scrolling .custom-scrollbar-y,
  80. html.scrolling .custom-scrollbar-x {
  81. opacity: 1;
  82. }
  83.  
  84. /* Show when hovering over scrollbar */
  85. .custom-scrollbar-y:hover,
  86. .custom-scrollbar-x:hover {
  87. opacity: 1;
  88. }
  89.  
  90. /* Always show when dragging */
  91. .dragging {
  92. opacity: 1 !important;
  93. }
  94. `;
  95. document.head.appendChild(style);
  96.  
  97. // Create Y-axis scrollbar
  98. const scrollbarY = document.createElement('div');
  99. scrollbarY.className = 'custom-scrollbar-y';
  100. const thumbY = document.createElement('div');
  101. thumbY.className = 'custom-scrollbar-thumb';
  102. scrollbarY.appendChild(thumbY);
  103. document.body.appendChild(scrollbarY);
  104.  
  105. // Create X-axis scrollbar
  106. const scrollbarX = document.createElement('div');
  107. scrollbarX.className = 'custom-scrollbar-x';
  108. const thumbX = document.createElement('div');
  109. thumbX.className = 'custom-scrollbar-thumb';
  110. scrollbarX.appendChild(thumbX);
  111. document.body.appendChild(scrollbarX);
  112.  
  113. // Global variables
  114. let isDraggingY = false;
  115. let isDraggingX = false;
  116. let startY, startX, startScrollTop, startScrollLeft;
  117. let scrollTimeout;
  118. const html = document.documentElement;
  119.  
  120. function getDocumentDimensions() {
  121. return {
  122. clientWidth: window.innerWidth || html.clientWidth || document.body.clientWidth,
  123. clientHeight: window.innerHeight || html.clientHeight || document.body.clientHeight,
  124. scrollWidth: Math.max(
  125. document.body.scrollWidth, html.scrollWidth,
  126. document.body.offsetWidth, html.offsetWidth,
  127. document.body.clientWidth, html.clientWidth
  128. ),
  129. scrollHeight: Math.max(
  130. document.body.scrollHeight, html.scrollHeight,
  131. document.body.offsetHeight, html.offsetHeight,
  132. document.body.clientHeight, html.clientHeight
  133. ),
  134. scrollLeft: window.pageXOffset || html.scrollLeft || document.body.scrollLeft,
  135. scrollTop: window.pageYOffset || html.scrollTop || document.body.scrollTop
  136. };
  137. }
  138.  
  139. function updateScrollbars() {
  140. const dims = getDocumentDimensions();
  141.  
  142. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  143. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  144.  
  145. const isScrollableY = dims.scrollHeight > dims.clientHeight;
  146. scrollbarY.style.display = isScrollableY ? 'block' : 'none';
  147.  
  148. if (isScrollableY) {
  149. const thumbHeightRatio = dims.clientHeight / dims.scrollHeight;
  150. const thumbHeight = Math.max(thumbHeightRatio * dims.clientHeight, 20);
  151. const scrollRatioY = maxScrollTop > 0 ? dims.scrollTop / maxScrollTop : 0;
  152. const thumbTop = scrollRatioY * (dims.clientHeight - thumbHeight);
  153.  
  154. thumbY.style.height = `${thumbHeight}px`;
  155. thumbY.style.top = `${thumbTop}px`;
  156. }
  157.  
  158. const isScrollableX = dims.scrollWidth > dims.clientWidth;
  159. scrollbarX.style.display = isScrollableX ? 'block' : 'none';
  160.  
  161. if (isScrollableX) {
  162. const thumbWidthRatio = dims.clientWidth / dims.scrollWidth;
  163. const thumbWidth = Math.max(thumbWidthRatio * dims.clientWidth, 20);
  164. const scrollRatioX = maxScrollLeft > 0 ? dims.scrollLeft / maxScrollLeft : 0;
  165. const thumbLeft = scrollRatioX * (dims.clientWidth - thumbWidth);
  166.  
  167. thumbX.style.width = `${thumbWidth}px`;
  168. thumbX.style.left = `${thumbLeft}px`;
  169. }
  170.  
  171. if (!isDraggingX && !isDraggingY) {
  172. html.classList.add('scrolling');
  173. clearTimeout(scrollTimeout);
  174. scrollTimeout = setTimeout(() => {
  175. html.classList.remove('scrolling');
  176. }, 500);
  177. }
  178. }
  179.  
  180. thumbY.addEventListener('mousedown', (e) => {
  181. e.preventDefault();
  182. e.stopPropagation();
  183.  
  184. const dims = getDocumentDimensions();
  185. isDraggingY = true;
  186. startY = e.clientY;
  187. startScrollTop = dims.scrollTop;
  188.  
  189. scrollbarY.classList.add('dragging');
  190. html.classList.add('scrolling');
  191. document.body.style.userSelect = 'none';
  192. });
  193.  
  194. thumbX.addEventListener('mousedown', (e) => {
  195. e.preventDefault();
  196. e.stopPropagation();
  197.  
  198. const dims = getDocumentDimensions();
  199. isDraggingX = true;
  200. startX = e.clientX;
  201. startScrollLeft = dims.scrollLeft;
  202.  
  203. scrollbarX.classList.add('dragging');
  204. html.classList.add('scrolling');
  205. document.body.style.userSelect = 'none';
  206. });
  207.  
  208. document.addEventListener('mousemove', (e) => {
  209. if (!isDraggingY && !isDraggingX) return;
  210.  
  211. const dims = getDocumentDimensions();
  212.  
  213. if (isDraggingY) {
  214. const deltaY = e.clientY - startY;
  215. const thumbHeight = parseFloat(thumbY.style.height) || 20;
  216. const trackHeight = dims.clientHeight;
  217. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  218.  
  219. const scrollRatio = maxScrollTop / (trackHeight - thumbHeight);
  220. const newScrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + (deltaY * scrollRatio)));
  221.  
  222. window.scrollTo(dims.scrollLeft, newScrollTop);
  223. }
  224.  
  225. if (isDraggingX) {
  226. const deltaX = e.clientX - startX;
  227. const thumbWidth = parseFloat(thumbX.style.width) || 20;
  228. const trackWidth = dims.clientWidth;
  229. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  230.  
  231. const scrollRatio = maxScrollLeft / (trackWidth - thumbWidth);
  232. const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + (deltaX * scrollRatio)));
  233.  
  234. document.documentElement.scrollLeft = newScrollLeft;
  235. document.body.scrollLeft = newScrollLeft;
  236. window.scrollTo(newScrollLeft, dims.scrollTop);
  237. }
  238.  
  239. updateScrollbars();
  240. });
  241.  
  242. scrollbarY.addEventListener('mousedown', (e) => {
  243. if (e.target !== scrollbarY) return;
  244.  
  245. const dims = getDocumentDimensions();
  246. const thumbHeight = parseFloat(thumbY.style.height) || 20;
  247. const clickPos = e.clientY;
  248. const thumbHalf = thumbHeight / 2;
  249.  
  250. const trackHeight = dims.clientHeight;
  251. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  252. const clickRatio = (clickPos - thumbHalf) / (trackHeight - thumbHeight);
  253. const newScrollTop = Math.max(0, Math.min(maxScrollTop, clickRatio * maxScrollTop));
  254.  
  255. window.scrollTo({
  256. top: newScrollTop,
  257. left: dims.scrollLeft,
  258. behavior: 'smooth'
  259. });
  260. });
  261.  
  262. scrollbarX.addEventListener('mousedown', (e) => {
  263. if (e.target !== scrollbarX) return;
  264.  
  265. const dims = getDocumentDimensions();
  266. const thumbWidth = parseFloat(thumbX.style.width) || 20;
  267. const clickPos = e.clientX;
  268. const thumbHalf = thumbWidth / 2;
  269.  
  270. const trackWidth = dims.clientWidth;
  271. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  272. const clickRatio = (clickPos - thumbHalf) / (trackWidth - thumbWidth);
  273. const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, clickRatio * maxScrollLeft));
  274.  
  275. window.scrollTo({
  276. top: dims.scrollTop,
  277. left: newScrollLeft,
  278. behavior: 'smooth'
  279. });
  280. });
  281.  
  282. function endDragging() {
  283. if (isDraggingY) {
  284. isDraggingY = false;
  285. scrollbarY.classList.remove('dragging');
  286. }
  287.  
  288. if (isDraggingX) {
  289. isDraggingX = false;
  290. scrollbarX.classList.remove('dragging');
  291. }
  292.  
  293. document.body.style.userSelect = '';
  294.  
  295. clearTimeout(scrollTimeout);
  296. scrollTimeout = setTimeout(() => {
  297. html.classList.remove('scrolling');
  298. }, 500);
  299. }
  300.  
  301. document.addEventListener('mouseup', endDragging);
  302. document.addEventListener('mouseleave', endDragging);
  303.  
  304. window.addEventListener('scroll', updateScrollbars, { passive: true });
  305. window.addEventListener('resize', updateScrollbars, { passive: true });
  306.  
  307. function initialize() {
  308. updateScrollbars();
  309. setTimeout(updateScrollbars, 100);
  310. setTimeout(updateScrollbars, 500);
  311. }
  312.  
  313. if (document.readyState === 'complete') {
  314. initialize();
  315. } else {
  316. window.addEventListener('load', initialize);
  317. }
  318. })();