Table

Structured data table for rows, columns, and comparisons.

Installation

npx shadcn@latest add @iconiq/table

File Structure

Usage

"use client";

import { Search } from "lucide-react";
import { useMemo, useState } from "react";
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableEmpty,
  TableHead,
  TableHeader,
  TableLoading,
  TablePagination,
  TableRow,
  TableSortButton,
  TableToolbar,
} from "@/components/ui/table";

type Row = {
  id: string;
  name: string;
  role: string;
  status: "Active" | "Pending" | "Archived";
  amount: number;
};

type SortKey = keyof Pick<Row, "name" | "role" | "status" | "amount">;

const rows: Row[] = [
  { id: "1", name: "Ada Lovelace", role: "Engineer", status: "Active", amount: 4200 },
  { id: "2", name: "Alan Turing", role: "Researcher", status: "Active", amount: 5800 },
  { id: "3", name: "Grace Hopper", role: "Architect", status: "Pending", amount: 3100 },
];

const statusStyles: Record<Row["status"], string> = {
  Active: "bg-foreground text-background",
  Pending: "bg-muted text-foreground",
  Archived: "border border-border bg-transparent text-muted-foreground",
};

export function TablePreview() {
  const [query, setQuery] = useState("");
  const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" }>({
    key: "name",
    dir: "asc",
  });
  const [loading, setLoading] = useState(false);

  const visible = useMemo(() => {
    const filtered = rows.filter((row) =>
      (row.name + row.role + row.status).toLowerCase().includes(query.toLowerCase())
    );

    return [...filtered].sort((a, b) => {
      const av = a[sort.key];
      const bv = b[sort.key];
      if (av < bv) return sort.dir === "asc" ? -1 : 1;
      if (av > bv) return sort.dir === "asc" ? 1 : -1;
      return 0;
    });
  }, [query, sort]);

  const toggleSort = (key: SortKey) =>
    setSort((current) => ({
      key,
      dir: current.key === key && current.dir === "asc" ? "desc" : "asc",
    }));

  return (
    <div className="w-full max-w-4xl">
      <TableToolbar tableId="team-table">
        <div className="relative max-w-sm flex-1">
          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
          <input
            className="h-10 w-full border-b border-border bg-transparent pl-9 pr-3 text-sm outline-none transition-colors focus:border-foreground"
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search…"
            value={query}
          />
        </div>
        <button
          className="text-muted-foreground text-sm transition-colors hover:text-foreground"
          onClick={() => setLoading((current) => !current)}
          type="button"
        >
          {loading ? "Show data" : "Simulate loading"}
        </button>
      </TableToolbar>

      <Table id="team-table">
        <TableHeader>
          <TableRow variant="header">
            <TableHead sortDirection={sort.key === "name" ? sort.dir : "none"}>
              <TableSortButton
                active={sort.key === "name"}
                direction={sort.dir}
                onClick={() => toggleSort("name")}
              >
                Name
              </TableSortButton>
            </TableHead>
            <TableHead sortDirection={sort.key === "role" ? sort.dir : "none"}>
              <TableSortButton
                active={sort.key === "role"}
                direction={sort.dir}
                onClick={() => toggleSort("role")}
              >
                Role
              </TableSortButton>
            </TableHead>
            <TableHead sortDirection={sort.key === "status" ? sort.dir : "none"}>
              <TableSortButton
                active={sort.key === "status"}
                direction={sort.dir}
                onClick={() => toggleSort("status")}
              >
                Status
              </TableSortButton>
            </TableHead>
            <TableHead align="right" sortDirection={sort.key === "amount" ? sort.dir : "none"}>
              <TableSortButton
                active={sort.key === "amount"}
                align="right"
                direction={sort.dir}
                onClick={() => toggleSort("amount")}
              >
                Amount
              </TableSortButton>
            </TableHead>
          </TableRow>
        </TableHeader>

        <TableBody className="min-h-[240px]">
          {loading ? (
            <TableLoading rows={3} />
          ) : (
            <>
              {visible.map((row, index) => (
                <TableRow hoverable index={index} key={row.id}>
                  <TableCell className="font-medium text-foreground">{row.name}</TableCell>
                  <TableCell className="text-muted-foreground">{row.role}</TableCell>
                  <TableCell>
                    <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusStyles[row.status]}`}>
                      {row.status}
                    </span>
                  </TableCell>
                  <TableCell align="right" className="tabular-nums text-foreground">
                    ${row.amount.toLocaleString()}
                  </TableCell>
                </TableRow>
              ))}

              {visible.length === 0 ? <TableEmpty>No results.</TableEmpty> : null}
            </>
          )}
        </TableBody>
      </Table>

      <TablePagination
        onPageChange={() => {}}
        page={1}
        pageCount={1}
        pageSize={10}
        totalItems={visible.length}
      />
    </div>
  );
}

Props

Props
Description

Table

children

Compose TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, and optional helper primitives inside the root.

Type ReactNode

columns

Shared grid-template-columns value applied to every header and body row so the native table semantics still keep the custom grid layout aligned.

Type string·Default "minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)"

size

Controls row density by tightening header and body cell padding across the table.

Type "default" | "compact"·Default default

stickyHeader

When true, TableHeader stays pinned while the surrounding scroll container moves.

Type boolean·Default false

className

Merged onto the native table element when you need to adjust width, spacing, or placement.

Type string

TableToolbar

children

Usually a search field, actions, filters, or bulk controls placed above the table.

Type ReactNode

tableId

Optional id of the related table, exposed through aria-controls for toolbar controls.

Type string

className

Merged onto the toolbar wrapper.

Type string

TableHeader

children

Usually one header TableRow.

Type ReactNode

sticky

Overrides the root stickyHeader setting for this header section.

Type boolean

className

Merged onto the header wrapper.

Type string

TableBody

children

One or more TableRow elements, plus optional TableEmpty or TableLoading when no rows are visible or data is still loading.

Type ReactNode

className

Merged onto the body wrapper.

Type string

TableFooter

children

Usually one footer TableRow containing TablePagination or summary cells.

Type ReactNode

className

Merged onto the footer wrapper.

Type string

TableRow

variant

Header rows skip mount and exit motion, while body rows keep the original motion defaults.

Type "header" | "body"·Default body

index

Optional row index used to apply a subtle stagger to body row entry motion.

Type number·Default 0

hoverable

When true, body rows get the muted hover wash. Defaults to false so informational rows do not imply clickability.

Type boolean

selected

Applies selected styling and exposes data-state="selected" for active row selection.

Type boolean·Default false

className

Merged onto the row shell for spacing or color overrides.

Type string

Motion tr props

Additional motion.tr props such as layout, transition, whileHover, and exit can still be passed directly.

Type ComponentPropsWithoutRef<typeof motion.tr>

TableHead

align

Controls left, center, or right alignment for the header cell content.

Type "left" | "center" | "right"·Default left

sortDirection

Explicit aria-sort source for sortable columns. Pass "none" on inactive sortable headers.

Type "asc" | "desc" | "none"

children

Header label or a custom control such as TableSortButton.

Type ReactNode

className

Merged onto the header cell wrapper.

Type string

TableCell

align

Controls left, center, or right alignment for the cell content.

Type "left" | "center" | "right"·Default left

children

Rendered cell content.

Type ReactNode

className

Merged onto the cell wrapper.

Type string

TableSelectHead

checked

Controlled checked state for the select-all checkbox.

Type boolean·Default false

indeterminate

Shows the mixed selection state when only some visible rows are selected.

Type boolean·Default false

onCheckedChange

Called when the select-all checkbox toggles.

Type (checked: boolean) => void

aria-label

Accessible name for the select-all checkbox.

Type string·Default "Select all rows"

TableSelectCell

checked

Controlled checked state for the row checkbox.

Type boolean·Default false

onCheckedChange

Called when the row checkbox toggles.

Type (checked: boolean) => void

aria-label

Accessible name for the row checkbox, such as the row title.

Type string

TableCaption

children

Caption copy, summary text, or count information.

Type ReactNode

className

Merged onto the caption paragraph.

Type string

TableEmpty

children

Empty-state copy or a richer no-data message.

Type ReactNode

colSpan

Overrides the automatically derived column span when the empty row should cover a different number of columns.

Type number

Motion tr props

You can still override animate, initial, transition, or className when customizing the empty state row.

Type ComponentPropsWithoutRef<typeof motion.tr>

TableLoading

rows

Number of placeholder rows to render.

Type number·Default 3

TableRow props

Optional TableRow props such as index or className passed to each loading row.

Type Omit<TableRowProps, "children" | "variant">

TableSortButton

active

Strengthens the visual treatment and enables the active sort direction state for the parent column header.

Type boolean·Default false

direction

Rotates the chevron when the current active sort direction is descending.

Type "asc" | "desc"·Default asc

align

Keeps the sort button aligned with the header cell it lives in, including full-width right-aligned targets.

Type "left" | "center" | "right"·Default left

children

Visible sort label.

Type ReactNode

aria-label

Optional override for the generated sort label announced to screen readers.

Type string

className

Merged onto the button wrapper.

Type string

TablePagination

align

Aligns the page summary and previous/next controls as a group below the table.

Type "left" | "center" | "right"·Default right

page

Current one-based page index.

Type number

pageCount

Total number of available pages.

Type number

onPageChange

Called when the previous or next control changes pages.

Type (page: number) => void

pageSize

Used with totalItems to render a range label such as 1–5 of 12.

Type number

totalItems

Total item count shown in the optional range label.

Type number

showPageInfo

Toggles the range or page summary text.

Type boolean·Default true

Registry bundle

Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.

Dependencies: motion, lucide-react.

This page lives in the Components section, but the install itself is the shared Iconiq table primitive rather than a Radix or Base UI table wrapper.

The provider switch is shown for section consistency, but both Radix UI and Base UI options are disabled because there is only one table install on this page.

The generated registry file is /r/table.json.

Contact

Additionally, if you find any bug or issue, feel free to raise an issue.

Ada LovelaceEngineerActive$4,200
Alan TuringResearcherActive$5,800
Grace HopperArchitectPending$3,100
Katherine JohnsonAnalystActive$5,100
Linus TorvaldsMaintainerArchived$2,750
Margaret HamiltonLeadActive$6,400
Radia PerlmanEngineerActive$4,700
Tim Berners-LeeArchitectPending$3,900
8 of 8 entries