From 01498f5cf640b9755b6f6da012bed45de9f95196 Mon Sep 17 00:00:00 2001 From: Emma DaPonte Date: Thu, 6 Aug 2026 13:33:20 -0700 Subject: [PATCH 1/3] Move scrollbar width measurement into an effect so that it won't run during SSR --- src/useCustomScroller.ts | 50 +++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/useCustomScroller.ts b/src/useCustomScroller.ts index f92c1e2..f18cc52 100644 --- a/src/useCustomScroller.ts +++ b/src/useCustomScroller.ts @@ -1,10 +1,19 @@ -import type { MouseEvent as ReactMouseEvent, MutableRefObject } from 'react'; -import { useLayoutEffect, useRef, useState, useCallback } from 'react'; +import type { MutableRefObject, MouseEvent as ReactMouseEvent } from 'react'; +import { useCallback, useLayoutEffect, useRef, useState } from 'react'; /** - * We use a negative right on the content to hide original OS scrollbars + * We need this for OSs that automatically hide the scrollbar (so the offset + * doesn't change in such case). Eg: macOS with "Automatically based on mouse". */ -const OS_SCROLLBAR_WIDTH = (() => { +const DEFAULT_SCROLLBAR_WIDTH = 20; + +let cachedOSScrollbarWidth: number | undefined; + +/** + * We use a negative right on the content to hide original OS scrollbars. + * Client-only — must run inside an effect, not at module scope. + */ +function measureOSScrollbarWidth(): number { const outer = document.createElement('div'); const inner = document.createElement('div'); outer.style.overflow = 'scroll'; @@ -18,13 +27,23 @@ const OS_SCROLLBAR_WIDTH = (() => { document.body.removeChild(outer); return scrollbarWidth; -})(); +} -/** - * We need this for OSs that automatically hide the scrollbar (so the offset - * doesn't change in such case). Eg: macOS with "Automatically based on mouse". - */ -const SCROLLBAR_WIDTH = OS_SCROLLBAR_WIDTH || 20; +/** Measures once, cached for every instance; skips re-measuring on remount. */ +function useOSScrollbarWidth(): number { + const [osScrollbarWidth, setOSScrollbarWidth] = useState( + cachedOSScrollbarWidth ?? DEFAULT_SCROLLBAR_WIDTH, + ); + + useLayoutEffect(() => { + if (cachedOSScrollbarWidth !== undefined) return; + + cachedOSScrollbarWidth = measureOSScrollbarWidth(); + setOSScrollbarWidth(cachedOSScrollbarWidth); + }, []); + + return osScrollbarWidth; +} export default function useCustomScroller( customRef: MutableRefObject | undefined, @@ -33,6 +52,9 @@ export default function useCustomScroller( const [scrollRatio, setScrollRatio] = useState(1); const [isDraggingTrack, setIsDraggingTrack] = useState(false); + const osScrollbarWidth = useOSScrollbarWidth(); + const scrollbarWidth = osScrollbarWidth || DEFAULT_SCROLLBAR_WIDTH; + const ref = useRef(null); const scrollerRef = customRef || ref; const trackRef = useRef(null); @@ -156,7 +178,7 @@ export default function useCustomScroller( const wrapperProps = { style: { - marginLeft: `-${SCROLLBAR_WIDTH}px`, + marginLeft: `-${scrollbarWidth}px`, }, }; @@ -164,9 +186,9 @@ export default function useCustomScroller( ref: scrollerRef, onScroll: disabled ? undefined : onScroll, style: { - right: `-${SCROLLBAR_WIDTH}px`, - padding: `0 ${SCROLLBAR_WIDTH}px 0 0`, - width: `calc(100% + ${OS_SCROLLBAR_WIDTH}px)`, + right: `-${scrollbarWidth}px`, + padding: `0 ${scrollbarWidth}px 0 0`, + width: `calc(100% + ${osScrollbarWidth}px)`, }, }; From 857083d0f10e86f627e6d9092d6a1ccc59b17750 Mon Sep 17 00:00:00 2001 From: Emma DaPonte Date: Mon, 10 Aug 2026 09:21:44 -0700 Subject: [PATCH 2/3] Use simpler approach to only check scrollbar measurement when window exists --- src/useCustomScroller.ts | 46 ++++++++++++---------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/useCustomScroller.ts b/src/useCustomScroller.ts index f18cc52..107a001 100644 --- a/src/useCustomScroller.ts +++ b/src/useCustomScroller.ts @@ -1,19 +1,12 @@ import type { MutableRefObject, MouseEvent as ReactMouseEvent } from 'react'; import { useCallback, useLayoutEffect, useRef, useState } from 'react'; -/** - * We need this for OSs that automatically hide the scrollbar (so the offset - * doesn't change in such case). Eg: macOS with "Automatically based on mouse". - */ -const DEFAULT_SCROLLBAR_WIDTH = 20; - -let cachedOSScrollbarWidth: number | undefined; +const DEFAULT_SCROLLBAR_WIDTH = 20 /** - * We use a negative right on the content to hide original OS scrollbars. - * Client-only — must run inside an effect, not at module scope. + * We use a negative right on the content to hide original OS scrollbars */ -function measureOSScrollbarWidth(): number { +const OS_SCROLLBAR_WIDTH = typeof window === 'undefined' ? DEFAULT_SCROLLBAR_WIDTH : (() => { const outer = document.createElement('div'); const inner = document.createElement('div'); outer.style.overflow = 'scroll'; @@ -27,23 +20,13 @@ function measureOSScrollbarWidth(): number { document.body.removeChild(outer); return scrollbarWidth; -} - -/** Measures once, cached for every instance; skips re-measuring on remount. */ -function useOSScrollbarWidth(): number { - const [osScrollbarWidth, setOSScrollbarWidth] = useState( - cachedOSScrollbarWidth ?? DEFAULT_SCROLLBAR_WIDTH, - ); +})(); - useLayoutEffect(() => { - if (cachedOSScrollbarWidth !== undefined) return; - - cachedOSScrollbarWidth = measureOSScrollbarWidth(); - setOSScrollbarWidth(cachedOSScrollbarWidth); - }, []); - - return osScrollbarWidth; -} +/** + * We need this for OSs that automatically hide the scrollbar (so the offset + * doesn't change in such case). Eg: macOS with "Automatically based on mouse". + */ +const SCROLLBAR_WIDTH = OS_SCROLLBAR_WIDTH || DEFAULT_SCROLLBAR_WIDTH; export default function useCustomScroller( customRef: MutableRefObject | undefined, @@ -52,9 +35,6 @@ export default function useCustomScroller( const [scrollRatio, setScrollRatio] = useState(1); const [isDraggingTrack, setIsDraggingTrack] = useState(false); - const osScrollbarWidth = useOSScrollbarWidth(); - const scrollbarWidth = osScrollbarWidth || DEFAULT_SCROLLBAR_WIDTH; - const ref = useRef(null); const scrollerRef = customRef || ref; const trackRef = useRef(null); @@ -178,7 +158,7 @@ export default function useCustomScroller( const wrapperProps = { style: { - marginLeft: `-${scrollbarWidth}px`, + marginLeft: `-${SCROLLBAR_WIDTH}px`, }, }; @@ -186,9 +166,9 @@ export default function useCustomScroller( ref: scrollerRef, onScroll: disabled ? undefined : onScroll, style: { - right: `-${scrollbarWidth}px`, - padding: `0 ${scrollbarWidth}px 0 0`, - width: `calc(100% + ${osScrollbarWidth}px)`, + right: `-${SCROLLBAR_WIDTH}px`, + padding: `0 ${SCROLLBAR_WIDTH}px 0 0`, + width: `calc(100% + ${OS_SCROLLBAR_WIDTH}px)`, }, }; From bba5c50cb6df7129d4e49e3884075ac944a435df Mon Sep 17 00:00:00 2001 From: Emma DaPonte Date: Mon, 10 Aug 2026 09:37:21 -0700 Subject: [PATCH 3/3] Bump version for SSR patch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd178ae..53a501f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-custom-scroller", - "version": "2.2.0", + "version": "2.2.1", "description": "Super simple React component for creating a custom scrollbar cross-browser and cross-devices", "author": "Vitor Buzinaro ", "license": "MIT",