# Data Table

> Table with the features list pages need: sorting, row selection with a bulk action bar, column toggles, sticky header and expandable rows.

Component · Data · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/DataTable/)

Table with the features list pages need: sorting, row selection with a bulk action bar, column toggles, sticky header and expandable rows. `FilterBar` sits above it.

DataTable props: `columns` ({key, label, render, align, sortable, sortValue(row), hideable, hidden, width}), `rows`, `rowKey` ("id"), `selectable`, `bulkActions` ({label, icon, danger, onClick(rows, clear)}), `renderExpanded(row)`, `stickyHeader`, `maxHeight`, `defaultSort` ({key, dir}), `toolbar`, `columnToggle` (false hides), `empty`.
FilterBar props: `filters` ({key, label, icon, options}), `value` / `onChange` ({key: option}), `search` (placeholder), `query` / `onQuery`, `right`.

- Sort cycles ascending → descending → off; the arrow only shows on the sorted or hovered column.
- The bulk bar floats at the bottom of the table while rows are selected; destructive actions are red and go through an Undo toast or ConfirmDialog.
- Active filters are ink chips you can click to change or × to remove. "Clear all" appears from two filters on.
- The header checkbox shows a dash when some, not all, rows are selected.
- Put it in `tf-panel tf-panel-flush` so cells line up at 24px with other panels.

## Example

```tsx
import * as React from "react";
import { Avatar, Badge, DataTable, FilterBar } from "@twentyfirstmedia/pro";
import type { Tone } from "@twentyfirstmedia/pro";

const ROWS = [
  {
    id: 1,
    name: "Ava Vogel",
    email: "ava@halo.app",
    role: "Editor",
    team: "Engineering",
    status: "Active",
    seats: 3,
  },
  {
    id: 2,
    name: "Ben Okafor",
    email: "ben@halo.app",
    role: "Viewer",
    team: "Sales",
    status: "Invited",
    seats: 1,
  },
  {
    id: 3,
    name: "Clara Haddad",
    email: "clara@halo.app",
    role: "Admin",
    team: "Design",
    status: "Active",
    seats: 5,
  },
  {
    id: 4,
    name: "Elif Sato",
    email: "elif@halo.app",
    role: "Editor",
    team: "Engineering",
    status: "Away",
    seats: 2,
  },
  {
    id: 5,
    name: "Jonah Petit",
    email: "jonah@halo.app",
    role: "Editor",
    team: "Growth",
    status: "Active",
    seats: 4,
  },
];
const TONE: Record<string, Tone> = {
  Active: "success",
  Invited: "info",
  Away: "warning",
};
function App() {
  const f = React.useState<Record<string, string>>({
      Team: "Engineering",
    }),
    q = React.useState("");
  const rows = ROWS.filter(
    (r) =>
      (!f[0].Team || r.team === f[0].Team) &&
      (!f[0].Status || r.status === f[0].Status) &&
      (!q[0] || r.name.toLowerCase().indexOf(q[0].toLowerCase()) >= 0),
  );
  return (
    <div
      className="tf-col"
      style={{
        gap: 16,
      }}
    >
      <FilterBar
        search="Search people"
        query={q[0]}
        onQuery={q[1]}
        value={f[0]}
        onChange={f[1]}
        filters={[
          {
            key: "Team",
            label: "Team",
            options: ["Design", "Engineering", "Growth", "Sales"],
          },
          {
            key: "Status",
            label: "Status",
            options: ["Active", "Invited", "Away"],
          },
        ]}
      />
      <div className="tf-panel tf-panel-flush">
        <DataTable
          rows={rows}
          selectable
          stickyHeader
          defaultSort={{
            key: "name",
            dir: "asc",
          }}
          toolbar={<span className="tf-card-desc">{rows.length + " of " + ROWS.length}</span>}
          bulkActions={[
            {
              label: "Change role",
              icon: "edit",
              onClick: () => {},
            },
            {
              label: "Remove",
              icon: "trash",
              danger: true,
              onClick: (r, clear) => {
                clear();
              },
            },
          ]}
          renderExpanded={(r) => (
            <span className="tf-card-desc">{r.email + " · " + r.seats + " seats assigned"}</span>
          )}
          columns={[
            {
              key: "name",
              label: "Name",
              sortable: true,
              hideable: false,
              render: (r) => (
                <span
                  className="tf-row"
                  style={{
                    flexWrap: "nowrap",
                  }}
                >
                  <Avatar name={r.name} size={28} />
                  <b>{r.name}</b>
                </span>
              ),
            },
            {
              key: "role",
              label: "Role",
              sortable: true,
            },
            {
              key: "team",
              label: "Team",
              sortable: true,
            },
            {
              key: "status",
              label: "Status",
              sortable: true,
              render: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge>,
            },
            {
              key: "seats",
              label: "Seats",
              align: "right",
              sortable: true,
            },
          ]}
        />
      </div>
    </div>
  );
}
export default App;
```

## Props

### Avatar

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `src?` | `string` |  |
| `size?` | `number` |  |
| `tint?` | `Tint \| "surface-sunken"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Badge

| Prop | Type | Notes |
| --- | --- | --- |
| `tone?` | `Tone` |  |
| `dot?` | `boolean` |  |
| `live?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### DataTable

| Prop | Type | Notes |
| --- | --- | --- |
| `columns` | `DataColumn<R>[]` | Required |
| `rows` | `R[]` | Required |
| `rowKey?` | `string` |  |
| `selectable?` | `boolean` |  |
| `bulkActions?` | `{ label: string; icon?: IconName; danger?: boolean; onClick: (rows: R[], clear: () => void) => void; }[]` |  |
| `renderExpanded?` | `(row: R) => ReactNode` |  |
| `stickyHeader?` | `boolean` |  |
| `maxHeight?` | `number \| string` |  |
| `defaultSort?` | `{ key: string; dir: "asc" \| "desc" }` |  |
| `toolbar?` | `ReactNode` |  |
| `columnToggle?` | `boolean` |  |
| `empty?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### FilterBar

| Prop | Type | Notes |
| --- | --- | --- |
| `filters` | `FilterDef[]` | Required |
| `value` | `Record<string, string>` | Required |
| `onChange` | `(v: Record<string, string>) => void` | Required |
| `search?` | `string` |  |
| `query?` | `string` |  |
| `onQuery?` | `(q: string) => void` |  |
| `right?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
