React Responsive Overflow List

A responsive component that automatically handles overflow items

View on GitHub

Basic Example

Simple list with default overflow element

<OverflowList
  items={fruits}
  renderItem={(item, index) => (
    <span key={index} className="fruit-item">
      {item}
    </span>
  )}
  style={{ gap: "8px" }}
/>
AppleBananaCherryDateElderberryFigGrapeHoneydewKiwiLemon

Children Pattern

Using children instead of items array

<OverflowList>
  <button>Action 1</button>
  <button>Action 2</button>
  ...
</OverflowList>

Multi-row Example

Allow up to 2 rows before overflow

<OverflowList
  items={fruits.concat(tags).concat(menuItems)}
  renderItem={(item) => <span className="multi-item">{item}</span>}
  maxRows={2}
  style={{ gap: "4px" }}
/>
AppleBananaCherryDateElderberryFigGrapeHoneydewKiwiLemonReactTypeScriptCSSHTMLJavaScriptNode.jsExpressMongoDBViteESLintHomeAboutServicesPortfolioBlogContactCareersSupport

Custom Overflow Element

You would generally want to provide your own overflow element

Simple Overflow Function

<OverflowList
  items={tags}
  renderItem={(tag) => <span className="tag">#{tag}</span>}
  renderOverflow={(items) => <div>{items.length} items are hidden</div>}
  maxRows={1}
  style={{ gap: "6px" }}
/>
#React#TypeScript#CSS#HTML#JavaScript#Node.js#Express#MongoDB#Vite#ESLint

ForwardRef Component

When you provide a custom overflow component, it should be a forwardRef component to ensure proper ref forwarding for measurements.

// Custom overflow component with forwardRef
const CustomOverflowElement = React.forwardRef<HTMLDivElement, { items: string[] }>(
  ({ items }, ref) => (
    <div ref={ref} className="custom-overflow">
      <span>+{items.length} more items</span>
    </div>
  )
);

<OverflowList
  items={tags}
  renderItem={(tag) => <span className="tag">#{tag}</span>}
  renderOverflow={(items) => <CustomOverflowElement items={items} />}
  maxRows={1}
  style={{ gap: "6px" }}
/>
#React#TypeScript#CSS#HTML#JavaScript#Node.js#Express#MongoDB#Vite#ESLint

Radix UI Dropdown Menu

Advanced example using Radix UI's dropdown menu with proper accessibility and keyboard navigation.

<OverflowList
  items={tags}
  renderItem={(tag) => <span className="tag">#{tag}</span>}
  renderOverflow={(items) => <RadixOverflowMenu items={items} />}
  maxRows={1}
  style={{ gap: "6px" }}
/>

// Implementation:
import React from "react";
import { DropdownMenu } from "@radix-ui/themes";

const RadixOverflowMenu = React.forwardRef<HTMLButtonElement, { items: string[] }>(({ items }, ref) => (
  <DropdownMenu.Root modal={false}>
    <DropdownMenu.Trigger>
      <button ref={ref} className="demo-button demo-button--primary">
        +{items.length} more
      </button>
    </DropdownMenu.Trigger>
    <DropdownMenu.Content>
      {items.map((item, index) => (
        <DropdownMenu.Item key={index}>#{item}</DropdownMenu.Item>
      ))}
    </DropdownMenu.Content>
  </DropdownMenu.Root>
));

export { RadixOverflowMenu };
#React#TypeScript#CSS#HTML#JavaScript#Node.js#Express#MongoDB#Vite#ESLint

Virtualized Radix UI Dropdown (10,000+ Items)

Advanced example combining Radix UI with TanStack Virtual for handling thousands of items efficiently. The maxVisibleItems prop (default: 100) ensures that only the first N items are rendered in the main list, while everything beyond that threshold gets moved to the overflow dropdown.

#Item 1#Item 2#Item 3#Item 4#Item 5#Item 6#Item 7#Item 8#Item 9#Item 10#Item 11#Item 12#Item 13#Item 14#Item 15#Item 16#Item 17#Item 18#Item 19#Item 20#Item 21#Item 22#Item 23#Item 24#Item 25#Item 26#Item 27#Item 28#Item 29#Item 30#Item 31#Item 32#Item 33#Item 34#Item 35#Item 36#Item 37#Item 38#Item 39#Item 40#Item 41#Item 42#Item 43#Item 44#Item 45#Item 46#Item 47#Item 48#Item 49#Item 50#Item 51#Item 52#Item 53#Item 54#Item 55#Item 56#Item 57#Item 58#Item 59#Item 60#Item 61#Item 62#Item 63#Item 64#Item 65#Item 66#Item 67#Item 68#Item 69#Item 70#Item 71#Item 72#Item 73#Item 74#Item 75#Item 76#Item 77#Item 78#Item 79#Item 80#Item 81#Item 82#Item 83#Item 84#Item 85#Item 86#Item 87#Item 88#Item 89#Item 90#Item 91#Item 92#Item 93#Item 94#Item 95#Item 96#Item 97#Item 98#Item 99#Item 100
Note: This example demonstrates how the maxVisibleItems prop (default: 100) ensures only the first 100 items are rendered in the main list, even on the initial render. The remaining 9,900 items are efficiently handled by the virtualized dropdown, which only renders the visible portion of the scrollable list.

Performance Tip: Lower maxVisibleItems values provide better performance (fewer DOM nodes on initial render), while higher values allow more items to be visible before overflowing but with reduced performance. The default of 100 provides a good balance between performance and usability.

Custom Host Element

Using the 'as' prop to render as different HTML elements

<OverflowList as="nav" style={{ gap: "8px" }}>
  <a href="#home">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
</OverflowList>

Extending OverflowList

This is an example implementation showing how to wrap OverflowList with Radix UI dropdown and virtualization. In real-world applications, it's expected that you'll wrap OverflowList with your own components tailored to your specific needs, design system, etc.

import { RadixVirtualizedOverflowList } from "../components/RadixVirtualizedOverflowList";

// Small dataset - uses simple dropdown
<RadixVirtualizedOverflowList
  items={tags}
  renderItem={(tag) => <span className="tag">#{tag}</span>}
  style={{ gap: "6px" }}
/>

// Large dataset - automatically uses virtualization
<RadixVirtualizedOverflowList
  items={Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`)}
  renderItem={(item) => <span className="tag">#{item}</span>}
  virtualizationThreshold={50}
  enableSearch={true}
  maxRows={1}
  style={{ gap: "6px" }}
/>

Small Dataset (Simple Dropdown)

#React#TypeScript#CSS#HTML#JavaScript#Node.js#Express#MongoDB#Vite#ESLint

Large Dataset (Virtualized Dropdown with Search)

Controls how many rows of items are visible before overflow
#Item 1#Item 2#Item 3#Item 4#Item 5#Item 6#Item 7#Item 8#Item 9#Item 10#Item 11#Item 12#Item 13#Item 14#Item 15#Item 16#Item 17#Item 18#Item 19#Item 20#Item 21#Item 22#Item 23#Item 24#Item 25#Item 26#Item 27#Item 28#Item 29#Item 30#Item 31#Item 32#Item 33#Item 34#Item 35#Item 36#Item 37#Item 38#Item 39#Item 40#Item 41#Item 42#Item 43#Item 44#Item 45#Item 46#Item 47#Item 48#Item 49#Item 50#Item 51#Item 52#Item 53#Item 54#Item 55#Item 56#Item 57#Item 58#Item 59#Item 60#Item 61#Item 62#Item 63#Item 64#Item 65#Item 66#Item 67#Item 68#Item 69#Item 70#Item 71#Item 72#Item 73#Item 74#Item 75#Item 76#Item 77#Item 78#Item 79#Item 80#Item 81#Item 82#Item 83#Item 84#Item 85#Item 86#Item 87#Item 88#Item 89#Item 90#Item 91#Item 92#Item 93#Item 94#Item 95#Item 96#Item 97#Item 98#Item 99#Item 100
This example demonstrates:
  • Automatic virtualization: Switches to virtualized dropdown when item count exceeds threshold
  • Search functionality: Built-in search/filter for large datasets
  • Radix UI integration: Full accessibility and keyboard navigation support
  • Customizable: Configurable thresholds, styling, and behavior
  • Performance optimized: Efficient rendering for thousands of items

Note: This is just an example implementation. In real-world applications, it's expected that you'll wrap OverflowList with your own components tailored to your specific needs and design system.
Source: View implementation on GitHub

Flush Immediately Example

Control how updates are applied when the container resizes.flushImmediately=false(default: true)

<OverflowList
  items={fruits.concat(tags)}
  renderItem={(item) => <span className="multi-item">{item}</span>}
  flushImmediately={false}
  style={{ gap: "4px" }}
/>
Disabled (better performance)

Trade-offs:

  • flushImmediately=true: Updates are applied immediately using flushSync, avoiding flickering but may impact performance
  • flushImmediately=false: Updates are applied in the requestAnimationFrame callback, avoiding forced reflow and improving performance but may cause slight flickering
  • Default behavior: flushImmediately is true by default to prioritize smooth visual experience
Resize quickly below to observe the difference!
AppleBananaCherryDateElderberryFigGrapeHoneydewKiwiLemon

One Item Wider Than Container

What happens when a single item is wider than the available container space? The component gracefully handles this by showing only the overflow element.

<OverflowList
  items={["This is a very long item that exceeds container width"]}
  renderItem={(item) => (
    <span style={{ whiteSpace: "nowrap", padding: "8px 16px", backgroundColor: "#e3f2fd" }}>
      {item}
    </span>
  )}
  style={{ gap: "8px", width: "200px" }}
/>
This is a very long item that exceeds container width
Behavior: When a single item is wider than the container, the component shows only the overflow element (the "+X more" button), ensuring the layout remains stable.

Reverse Order Example

Compare normal overflow (shrinks from end) vs reverse overflow (shrinks from start)

// Normal overflow - shrinks from end
<OverflowList
  items={tags}
  renderItem={(item, index) => (
    <span key={index} className="tag-item">
      {item}
    </span>
  )}
  style={{ gap: "8px" }}
/>

// Reverse overflow - shrinks from start
<OverflowList
  items={[...tags].reverse()}
  renderItem={(item, index) => (
    <span key={index} className="tag-item">
      {item}
    </span>
  )}
  style={{ 
    gap: "8px",
    flexDirection: "row-reverse",
    justifyContent: "flex-end"
  }}
/>

Normal Overflow (shrinks from end)

ReactTypeScriptCSSJavaScriptHTMLNode.jsExpressMongoDBPostgreSQLRedis

Reverse Overflow (shrinks from start)

RedisPostgreSQLMongoDBExpressNode.jsHTMLJavaScriptCSSTypeScriptReact