Kaapi UI

Table

A powerful data table component built with TanStack Table v8. Supports sorting, filtering, pagination, row selection, and many customization options.

← Back to components

Examples

Basic

A simple table with user data, avatars, and basic styling.

Name
Email
Role
Status

Mark Rhye

@mark

mark@untitledui.comAdminActive

Phoenix Baker

@phoenix

phoenix@untitledui.comEditorActive

Brice Steiner

@brice

brice@untitledui.comViewerPending
const userColumns = [
  {
    accessorKey: "name",
    header: "Name",
    cell: ({ row }) => (
      <div className="flex items-center gap-3">
        <Avatar src={row.original.avatar} alt={row.original.name} size="md" />
        <div>
          <p className="font-medium">{row.original.name}</p>
          <p className="text-sm text-muted-foreground">{row.original.username}</p>
        </div>
      </div>
    ),
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "role",
    header: "Role",
    cell: ({ getValue }) => (
      <Badge variant="secondary">{getValue() as string}</Badge>
    ),
  },
];

<Table data={usersData} columns={userColumns} size="md" />

With row selection

Enable single or multiple row selection with checkboxes.

Name
Email
Role

Mark Rhye

@mark

mark@untitledui.comAdmin

Phoenix Baker

@phoenix

phoenix@untitledui.comEditor

Brice Steiner

@brice

brice@untitledui.comViewer
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

<Table
  data={usersData}
  columns={userColumns}
  enableRowSelection
  enableMultiRowSelection
  rowSelection={rowSelection}
  onRowSelectionChange={setRowSelection}
  size="md"
/>

With sorting

Click on column headers to sort data in ascending or descending order.

Name
Email
Role
Last seen

Mark Rhye

@mark

mark@untitledui.comAdmin2 hours ago

Phoenix Baker

@phoenix

phoenix@untitledui.comEditor1 day ago

Brice Steiner

@brice

brice@untitledui.comViewer3 days ago
const [sorting, setSorting] = useState<SortingState>([]);

<Table
  data={usersData}
  columns={userColumns}
  enableSorting
  sorting={sorting}
  onSortingChange={setSorting}
  size="md"
/>

Complex data

Table with complex data types including progress bars, team avatars, and custom badges.

Project
Status
Progress
Team
Budget
Website Redesign
In Progress
75%
$25,000.00
Mobile App
Completed
100%
$45,000.00
const projectColumns = [
  projectColumnHelper.accessor("name", {
    header: "Project",
    cell: ({ getValue }) => (
      <div className="font-medium">{getValue()}</div>
    ),
  }),
  projectColumnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      const status = getValue();
      const config = {
        "Completed": { color: "success", icon: CheckCircle },
        "In Progress": { color: "warning", icon: Clock },
        "On Hold": { color: "gray", icon: XCircle },
        "Cancelled": { color: "error", icon: XCircle }
      };
      const { color, icon: Icon } = config[status];
      
      return (
        <div className="flex items-center gap-2">
          <Icon className="size-4" />
          <Badge color={color}>{status}</Badge>
        </div>
      );
    },
  }),
  projectColumnHelper.accessor("progress", {
    header: "Progress",
    cell: ({ getValue }) => {
      const progress = getValue();
      return (
        <div className="flex items-center gap-2">
          <div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
            <div 
              className="h-full bg-blue-500 rounded-full transition-all" 
              style={{ width: `${progress}%` }}
            />
          </div>
          <span className="text-sm text-muted-foreground">{progress}%</span>
        </div>
      );
    },
  }),
];

Table card

Wrap your table in a card with header, badge, and action buttons.

Team members

24 users

Manage your team members and their access levels

Name
Email
Role

Mark Rhye

@mark

mark@untitledui.comAdmin

Phoenix Baker

@phoenix

phoenix@untitledui.comEditor

Brice Steiner

@brice

brice@untitledui.comViewer
<TableCard
  title="Team members"
  badge="24 users"
  description="Manage your team members and their access levels"
  contentTrailing={
    <div className="flex gap-2">
      <Button variant="secondary" size="sm" leftIcon={<DownloadCloud01 />}>
        Download
      </Button>
      <Button size="sm" leftIcon={<Plus />}>
        Add member
      </Button>
    </div>
  }
>
  <Table
    data={usersData}
    columns={userColumns}
    enableRowSelection
    enableSorting
    size="md"
  />
</TableCard>

Alternating rows

Add zebra striping to table rows for better readability.

Name
Email
Role
Department

Mark Rhye

@mark

mark@untitledui.comAdminDesign

Phoenix Baker

@phoenix

phoenix@untitledui.comEditorEngineering

Brice Steiner

@brice

brice@untitledui.comViewerMarketing
<Table
  data={usersData}
  columns={userColumns}
  alternatingRows={true}
  size="md"
/>

With pagination

Add pagination controls for large datasets.

Invoices

3 invoices
Invoice
Customer
Amount
Status
Due date
INV-001Acme Corp$2,500.00Paid2/15/2024
INV-002TechStart Ltd$1,800.00Pending2/20/2024
const [pagination, setPagination] = useState({
  pageIndex: 0,
  pageSize: 2,
});

<TableCard title="Invoices" badge="247 invoices">
  <Table
    data={invoicesData}
    columns={invoiceColumns}
    pagination={pagination}
    onPaginationChange={setPagination}
    size="md"
  />
  
  {/* Custom pagination footer */}
    
    <Pagination
      page={pagination.pageIndex + 1}
      total={Math.ceil(invoicesData.length / pagination.pageSize)}
      onPageChange={(page) => setPagination(prev => ({ 
        ...prev, 
        pageIndex: page - 1 
      }))}
    />

</TableCard>

Empty state

Custom empty state when no data is available.

Name
Email
Role

No member found

Your search did not match. Please try again or create add a new member.

<Table
  data={[]}
  columns={userColumns}
  emptyMessage={
    <div className="text-center py-12">
      <div className="mx-auto size-12 bg-gray-100 rounded-full flex items-center justify-center mb-4">
        <Users className="size-6 text-gray-400" />
      </div>
      <h3 className="font-medium text-gray-900 mb-1">No team members</h3>
      <p className="text-sm text-quaternary mb-4">
        Get started by adding your first team member.
      </p>
      <Button size="sm" leftIcon={<Plus />}>
        Add team member
      </Button>
    </div>
  }
  size="md"
/>

Sizes

Two size variants: sm and md (default).

Small

Name
Email
Role

Mark Rhye

@mark

mark@untitledui.comAdmin

Phoenix Baker

@phoenix

phoenix@untitledui.comEditor

Medium (default)

Name
Email
Role

Mark Rhye

@mark

mark@untitledui.comAdmin

Phoenix Baker

@phoenix

phoenix@untitledui.comEditor
<Table data={usersData} columns={userColumns} size="sm" />
<Table data={usersData} columns={userColumns} size="md" />

API Reference

Table

PropsTypeDefaultDescription
dataTData[]-Array of data objects to display
columnsColumnDef<TData>[]-Column definitions
size?"sm" | "md""md"Table size variant
enableRowSelection?booleanfalseEnable row selection
enableMultiRowSelection?booleantrueAllow multiple row selection
enableSorting?booleantrueEnable column sorting
alternatingRows?booleanfalseAdd zebra striping
bordered?booleantrueShow borders between rows
emptyMessage?ReactNode"No results."Message shown when no data

TableCard

PropsTypeDefaultDescription
titlestring-Card header title
badge?ReactNode-Badge next to title
description?string-Description below title
contentTrailing?ReactNode-Content on the right side