The responsive pill of your dream
- julienmesser
- Feb 2
- 2 min read

Time for a new post, let's dive into an another exciting try with React...
Today's challenge: display a horizontal "pills" components. When there's not enough place to show everything, they wont't wrap to the next line. Instead, the items will be collapsed:

Let's explain how to realize it as a kind of tutorial:
Starting with a simple container with some pills
We start with a very simple container that displays pills:
export const PillContainer= ({ pills }) => {
const maxDisplayedPills = pills.length;
return (
<div className="flex flex-row">
{ pills.slice(0, maxDisplayedPills).map((pill) => (<Pill key={pill.id} label={pill.label} />))
}
</div>
);
}
const Pill = ({ label }) => {
return (<div className="font-mono px-1 mx-0.5 border
border-current rounded-full whitespace-nowrap">
{label}
</div>);
}
Getting the information about the width
So, now the challenge is to identify the available space and the width of each pill to be rendered. For this we will observe the container's width on resize and store it in a a state "windowWidth":
const [windowWidth, setWindowWidth] = useState(0);
// On initialization: setup an observer to watch the container's width
useEffect(() => {
const onResize = () => {
setWindowWidth(containerRef.current.offsetWidth);
};
onResize();
window.addEventListener('resize', onResize);
// Remove the event listener when the component unmounts
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
On the pill's side we will use a "useLayoutEffect" hook which is called after each component's refresh. At this point we will pass the size to the pill's container by calling a handler:
useLayoutEffect(() => {
onBoundingChange(pillKey, myRef.current.getBoundingClientRect());
}, [label]);
This handler store the size of all the pills in a dictionary (not an array: we need to identify the pills properly in case pills are added or removed):
const [pillsPositions, setPillsPositions] = useState({});
const onPillBoundingChange = (id, rect) => {
// Update the dictionary of pills's positions
setPillsPositions((prevAllPillsRightPosition) => ({ ...prevAllPillsRightPosition, [id]: { left: rect.left, right: rect.right } }));
};
Computation how many pills can be displayed
In the container we compute the number of displayable pills. This is done in a useEffect executed if the dictionary changes or if the window size is updated:
const [maxDisplayedPills, setMaxDisplayedPills] = useState(pills.length);
// On size change: calculate the amount of pills that can be displayed
useEffect(() => {
const offsetFirstPill = (pillsPositions.length > 0) ? pillsPositions['(Pill 1)'].left : 0;
const amountDisplayablePills = Object.values(pillsPositions).filter((value) => value.right - offsetFirstPill < windowWidth).length;
setMaxDisplayedPills(amountDisplayablePills);
}, [pillsPositions, windowWidth]);
And finally in the rendering we remove the pills which have to be collapsed with a "slice" and summarize the number of collapsed pills:
return (
<div ref={containerRef} className="flex flex-row">
{ pills.slice(0, maxDisplayedPills).map((pill) => (<Pill key={pill.id} pillKey={pill.id} label={pill.label}
onBoundingChange={onPillBoundingChange} />))
}
{ pills.length > maxDisplayedPills &&
<Pill pillKey={masterKey} label={`+${pills.length - maxDisplayedPills}`} onBoundingChange={() => {}} />}
</div>
);
References
You find the source code here: https://github.com/axis68/design-library/tree/main/src/components/pills
You find the component rendered live in Storybook here: https://axis68.github.io/design-library/?path=/docs/components-pillcontainer--docs
Comments