Skip to content

Instantly share code, notes, and snippets.

@alexlouden
Last active October 31, 2019 11:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexlouden/38eca81d46f49417045913cbdffc9514 to your computer and use it in GitHub Desktop.
Save alexlouden/38eca81d46f49417045913cbdffc9514 to your computer and use it in GitHub Desktop.
Custom styled scrollbars
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}
/>
)
})
// 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
}
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