# List

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/list
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=1101-2

```tsx
import { useState } from 'react';

import { USFlag } from '@commenda/pathfinder/flags/USFlag';
import { List } from '@commenda/pathfinder/list';

export const ListDefault = () => {
  const [selected, setSelected] = useState<string | null>('annual');
  const [pinned, setPinned] = useState(false);

  return (
    <List
      label="Inbox"
      selectedId={selected}
      onSelectionChange={setSelected}
      items={[
        {
          id: 'annual',
          title: 'Delaware annual report',
          description:
            'Thanks. I’ll file it today and send you the receipt here.',
          timestamp: { label: '9:41 AM', dateTime: '2026-10-13T09:41:00' },
          entityConfig: {
            label: 'Commenda Technologies Inc.',
            flag: <USFlag size={12} />,
          },
          unread: true,
          pinned,
          onAction: () => {},
          menuConfig: {
            groups: [
              {
                id: 'actions',
                items: [
                  {
                    id: 'pin',
                    type: 'action',
                    label: pinned ? 'Unpin record' : 'Pin record',
                    onClick: () => {
                      setPinned(!pinned);
                    },
                  },
                ],
              },
            ],
          },
        },
        {
          id: 'tax',
          title: 'Tax registration documents',
          description: 'Your documents are ready for review.',
          timestamp: { label: 'Yesterday' },
          onAction: () => {},
        },
      ]}
    />
  );
};
```


Import from `@commenda/pathfinder/list` and load the shared stylesheet once. This component represents inbox/mailroom records, not bullet lists or selection options.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `label` | `string` | required | Accessible list name |
| `items` | `readonly ListItem[]` | required | Stable ID and title; each item has `onAction` or `linkConfig` |
| `selectedId` | `string \| null` | optional | The currently open record |
| `onSelectionChange` | `((id: string) => void)` | optional | Activation notification; app owns selection |
| `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Announces loading and marks list busy |
| `emptyContent` | `ReactNode` | optional | Defaults to `No items.` |
| `paginationConfig` | `{ hasMore: boolean; isLoading: boolean; onLoadMore: () => void; }` | optional | App-owned paging with Load more |
| `className` | `string` | optional | Root styling |

### loadingConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isLoading` | `boolean` | Yes |  |
| `label` | `string` | No |  |

### paginationConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `hasMore` | `boolean` | Yes |  |
| `isLoading` | `boolean` | Yes |  |
| `onLoadMore` | `() => void` | Yes |  |

Items also accept `description`, `leadingIcon`, `entityConfig: { label, flag }`, `timestamp: { label, dateTime? }`, `unread`, `pinned`, and `menuConfig` using Dropdown's existing menu groups. `linkConfig` preserves native link behavior and supports an anchor-compatible `render` function for Next Link. More is a separate control; it never activates the row.

Titles, entity names, and previews truncate with the full text retained in the DOM and native title. The selected row retains the same font weight. Time and More share reserved space, with menus available on focus and touch.

## Loading and empty states [#loading-and-empty-states]


```tsx
import { List } from '@commenda/pathfinder/list';

export const ListStates = () => (
  <div style={{ display: 'grid', gap: 20 }}>
    <List label="Empty inbox" items={[]} emptyContent="No messages yet." />
    <List
      label="Loading inbox"
      items={[]}
      loadingConfig={{ isLoading: true }}
    />
  </div>
);
```


## Paging [#paging]


```tsx
import { useState } from 'react';

import { List } from '@commenda/pathfinder/list';

export const ListPagination = () => {
  const [count, setCount] = useState(3);

  return (
    <List
      label="Filings"
      items={Array.from({ length: count }, (_, index) => ({
        id: String(index),
        title: `Filing ${String(index + 1)}`,
        linkConfig: { href: `#filing-${String(index + 1)}` },
      }))}
      paginationConfig={{
        hasMore: count < 9,
        isLoading: false,
        onLoadMore: () => {
          setCount(count + 3);
        },
      }}
    />
  );
};
```


Use server paging for large datasets. List renders the provided page without virtualization; avoid loading an entire unbounded inbox. It does not fetch or automatically advance pages. Read the [record component rules](/docs/components/list#design-rules).

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Source rules [#source-rules]

| Rule     | Contract and provenance                                                                                                          |
| -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| LIST-001 | Figma 1101:3: 80px minimum inbox/mailroom rows, stable font weight; selected soft-pearl 300.                                     |
| LIST-002 | Native record link/action and a separate More button. No nested buttons or accidental record activation from its menu.           |
| LIST-003 | Application owns paging, selection, and remote state. Timestamp swaps with More without shifting content; touch exposes actions. |
