1. Components
  2. Data display
  3. Table

Table

A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

export default function Demo() {
  return (
    <TableRoot aria-label="Files">
      <TableHeader>
        <TableColumn isRowHeader>Name</TableColumn>
        <TableColumn>Type</TableColumn>
        <TableColumn>Date Modified</TableColumn>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell>Games</TableCell>
          <TableCell>File folder</TableCell>
          <TableCell>6/7/2020</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>Program Files</TableCell>
          <TableCell>File folder</TableCell>
          <TableCell>4/7/2021</TableCell>
        </TableRow>
        <TableRow>
          <TableCell>bootmgr</TableCell>
          <TableCell>System file</TableCell>
          <TableCell>11/20/2010</TableCell>
        </TableRow>
      </TableBody>
    </TableRoot>
  );
}

Installation

npx dotui-cli@latest add table

Anatomy

A table consists of a container element, with columns and rows of cells containing data inside. The cells within a table may contain focusable elements or plain text content.

import {
  TableRoot,
  TableHeader,
  TableColumn,
  TableBody,
  TableRow,
  TableCell,
} from "@/components/core/table";
 
<TableRoot>
  <TableHeader>
    <TableColumn>#</TableColumn>
    <TableColumn>Name</TableColumn>
    <TableColumn>Email</TableColumn>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>1</TableCell>
      <TableCell>Mehdi BHA</TableCell>
      <TableCell>hello@mehdibha.com</TableCell>
    </TableRow>
    <TableRow>
      <TableCell>2</TableCell>
      <TableCell>Devon Govett</TableCell>
      <TableCell>devon@govett.com</TableCell>
    </TableRow>
    <TableRow>
      <TableCell>3</TableCell>
      <TableCell>Theo Browne</TableCell>
      <TableCell>theo@ping.gg</TableCell>
    </TableRow>
  </TableBody>
</TableRoot>;

Variants

Use the variant prop to change the appearance of the Table.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
Variant
"use client";

import React from "react";
import { Radio, RadioGroup } from "@/components/dynamic-core/radio-group";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/components/dynamic-core/table";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

type Variant = "solid" | "line" | "bordered" | "quiet";

export default function Demo() {
  const [variant, setVariant] = React.useState<Variant>("solid");
  return (
    <div className="flex gap-14">
      <TableRoot variant={variant} aria-label="Files">
        <TableHeader columns={columns}>
          {(column) => (
            <TableColumn isRowHeader={column.isRowHeader}>
              {column.name}
            </TableColumn>
          )}
        </TableHeader>
        <TableBody items={data}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
      <RadioGroup
        label="Variant"
        value={variant}
        onChange={(value) => setVariant(value as Variant)}
      >
        <Radio value="solid">Solid</Radio>
        <Radio value="bordered">Bordered</Radio>
        <Radio value="line">Line</Radio>
        <Radio value="quiet">quiet</Radio>
      </RadioGroup>
    </div>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Dynamic collections

The first example have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API, or updates over time. In the example below, both the columns and the rows are provided to the table via a render function. You can also make the columns static and only the rows dynamic.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot aria-label="Files">
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Selection

Selection mode

By default, Table doesn't allow row selection but this can be enabled using the selectionMode prop.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <div className="space-y-8">
      <TableRoot aria-label="Files" selectionMode="single">
        <TableHeader columns={columns}>
          {(column) => (
            <TableColumn isRowHeader={column.isRowHeader}>
              {column.name}
            </TableColumn>
          )}
        </TableHeader>
        <TableBody items={data}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
      <TableRoot aria-label="Files" selectionMode="multiple">
        <TableHeader columns={columns}>
          {(column) => (
            <TableColumn isRowHeader={column.isRowHeader}>
              {column.name}
            </TableColumn>
          )}
        </TableHeader>
        <TableBody items={data}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
    </div>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Uncontrolled selection

Use defaultSelectedKeys to provide a default set of selected rows. Note that the value of the selected keys must match the id prop of the row.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot
      aria-label="Files"
      selectionMode="single"
      defaultSelectedKeys={[2]}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Controlled selection

To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The id prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import React from "react";
import { type Selection } from "react-aria-components";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  const [selectedKeys, setSelectedKeys] = React.useState<Selection>(
    new Set([2, 3])
  );
  return (
    <TableRoot
      aria-label="Files"
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Selection variant

Use the selectionVariant prop to change the appearance of the selected rows.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
Variant
"use client";

import React from "react";
import { RadioGroup, Radio } from "@/components/dynamic-core/radio-group";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/components/dynamic-core/table";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

type Variant = "primary" | "accent";

export default function Demo() {
  const [variant, setVariant] = React.useState<Variant>("primary");
  return (
    <div className="flex gap-12">
      <TableRoot
        aria-label="Files"
        selectionMode="single"
        selectionVariant={variant}
        defaultSelectedKeys={[2]}
      >
        <TableHeader columns={columns}>
          {(column) => (
            <TableColumn isRowHeader={column.isRowHeader}>
              {column.name}
            </TableColumn>
          )}
        </TableHeader>
        <TableBody items={data}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
      <RadioGroup
        label="Variant"
        value={variant}
        onChange={(value) => setVariant(value as Variant)}
      >
        <Radio value="primary">Primary</Radio>
        <Radio value="accent">Accent</Radio>
      </RadioGroup>
    </div>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Disallow empty selection

Table also supports a disallowEmptySelection prop which forces the user to have at least one row in the Table selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot
      aria-label="Files"
      selectionMode="single"
      defaultSelectedKeys={[2]}
      disallowEmptySelection
    >
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Selection Behavior

You can control how multiple selection should behave in the collection using the selectionBehavior prop.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
Behavior
"use client";

import React from "react";
import { RadioGroup, Radio } from "@/components/dynamic-core/radio-group";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/components/dynamic-core/table";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

type SelectionBehavior = "toggle" | "replace";

export default function Demo() {
  const [selectionBehavior, setSelectionBehavior] =
    React.useState<SelectionBehavior>("toggle");
  return (
    <div className="flex gap-12">
      <TableRoot
        aria-label="Files"
        selectionMode="multiple"
        selectionBehavior={selectionBehavior}
        defaultSelectedKeys={[2, 3]}
      >
        <TableHeader columns={columns}>
          {(column) => (
            <TableColumn isRowHeader={column.isRowHeader}>
              {column.name}
            </TableColumn>
          )}
        </TableHeader>
        <TableBody items={data}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
      <RadioGroup
        label="Behavior"
        value={selectionBehavior}
        onChange={(value) => setSelectionBehavior(value as SelectionBehavior)}
        className="text-sm"
      >
        <Radio value="toggle">Toggle</Radio>
        <Radio value="accent">Replace</Radio>
      </RadioGroup>
    </div>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Row options

Action

Table supports row actions via the onRowAction prop

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot
      aria-label="Files"
      onRowAction={(key) => alert(`Opening item ${key}...`)}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Rows may also have a row action specified by directly applying onAction on the Row itself. This may be especially convenient in static collections.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

export default function Demo() {
  return (
    <TableRoot aria-label="Files">
      <TableHeader>
        <TableColumn isRowHeader>Name</TableColumn>
        <TableColumn>Type</TableColumn>
        <TableColumn>Date Modified</TableColumn>
      </TableHeader>
      <TableBody>
        <TableRow onAction={() => alert("Opening games")}>
          <TableCell>Games</TableCell>
          <TableCell>File folder</TableCell>
          <TableCell>6/7/2020</TableCell>
        </TableRow>
        <TableRow onAction={() => alert("Opening program files")}>
          <TableCell>Program Files</TableCell>
          <TableCell>File folder</TableCell>
          <TableCell>4/7/2021</TableCell>
        </TableRow>
        <TableRow onAction={() => alert("Opening bootmgr")}>
          <TableCell>bootmgr</TableCell>
          <TableCell>System file</TableCell>
          <TableCell>11/20/2010</TableCell>
        </TableRow>
      </TableBody>
    </TableRoot>
  );
}

Table rows may also be links to another page or website. This can be achieved by passing the href prop to the <TableRow> component.

Name
URL
Date added
Adobehttps://adobe.com/January 28, 2023
Googlehttps://google.com/April 5, 2023
New York Timeshttps://nytimes.com/July 12, 2023
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot aria-label="Files">
      <TableHeader columns={columns}>
        <TableColumn isRowHeader>Name</TableColumn>
        <TableColumn>URL</TableColumn>
        <TableColumn>Date added</TableColumn>
      </TableHeader>
      <TableBody items={data}>
        <TableRow href="https://adobe.com/" target="_blank">
          <TableCell>Adobe</TableCell>
          <TableCell>https://adobe.com/</TableCell>
          <TableCell>January 28, 2023</TableCell>
        </TableRow>
        <TableRow href="https://google.com/" target="_blank">
          <TableCell>Google</TableCell>
          <TableCell>https://google.com/</TableCell>
          <TableCell>April 5, 2023</TableCell>
        </TableRow>
        <TableRow href="https://nytimes.com/" target="_blank">
          <TableCell>New York Times</TableCell>
          <TableCell>https://nytimes.com/</TableCell>
          <TableCell>July 12, 2023</TableCell>
        </TableRow>
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Disabled

A Row can be disabled with the isDisabled prop.

<TableRow isDisabled>
  <TableCell>1</TableCell>
  <TableCell>Mehdi BHA</TableCell>
  <TableCell>hello@mehdibha.com</TableCell>
</TableRow>

In dynamic collections, it may be more convenient to use the disabledKeys prop at the Table level instead of isDisabled on individual rows.

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const data: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableRoot aria-label="Files" disabledKeys={[3, 4]}>
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={data}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Sorting

Table supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with the allowsSorting prop. The Table accepts a sortDescriptor prop that defines the current column key to sort by and the sort direction (ascending/descending). When the user presses a sortable column header, the column's key and sort direction is passed into the onSortChange callback, allowing you to update the sortDescriptor appropriately.

Name
Type
Date Modified
bootmgrSystem file11/20/2010
GamesFile folder6/7/2020
log.txtText Document1/18/2016
Program FilesFile folder4/7/2021
"use client";

import React from "react";
import type { SortDescriptor } from "react-aria-components";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const items: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  const [sortDescriptor, setSortDescriptor] = React.useState<SortDescriptor>({
    column: "name",
    direction: "ascending",
  });

  const sortedItems = React.useMemo(() => {
    return items.sort((a, b) => {
      const first = a[sortDescriptor.column as keyof Item] as string;
      const second = b[sortDescriptor.column as keyof Item] as string;
      let cmp = first.localeCompare(second);
      if (sortDescriptor.direction === "descending") {
        cmp *= -1;
      }
      return cmp;
    });
  }, [sortDescriptor]);

  return (
    <TableRoot
      aria-label="Files"
      sortDescriptor={sortDescriptor}
      onSortChange={setSortDescriptor}
    >
      <TableHeader columns={columns}>
        <TableColumn id="name" isRowHeader allowsSorting>
          Name
        </TableColumn>
        <TableColumn id="type" allowsSorting>
          Type
        </TableColumn>
        <TableColumn id="date">Date Modified</TableColumn>
      </TableHeader>
      <TableBody items={sortedItems}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Empty state

Use the renderEmptyState prop to customize what the TableBody will display if there are no items. By default the TableBody will display a message saying "No results found.".

Name
Type
Date Modified
No results found.
Name
Type
Date Modified
Nothing here.
"use client";

import {
  TableRoot,
  TableHeader,
  TableBody,
  TableColumn,
} from "@/registry/core/table_basic";

export default function Demo() {
  return (
    <div className="flex gap-8">
      <TableRoot aria-label="Files">
        <TableHeader>
          <TableColumn id="name" isRowHeader>
            Name
          </TableColumn>
          <TableColumn id="type">Type</TableColumn>
          <TableColumn id="date">Date Modified</TableColumn>
        </TableHeader>
        <TableBody>{[]}</TableBody>
      </TableRoot>

      <TableRoot aria-label="Files">
        <TableHeader>
          <TableColumn id="name" isRowHeader>
            Name
          </TableColumn>
          <TableColumn id="type">Type</TableColumn>
          <TableColumn id="date">Date Modified</TableColumn>
        </TableHeader>
        <TableBody renderEmptyState={() => "Nothing here."}>{[]}</TableBody>
      </TableRoot>
    </div>
  );
}

Column resizing

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import React from "react";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
  TableContainer,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

const items: Item[] = [
  { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
  { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
  { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
  { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
];

export default function Demo() {
  return (
    <TableContainer resizable>
      <TableRoot aria-label="Files">
        <TableHeader columns={columns}>
          <TableColumn id="name" isRowHeader allowsResizing>
            Name
          </TableColumn>
          <TableColumn id="type" allowsResizing>
            Type
          </TableColumn>
          <TableColumn id="date">Date Modified</TableColumn>
        </TableHeader>
        <TableBody items={items}>
          {(item) => (
            <TableRow columns={columns}>
              {(column) => <TableCell>{item[column.id]}</TableCell>}
            </TableRow>
          )}
        </TableBody>
      </TableRoot>
    </TableContainer>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

Drag and drop

Reordable

Name
Type
Date Modified
GamesFile folder6/7/2020
Program FilesFile folder4/7/2021
bootmgrSystem file11/20/2010
log.txtText Document1/18/2016
"use client";

import { useDragAndDrop } from "react-aria-components";
import { useListData } from "react-stately";
import {
  TableRoot,
  TableHeader,
  TableBody,
  TableRow,
  TableColumn,
  TableCell,
} from "@/registry/core/table_basic";

const columns: Column[] = [
  { name: "Name", id: "name", isRowHeader: true },
  { name: "Type", id: "type" },
  { name: "Date Modified", id: "date" },
];

export default function Demo() {
  const list = useListData<Item>({
    initialItems: [
      { id: 1, name: "Games", date: "6/7/2020", type: "File folder" },
      { id: 2, name: "Program Files", date: "4/7/2021", type: "File folder" },
      { id: 3, name: "bootmgr", date: "11/20/2010", type: "System file" },
      { id: 4, name: "log.txt", date: "1/18/2016", type: "Text Document" },
    ],
  });

  const { dragAndDropHooks } = useDragAndDrop({
    getItems: (keys) =>
      [...keys].map((key) => {
        const item = list.getItem(key);
        return {
          "text/plain": item?.name ?? "",
        };
      }),
    onReorder(e) {
      if (e.target.dropPosition === "before") {
        list.moveBefore(e.target.key, e.keys);
      } else if (e.target.dropPosition === "after") {
        list.moveAfter(e.target.key, e.keys);
      }
    },
  });

  return (
    <TableRoot aria-label="Files" dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {(column) => (
          <TableColumn isRowHeader={column.isRowHeader}>
            {column.name}
          </TableColumn>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {(item) => (
          <TableRow columns={columns}>
            {(column) => <TableCell>{item[column.id]}</TableCell>}
          </TableRow>
        )}
      </TableBody>
    </TableRoot>
  );
}

type Item = {
  id: number;
  name: string;
  date: string;
  type: string;
};

interface Column {
  id: keyof Omit<Item, "id">;
  name: string;
  isRowHeader?: boolean;
}

API Reference

TableRoot

PropTypeDefaultDescription
childrenReactNode-The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows.
selectionBehavior'toggle' | 'replace''toggle'How multiple selection should behave in the collection.
disabledBehavior'selection' | 'all''selectionWhether disabledKeys applies to all interactions, or only selection.
dragAndDropHooksDragAndDropHooks-The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the Table.
disabledKeysIterable<Key>-A list of row keys to disable.
selectionMode'none'| 'single'| 'multiple'-The type of selection that is allowed in the collection.
disallowEmptySelectionboolean-Whether the collection allows empty selection.
selectedKeys'all' | Iterable<Key>-The currently selected keys in the collection (controlled).
defaultSelectedKeys'all' | Iterable<Key>-The initial selected keys in the collection (uncontrolled).
sortDescriptorSortDescriptor-The current sorted column and direction.
classNamestring | (values: TableRenderProps & {defaultClassName: string | undefined}) => string-The CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties | (values: TableRenderProps & {defaultStyle: CSSProperties}) => CSSProperties-The inline style for the element. A function may be provided to compute the style based on component state.
EventTypeDescription
onRowAction(key: Key) => voidHandler that is called when a user performs an action on the row.
onSelectionChange(key: Selection) => voidHandler that is called when the selection changes.
onSortChange(descriptor: SortDescriptor) => anyHandler that is called when the sorted column or direction changes.
onScroll(e: UIEvent<Element>) => voidHandler that is called when a user scrolls. See MDN.

Last updated on 6/19/2024

dotUI

Bringing singularity to the web.

Built by mehdibha. The source code is available on GitHub.