親祖先要素にoverflowが使われている場合、position : stickyが使えない。その場合の対策として、HTMLのDOM構成を変える対策もあるが、本記事ではscrollイベントをリスナーしてstickyのような動作を実現させることを目的とした。
コード全体はこちら。
import { useEffect, useRef, useCallback, RefObject } from 'react'
export function StickyItem(boxRef: RefObject<HTMLDivElement>) {
const stickyRef = useRef<HTMLDivElement>(null)
const handleScroll = useCallback(() => {
if (!stickyRef.current || !boxRef.current) return
if (boxRef.current.getBoundingClientRect().top <= 50) {
// fixed element
stickyRef.current.style.position = 'absolute'
stickyRef.current.style.top = Math.abs(boxRef.current.getBoundingClientRect().top) + 'px'
} else {
// change to static
stickyRef.current.style.position = 'static'
}
}, [stickyRef, boxRef])
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return (
<div style={{ position: 'relative' }}>
<div
style={{
transition: 'top .7s ease',
top: 0,
willChange: 'top'
}}
ref={stickyRef}
>
<p>sticky contents</p>
</div>
</div>
)
}
コンポーネントを呼び出したタイミングでscrollイベントの登録を行います。
StickyItemにスクロール位置を監視したい親要素をrefで受け取り、スクロール位置を監視しています。
渡されたboxRefのスクロール位置が50px以上になった時に、stickyRefのpositionをstaticからabsoluteに変更しています。
これでstickyのような動作を実現しています。
目次
Summary
ref要素のstyleを直接書き換えているが、stateで持った方が保守性が高くなると思います。(わかりやすい)
JavaScriptで実装していますが、scrollイベントは重いので、できる限りCSSで対応できるように変更した方が良さそうです。