June 22, 20266 min read

Building the data table.

shadcn ships a basic table. It renders correctly, takes tokens, and handles simple layouts. It doesn't sort, scroll horizontally, show row details on mobile, or know what column type it's rendering. Belin's DataTable does all of those. Here's how it was designed.

The problem with generic table components.

A table component that renders <thead> and <tbody> does very little. Real data tables have opinions: numbers are right-aligned and monospaced, status indicators use color, names are bold, long text is truncated. A generic component either enforces none of this or forces you to repeat the same rendering logic in every table you build.

The design decision was to give every column a type field. The renderer looks up the type, picks the right cell component, and applies the right text style. You describe your columns once; the table renders them correctly everywhere.

Twenty column types.

The column type system covers the data you actually find in real apps:

  • Textname-link, long-text, location
  • Numberscurrency, delta-currency, percentage, integer, score, duration
  • Statusstatus-badge, priority, boolean-icon, progress-bar
  • Timedate, relative-time
  • Compositeavatar-name, multi-tag, file, external-link

Each type maps to a small component in belin/components/data-table/cell-types/. The cell renderer is a switch that reads column.type and returns the right one. Nothing happens outside that switch — no conditional classes in the parent, no per-column overrides scattered through the table template.

tsx
// cell-renderer.tsx (simplified)
switch (column.type) {
  case "currency":       return <CurrencyCell value={value} />;
  case "status-badge":   return <StatusBadgeCell value={value} options={column.options} />;
  case "progress-bar":   return <ProgressBarCell value={value} />;
  default:               return <span className="ts-table-cell-default">{String(value)}</span>;
}

Text styles, not ad-hoc classes.

Every cell type uses a named text style instead of ad-hoc Tailwind classes. Header cells use ts-table-header, currency columns use ts-table-cell-mono, muted metadata uses ts-table-cell-muted. The customizer exposes all eight table text styles directly: you change the weight of column headers and every table on the site updates.

This matters because it's the same system as everything else in Belin. The Typography component, the blog articles, the UI kit labels — all of them use text styles. The DataTable isn't a special case; it follows the same rules. It also rides the same theme export: table padding, text styles, and colors travel in the exported theme like every other component, so a second project gets your table looking the same.

Mobile: collapse, then expand.

Below 768px, the table shows three columns plus a sticky expand icon. Tapping the icon opens an accordion row with all the hidden fields. This approach was chosen over horizontal scrolling because it lets the data stay readable at small sizes — you're choosing what to read, not squinting at a miniaturised full table.

The three visible columns on mobile are the ones marked mobileVisible: true in the column definition. The expand accordion shows everything else in a two-column grid of label-value pairs.

The customizer surface.

The Data table editor in the Components tab has four sections:

  1. Cell padding — four sliders (cell horizontal/vertical, header horizontal/vertical). The values write to four CSS variables (--table-cell-px, etc.) that every row component reads.
  2. Text styles — an accordion with one item per table text style. Each has Family, Size, Weight, and Color controls. Changes write to the same storage key as the rest of the text style system.
  3. Colors — header text, cell text, row hover, border. Each is a token reference so it stays in sync with the rest of the theme.
  4. Text wrap — a toggle that switches truncated cells to wrapping. Useful for long-text columns in dense views.

The goal was a table that feels designed, not assembled. Every visual decision should have a single place to change it.

What's next.

The current DataTable is a v1 — it proves the cell-type model and the customizer surface work. Three things are on the v3 list for tables: a TanStack Table integration for real sorting and filtering on large datasets, a column reorder interface, and a row selection mode. The cell-type schema stays the same; the renderer gets smarter.

The full table specification lives in docs/belin-table-spec-v0.3.md. The implementation is in belin/components/data-table/. The live version is at /data-table.