CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-virtuoso

A virtual scroll React component for efficiently rendering large scrollable lists, grids, tables, and feeds

Pending
Overview
Eval results
Files

grouped-lists.mddocs/

Grouped Lists

Virtualization component for lists with grouped data and sticky group headers. Perfect for categorized content like contact lists, file browsers, or any scenario where data needs to be organized into sections with persistent headers.

Capabilities

GroupedVirtuoso Component

Specialized virtualization component that renders grouped data with sticky group headers that remain visible while scrolling through group items.

/**
 * Virtualization component for grouped lists with sticky headers
 * @param props - Configuration options for the grouped virtualized list
 * @returns JSX.Element representing the grouped virtualized list
 */
function GroupedVirtuoso<D = any, C = any>(props: GroupedVirtuosoProps<D, C>): JSX.Element;

interface GroupedVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'itemContent' | 'totalCount'> {
  /** Specifies the amount of items in each group (and how many groups there are) */
  groupCounts?: number[];
  /** Specifies how each group header gets rendered */
  groupContent?: GroupContent<C>;
  /** Specifies how each item gets rendered with group context */
  itemContent?: GroupItemContent<D, C>;
  /** Use when implementing inverse infinite scrolling - decrease this value in combination with groupCounts changes */
  firstItemIndex?: number;
}

type GroupContent<C> = (index: number, context: C) => React.ReactNode;
type GroupItemContent<D, C> = (index: number, groupIndex: number, data: D, context: C) => React.ReactNode;

Usage Examples:

import React from 'react';
import { GroupedVirtuoso } from 'react-virtuoso';

// Basic grouped list
function ContactList() {
  const groups = [
    { letter: 'A', contacts: ['Alice', 'Andrew', 'Anna'] },
    { letter: 'B', contacts: ['Bob', 'Barbara', 'Ben'] },
    { letter: 'C', contacts: ['Charlie', 'Catherine', 'Chris'] },
  ];

  const groupCounts = groups.map(group => group.contacts.length);
  const items = groups.flatMap(group => 
    group.contacts.map(contact => ({ contact, letter: group.letter }))
  );

  return (
    <GroupedVirtuoso
      style={{ height: '400px' }}
      groupCounts={groupCounts}
      groupContent={(index) => (
        <div style={{ 
          padding: '8px 16px', 
          backgroundColor: '#f0f0f0', 
          fontWeight: 'bold',
          position: 'sticky',
          top: 0,
          zIndex: 1
        }}>
          {groups[index].letter}
        </div>
      )}
      itemContent={(index, groupIndex, item) => (
        <div style={{ padding: '12px 16px', borderBottom: '1px solid #eee' }}>
          {item.contact}
        </div>
      )}
      data={items}
    />
  );
}

// File browser with grouped folders
function FileBrowser() {
  const folders = [
    { 
      name: 'Documents', 
      files: ['report.pdf', 'notes.txt', 'presentation.pptx'] 
    },
    { 
      name: 'Images', 
      files: ['photo1.jpg', 'photo2.jpg', 'screenshot.png'] 
    },
    { 
      name: 'Downloads', 
      files: ['installer.exe', 'data.csv', 'backup.zip'] 
    }
  ];

  const groupCounts = folders.map(folder => folder.files.length);
  const files = folders.flatMap(folder => 
    folder.files.map(file => ({ file, folder: folder.name }))
  );

  return (
    <GroupedVirtuoso
      style={{ height: '500px' }}
      groupCounts={groupCounts}
      groupContent={(index) => (
        <div style={{ 
          padding: '12px 16px', 
          backgroundColor: '#e3f2fd', 
          fontWeight: 'bold',
          borderLeft: '4px solid #2196f3',
          display: 'flex',
          alignItems: 'center'
        }}>
          📁 {folders[index].name}
        </div>
      )}
      itemContent={(index, groupIndex, item) => (
        <div style={{ 
          padding: '8px 24px', 
          borderBottom: '1px solid #f5f5f5',
          display: 'flex',
          alignItems: 'center'
        }}>
          📄 {item.file}
        </div>
      )}
      data={files}
    />
  );
}

// Dynamic groups with infinite scrolling
function DynamicGroupedList() {
  const [groups, setGroups] = React.useState([
    { title: 'Today', items: ['Item 1', 'Item 2'] },
    { title: 'Yesterday', items: ['Item 3', 'Item 4', 'Item 5'] }
  ]);

  const loadMore = () => {
    setGroups(prev => [
      ...prev,
      {
        title: `Day ${prev.length + 1}`,
        items: Array.from({ length: 3 }, (_, i) => `Item ${prev.flatMap(g => g.items).length + i + 1}`)
      }
    ]);
  };

  const groupCounts = groups.map(group => group.items.length);
  const allItems = groups.flatMap((group, groupIndex) => 
    group.items.map(item => ({ item, groupTitle: group.title, groupIndex }))
  );

  return (
    <GroupedVirtuoso
      style={{ height: '400px' }}
      groupCounts={groupCounts}
      endReached={loadMore}
      groupContent={(index) => (
        <div style={{ 
          padding: '16px', 
          backgroundColor: '#fff3e0', 
          fontWeight: 'bold',
          borderBottom: '2px solid #ff9800'
        }}>
          {groups[index].title}
        </div>
      )}
      itemContent={(index, groupIndex, data) => (
        <div style={{ padding: '12px 24px' }}>
          {data.item}
        </div>
      )}
      data={allItems}
    />
  );
}

Group Content Rendering

Customize how group headers are rendered with full access to styling and interactivity.

/**
 * Callback function to render group headers
 * @param index - Zero-based index of the group
 * @param context - Additional context passed to the component
 * @returns React node representing the group header
 */
type GroupContent<C> = (index: number, context: C) => React.ReactNode;

Usage Example:

// Interactive group headers
<GroupedVirtuoso
  groupContent={(index) => (
    <div 
      style={{ 
        padding: '12px', 
        backgroundColor: '#f5f5f5',
        cursor: 'pointer',
        userSelect: 'none'
      }}
      onClick={() => console.log(`Clicked group ${index}`)}
    >
      <strong>Group {index + 1}</strong>
      <span style={{ float: 'right' }}>({groupCounts[index]} items)</span>
    </div>
  )}
  // ... other props
/>

Group Item Content Rendering

Render individual items within groups with access to both item and group context.

/**
 * Callback function to render items within groups
 * @param index - Zero-based index of the item within the entire list
 * @param groupIndex - Zero-based index of the group containing this item
 * @param data - The data item to render
 * @param context - Additional context passed to the component
 * @returns React node representing the item
 */
type GroupItemContent<D, C> = (
  index: number, 
  groupIndex: number, 
  data: D, 
  context: C
) => React.ReactNode;

Usage Example:

// Item rendering with group awareness
<GroupedVirtuoso
  itemContent={(index, groupIndex, data) => (
    <div style={{ 
      padding: '8px 16px',
      backgroundColor: groupIndex % 2 === 0 ? '#fff' : '#f9f9f9'
    }}>
      <span>Item {index}</span>
      <small style={{ color: '#666' }}>Group {groupIndex}</small>
      <div>{data.content}</div>
    </div>
  )}
  // ... other props
/>

Inverse Scrolling for Groups

Support for prepending groups at the top of the list, useful for chat applications or infinite scrolling scenarios.

interface GroupedVirtuosoProps<D, C> {
  /**
   * Use when implementing inverse infinite scrolling - decrease this value
   * in combination with groupCounts changes to prepend groups to the top
   * 
   * The delta should equal the amount of new items introduced, excluding groups themselves.
   * Example: prepending 2 groups with 20 and 30 items each requires decreasing by 50.
   * 
   * Warning: firstItemIndex should be a positive number based on total items
   */
  firstItemIndex?: number;
}

Usage Example:

function InverseGroupedList() {
  const [messages, setMessages] = React.useState([
    { date: '2023-12-01', msgs: ['Hello', 'How are you?'] },
    { date: '2023-12-02', msgs: ['Good morning', 'Ready for work'] }
  ]);
  const [firstIndex, setFirstIndex] = React.useState(1000);

  const loadOlderMessages = () => {
    const newMessages = [
      { date: '2023-11-30', msgs: ['Previous day message 1', 'Previous day message 2'] }
    ];
    
    const newItemCount = newMessages.reduce((sum, group) => sum + group.msgs.length, 0);
    
    setMessages(prev => [...newMessages, ...prev]);
    setFirstIndex(prev => prev - newItemCount);
  };

  return (
    <GroupedVirtuoso
      firstItemIndex={firstIndex}
      startReached={loadOlderMessages}
      groupCounts={messages.map(day => day.msgs.length)}
      groupContent={(index) => (
        <div style={{ padding: '8px', backgroundColor: '#e8f5e8', textAlign: 'center' }}>
          {messages[index].date}
        </div>
      )}
      itemContent={(index, groupIndex, data) => (
        <div style={{ padding: '8px 16px' }}>
          {data}
        </div>
      )}
      data={messages.flatMap(day => day.msgs)}
    />
  );
}

Custom Group Components

Fully customize the appearance and behavior of group elements through the components prop.

interface Components<Data, Context> {
  /** Set to customize the group item wrapping element */
  Group?: React.ComponentType<GroupProps & ContextProp<Context>>;
}

type GroupProps = Pick<React.ComponentProps<'div'>, 'children' | 'style'> & {
  'data-index': number;
  'data-item-index': number;
  'data-known-size': number;
};

Usage Example:

<GroupedVirtuoso
  components={{
    Group: ({ children, ...props }) => (
      <div 
        {...props} 
        style={{
          ...props.style,
          borderRadius: '8px',
          margin: '4px',
          overflow: 'hidden',
          boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
        }}
      >
        {children}
      </div>
    )
  }}
  // ... other props
/>

Types

interface GroupItem<D> extends Item<D> {
  originalIndex?: number;
  type: 'group';
}

interface RecordItem<D> extends Item<D> {
  data?: D;
  groupIndex?: number;
  originalIndex?: number;
  type?: undefined;
}

type ListItem<D> = GroupItem<D> | RecordItem<D>;

interface GroupIndexLocationWithAlign extends LocationOptions {
  groupIndex: number;
}

interface GroupedScrollIntoViewLocation extends ScrollIntoViewLocationOptions {
  groupIndex: number;
}

Install with Tessl CLI

npx tessl i tessl/npm-react-virtuoso

docs

component-handles.md

grid-virtualization.md

grouped-lists.md

index.md

list-virtualization.md

table-virtualization.md

tile.json