Wuko
Components

DataTable

An opinionated table with sorting, pagination, filtering, row selection, and column visibility built on @tanstack/react-table and Wuko's Table primitive. Pass typed columns and data, opt into features with props. For full control, see Building your own at the bottom of this page.

Installation

terminal
npx shadcn@latest add @wuko/data-table

Usage

Pass a columns array and data array. Column definitions follow TanStack Table's API.

DeviceRegionStatusFirmware
KIOSK-4729us-westhealthy2.4.1
KIOSK-3310us-eastupdating2.4.0
KIOSK-3300us-westoffline2.4.1
KIOSK-2150eu-northhealthy2.4.1
KIOSK-2199eu-northdegraded2.4.0
Showing 15 of 5
components/example.tsx
import type { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@/components/ui/data-table";

type Device = {
  id: string;
  region: string;
  status: string;
};

const columns: ColumnDef<Device>[] = [
  { accessorKey: "id", header: "Device" },
  { accessorKey: "region", header: "Region" },
  { accessorKey: "status", header: "Status" },
];

const data: Device[] = [
  { id: "KIOSK-4729", region: "us-west", status: "healthy" },
  { id: "KIOSK-3310", region: "us-east", status: "updating" },
];

export function Example() {
  return <DataTable columns={columns} data={data} />;
}

Sorting

Use the SortableHeader helper inside a column's header function to make a column sortable. Clicking the header toggles ascending, descending, and unsorted.

DeviceRegionFirmware
KIOSK-4729us-westhealthy2.4.1
KIOSK-3310us-eastupdating2.4.0
KIOSK-3300us-westoffline2.4.1
KIOSK-2150eu-northhealthy2.4.1
KIOSK-2199eu-northdegraded2.4.0
Showing 15 of 5
tsx
import { SortableHeader } from "@/components/ui/data-table";

const columns: ColumnDef<Device>[] = [
  { accessorKey: "id", header: "Device" },
  {
    accessorKey: "status",
    header: ({ column }) => (
      <SortableHeader column={column}>Status</SortableHeader>
    ),
  },
];

Pagination

Pagination is built in. Pass pageSize to control rows per page. The footer shows the current range and total count, with previous/next buttons.

DeviceRegionStatusFirmware
KIOSK-4729us-westhealthy2.4.1
KIOSK-3310us-eastupdating2.4.0
KIOSK-3300us-westoffline2.4.1
Showing 13 of 5
tsx
<DataTable
  columns={columns}
  data={data}
  pageSize={10}
/>

Filtering

Pass filterColumn with the accessorKey of the column to filter on. A search input renders above the table. Filtering happens client-side via TanStack's getFilteredRowModel.

DeviceRegionStatusFirmware
KIOSK-4729us-westhealthy2.4.1
KIOSK-3310us-eastupdating2.4.0
KIOSK-3300us-westoffline2.4.1
KIOSK-2150eu-northhealthy2.4.1
KIOSK-2199eu-northdegraded2.4.0
Showing 15 of 5
tsx
<DataTable
  columns={columns}
  data={data}
  filterColumn="region"
  filterPlaceholder="Filter by region..."
/>

Row selection

Add a column with id: "select" rendering a Checkbox in both header and cell to enable row selection. The selected count appears in the footer. Selected rows get a subtle accent background.

DeviceRegionStatus
KIOSK-4729us-westhealthy
KIOSK-3310us-eastupdating
KIOSK-3300us-westoffline
KIOSK-2150eu-northhealthy
KIOSK-2199eu-northdegraded
Showing 15 of 5
tsx
import { Checkbox } from "@/components/ui/checkbox";

const columns: ColumnDef<Device>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) =>
          table.toggleAllPageRowsSelected(!!value)
        }
        aria-label="Select all"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
  },
  { accessorKey: "id", header: "Device" },
];

Column visibility

Pass enableColumnVisibility to add a Columns dropdown in the header bar. Users can toggle individual columns on or off without losing data.

DeviceRegionStatusFirmware
KIOSK-4729us-westhealthy2.4.1
KIOSK-3310us-eastupdating2.4.0
KIOSK-3300us-westoffline2.4.1
KIOSK-2150eu-northhealthy2.4.1
KIOSK-2199eu-northdegraded2.4.0
Showing 15 of 5
tsx
<DataTable
  columns={columns}
  data={data}
  enableColumnVisibility
/>

Row actions

Add a column with a kebab icon trigger and DropdownMenu to provide per-row actions like view, edit, or delete. Set enableSorting: false on the action column.

DeviceRegionStatus
KIOSK-4729us-westhealthy
KIOSK-3310us-eastupdating
KIOSK-3300us-westoffline
KIOSK-2150eu-northhealthy
KIOSK-2199eu-northdegraded
Showing 15 of 5
tsx
import { MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

const columns: ColumnDef<Device>[] = [
  { accessorKey: "id", header: "Device" },
  {
    id: "actions",
    cell: ({ row }) => (
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="ghost" size="icon-sm" aria-label="Open menu">
            <MoreHorizontal className="size-4" />
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuLabel>Actions</DropdownMenuLabel>
          <DropdownMenuItem>Copy ID</DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    ),
    enableSorting: false,
  },
];

Props

PropTypeDefaultDescription
columnsColumnDef<TData, TValue>[]Required. Array of column definitions from @tanstack/react-table. Each column needs an accessorKey or accessorFn and a header.
dataTData[]Required. Array of row objects to display.
pageSizenumber10Number of rows per page when pagination is enabled.
filterColumnstringColumn accessorKey to filter on. When set, a search input renders above the table.
filterPlaceholderstring"Filter..."Placeholder text for the filter input.
enableColumnVisibilitybooleanfalseWhen true, renders a Columns dropdown menu in the header bar for toggling column visibility.

Building your own

Wuko's DataTable is opinionated. If you need different behavior — server-side pagination, custom filter UI, row expansion, virtualized rows, or anything else — compose your own using Table and useReactTable directly. The DataTable source is a useful starting point.

tsx
// If Wuko's DataTable defaults don't fit, compose your own with the underlying primitives.
"use client";

import * as React from "react";
import {
  type ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

export function CustomDataTable<TData>({
  columns,
  data,
}: {
  columns: ColumnDef<TData>[];
  data: TData[];
}) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((headerGroup) => (
          <TableRow key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <TableHead key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

For more advanced patterns, see TanStack Table's documentation.

Accessibility

  • Built on the Table primitive, which renders native HTML table elements. Screen readers announce row and column counts and header relationships.
  • Sortable column headers are rendered as buttons inside the <th>. The sort state is exposed via TanStack's column API; consumers can add aria-sort for additional screen reader support.
  • Selection checkboxes use the Wuko Checkbox primitive, which supports indeterminate state for the "some rows selected" header.
  • Column visibility dropdown is keyboard-navigable via the underlying DropdownMenu primitive (Tab, Arrow keys, Esc).
  • Pagination controls are buttons with appropriate disabled states when on the first or last page. The current page range and selected count announce changes for screen readers.