or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

component-handles.mdgrid-virtualization.mdgrouped-lists.mdindex.mdlist-virtualization.mdtable-virtualization.md

grouped-lists.mddocs/

0

# Grouped Lists

1

2

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.

3

4

## Capabilities

5

6

### GroupedVirtuoso Component

7

8

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

9

10

```typescript { .api }

11

/**

12

* Virtualization component for grouped lists with sticky headers

13

* @param props - Configuration options for the grouped virtualized list

14

* @returns JSX.Element representing the grouped virtualized list

15

*/

16

function GroupedVirtuoso<D = any, C = any>(props: GroupedVirtuosoProps<D, C>): JSX.Element;

17

18

interface GroupedVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'itemContent' | 'totalCount'> {

19

/** Specifies the amount of items in each group (and how many groups there are) */

20

groupCounts?: number[];

21

/** Specifies how each group header gets rendered */

22

groupContent?: GroupContent<C>;

23

/** Specifies how each item gets rendered with group context */

24

itemContent?: GroupItemContent<D, C>;

25

/** Use when implementing inverse infinite scrolling - decrease this value in combination with groupCounts changes */

26

firstItemIndex?: number;

27

}

28

29

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

30

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

31

```

32

33

**Usage Examples:**

34

35

```typescript

36

import React from 'react';

37

import { GroupedVirtuoso } from 'react-virtuoso';

38

39

// Basic grouped list

40

function ContactList() {

41

const groups = [

42

{ letter: 'A', contacts: ['Alice', 'Andrew', 'Anna'] },

43

{ letter: 'B', contacts: ['Bob', 'Barbara', 'Ben'] },

44

{ letter: 'C', contacts: ['Charlie', 'Catherine', 'Chris'] },

45

];

46

47

const groupCounts = groups.map(group => group.contacts.length);

48

const items = groups.flatMap(group =>

49

group.contacts.map(contact => ({ contact, letter: group.letter }))

50

);

51

52

return (

53

<GroupedVirtuoso

54

style={{ height: '400px' }}

55

groupCounts={groupCounts}

56

groupContent={(index) => (

57

<div style={{

58

padding: '8px 16px',

59

backgroundColor: '#f0f0f0',

60

fontWeight: 'bold',

61

position: 'sticky',

62

top: 0,

63

zIndex: 1

64

}}>

65

{groups[index].letter}

66

</div>

67

)}

68

itemContent={(index, groupIndex, item) => (

69

<div style={{ padding: '12px 16px', borderBottom: '1px solid #eee' }}>

70

{item.contact}

71

</div>

72

)}

73

data={items}

74

/>

75

);

76

}

77

78

// File browser with grouped folders

79

function FileBrowser() {

80

const folders = [

81

{

82

name: 'Documents',

83

files: ['report.pdf', 'notes.txt', 'presentation.pptx']

84

},

85

{

86

name: 'Images',

87

files: ['photo1.jpg', 'photo2.jpg', 'screenshot.png']

88

},

89

{

90

name: 'Downloads',

91

files: ['installer.exe', 'data.csv', 'backup.zip']

92

}

93

];

94

95

const groupCounts = folders.map(folder => folder.files.length);

96

const files = folders.flatMap(folder =>

97

folder.files.map(file => ({ file, folder: folder.name }))

98

);

99

100

return (

101

<GroupedVirtuoso

102

style={{ height: '500px' }}

103

groupCounts={groupCounts}

104

groupContent={(index) => (

105

<div style={{

106

padding: '12px 16px',

107

backgroundColor: '#e3f2fd',

108

fontWeight: 'bold',

109

borderLeft: '4px solid #2196f3',

110

display: 'flex',

111

alignItems: 'center'

112

}}>

113

πŸ“ {folders[index].name}

114

</div>

115

)}

116

itemContent={(index, groupIndex, item) => (

117

<div style={{

118

padding: '8px 24px',

119

borderBottom: '1px solid #f5f5f5',

120

display: 'flex',

121

alignItems: 'center'

122

}}>

123

πŸ“„ {item.file}

124

</div>

125

)}

126

data={files}

127

/>

128

);

129

}

130

131

// Dynamic groups with infinite scrolling

132

function DynamicGroupedList() {

133

const [groups, setGroups] = React.useState([

134

{ title: 'Today', items: ['Item 1', 'Item 2'] },

135

{ title: 'Yesterday', items: ['Item 3', 'Item 4', 'Item 5'] }

136

]);

137

138

const loadMore = () => {

139

setGroups(prev => [

140

...prev,

141

{

142

title: `Day ${prev.length + 1}`,

143

items: Array.from({ length: 3 }, (_, i) => `Item ${prev.flatMap(g => g.items).length + i + 1}`)

144

}

145

]);

146

};

147

148

const groupCounts = groups.map(group => group.items.length);

149

const allItems = groups.flatMap((group, groupIndex) =>

150

group.items.map(item => ({ item, groupTitle: group.title, groupIndex }))

151

);

152

153

return (

154

<GroupedVirtuoso

155

style={{ height: '400px' }}

156

groupCounts={groupCounts}

157

endReached={loadMore}

158

groupContent={(index) => (

159

<div style={{

160

padding: '16px',

161

backgroundColor: '#fff3e0',

162

fontWeight: 'bold',

163

borderBottom: '2px solid #ff9800'

164

}}>

165

{groups[index].title}

166

</div>

167

)}

168

itemContent={(index, groupIndex, data) => (

169

<div style={{ padding: '12px 24px' }}>

170

{data.item}

171

</div>

172

)}

173

data={allItems}

174

/>

175

);

176

}

177

```

178

179

### Group Content Rendering

180

181

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

182

183

```typescript { .api }

184

/**

185

* Callback function to render group headers

186

* @param index - Zero-based index of the group

187

* @param context - Additional context passed to the component

188

* @returns React node representing the group header

189

*/

190

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

191

```

192

193

**Usage Example:**

194

195

```typescript

196

// Interactive group headers

197

<GroupedVirtuoso

198

groupContent={(index) => (

199

<div

200

style={{

201

padding: '12px',

202

backgroundColor: '#f5f5f5',

203

cursor: 'pointer',

204

userSelect: 'none'

205

}}

206

onClick={() => console.log(`Clicked group ${index}`)}

207

>

208

<strong>Group {index + 1}</strong>

209

<span style={{ float: 'right' }}>({groupCounts[index]} items)</span>

210

</div>

211

)}

212

// ... other props

213

/>

214

```

215

216

### Group Item Content Rendering

217

218

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

219

220

```typescript { .api }

221

/**

222

* Callback function to render items within groups

223

* @param index - Zero-based index of the item within the entire list

224

* @param groupIndex - Zero-based index of the group containing this item

225

* @param data - The data item to render

226

* @param context - Additional context passed to the component

227

* @returns React node representing the item

228

*/

229

type GroupItemContent<D, C> = (

230

index: number,

231

groupIndex: number,

232

data: D,

233

context: C

234

) => React.ReactNode;

235

```

236

237

**Usage Example:**

238

239

```typescript

240

// Item rendering with group awareness

241

<GroupedVirtuoso

242

itemContent={(index, groupIndex, data) => (

243

<div style={{

244

padding: '8px 16px',

245

backgroundColor: groupIndex % 2 === 0 ? '#fff' : '#f9f9f9'

246

}}>

247

<span>Item {index}</span>

248

<small style={{ color: '#666' }}>Group {groupIndex}</small>

249

<div>{data.content}</div>

250

</div>

251

)}

252

// ... other props

253

/>

254

```

255

256

### Inverse Scrolling for Groups

257

258

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

259

260

```typescript { .api }

261

interface GroupedVirtuosoProps<D, C> {

262

/**

263

* Use when implementing inverse infinite scrolling - decrease this value

264

* in combination with groupCounts changes to prepend groups to the top

265

*

266

* The delta should equal the amount of new items introduced, excluding groups themselves.

267

* Example: prepending 2 groups with 20 and 30 items each requires decreasing by 50.

268

*

269

* Warning: firstItemIndex should be a positive number based on total items

270

*/

271

firstItemIndex?: number;

272

}

273

```

274

275

**Usage Example:**

276

277

```typescript

278

function InverseGroupedList() {

279

const [messages, setMessages] = React.useState([

280

{ date: '2023-12-01', msgs: ['Hello', 'How are you?'] },

281

{ date: '2023-12-02', msgs: ['Good morning', 'Ready for work'] }

282

]);

283

const [firstIndex, setFirstIndex] = React.useState(1000);

284

285

const loadOlderMessages = () => {

286

const newMessages = [

287

{ date: '2023-11-30', msgs: ['Previous day message 1', 'Previous day message 2'] }

288

];

289

290

const newItemCount = newMessages.reduce((sum, group) => sum + group.msgs.length, 0);

291

292

setMessages(prev => [...newMessages, ...prev]);

293

setFirstIndex(prev => prev - newItemCount);

294

};

295

296

return (

297

<GroupedVirtuoso

298

firstItemIndex={firstIndex}

299

startReached={loadOlderMessages}

300

groupCounts={messages.map(day => day.msgs.length)}

301

groupContent={(index) => (

302

<div style={{ padding: '8px', backgroundColor: '#e8f5e8', textAlign: 'center' }}>

303

{messages[index].date}

304

</div>

305

)}

306

itemContent={(index, groupIndex, data) => (

307

<div style={{ padding: '8px 16px' }}>

308

{data}

309

</div>

310

)}

311

data={messages.flatMap(day => day.msgs)}

312

/>

313

);

314

}

315

```

316

317

### Custom Group Components

318

319

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

320

321

```typescript { .api }

322

interface Components<Data, Context> {

323

/** Set to customize the group item wrapping element */

324

Group?: React.ComponentType<GroupProps & ContextProp<Context>>;

325

}

326

327

type GroupProps = Pick<React.ComponentProps<'div'>, 'children' | 'style'> & {

328

'data-index': number;

329

'data-item-index': number;

330

'data-known-size': number;

331

};

332

```

333

334

**Usage Example:**

335

336

```typescript

337

<GroupedVirtuoso

338

components={{

339

Group: ({ children, ...props }) => (

340

<div

341

{...props}

342

style={{

343

...props.style,

344

borderRadius: '8px',

345

margin: '4px',

346

overflow: 'hidden',

347

boxShadow: '0 2px 4px rgba(0,0,0,0.1)'

348

}}

349

>

350

{children}

351

</div>

352

)

353

}}

354

// ... other props

355

/>

356

```

357

358

## Types

359

360

```typescript { .api }

361

interface GroupItem<D> extends Item<D> {

362

originalIndex?: number;

363

type: 'group';

364

}

365

366

interface RecordItem<D> extends Item<D> {

367

data?: D;

368

groupIndex?: number;

369

originalIndex?: number;

370

type?: undefined;

371

}

372

373

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

374

375

interface GroupIndexLocationWithAlign extends LocationOptions {

376

groupIndex: number;

377

}

378

379

interface GroupedScrollIntoViewLocation extends ScrollIntoViewLocationOptions {

380

groupIndex: number;

381

}

382

```