Skip to content

About

Built-in keyboard navigation, selection management, ARIA support, and customizable rendering. Lightweight and highly performant.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

react-generic-list

A generic, accessible, and performant list component for React 19 — written in TypeScript with full type safety, keyboard navigation, selection management, and optional virtualization built in.


Features

  • Generic & type-safe — works with any data shape via TypeScript generics
  • Accessible by default — semantic ul/li with ARIA roles, labels, and keyboard navigation (WAI-ARIA listbox/list patterns)
  • Flexible rendering — custom render functions, custom wrappers, and configurable item elements
  • Selection management — controlled or uncontrolled selection out of the box
  • Keyboard navigation — Arrow keys, Home/End, Enter/Space, Tab, with optional looping
  • Scroll management — auto-scrolls to selected or focused items
  • Virtualization hook — handles large lists (> 100 items by default) automatically
  • Composable hooks — useListSelection, useListKeyboard, useListScroll are all individually exported
  • Dual output mode — semantic (ul/li) or generic (div/div)
  • Zero runtime dependencies — peer-deps are only react and react-dom
  • ESM + UMD — ships both module formats with TypeScript declarations

Installation

npm install react-generic-list
# or
yarn add react-generic-list
# or
pnpm add react-generic-list

Peer dependencies (install separately if not already present):

npm install react@^19.0.0 react-dom@^19.0.0

Quick Start

import { List } from "react-generic-list";

type User = { id: number; name: string; email: string };

const users: User[] = [
  { id: 1, name: "Alice", email: "alice@example.com" },
  { id: 2, name: "Bob", email: "bob@example.com" },
];

export default function App() {
  return (
    <List
      items={users}
      keyExtractor={(user) => user.id}
      render={(user) => (
        <span>
          {user.name} — {user.email}
        </span>
      )}
    />
  );
}

Props (ListProps<T>)

Core

Prop Type Required Description
items T[] ✅ Array of items to render
keyExtractor (item: T, index: number) => string | number ✅ Unique key for each item
render (item: T, index: number) => ReactNode ✅ Render function for each item

Mode & Custom Wrappers

Prop Type Default Description
mode "semantic" | "generic" "semantic" "semantic" renders ul/li; "generic" renders div/div
wrapper ElementType — Overrides the container element entirely
itemWrapper ElementType — Overrides the item element entirely

Styling

Prop Type Default Description
className string — CSS class for the container
style CSSProperties — Inline styles for the container
id string — id attribute on the container
childProps ComponentProps<"li"> | ((item: T, index: number) => ComponentProps<"li">) — Static or dynamic props applied to every item element
preserveStyles boolean true Removes default list-style-type in semantic mode

Selection

Prop Type Default Description
onItemSelect (item: T, index: number) => void — Callback when an item is selected. Providing this makes the list selectable
selectedItem T | null — Controlled selected item

State

Prop Type Default Description
loading boolean false Shows loading state
loadingComponent ReactNode "Loading..." Custom loading UI
emptyMessage string "No items to display" Message shown when items is empty
emptyComponent ReactNode — Custom empty state UI

Keyboard Navigation

Prop Type Default Description
keyboardNavigation boolean true Enable/disable keyboard navigation
loopNavigation boolean true Whether arrow keys wrap around at the ends
scrollIntoViewOnSelect boolean true Auto-scroll to selected item

Accessibility

Prop Type Default Description
ariaLabel string "list" aria-label for the container
ariaLabelledBy string — aria-labelledby for the container

Examples

Controlled selection

const [selected, setSelected] = useState<User | null>(null);

<List
  items={users}
  keyExtractor={(u) => u.id}
  render={(u) => <span>{u.name}</span>}
  selectedItem={selected}
  onItemSelect={(user) => setSelected(user)}
/>;

Custom loading & empty states

<List
  items={[]}
  keyExtractor={(u) => u.id}
  render={(u) => <span>{u.name}</span>}
  loading={isLoading}
  loadingComponent={<Spinner />}
  emptyComponent={<p>No users found.</p>}
/>

Generic (non-semantic) mode with custom wrapper

<List
  items={items}
  keyExtractor={(i) => i.id}
  render={(i) => <Card {...i} />}
  mode="generic"
  className="card-grid"
/>

Per-item dynamic props

<List
  items={items}
  keyExtractor={(i) => i.id}
  render={(i) => <span>{i.label}</span>}
  childProps={(item, index) => ({
    className: item.isActive ? "active" : "",
    "data-index": index,
  })}
/>

Disable keyboard navigation

<List
  items={items}
  keyExtractor={(i) => i.id}
  render={(i) => <span>{i.name}</span>}
  keyboardNavigation={false}
/>

Exported Hooks

The package exports three standalone hooks for building custom list UIs.

useListSelection<T>

Manages controlled/uncontrolled item selection.

import { useListSelection } from "react-generic-list";

const { selectedItem, isSelected, handleItemSelect, clearSelection } =
  useListSelection({
    items,
    keyExtractor: (item) => item.id,
    onItemSelect: (item, index) => console.log(item),
    selectedItem: externalValue, // optional, for controlled usage
  });

Returns:

Key Type Description
selectedItem T | null Currently selected item
selectedIndex number Index of selected item (-1 if none)
isSelected (item, index) => boolean Check if an item is selected
handleItemSelect (item, index) => void Select an item
clearSelection () => void Clear current selection

useListKeyboard<T>

Manages keyboard-driven focus and selection within a list.

import { useListKeyboard } from "react-generic-list";

const {
  focusedIndex,
  handleKeyDown,
  handleItemKeyDown,
  handleItemFocus,
  resetFocus,
} = useListKeyboard({
  items,
  onItemSelect: (item, index) => select(item),
  enabled: true,
  loopNavigation: true,
});

Supported keys: ArrowDown, ArrowUp, Home, End, Enter, Space, Tab

Returns:

Key Type Description
focusedIndex number Currently focused item index
setFocusedIndex (index) => void Programmatically set focus
handleKeyDown KeyboardEventHandler<HTMLUListElement> Attach to the container
handleItemKeyDown (e, item, index) => void Attach to individual items
handleItemFocus (index) => void Called on item focus
resetFocus () => void Reset focused index to -1

useListScroll

Scrolls the list container to a specific item index.

import { useListScroll } from "react-generic-list";

const { containerRef, scrollToIndex, scrollToSelected } = useListScroll({
  scrollIntoViewOnFocus: true,
  scrollBehavior: "smooth",
});

Returns:

Key Type Description
containerRef RefObject<HTMLUListElement> Attach to the list container
scrollToIndex (index: number) => void Scroll to a specific index
scrollToSelected (selectedIndex: number) => void Scroll to selected index (no-op if < 0)

Virtualization (Internal)

The library includes a useListVirtualization hook (internal, not exported) that automatically activates when the list contains more than 100 items. It uses ResizeObserver and scroll events to compute a visible window, renders only the visible items plus an overscan buffer, and maintains correct total height for the scrollbar.

If you need direct access for advanced use cases, copy the hook from src/hooks/useListVirtualization.ts.


Keyboard Navigation Reference

Key Action
↓ ArrowDown Move focus to next item (wraps with loopNavigation)
↑ ArrowUp Move focus to previous item (wraps with loopNavigation)
Home Move focus to first item
End Move focus to last item
Enter / Space Select focused item
Tab Exit list, reset focus

Accessibility

  • Container renders as <ul role="listbox"> when onItemSelect is provided, otherwise <ul role="list">
  • Each item gets role="option" (selectable) or role="listitem"
  • Selected items receive aria-selected="true" and aria-current="true"
  • Items are auto-labelled as "<ariaLabel> item <n>"
  • In generic mode, ARIA roles are intentionally omitted to avoid incorrect semantics

TypeScript

All props and hook signatures are fully typed. The List component is generic over your item type T — no casting needed:

// TypeScript infers T as User automatically
<List<User>
  items={users}
  keyExtractor={(u) => u.id}
  render={(u) => <span>{u.name}</span>}
/>

License

MIT © Usama Imran

About

Built-in keyboard navigation, selection management, ARIA support, and customizable rendering. Lightweight and highly performant.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages