Last active
October 31, 2019 11:33
-
-
Save alexlouden/38eca81d46f49417045913cbdffc9514 to your computer and use it in GitHub Desktop.
Custom styled scrollbars
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import useCustomScrollbars, { scrollbarWidth } from './useCustomScrollbars' | |
const Container = React.forwardRef(({ bg, children, ...props }, ref) => { | |
const [localRef, scrollbarsShown, scrollbarStyles] = useCustomScrollbars( | |
children, | |
ref, | |
bg | |
) | |
return ( | |
<Box | |
pr={ | |
40 - | |
(scrollbarsShown ? scrollbarWidth : 0) | |
} | |
css={{ | |
'overflow-y': 'auto', | |
...scrollbarStyles | |
}} | |
ref={localRef} | |
children={children} | |
{...props} | |
/> | |
) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// you could probably remove this dependency if you wanted :) | |
import css from 'dom-css' | |
let scrollbarWidth = false | |
export default function getScrollbarWidth() { | |
if (scrollbarWidth !== false) return scrollbarWidth | |
/* istanbul ignore else */ | |
if (typeof document !== 'undefined') { | |
const div = document.createElement('div') | |
css(div, { | |
width: 100, | |
height: 100, | |
position: 'absolute', | |
top: -9999, | |
overflow: 'scroll', | |
MsOverflowStyle: 'scrollbar' | |
}) | |
document.body.appendChild(div) | |
scrollbarWidth = div.offsetWidth - div.clientWidth | |
document.body.removeChild(div) | |
} else { | |
scrollbarWidth = 0 | |
} | |
return scrollbarWidth || 0 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
useCallback, | |
useEffect, | |
useLayoutEffect, | |
useImperativeHandle, | |
useRef, | |
useState | |
} from 'react' | |
import { memoize } from 'lodash' | |
import { getLuminance, darken, lighten } from 'polished' | |
import getBrowserScrollbarWidth from './getScrollbarWidth' | |
export const scrollbarWidth = 8 | |
export const customScrollbarStyle = memoize(bg => { | |
// your logic here! | |
const lum = getLuminance(bg) | |
const scrollbarColor = | |
lum > 0.9 | |
? darken(0.3, bg) | |
: lum > 0.05 | |
? darken(0.15, bg) | |
: lighten(0.2, bg) | |
return { | |
'::-webkit-scrollbar': { | |
width: `${scrollbarWidth}px` | |
}, | |
'::-webkit-scrollbar-thumb': { | |
borderRadius: `${scrollbarWidth / 2}px`, | |
backgroundColor: scrollbarColor | |
}, | |
scrollbarColor: `${scrollbarColor} transparent` | |
} | |
}) | |
export const noScrollbarStyle = { | |
'::-webkit-scrollbar': { width: 0 } | |
} | |
const useCustomScrollbars = (children, parentRef = null, bg) => { | |
const ref = useRef(null) | |
const [contentCanScroll, setContentCanScroll] = useState(false) | |
const [shouldStyleScrollbars, setShouldStyleScrollbars] = useState(false) | |
const [style, setStyle] = useState({}) | |
const hasVisibleScrollbars = getBrowserScrollbarWidth() > 0 | |
useImperativeHandle(parentRef, () => ref.current) | |
// call when children size changes | |
const handleResize = useCallback( | |
() => { | |
if (!ref.current) return | |
const node = ref.current | |
setContentCanScroll(node.scrollHeight > node.clientHeight) | |
}, | |
[ref] | |
) | |
// handle ref change | |
const setRef = useCallback(node => { | |
if (ref.current) { | |
// cleanup on old ref if necessary | |
} | |
if (node) { | |
setContentCanScroll(node.scrollHeight > node.clientHeight) | |
} | |
ref.current = node | |
}, []) | |
// handle resize, children change, etc | |
// some logic from https://github.com/rehooks/component-size | |
useLayoutEffect( | |
() => { | |
if (!ref.current) return | |
if (!hasVisibleScrollbars) return | |
handleResize() | |
if (typeof ResizeObserver === 'function') { | |
let resizeObserver = new ResizeObserver(() => { | |
handleResize() | |
}) | |
resizeObserver.observe(ref.current) | |
return () => { | |
resizeObserver.disconnect(ref.current) | |
resizeObserver = null | |
} | |
} | |
}, | |
[ref, children, handleResize, hasVisibleScrollbars] | |
) | |
useEffect( | |
() => { | |
setShouldStyleScrollbars(!!(contentCanScroll && hasVisibleScrollbars)) | |
}, | |
[contentCanScroll, hasVisibleScrollbars] | |
) | |
useEffect( | |
() => { | |
setStyle( | |
hasVisibleScrollbars | |
? shouldStyleScrollbars | |
? customScrollbarStyle(bg) | |
: noScrollbarStyle | |
: {} | |
) | |
}, | |
[shouldStyleScrollbars, hasVisibleScrollbars, setStyle, bg] | |
) | |
return [setRef, shouldStyleScrollbars, style] | |
} | |
export default useCustomScrollbars |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment