usePopper with styled-components for React — React Popper 2.×

The usePopper hook for React Popper — Popper.js

Kitson Broadhurst
May 19 · 4 min read

I recently upgraded the popper in our component library.

The complexities involved in managing the placement, flipping and overflow prevention of a popper make writing your own implementation from scratch intimidating (and above all time-consuming).

Popper.js does a better job than I would at explaining why you should use them.

React Popper 2.× usePopper hook

The documentation on implementing the usePopper hook is relatively limited. (It took me way longer than it should have to get a working version 😅)

The usePopper hook takes three parameters:

and returns two variables :

const { styles, attributes } = usePopper(
buttonRef.current,
popperRef.current,
{
modifiers: [
{
name: "arrow",
options: {
element: arrowRef
}
},
{
name: "offset",
options: {
offset: [0, 10]
}
}
]
}
);

Handling the button and popper ref — useRef()

When creating a reference to our button and popper we can useRef to create a reference object and assign it to our button. Same for our popper:

const buttonRef = useRef(null);
const popperRef = useRef(null);
...<Button ref={buttonRef} ......
<PopperContainer ref={popperRef} ...

Then, when we come to pass the ref to usePopper we simply use the .current property.

Easy.

Using a callback ref for our arrow with useState()

When creating a reference for our arrow, we need to use a callback ref, not a ref object.

Huh? A ref object?

By choosing useState, we can create a callback ref instead of the ref object created byuseRef.

We pass in our setArrowRef function to the ref on our arrow div:

// the ref for the arrow must be a callback ref
const [arrowRef, setArrowRef] = useState(null);
...<div ref={setArrowRef} />

Then when we pass the ref variable to the usePopper hook we just use arrowRef.

The config object — modifiers

We pass in a config object to our usePopper hook and specify modifiers for our specific use case.

We’ll be specifying an arrow element, popper.js handles positioning for us, and an offset which will match our arrow size.

modifiers: [
{
name: "arrow",
options: {
element: arrowRef
}
},
{
name: "offset",
options: {
offset: [0, 10]
}
}
]

The full list of modifiers available for the config object can be found in the non-React popper docs.

Applying usePopper styles and attributes

For the purpose of this article, we’ll use a basic styled-component popper:

const PopperContainer = styled.div`
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-color: white;
padding: 20px;
text-align: center;
`

Then, inside of our component, we’ll add the PopperContainer and apply the ref, styles and attributes:

<PopperContainer
ref={popperRef}
style={styles.popper}
{...attributes.popper}
>
<div ref={setArrowRef} style={styles.arrow} className="arrow" />
<p>I'm a popper</p>
</PopperContainer>

Styling our arrow to match the popper placement

We already pass in styles.arrow to our arrow div, which handles the overall positioning, we still need to manually add some styles to make it work.

Inside of our PopperContainer styled-component, we target the .arrow className:

const PopperContainer = styled.div`  ....arrow {
position: absolute;
width: 10px;
height: 10px;

&:after {
content: " ";
position: absolute;
top: -25px; // we account for the PopperContainer padding
left: 0;
transform: rotate(45deg);
width: 10px;
height: 10px;
background-color: white;
box-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
}
}
`

Currently, our arrow is styled to sit at the top of our popper div and looks good.

Until we scroll the page 😅:

Since we already added the popper attributes to our PopperContainer, we can access data-popper-placement in our styled-component:

const PopperContainer = styled.div`...  .arrow {
...
}
&[data-popper-placement^='top'] > .arrow {
bottom: -30px;
:after {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
}
}
`

Here we adapt our arrow to display at the bottom of the popper when its positioning flips direction.

If we wanted to style the arrow for different orientations then we can simply access bottom, left and right on the data-popper-placement attribute in the same way.

The final result

Resources:

Thanks for reading!

Kitson

A note from the Plain English team

Did you know that we have four publications and a YouTube channel? You can find all of this from our homepage at plainenglish.io — show some love by giving our publications a follow and subscribing to our YouTube channel!

JavaScript In Plain English

New articles every day.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store