---
title: "Table"
description: "Records in rows and columns, in a rounded frame. Arrow keys move through it, rows open with a click, Enter or a tap, columns sort, and rows can be selected. Use DataTable when TanStack Table drives the list."
---

> Documentation Index
> Fetch the complete documentation index at: https://epidermis.getarbol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Table

## Basic

```tsx
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';

const APPOINTMENTS = [
  {
    id: 'a1',
    patient: 'Ana Torres',
    document: 'CC 1.020.304.050',
    service: 'General check-up',
    time: '09:30',
    minutes: 30,
  },
  {
    id: 'a2',
    patient: 'Luis Gómez',
    document: 'CC 80.111.222',
    service: 'Dental cleaning',
    time: '10:15',
    minutes: 45,
  },
  {
    id: 'a3',
    patient: 'Marta Ruiz',
    document: 'TI 1.001.234.567',
    service: 'Vaccination',
    time: '11:00',
    minutes: 15,
  },
];

<Table aria-label="Upcoming appointments">
  <TableHeader>
    <Column isRowHeader>Patient</Column>
    <Column>Service</Column>
    <Column align="end">Time</Column>
  </TableHeader>
  <TableBody>
    {APPOINTMENTS.map((appointment) => (
      <Row id={appointment.id} key={appointment.id}>
        <Cell>{appointment.patient}</Cell>
        <Cell>{appointment.service}</Cell>
        <Cell align="end">{appointment.time}</Cell>
      </Row>
    ))}
  </TableBody>
</Table>
```

## Rows that open their record

A press anywhere on a row opens the patient; the name is a real link (new tab, copy link), and the “⋯” menu holds the rest. Arrow keys reach the link and the menu.

```tsx
import { Avatar, Badge, Cell, Column, Row, RowMenu, RowTitle, Table, TableBody, TableHeader } from '@arbol-ai/ui';

const APPOINTMENTS = [
  {
    id: 'a1',
    patient: 'Ana Torres',
    document: 'CC 1.020.304.050',
    service: 'General check-up',
    time: '09:30',
    minutes: 30,
  },
  {
    id: 'a2',
    patient: 'Luis Gómez',
    document: 'CC 80.111.222',
    service: 'Dental cleaning',
    time: '10:15',
    minutes: 45,
  },
  {
    id: 'a3',
    patient: 'Marta Ruiz',
    document: 'TI 1.001.234.567',
    service: 'Vaccination',
    time: '11:00',
    minutes: 15,
  },
];

<Table aria-label="Patients">
  <TableHeader>
    <Column isRowHeader>Patient</Column>
    <Column>Service</Column>
    <Column align="end">Time</Column>
    <Column className="w-12">
      <span className="sr-only">Actions</span>
    </Column>
  </TableHeader>
  <TableBody>
    {APPOINTMENTS.map((appointment) => (
      <Row href={`#/patients/${appointment.id}`} id={appointment.id} key={appointment.id}>
        <Cell>
          <RowTitle
            badge={appointment.minutes > 30 ? <Badge tone="info">Long visit</Badge> : undefined}
            description={appointment.document}
            href={`#/patients/${appointment.id}`}
            leading={<Avatar name={appointment.patient} />}
          >
            {appointment.patient}
          </RowTitle>
        </Cell>
        <Cell>{appointment.service}</Cell>
        <Cell align="end">{appointment.time}</Cell>
        <Cell>
          <RowMenu
            actions={[
              { label: 'Book again', href: `#/book?patient=${appointment.id}` },
              { label: 'Archive', isDestructive: true, onAction: () => undefined },
            ]}
            label={`Actions for ${appointment.patient}`}
          />
        </Cell>
      </Row>
    ))}
  </TableBody>
</Table>
```

## While the rows load

Placeholder rows hold the table’s place under its real header, across every column.

```tsx
import { Column, Table, TableBody, TableHeader } from '@arbol-ai/ui';

<Table aria-label="Upcoming appointments">
  <TableHeader>
    <Column isRowHeader>Patient</Column>
    <Column>Service</Column>
    <Column align="end">Time</Column>
  </TableHeader>
  <TableBody isLoading>{[]}</TableBody>
</Table>
```

## Sorting and row actions

Press a header to sort by it; press a row (or Enter on a focused one) to open it.

```tsx
import { useState } from 'react';
import type { SortDescriptor } from 'react-aria-components';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';

const APPOINTMENTS = [
  {
    id: 'a1',
    patient: 'Ana Torres',
    document: 'CC 1.020.304.050',
    service: 'General check-up',
    time: '09:30',
    minutes: 30,
  },
  {
    id: 'a2',
    patient: 'Luis Gómez',
    document: 'CC 80.111.222',
    service: 'Dental cleaning',
    time: '10:15',
    minutes: 45,
  },
  {
    id: 'a3',
    patient: 'Marta Ruiz',
    document: 'TI 1.001.234.567',
    service: 'Vaccination',
    time: '11:00',
    minutes: 15,
  },
];

const SortableExample = () => {
  const [sort, setSort] = useState<SortDescriptor>({ column: 'patient', direction: 'ascending' });
  const [opened, setOpened] = useState('');
  const rows = APPOINTMENTS.toSorted((a, b) => {
    const order = sort.column === 'minutes' ? a.minutes - b.minutes : a.patient.localeCompare(b.patient);
    return sort.direction === 'ascending' ? order : -order;
  });
  return (
    <div className="flex flex-col gap-2">
      <Table
        aria-label="Appointments"
        onRowAction={(id) => {
          setOpened(String(id));
        }}
        onSortChange={setSort}
        sortDescriptor={sort}
      >
        <TableHeader>
          <Column allowsSorting id="patient" isRowHeader>
            Patient
          </Column>
          <Column id="service">Service</Column>
          <Column align="end" allowsSorting id="minutes">
            Minutes
          </Column>
        </TableHeader>
        <TableBody items={rows}>
          {(appointment) => (
            <Row id={appointment.id}>
              <Cell>{appointment.patient}</Cell>
              <Cell>{appointment.service}</Cell>
              <Cell align="end">{appointment.minutes}</Cell>
            </Row>
          )}
        </TableBody>
      </Table>
      <p className="text-muted-foreground text-sm">Opened: {opened === '' ? '—' : opened}</p>
    </div>
  );
};

<SortableExample />
```

## Selection

selectionMode adds a checkbox per row and, for multiple, a select-all box.

```tsx
import { useState } from 'react';
import type { Selection } from 'react-aria-components';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';

const APPOINTMENTS = [
  {
    id: 'a1',
    patient: 'Ana Torres',
    document: 'CC 1.020.304.050',
    service: 'General check-up',
    time: '09:30',
    minutes: 30,
  },
  {
    id: 'a2',
    patient: 'Luis Gómez',
    document: 'CC 80.111.222',
    service: 'Dental cleaning',
    time: '10:15',
    minutes: 45,
  },
  {
    id: 'a3',
    patient: 'Marta Ruiz',
    document: 'TI 1.001.234.567',
    service: 'Vaccination',
    time: '11:00',
    minutes: 15,
  },
];

const SelectionExample = () => {
  const [selected, setSelected] = useState<Selection>(new Set(['a2']));
  const count = selected === 'all' ? APPOINTMENTS.length : selected.size;
  return (
    <div className="flex flex-col gap-2">
      <Table
        aria-label="Appointments to confirm"
        onSelectionChange={setSelected}
        selectedKeys={selected}
        selectionMode="multiple"
      >
        <TableHeader>
          <Column isRowHeader>Patient</Column>
          <Column>Service</Column>
        </TableHeader>
        <TableBody items={APPOINTMENTS}>
          {(appointment) => (
            <Row id={appointment.id}>
              <Cell>{appointment.patient}</Cell>
              <Cell>{appointment.service}</Cell>
            </Row>
          )}
        </TableBody>
      </Table>
      <p className="text-muted-foreground text-sm">{count} selected</p>
    </div>
  );
};

<SelectionExample />
```

## Empty

```tsx
import { Column, EmptyState, Table, TableBody, TableHeader } from '@arbol-ai/ui';

<Table aria-label="Campaigns">
  <TableHeader>
    <Column isRowHeader>Campaign</Column>
    <Column>Status</Column>
  </TableHeader>
  <TableBody renderEmptyState={() => <EmptyState title="No campaigns yet" />}>{[]}</TableBody>
</Table>
```

## Source

The examples above are [`src/components/table/table.examples.tsx`](https://github.com/getarbol/project-arbol-ui/blob/main/src/components/table/table.examples.tsx), which the library's tests render too.

Source: https://epidermis.getarbol.com/components/data/table/index.mdx
