Skip to content
Dethink Components

ComponentsDataTable

DataTable

Display rows of data with sorting, filtering, selection, and pagination.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  DataTable,
  type DataTableColumnDef,
} from "@dethink/components";

Try the examples, then open the code to use them in your app. Headers are real buttons — sort with Enter or Space.

Sorting and filtering

Column defs with a custom status cell, default sort, and the global filter toolbar.

Latest deployments
api-gatewayproductionsuccess4m 12s
api-gatewaystagingsuccess3m 58s
billingproductionfailed1m 03s
web-appstagingsuccess6m 41s
workerproductionrunning
Show sourceexamples/data-table/basic.tsx
examples/data-table/basic.tsx
"use client";

import { DataTable, type DataTableColumnDef } from "@dethink/components";

type Deploy = {
  id: string;
  service: string;
  env: string;
  status: "success" | "failed" | "running";
  duration: string;
};

const deploys: Deploy[] = [
  {
    id: "d1",
    service: "api-gateway",
    env: "production",
    status: "success",
    duration: "4m 12s",
  },
  {
    id: "d2",
    service: "billing",
    env: "production",
    status: "failed",
    duration: "1m 03s",
  },
  {
    id: "d3",
    service: "web-app",
    env: "staging",
    status: "success",
    duration: "6m 41s",
  },
  {
    id: "d4",
    service: "worker",
    env: "production",
    status: "running",
    duration: "—",
  },
  {
    id: "d5",
    service: "api-gateway",
    env: "staging",
    status: "success",
    duration: "3m 58s",
  },
];

const columns: DataTableColumnDef<Deploy>[] = [
  { accessorKey: "service", header: "Service" },
  { accessorKey: "env", header: "Environment" },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => {
      const status = row.original.status;
      const tone =
        status === "success"
          ? "text-success"
          : status === "failed"
            ? "text-destructive"
            : "text-info";
      return <span className={`font-medium ${tone}`}>{status}</span>;
    },
  },
  { accessorKey: "duration", header: "Duration" },
];

export function DataTableBasic() {
  return (
    <DataTable
      columns={columns}
      data={deploys}
      getRowId={(row) => row.id}
      caption="Latest deployments"
      enableGlobalFilter
      globalFilterPlaceholder="Filter deployments…"
      defaultSorting={[{ id: "service", desc: false }]}
    />
  );
}

Empty, loading, and error states

The three non-data states render inside the table region and announce to assistive tech.

No projects yet. Create one to get started.
Show sourceexamples/data-table/states.tsx
examples/data-table/states.tsx
"use client";

import { useState } from "react";
import {
  Button,
  DataTable,
  type DataTableColumnDef,
} from "@dethink/components";

type Row = { id: string; name: string };

const columns: DataTableColumnDef<Row>[] = [
  { accessorKey: "name", header: "Name" },
];

export function DataTableStates() {
  const [state, setState] = useState<"loading" | "empty" | "error">("empty");

  return (
    <div className="space-y-3">
      <div className="flex justify-center gap-2">
        {(["empty", "loading", "error"] as const).map((mode) => (
          <Button
            key={mode}
            size="sm"
            variant={state === mode ? "solid" : "outline"}
            onClick={() => setState(mode)}
          >
            {mode}
          </Button>
        ))}
      </div>
      <DataTable
        columns={columns}
        data={[]}
        getRowId={(row) => row.id}
        loading={state === "loading"}
        error={
          state === "error"
            ? "Could not load projects — retry shortly."
            : undefined
        }
        emptyContent="No projects yet. Create one to get started."
      />
    </div>
  );
}

Examples that combine components for common tasks.

Incident dashboard with bulk actions

Sorting, filtering, multi-select, and pagination working together: select incidents across the filtered set, acknowledge them in bulk, and a live region confirms the change.

0 selected

5 rows
Elevated 5xx on checkoutbillingsev1open
Slow queries on searchsearchsev2open
Cache hit rate droppedapi-gatewaysev2open
Webhook retries spikingintegrationssev3acknowledged
Page 1 of 2
Show sourceexamples/data-table/recipe-ops-dashboard.tsx
examples/data-table/recipe-ops-dashboard.tsx
"use client";

import { useState } from "react";
import {
  Button,
  DataTable,
  type DataTableColumnDef,
  type DataTableRowSelectionState,
} from "@dethink/components";

type Incident = {
  id: string;
  title: string;
  service: string;
  severity: "sev1" | "sev2" | "sev3";
  status: "open" | "acknowledged" | "resolved";
};

const initialIncidents: Incident[] = [
  {
    id: "i1",
    title: "Elevated 5xx on checkout",
    service: "billing",
    severity: "sev1",
    status: "open",
  },
  {
    id: "i2",
    title: "Slow queries on search",
    service: "search",
    severity: "sev2",
    status: "open",
  },
  {
    id: "i3",
    title: "Webhook retries spiking",
    service: "integrations",
    severity: "sev3",
    status: "acknowledged",
  },
  {
    id: "i4",
    title: "Cache hit rate dropped",
    service: "api-gateway",
    severity: "sev2",
    status: "open",
  },
  {
    id: "i5",
    title: "Cert expiring in 7 days",
    service: "edge",
    severity: "sev3",
    status: "open",
  },
];

const severityTone: Record<Incident["severity"], string> = {
  sev1: "bg-destructive/10 text-destructive",
  sev2: "bg-warning/15 text-warning",
  sev3: "bg-muted text-muted-foreground",
};

const columns: DataTableColumnDef<Incident>[] = [
  { accessorKey: "title", header: "Incident" },
  { accessorKey: "service", header: "Service" },
  {
    accessorKey: "severity",
    header: "Severity",
    cell: ({ row }) => (
      <span
        className={`rounded-full px-2 py-0.5 text-xs font-medium uppercase ${severityTone[row.original.severity]}`}
      >
        {row.original.severity}
      </span>
    ),
  },
  { accessorKey: "status", header: "Status" },
];

/**
 * The full feature set in one seat: sorting, filtering, selection, and
 * pagination together, with a bulk action operating on the selected rows
 * and a live region confirming what changed.
 */
export function DataTableRecipeOpsDashboard() {
  const [incidents, setIncidents] = useState(initialIncidents);
  const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
    {},
  );
  const [statusMessage, setStatusMessage] = useState("");
  const selectedIds = Object.keys(rowSelection).filter(
    (id) => rowSelection[id],
  );

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between gap-3">
        <p aria-live="polite" className="text-muted-foreground text-sm">
          {statusMessage || `${selectedIds.length} selected`}
        </p>
        <Button
          size="sm"
          disabled={selectedIds.length === 0}
          onClick={() => {
            setIncidents((current) =>
              current.map((incident) =>
                selectedIds.includes(incident.id)
                  ? { ...incident, status: "acknowledged" }
                  : incident,
              ),
            );
            setStatusMessage(`Acknowledged ${selectedIds.length} incident(s).`);
            setRowSelection({});
          }}
        >
          Acknowledge selected
        </Button>
      </div>
      <DataTable
        columns={columns}
        data={incidents}
        getRowId={(row) => row.id}
        selectionMode="multiple"
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        enableGlobalFilter
        globalFilterPlaceholder="Filter incidents…"
        enablePagination
        defaultPagination={{ pageIndex: 0, pageSize: 4 }}
        pageSizeOptions={[4, 8]}
        defaultSorting={[{ id: "severity", desc: false }]}
      />
    </div>
  );
}

Every stateful feature is controllable (value + onChange) or uncontrolled (default value), and manual modes hand the work to your server.

DataTable props
PropWhat it doesDefault
columnsDataTableColumnDef<TData>[]TanStack-style column definitions: accessorKey, header, and an optional cell renderer.Not set
dataTData[]Row data; pair with getRowId for stable selection keys.Not set
enableSortingbooleanSortable headers with accessible sort buttons; control with sorting/onSortingChange or seed defaultSorting.true
enableGlobalFilterbooleanToolbar text filter across all columns; globalFilterPlaceholder labels it.false
enableColumnFilters / renderColumnFilterboolean / (column) => ReactNodePer-column filtering with a custom filter UI per column.false / —
selectionMode"none" | "single" | "multiple"Row selection with header select-all in multiple mode; control with rowSelection/onRowSelectionChange."none"
enablePaginationbooleanPagination footer with pageSizeOptions; control with pagination/onPaginationChange.false
loading / loadingContentboolean / ReactNodeLoading state announced to assistive tech.false / built-in
error / emptyContentReactNodeError and empty states rendered inside the table region.Not set
manualSorting / manualFiltering / manualPaginationbooleanServer-driven mode: the table emits state changes and renders what you pass, with pageCount/rowCount.false
renderRowActions(row) => ReactNodeTrailing actions cell per row — menus, buttons, links.Not set
density / caption / labelsdensity scale / ReactNode / DataTableLabelsVisual density, an accessible caption, and overridable UI strings for localization.Not set