A bare data table: describe `columns` once (a `title` plus a `dataIndex` or a custom `render`), pass `data`, get a `<table>` back. No sorting, pagination or empty state — add those around it.
registry/behivetech/atoms/table at v0.2.4, snapshotted 2026-11-09
table.tsx
1import { type ReactNode } from "react";2import { getClassName } from "@bht-component-registry/class-names";3import styles from "./table.module.scss";45export interface TableColumn<TRow> {6 /** Unique identity for this column; also used as the header cell's React key */7 key: string;8 /** Column header content */9 title: ReactNode;10 /** Reads a raw field off each row for this column, when no custom `render` is given */11 dataIndex?: keyof TRow;12 /** Renders a cell's content. Receives `row[dataIndex]` as `value` when `dataIndex` is set. */13 render?: (value: unknown, row: TRow) => ReactNode;14 /** Additional class name applied to every cell (header and body) in this column */15 className?: string;16}1718export interface TableProps<TRow> {19 /** Column definitions, rendered left to right */20 columns: TableColumn<TRow>[];21 /** Rows to render, in the order given — Table does not sort, paginate, or filter */22 data: TRow[];23 /** Returns a stable, unique key for a row, used as its React key */24 rowKey: (row: TRow) => string;25 /** Additional class names to merge with the component root element */26 className?: string;27}2829/**30 * A bare data table — describe `columns` once (each with a `title` and either a31 * `dataIndex` or a custom `render`), pass `data`, get a `<table>` back. No sorting,32 * pagination, or empty-state handling — those are the consuming app's concern to add33 * around this, not props on the component itself.34 */35export const Table = <TRow,>({36 columns,37 data,38 rowKey,39 className,40}: TableProps<TRow>) => {41 const [rootClass] = getClassName({42 className,43 rootClass: "BHT__Table",44 styles,45 });4647 return (48 <table className={rootClass}>49 <thead>50 <tr>51 {columns.map((column) => (52 <th key={column.key} className={column.className}>53 {column.title}54 </th>55 ))}56 </tr>57 </thead>58 <tbody>59 {data.map((row) => (60 <tr key={rowKey(row)}>61 {columns.map((column) => {62 const value = column.dataIndex63 ? row[column.dataIndex]64 : undefined;65 return (66 <td key={column.key} className={column.className}>67 {column.render68 ? column.render(value, row)69 : value !== undefined70 ? String(value)71 : null}72 </td>73 );74 })}75 </tr>76 ))}77 </tbody>78 </table>79 );80};
table.module.scss
1// Material Design 3 has no table spec; this follows its data-table guidance:2// title-small headers on `on-surface-variant`, body-medium cells, and3// `outline-variant` row dividers.45.BHT__Table {6 width: 100%;7 border-collapse: collapse;89 th {10 text-align: left;11 padding: var(--spacing-2, 0.5rem) var(--spacing-4, 1rem);12 font-family: var(--md-sys-typescale-title-small-font, var(--font-body));13 font-size: var(--md-sys-typescale-title-small-size, 0.875rem);14 font-weight: var(--md-sys-typescale-title-small-weight, 500);15 line-height: var(--md-sys-typescale-title-small-line-height, 1.25rem);16 letter-spacing: var(--md-sys-typescale-title-small-tracking, 0.0071em);17 color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));18 background: var(19 --md-sys-color-surface-container,20 var(--color-surface-raised)21 );22 border-bottom: 1px solid23 var(--md-sys-color-outline-variant, var(--color-border));24 }2526 td {27 position: relative;28 padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);29 font-family: var(--md-sys-typescale-body-medium-font, var(--font-body));30 font-size: var(--md-sys-typescale-body-medium-size, 0.875rem);31 font-weight: var(--md-sys-typescale-body-medium-weight, 400);32 line-height: var(--md-sys-typescale-body-medium-line-height, 1.25rem);33 letter-spacing: var(--md-sys-typescale-body-medium-tracking, 0.0179em);34 color: var(--md-sys-color-on-surface, var(--color-text-body));35 border-bottom: 1px solid36 var(--md-sys-color-outline-variant, var(--color-border));3738 // Row hover state layer. It sits on each cell rather than the `<tr>`39 // because a positioned table row is still unreliable as a containing40 // block for an absolutely positioned pseudo-element.41 &::after {42 content: "";43 position: absolute;44 inset: 0;45 background: currentColor;46 opacity: 0;47 pointer-events: none;48 transition: opacity var(--md-sys-motion-duration-short-2, 100ms)49 var(--md-sys-motion-easing-standard, ease);50 }51 }5253 tr:last-child td {54 border-bottom: none;55 }5657 tr:hover td::after {58 opacity: var(--md-sys-state-hover-opacity, 0.08);59 }60}
index.ts
1export { Table } from "./table.js";2export type { TableProps, TableColumn } from "./table.js";
table.example.tsx
1import { Table, type TableColumn } from "./table.js";23interface Invoice {4 id: string;5 customer: string;6 amount: number;7 status: "paid" | "overdue";8}910const invoices: Invoice[] = [11 { id: "inv-1", customer: "Acme Co", amount: 240, status: "paid" },12 { id: "inv-2", customer: "Globex", amount: 90, status: "overdue" },13 { id: "inv-3", customer: "Initech", amount: 512, status: "paid" },14];1516// Simplest case — dataIndex only, no custom rendering needed.17const basicColumns: TableColumn<Invoice>[] = [18 { key: "customer", title: "Customer", dataIndex: "customer" },19 { key: "amount", title: "Amount", dataIndex: "amount" },20];2122export const BasicTable = () => (23 <Table columns={basicColumns} data={invoices} rowKey={(row) => row.id} />24);2526// render() for custom cell content — formatting a value and reacting to the row.27const richColumns: TableColumn<Invoice>[] = [28 { key: "customer", title: "Customer", dataIndex: "customer" },29 {30 key: "amount",31 title: "Amount",32 dataIndex: "amount",33 render: (value) => `$${value}`,34 },35 {36 key: "status",37 title: "Status",38 render: (_value, row) =>39 row.status === "overdue" ? "⚠️ overdue" : "✓ paid",40 },41];4243export const TableWithCustomCells = () => (44 <Table columns={richColumns} data={invoices} rowKey={(row) => row.id} />45);4647export const EmptyTable = () => (48 <Table columns={basicColumns} data={[]} rowKey={(row) => row.id} />49);