Skip to content

SincerelyFaust/react-fit-list

Repository files navigation

react-fit-list

npm version

Demo

👉 Live demo: https://sincerelyfaust.github.io/react-fit-list/

📦 npm: https://www.npmjs.com/package/react-fit-list

react-fit-list is a headless React utility for responsive lists that collapse overflowing items into a customizable disclosure.

It ships with:

  • a ready-to-use <FitList /> component
  • a headless useFitList() hook for custom renderers
  • TypeScript types for component and hook APIs

Installation

npm install react-fit-list

Quick start

import { FitList } from 'react-fit-list'

const items = [
  { id: 1, label: 'Security' },
  { id: 2, label: 'Startups' },
  { id: 3, label: 'Fintech' },
  { id: 4, label: 'B2B SaaS' },
]

export function Example() {
  return (
    <div style={{ width: 240 }}>
      <FitList
        items={items}
        getItemKey={(item) => item.id}
        renderItem={(item) => (
          <span
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              border: '1px solid #d0d7de',
              borderRadius: 999,
              padding: '4px 10px',
              fontSize: 12,
              whiteSpace: 'nowrap',
            }}
          >
            {item.label}
          </span>
        )}
      />
    </div>
  )
}

Component API

<FitList />

Use the component when you want the library to handle the layout, hidden measurement nodes, and default disclosure rendering for you.

Prop Type Default Description
items readonly T[] Items to fit into a single row.
getItemKey (item, index) => React.Key Returns a stable React key for each item.
renderItem (item, index) => React.ReactNode Renders a single item.
renderDisclosure (args) => React.ReactNode +hiddenCount button Renders the control shown when items are hidden. Return a custom button or menu trigger here when you need custom behavior.
className string Class applied to the root row.
listClassName string Class applied to the visible-items wrapper.
itemClassName string Class applied to each visible item wrapper.
disclosureClassName string Class applied to the default disclosure button.
rootProps HTMLAttributes<HTMLDivElement> Props spread onto the root container. Useful for roles, labels, data attributes, and event handlers.
listProps HTMLAttributes<HTMLDivElement> Props spread onto the visible-items wrapper.
itemProps HTMLAttributes<HTMLDivElement> | ((item, index) => HTMLAttributes<HTMLDivElement>) Props spread onto each visible item wrapper.
disclosureWrapperProps HTMLAttributes<HTMLDivElement> Props spread onto the disclosure wrapper.
sizerClassName string itemClassName Class applied to hidden measurement nodes when sizing depends on matching CSS.
emptyFallback React.ReactNode null Content rendered when items is empty.
spacing number 8 Space between items and the disclosure.
trimFrom 'end' | 'start' 'end' Which side of the list hides items first.
disclosurePlacement 'edge' | 'adjacent' 'edge' Keep the disclosure at the row edge or place it next to the trimmed side.
maxVisibleItems number Caps how many items may be shown while the list is closed, even when more items would fit.
reserveDisclosureSpace boolean false Reserve room for the disclosure even when everything fits.
disclosureWidth number auto Fixed disclosure width in pixels. Useful when the control size is known.
measureDisclosureWidth (hiddenCount: number) => number Custom disclosure width measurement callback. Useful when a custom disclosure label changes size as the hidden count changes.
estimateItemWidth number | ((item, index) => number) fallback 96 Width estimate used in estimated mode or before actual measurements are available.
measurementMode 'actual' | 'estimated' 'actual' Width calculation strategy.
open boolean uncontrolled Controlled open state.
defaultOpen boolean false Initial open state for uncontrolled usage.
onOpenChange (open: boolean) => void Called when open state changes.

Disclosure render args

renderDisclosure receives the current fit state plus open-state helpers:

{
  hiddenCount: number
  hiddenItems: T[]
  visibleItems: T[]
  closedVisibleItems: T[]
  closedHiddenItems: T[]
  isOverflowing: boolean
  isOpen: boolean
  setOpen: (open: boolean) => void
  toggleOpen: () => void
}

A custom disclosure can render its own button and event handling. hiddenCount and hiddenItems describe the closed overflow segment, so they stay available even while the list is open:

<FitList
  items={items}
  getItemKey={(item) => item.id}
  renderItem={(item) => <Tag>{item.label}</Tag>}
  renderDisclosure={({ hiddenCount, hiddenItems }) => (
    <button onClick={() => console.log(hiddenItems)}>
      Show {hiddenCount} more
    </button>
  )}
/>

Hook API

useFitList()

Use the hook when you want the fitting calculation but need full control over markup.

import { useFitList } from 'react-fit-list'

const fit = useFitList({
  items,
  getItemKey: (item) => item.id,
  spacing: 8,
})

Options

Option Type Default Description
items readonly T[] Items to measure.
getItemKey (item, index) => React.Key Returns a stable React key for each item.
spacing number 8 Space between items and disclosure.
trimFrom 'end' | 'start' 'end' Which side hides items first.
maxVisibleItems number Caps how many items may be visible while closed.
reserveDisclosureSpace boolean false Reserve disclosure space even when all items fit.
disclosureWidth number auto Fixed disclosure width in pixels.
estimateItemWidth number | ((item, index) => number) fallback 96 Width estimate for estimated mode.
measurementMode 'actual' | 'estimated' 'actual' Width calculation strategy.
open boolean uncontrolled Controlled open state.
defaultOpen boolean false Initial open state.
onOpenChange (open: boolean) => void Called whenever open state changes.
measureDisclosureWidth (hiddenCount: number) => number Custom disclosure width measurement callback.

Return value

Field Type Description
containerRef RefObject<HTMLDivElement | null> Attach to the outer container.
registerItem (key) => (node) => void Registers visible item nodes for measurement.
registerMeasureItem (key) => (node) => void Registers hidden measurement nodes.
registerDisclosure (node) => void Registers the disclosure node.
visibleItems T[] Items currently visible in the rendered row. When open, this contains all items.
hiddenItems T[] Items currently hidden in the rendered row. When open, this is empty.
hiddenCount number Number of currently hidden rendered items. When open, this is 0.
closedVisibleItems T[] Items that fit while the list is closed.
closedHiddenItems T[] Items that overflow while the list is closed.
closedHiddenCount number Number of items that overflow while the list is closed.
isOverflowing boolean Whether the closed list has overflow items.
isOpen boolean Whether the list is open.
setOpen (open: boolean) => void Sets open state directly.
toggleOpen () => void Toggles open state.
recompute () => void Re-runs the fit calculation immediately using current measurements.

Accessibility and semantic markup

FitList is headless and does not force list semantics, but you can add them with pass-through props:

<FitList
  items={items}
  getItemKey={(item) => item.id}
  renderItem={(item) => <Tag>{item.label}</Tag>}
  rootProps={{ role: 'list', 'aria-label': 'Selected filters' }}
  itemProps={{ role: 'listitem' }}
/>

The default disclosure is a button with aria-expanded and an accessible label. It renders +N while closed and Show less while open.

Choosing a measurement mode

Use measurementMode="actual" when item widths depend on text, fonts, or CSS. This is the most accurate option and uses hidden measurement nodes.

Use measurementMode="estimated" when you know roughly how wide items are and want cheaper calculations. The fit calculation uses prefix sums internally, so recomputing which items fit is linear in the number of items instead of repeatedly summing visible slices:

<FitList
  items={items}
  getItemKey={(item) => item.id}
  renderItem={(item) => <Tag>{item.label}</Tag>}
  measurementMode="estimated"
  estimateItemWidth={(item) => item.label.length * 8 + 24}
/>

SSR and hydration

On the server, react-fit-list renders from the data you provide and measures after hydration in the browser. Use measurementMode="estimated" with estimateItemWidth when you prefer predictable first-pass sizing over DOM measurement.

Notes

The component renders a visually hidden measurement tree so item widths can be calculated without changing the visible layout. Keep renderItem deterministic and use sizerClassName when your item width depends on CSS classes that are not already applied through itemClassName.

Releases

No releases published

Packages

 
 
 

Contributors

Languages