Mark Thomas Miller's logo

How to target 'stuck' sticky elements in React

November 7, 2020

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 right
  • stuckClasses: classes when the component is stuck
  • unstuckClasses: classes when the component isn't stuck
  • stuckStyles: a style object when the component is stuck
  • unstuckStyles: 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.