Data Table

v0.2.1

TanStack-table wrapper with sorting, pagination, column toggle, row selection, and widget-owned loading/empty/error states.

View as Markdown

Install

npx shadcn@latest add @signalos/data-table

Requires a configured @signalos registry and a valid SIGNALOS_REGISTRY_TOKEN - get access. Registry dependencies (@signalos/tokens, @signalos/utils, @signalos/button, @signalos/dropdown-menu, @signalos/select, @signalos/skeleton) are pulled automatically.

Preview

Example & code

data-table.example.tsx
// Example: three data-table variants from the same widget.
// This file is consumer-side code - copy it into a screen and adapt.
import { useState } from "react"
import type { ColumnDef, RowSelectionState } from "@tanstack/react-table"

import { DataTable } from "@/components/widgets/data-table/DataTable"
import { StatusBadge } from "@/components/widgets/status-badge/StatusBadge"
import type { Severity } from "@/components/widgets/status-badge/StatusBadge.types"

interface Signal {
  id: string
  title: string
  owner: string
  severity: Severity
}

const columns: ColumnDef<Signal, string>[] = [
  { accessorKey: "id", header: "ID" },
  { accessorKey: "title", header: "Title" },
  { accessorKey: "owner", header: "Owner" },
  {
    accessorKey: "severity",
    header: "Severity",
    // Screens map domain values → widget props right in the column def:
    cell: ({ row }) => (
      <StatusBadge severity={row.original.severity} appearance="chip" />
    ),
  },
]

const signals: Signal[] = [
  {
    id: "SIG-1042",
    title: "Latency spike on ingest queue",
    owner: "M. Chen",
    severity: "critical",
  },
  {
    id: "SIG-1041",
    title: "Duplicate decision records",
    owner: "A. Okafor",
    severity: "high",
  },
  {
    id: "SIG-1038",
    title: "Stale cache on summary tiles",
    owner: "R. Patel",
    severity: "medium",
  },
]

/** Variant 1 - server-paginated table with column toggle. */
export function PaginatedSignalsTable() {
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(10)

  return (
    <DataTable
      columns={columns}
      data={signals}
      showColumnToggle
      pagination={{
        page,
        pageSize,
        total: 128,
        totalPages: Math.ceil(128 / pageSize),
        onPageChange: setPage,
        onRowsPerPageChange: setPageSize,
      }}
      onRowClick={(signal) => console.info("open", signal.id)}
    />
  )
}

/** Variant 2 - selectable rows (bulk actions). */
export function SelectableSignalsTable() {
  const [selection, setSelection] = useState<RowSelectionState>({})

  return (
    <DataTable
      columns={columns}
      data={signals}
      rowSelection={selection}
      onRowSelectionChange={setSelection}
      maxHeight="320px"
    />
  )
}

/** Variant 3 - compact embedded list (no pagination, capped height). */
export function CompactSignalsTable() {
  return (
    <DataTable
      columns={columns}
      data={signals}
      maxHeight="240px"
      emptyMessage="Nothing routed to you."
    />
  )
}

export default function Example() {
  return (
    <div className="flex flex-col gap-8">
      <PaginatedSignalsTable />
      <CompactSignalsTable />
    </div>
  )
}

Props

PaginationConfig

PropTypeDefaultDescription
page*number-Current page (1-indexed).
pageSize*number-
total*number-Total record count across all pages.
totalPages*number-
pageSizeOptionsnumber[] | undefined[5, 10, 25, 50]
onPageChange*(page: number) => void-
onRowsPerPageChange*(pageSize: number) => void-

DataTableErrorConfig

PropTypeDefaultDescription
messagestring | undefined"Something went wrong."Error message shown in the table body.
onRetry(() => void) | undefined-When provided, renders a Retry button.

DataTableProps

PropTypeDefaultDescription
columns*ColumnDef<TData, TValue>[]-
data*TData[]-
maxHeightstring | undefined"calc(100vh - 400px)"Max height of the scrollable body.
isLoadingboolean | undefined-Renders skeleton rows while true. Takes precedence over error/empty.
errorDataTableErrorConfig | null | undefined-Error state owned by the widget. Rendered when set and not loading.
showColumnToggleboolean | undefined-Shows the column visibility toggle.
onRowClick((row: TData) => void) | undefined-
emptyMessagestring | undefined"No results found."
paginationPaginationConfig | undefined-
rowSelectionRowSelectionState | undefined-
onRowSelectionChangeOnChangeFn<RowSelectionState> | undefined-
classNamestring | undefined-Merged with `cn()` onto the root element.
data-testidstring | undefined"data-table"Test identifier rendered as `data-testid` on the root element. Internal parts (`-empty`, `-error`, `-pagination`, etc.) derive their ids from this value.

DataTableSkeletonProps

PropTypeDefaultDescription
columnsnumber | undefined6
rowsnumber | undefined5
showPaginationboolean | undefinedtrue
classNamestring | undefined-Merged with `cn()` onto the root element.
data-testidstring | undefined"data-table-skeleton"Test identifier rendered as `data-testid` on the root element.

npm dependencies

@tanstack/react-table@^8.21.3lucide-react@^1.7.0

Changelog

0.2.1

  • Fix the record count rendering as 1-10 of 42records. JSX drops the newline between an expression and text on the following line, so a prettier line-wrap silently removed the space. The test only asserted the data-total attribute, not the visible string; it now asserts both.

data-table

0.2.0

  • Add className (merged onto the root element) and a "data-testid" prop (@default "data-table"); all internal ids (-empty, -error, -retry, -column-toggle, -pagination, -page-size-trigger, -record-count, -prev-page, -next-page, -page-{n}, -sort-{id}) now derive from it. Default behavior is unchanged for existing consumers.
  • Sortable column headers now render as a real <button> (keyboard-reachable, Enter/Space toggles sort) instead of a plain <div onClick>, and the <th> now sets aria-sort="ascending" | "descending" | "none" from the column's sort state.
  • Error state div now has role="alert".

0.1.2

  • Use semantic status tokens (text-text-error, bg-bg-warning, border-border-, bg-status-) instead of raw palette scales so client theme overrides restyle status colors.

0.1.1

  • Content sync fix to bring source in line with published registry payload.

0.1.0

  • Initial release, lifted near-verbatim from signal-core-ui common/data-table.tsx (all data-testids, skeleton rows, pagination, column toggle, row selection preserved).
  • Added a widget-owned error state (error: { message?, onRetry? }) with render precedence loading → error → rows → empty, plus data-table-empty / data-table-error / data-table-retry test ids.
  • Prop interfaces extracted to DataTable.types.ts.