Data table

Pro

Rows of records: search, sort, filter chips, column settings, selection and pages. Counts are of what's actually loaded ("Showing 1–10 of 248"). A blank cell shows a dash, never an empty gap that might be read as zero. Empty explains itself — no rows at all, or none matching the search.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/data-table

Brings in: data, dropdown-menu, filter-chips, locale, skeleton, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { DataTable } from "@/components/data-table"
import type { Column, FilterChip } from "@/lib/data"
import { Button } from "@/components/ui/button"

type Migration = { id: string; name: string; repo: string; env: "Production" | "Staging"; rows: number | null; author: string; applied: string }

const migrations: Migration[] = [
  { id: "m142", name: "0142_add_region_to_projects", repo: "acme/api", env: "Staging", rows: 18_240, author: "Dev agent", applied: "2026-11-04" },
  { id: "m141", name: "0141_index_deployments_sha", repo: "acme/api", env: "Production", rows: null, author: "Dana", applied: "2026-11-02" },
  { id: "m140", name: "0140_drop_legacy_score", repo: "acme/api", env: "Production", rows: 9_812, author: "Jonas", applied: "2026-10-29" },
  { id: "m139", name: "0139_backfill_owner_id", repo: "acme/workers", env: "Production", rows: 42_100, author: "Dev agent", applied: "2026-10-27" },
  { id: "m138", name: "0138_add_flags_table", repo: "acme/web", env: "Production", rows: 0, author: "Mei", applied: "2026-10-22" },
  { id: "m137", name: "0137_split_invoice_lines", repo: "acme/api", env: "Staging", rows: 133_480, author: "Dev agent", applied: "2026-10-20" },
  { id: "m136", name: "0136_rename_plan_tier", repo: "acme/api", env: "Production", rows: 6_031, author: "Dana", applied: "2026-10-15" },
]

const columns: Column<Migration>[] = [
  { id: "name", label: "Migration", value: (m) => m.name, render: (m) => <code className="font-mono text-xs">{m.name}</code> },
  { id: "repo", label: "Repository", value: (m) => m.repo },
  { id: "env", label: "Environment", value: (m) => m.env },
  { id: "rows", label: "Rows touched", value: (m) => m.rows, numeric: true },
  { id: "author", label: "Author", value: (m) => m.author, optional: true },
  { id: "applied", label: "Applied", value: (m) => m.applied },
]

export default function Example() {
  const [filters, setFilters] = React.useState<FilterChip[]>([{ id: "repo", label: "Repository", value: "Not acme/web" }])
  const rows = filters.some((f) => f.id === "repo") ? migrations.filter((m) => m.repo !== "acme/web") : migrations
  return (
    <DataTable<Migration>
      title="Migrations"
      columns={columns}
      rows={rows}
      rowId={(m) => m.id}
      selectable
      pageSize={5}
      filters={filters}
      onRemoveFilter={(id) => setFilters((all) => all.filter((f) => f.id !== id))}
      onClearFilters={() => setFilters([])}
      actions={(selected) => (
        <Button size="xs" variant="outline" onClick={() => {}}>
          Ask Keel about {selected.size}
        </Button>
      )}
    />
  )
}

<DataTable>

PropTypeDefaultDescription
columnsrequiredColumn<Row>[]—The columns: header, how to read and show a row's value, and whether it's hidden until turned on.
rowsrequiredRow[]—The loaded records. Search, sort and paging work on these in the browser.
rowIdrequired(row: Row) => string—A stable id for a row, used for keys and selection.
titlestring | undefined—Heading over the table, also its accessible name. Defaults to "Records" for the name, with no heading shown.
loadingboolean | undefinedfalseRows are being fetched; shows skeleton rows instead of the empty message.
searchableboolean | undefinedtrueShows a search box that matches any column's value.
filtersFilterChip[] | undefined[]Filters already applied, shown as removable chips.
onRemoveFilter((id: string) => void) | undefined—Called with a filter's id when its chip is removed.
onClearFilters(() => void) | undefined—Called when a person clears all filters.
selectableboolean | undefinedfalseAdds a checkbox to each row and a select-all for the page.
selectedSet<string> | undefined—Ids of selected rows, when you control selection. Without it the table keeps its own.
onSelectedChange((ids: Set<string>) => void) | undefined—Called with the new set of selected ids when a person selects or clears rows.
onRowClick((row: Row) => void) | undefined—Called with the row a person clicks; rows look clickable when set.
pageSizenumber | undefined10Rows per page.
emptystring | undefined—Shown when there are no rows at all (not when a search or filter matches nothing).
actions((selected: Set<string>) => React.ReactNode) | undefined—Shown in the header when rows are selected, e.g. bulk actions.
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under dataTable in <LocaleProvider strings={{ … }}>; the table is exported as dataTableStrings.

KeyEnglish
recordsRecords
emptyNo records yet.
selected{count} selected
searchSearch
columnsColumns
showColumnsShow columns
selectAllSelect all on this page
selectRowSelect row
blankblank
noMatchNothing matches. Try a different search or remove a filter.
showingShowing {from}–{to} of {total}
inAll({total} in all)
previousPagePrevious page
nextPageNext page
page{page} / {pages}