jengacore.shadcn registry · Tailwind v4 · Radix and Base UI
Components
jengacore components · @jengacore/data-table

DataTable

Table + TanStack: sortable mono headers, row click, selected and fresh rows, empty state.

Install

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

Without the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/data-table.json

npm packages

@tanstack/react-table@^8lucide-react

Source

components/jc/data-table.tsx
"use client"

import * as React from "react"
import {
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  useReactTable,
  type ColumnDef,
  type SortingState,
} from "@tanstack/react-table"
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { EmptyState } from "@/components/jc/empty-state"
import { cn } from "@/lib/utils"

export type { ColumnDef } from "@tanstack/react-table"

/**
 * The workhorse of all four apps: shadcn Table + TanStack. Quiet headers, hairline rows, accent hover,
 * `secondary` for the selected row, mint flash for rows that arrive live (`isFresh`).
 * Put `meta: { align: "right" }` on amount columns.
 */
export function DataTable<TData, TValue>({
  columns,
  data,
  density = "default",
  sorting: controlledSorting,
  onSortingChange,
  getRowId,
  onRowClick,
  selectedId,
  isFresh,
  empty = { title: "Nothing here yet." },
  className,
}: {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
  density?: "default" | "compact"
  sorting?: SortingState
  onSortingChange?: (sorting: SortingState) => void
  getRowId?: (row: TData) => string
  onRowClick?: (row: TData) => void
  selectedId?: string
  isFresh?: (row: TData) => boolean
  empty?: { title: string; description?: React.ReactNode; action?: React.ReactNode }
  className?: string
}) {
  const [innerSorting, setInnerSorting] = React.useState<SortingState>([])
  const sorting = controlledSorting ?? innerSorting
  const table = useReactTable({
    data,
    columns,
    getRowId,
    state: { sorting },
    onSortingChange: (updater) => {
      const next = typeof updater === "function" ? updater(sorting) : updater
      if (onSortingChange) onSortingChange(next)
      else setInnerSorting(next)
    },
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  })

  return (
    <div data-slot="data-table" className={cn("overflow-hidden rounded-lg border border-border bg-card shadow-card contrast:border-input", className)}>
      <Table density={density}>
        <TableHeader>
          {table.getHeaderGroups().map((hg) => (
            <TableRow key={hg.id} className="hover:[&>td]:bg-transparent">
              {hg.headers.map((h) => {
                const align = (h.column.columnDef.meta as { align?: "right" } | undefined)?.align
                const sorted = h.column.getIsSorted()
                return (
                  <TableHead
                    key={h.id}
                    className={cn(align === "right" && "text-right")}
                    aria-sort={sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : undefined}
                  >
                    {h.isPlaceholder ? null : h.column.getCanSort() ? (
                      <button
                        type="button"
                        onClick={h.column.getToggleSortingHandler()}
                        className={cn("inline-flex items-center gap-1 uppercase hover:text-foreground", align === "right" && "flex-row-reverse")}
                      >
                        {flexRender(h.column.columnDef.header, h.getContext())}
                        {sorted === "asc" ? <ArrowUp className="size-3" /> : sorted === "desc" ? <ArrowDown className="size-3" /> : <ArrowUpDown className="size-3 opacity-40" />}
                      </button>
                    ) : (
                      flexRender(h.column.columnDef.header, h.getContext())
                    )}
                  </TableHead>
                )
              })}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows.length ? (
            table.getRowModel().rows.map((row) => (
              <TableRow
                key={row.id}
                selected={selectedId !== undefined && row.id === selectedId}
                fresh={isFresh?.(row.original)}
                onClick={onRowClick ? () => onRowClick(row.original) : undefined}
                onKeyDown={
                  onRowClick
                    ? (e) => {
                        if (e.key === "Enter") onRowClick(row.original)
                      }
                    : undefined
                }
                tabIndex={onRowClick ? 0 : undefined}
                className={cn(onRowClick && "cursor-pointer focus-ring [--focus-offset:-2px]")}
              >
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id} className={cn((cell.column.columnDef.meta as { align?: "right" } | undefined)?.align === "right" && "text-right")}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </TableCell>
                ))}
              </TableRow>
            ))
          ) : (
            <TableRow className="hover:[&>td]:bg-transparent">
              <TableCell colSpan={columns.length} className="h-auto whitespace-normal">
                <EmptyState {...empty} />
              </TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  )
}