Sorted in the browser
Text columns start A to Z, figures highest first; each press flips the order.
| Name | Channel | Conversations |
|---|---|---|
| Ana Torres | Phone | 4 |
| Luis Gómez | 12 | |
| Marta Ruiz | Web chat | 27 |
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
interface Contact {
id: string;
name: string;
channel: string;
conversations: number;
}
const CONTACTS: Contact[] = [
{ id: 'c1', name: 'Luis Gómez', channel: 'WhatsApp', conversations: 12 },
{ id: 'c2', name: 'Ana Torres', channel: 'Phone', conversations: 4 },
{ id: 'c3', name: 'Marta Ruiz', channel: 'Web chat', conversations: 27 },
];
const COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{ accessorKey: 'channel', enableSorting: false, header: 'Channel', size: 140 },
{
accessorKey: 'conversations',
header: 'Conversations',
size: 150,
meta: { align: 'end', cellClassName: 'tabular-nums' },
},
];
<DataTable
aria-label="Contacts"
columns={COLUMNS}
data={CONTACTS}
defaultSorting={[{ id: 'name', desc: false }]}
getRowHref={(contact) => `#/contacts/${contact.id}`}
getRowId={(contact) => contact.id}
/>Sorted by the server
With manualSorting the rows stay in the order they came in; while the next page loads they dim.
| Name | Channel | Conversations |
|---|---|---|
| Luis Gómez | 12 | |
| Ana Torres | Phone | 4 |
| Marta Ruiz | Web chat | 27 |
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { useState } from 'react';
import { DataTable } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
interface Contact {
id: string;
name: string;
channel: string;
conversations: number;
}
const CONTACTS: Contact[] = [
{ id: 'c1', name: 'Luis Gómez', channel: 'WhatsApp', conversations: 12 },
{ id: 'c2', name: 'Ana Torres', channel: 'Phone', conversations: 4 },
{ id: 'c3', name: 'Marta Ruiz', channel: 'Web chat', conversations: 27 },
];
const COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{ accessorKey: 'channel', enableSorting: false, header: 'Channel', size: 140 },
{
accessorKey: 'conversations',
header: 'Conversations',
size: 150,
meta: { align: 'end', cellClassName: 'tabular-nums' },
},
];
const ServerSortedExample = () => {
const [sorting, setSorting] = useState<SortingState>([{ id: 'conversations', desc: true }]);
const [isUpdating, setIsUpdating] = useState(false);
return (
<DataTable
aria-label="Contacts"
columns={COLUMNS}
data={CONTACTS}
getRowId={(contact) => contact.id}
isUpdating={isUpdating}
layout="fixed"
manualSorting
onSortingChange={(next) => {
setSorting(next);
setIsUpdating(true);
setTimeout(() => {
setIsUpdating(false);
}, 1200);
}}
sorting={sorting}
/>
);
};
<ServerSortedExample />Every state of a list
Inside its frame and under its header: placeholder rows on the first load, a failed load with a retry, nothing yet, and nothing that matches the filters.
| Name | Channel | Conversations |
|---|---|---|
import { UsersIcon } from '@phosphor-icons/react';
import type { ColumnDef } from '@tanstack/react-table';
import { useState } from 'react';
import { Button, DataTable, EmptyState, SegmentedControl } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
interface Contact {
id: string;
name: string;
channel: string;
conversations: number;
}
const COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{ accessorKey: 'channel', enableSorting: false, header: 'Channel', size: 140 },
{
accessorKey: 'conversations',
header: 'Conversations',
size: 150,
meta: { align: 'end', cellClassName: 'tabular-nums' },
},
];
type ListState = 'loading' | 'error' | 'empty' | 'filtered';
const NO_CONTACTS: Contact[] = [];
const StatesExample = () => {
const [state, setState] = useState<ListState>('loading');
return (
<div className="flex flex-col gap-3">
<SegmentedControl
label="State"
onChange={setState}
options={[
{ label: 'Loading', value: 'loading' },
{ label: 'Failed', value: 'error' },
{ label: 'Nothing yet', value: 'empty' },
{ label: 'No matches', value: 'filtered' },
]}
size="sm"
value={state}
/>
<DataTable
aria-label="Contacts"
columns={COLUMNS}
data={NO_CONTACTS}
emptyState={
<EmptyState
actions={<Button>Add contact</Button>}
description="Import a CSV or add your first contact by hand."
icon={<UsersIcon aria-hidden />}
title="No contacts yet"
/>
}
errorDescription="Check your connection and try again."
isError={state === 'error'}
isFiltered={state === 'filtered'}
isLoading={state === 'loading'}
layout="fixed"
onClearFilters={() => {
setState('empty');
}}
onRetry={() => {
setState('loading');
}}
/>
</div>
);
};
<StatesExample />Columns that leave a narrow table
Each column with minTableWidth leaves when the table is narrower, header and cells, and comes back when there is room. Narrow the page to see the channel go first.
| Name | Channel | Conversations |
|---|---|---|
| Luis Gómez | 12 | |
| Ana Torres | Phone | 4 |
| Marta Ruiz | Web chat | 27 |
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
interface Contact {
id: string;
name: string;
channel: string;
conversations: number;
}
const CONTACTS: Contact[] = [
{ id: 'c1', name: 'Luis Gómez', channel: 'WhatsApp', conversations: 12 },
{ id: 'c2', name: 'Ana Torres', channel: 'Phone', conversations: 4 },
{ id: 'c3', name: 'Marta Ruiz', channel: 'Web chat', conversations: 27 },
];
const RESPONSIVE_COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{
accessorKey: 'channel',
enableSorting: false,
header: 'Channel',
size: 140,
meta: { minTableWidth: 720 },
},
{
accessorKey: 'conversations',
header: 'Conversations',
size: 150,
meta: { align: 'end', cellClassName: 'tabular-nums', minTableWidth: 480 },
},
];
<DataTable
aria-label="Contacts"
columns={RESPONSIVE_COLUMNS}
data={CONTACTS}
getRowId={(contact) => contact.id}
layout="fixed"
/>Hidden columns and an empty state
| Name | Conversations |
|---|---|
No results for these filters | |
import type { ColumnDef } from '@tanstack/react-table';
import { Button, DataTable, EmptyState } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
interface Contact {
id: string;
name: string;
channel: string;
conversations: number;
}
const COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{ accessorKey: 'channel', enableSorting: false, header: 'Channel', size: 140 },
{
accessorKey: 'conversations',
header: 'Conversations',
size: 150,
meta: { align: 'end', cellClassName: 'tabular-nums' },
},
];
<DataTable
aria-label="Contacts"
columnVisibility={{ channel: false }}
columns={COLUMNS}
data={[]}
emptyState={
<EmptyState actions={<Button variant="outline">Clear filters</Button>} variant="filtered" />
}
/>Source
The examples above are src/components/data-table/data-table.examples.tsx, which the library’s tests render too.