How to target 'stuck' sticky elements in React
When an element is using position: sticky
, you might want to apply different styles when it's stuck versus when it's not. This feature isn't coming to CSS anytime soon, but you can make it work with a little bit of JavaScript via IntersectionObserver
. In this post, we'll create a reusable React component that can handle it for us.
The API for our component will look like this. It takes a prop for the part of the screen it sticks to, as well as (optional) styles and classes for when it's stuck and unstuck. It can also accept children.
<Sticky
position="top" // top, right, bottom, left
stuckClasses="foo bar"
unstuckClasses="baz qux"
stuckStyles={{ color: "red" }}
unstuckStyles={{ color: "blue" }}
>
Add child elements here...
</Sticky>
Here's a working example of the code we'd need to create this component. Below, I'll explain how it works.
function Sticky({
position,
stuckClasses = "",
unstuckClasses = "",
stuckStyles = {},
unstuckStyles = {},
children
}) {
const [stuck, setStuck] = useState(false)
const ref = React.createRef()
const classes = stuck ? stuckClasses : unstuckClasses
const styles = stuck ? stuckStyles : unstuckStyles
const inlineStyles = {
position: "sticky",
[position]: -1,
...styles
}
useEffect(() => {
const cachedRef = ref.current
const observer = new IntersectionObserver(
([e]) => setStuck(e.intersectionRatio < 1),
{ threshold: [1] }
)
observer.observe(cachedRef)
return () => observer.unobserve(cachedRef)
}, [ref])
return (
<div style={inlineStyles} className={classes} ref={ref}>
{children}
</div>
)
}
Let's explain how this works! Our sticky component takes several props, and everything is optional except for position
:
position
: top, bottom, left, or rightstuckClasses
: classes when the component is stuckunstuckClasses
: classes when the component isn't stuckstuckStyles
: a style object when the component is stuckunstuckStyles
: a style object when the component isn't stuck
On the first lines, we create a new stateful variable called stuck
, which will be either true
or false
. We can listen to this to choose which classes and styles to use.
Then, we create a ref
which we attach to the rendered element. You can think of this as a reference to the element that we can listen to to find out if it's stuck or not.
After that, we come to classes
, which chooses which classes to display depending on if the element is stuck.
We also define styles
. This sets position: sticky
and takes the position
prop we passed in and sets it to -1
. (For instance, if you used position='bottom'
as a prop, this would become bottom: -1
.) This needs to be a negative value so the overlap can be detected by IntersectionObserver
.
Then, we come to the useEffect
. Its purpose is to run IntersectionObserver
on the sticky element. We cache the ref
so we can observe and unobserve the sticky element when necessary.
Finally, we return the actual JSX, where we apply the stuck or unstuck classes, styles, and attach our ref
from above.
And that's that! You can use this to create navbars, sets of controls, and other elements that change their styles as you scroll.