Responsive list for React that shows only items that fit and groups the rest into a customizable overflow element. Recalculates on resize.
🔗 Live demo: https://eliav2.github.io/react-responsive-overflow-list/
- Accurate & responsive: measures real layout after paint (ResizeObserver), not guessed widths
- Two usage modes:
children(simple) oritems + renderItem(structured) - Customizable overflow element; ships with a lightweight default
- Multi-row support (via
maxRows) - Handles uneven widths, including a single ultra-wide item
- TypeScript types; zero runtime deps (React as peer)
- SSR-friendly: measurement runs on the client
- No implicit wrappers around your items. (layout behaves as you expect)
npm i react-responsive-overflow-listInstall as a styled shadcn component with dropdown menu overflow:
# Radix UI primitives (recommended)
npx shadcn@latest add https://eliav2.github.io/react-responsive-overflow-list/r/styles/radix-vega/overflow-list.json
# Base UI primitives
npx shadcn@latest add https://eliav2.github.io/react-responsive-overflow-list/r/styles/base-vega/overflow-list.jsonSee shadcn/ui docs for usage.
In real apps, you’ll typically wrap OverflowList in your own component—design tokens, accessible menus, virtualization, or search. See 'Wrap & extend' below and the demo for a full wrapper example.
Minimal usage with an items array and render function.
import { OverflowList } from "react-responsive-overflow-list";
const items = ["One", "Two", "Three", "Four", "Five"];
export default function Example() {
return (
<OverflowList
items={items}
renderItem={(item) => <span style={{ padding: 4 }}>{item}</span>}
style={{ gap: 8 }} // root is display:flex; flex-wrap:wrap
maxRows={1}
/>
);
}Use children instead of items + renderItem.
<OverflowList style={{ gap: 8 }}>
<button>A</button>
<button>B</button>
<button>C</button>
<button>D</button>
</OverflowList>Provide your own overflow UI (button, menu, details/summary, etc.).
<OverflowList
items={items}
renderItem={(item) => <span>{item}</span>}
renderOverflow={(hidden) => <button>+{hidden.length} more</button>}
/>Render using a different HTML element via as.
<OverflowList as="nav">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</OverflowList>Trade visual smoothness vs peak performance during rapid resize.
<OverflowList
items={items}
renderItem={(item) => <span>{item}</span>}
flushImmediately={false} // uses rAF; faster under rapid resize, may flicker briefly
/>See the Flush Immediately example in the live demo.
| Prop | Type | Default | Notes |
|---|---|---|---|
items |
T[] |
— | Use with renderItem. Omit when using children. |
renderItem |
(item: T, index: number) => ReactNode |
— | How to render each item. |
children |
ReactNode |
— | Alternative to items + renderItem. |
as |
React.ElementType |
"div" |
Polymorphic root element. |
maxRows |
number |
1 |
Visible rows before overflow. |
maxVisibleItems |
number |
100 |
Hard cap on visible items. |
renderOverflow |
(hidden: T[]) => ReactNode |
default chip | Custom overflow UI. |
renderOverflowItem |
(item: T, i: number) => ReactNode |
renderItem |
For expanded lists/menus. |
renderOverflowProps |
Partial<OverflowElementProps<T>> |
— | Props for default overflow. |
flushImmediately |
boolean |
true |
true (flushSync, no flicker) vs false (rAF, faster under resize). |
renderItemVisibility |
(node: ReactNode, meta: RenderItemVisibilityMeta) => ReactNode |
internal | Control visibility of hidden items (defaults to React.Activity if available, otherwise simply return null). |
Styles: Root uses display:flex; flex-wrap:wrap; align-items:center;. Override via style/className.
Default overflow element: A tiny chip that renders +{count} more. Replace via renderOverflow.
Same measurement engine, bring your own layout. Use this when the <OverflowList /> render shape doesn't fit — e.g. a filter chip bar with custom slots, a layout that interleaves overflow logic with other elements, or any case where you want full control of the JSX.
Attach containerRef to your wrap container and overflowIndicatorRef to your +N element. Render all items during the "measuring" phase so widths are observable; unmount overflowed items (or use display:none / React.Activity) in the "normal" phase — they must not consume layout space, or the overflow indicator can't fit on the last row.
import { useOverflowList } from "react-responsive-overflow-list";
const items = ["One", "Two", "Three", "Four", "Five", "Six"];
export function CustomChipBar() {
const { containerRef, overflowIndicatorRef, visibleCount, hiddenCount, phase, showOverflow } =
useOverflowList<HTMLDivElement, HTMLSpanElement>({ itemCount: items.length, maxRows: 1 });
return (
<div ref={containerRef} style={{ display: "flex", flexWrap: "wrap", gap: 8, minWidth: 0 }}>
{items.map((item, index) => {
const visible = phase === "measuring" || index < visibleCount;
if (!visible) return null;
return <span key={index} style={{ padding: 4 }}>{item}</span>;
})}
{showOverflow && <span ref={overflowIndicatorRef}>+{hiddenCount} more</span>}
</div>
);
}| Option | Type | Default | Notes |
|---|---|---|---|
itemCount |
number |
— | Total items the consumer will render. |
maxRows |
number |
1 |
Visible rows before overflow. |
maxVisibleItems |
number |
100 |
Hard cap on visible items. |
flushImmediately |
boolean |
true |
true (flushSync, no flicker) vs false (rAF, smoother). |
| Property | Type | Notes |
|---|---|---|
containerRef |
React.RefObject<T | null> |
Attach to the flex-wrap container. |
overflowIndicatorRef |
React.RefObject<T | null> |
Attach to the +N indicator element so its width is measured. |
visibleCount |
number |
Final count of items that fit (already accounts for the indicator's reserved row). |
hiddenCount |
number |
itemCount - visibleCount. |
phase |
"normal" | "measuring" | "measuring-overflow-indicator" |
Gate item visibility on this — render all during "measuring". |
showOverflow |
boolean |
true when the overflow indicator should be rendered. |
It’s expected you’ll wrap OverflowList for product needs (design system styling, a11y menus, virtualization, search). for example:
-
Radix UI + Virtualization wrapper (search, large datasets, a11y, perf):
- Demo: see Radix UI + Virtualization in the live site
- Source
- Uses
@tanstack/react-virtualand the helpercreateLimitedRangeExtractor(...).
- Measure all items and compute how many fit within
maxRows. - Re-test with the overflow indicator; if it would create a new row, hide one more item.
- Render the stable “normal” state until container size changes.
flushImmediately=true → immediate, flicker-free (uses flushSync).
flushImmediately=false → defer with rAF; smoother under rapid resize, may flicker.
- Single wide item exceeding container width
maxRows/maxVisibleItemsrespected- Varying item widths, responsive content
- Multi-row overflow detection
Hidden items & React versions
- In React 19.2+, hidden items use
React.Activityso overflowed children stay mounted while toggling visibility. - In React 16–18, overflowed nodes unmount during measurement; pass
renderItemVisibilityif you need to keep custom elements or skeletons mounted and control visibility yourself.
- React ≥ 16.8 (hooks)
- Modern browsers with
ResizeObserver
MIT
