M
MetricUI
Data

DataTable

A data table with sorting, pagination, search, formatting, sticky headers, pinned columns, and a footer row.

import { DataTable } from "metricui";

Use DataTable when you need to display tabular data with rich formatting, sorting, pagination, and interactive features. It auto-infers columns from your data shape, supports typed columns (currency, badge, sparkline, progress, and more), multi-column sorting, expandable detail rows, conditional formatting, pinned columns, sticky headers, search, and footer rows. For displaying a single key metric, use KpiCard instead.

Basic Example

Pass just dataand columns are auto-inferred from the first row's keys. Numbers are right-aligned and formatted automatically.

NameRevenueGrowth
Acme Corp142.3K13
Globex Inc98.7K-3
Initech67.4K8
Umbrella Corp54.2K22
Stark Industries189K16
<DataTable
  data={[
    { name: "Acme Corp", revenue: 142300, growth: 12.5 },
    { name: "Globex Inc", revenue: 98700, growth: -3.2 },
    { name: "Initech", revenue: 67400, growth: 8.1 },
  ]}
/>

Column Types

The type property on a column definition controls formatting, alignment, and rendering automatically. This is the most powerful feature of DataTable — set a type and the component handles everything else: currency columns get locale-aware formatting, badge columns auto-map status strings to semantic colors, sparkline columns render inline charts, and progress columns show bars. Numeric types (number, currency, percent, bar) use a monospace font for easy scanning; text columns use the body font.

TypeRenders asAlignFont
"text"Plain textLeftBody
"number"Locale-formatted numberRightMono
"currency"Currency-formatted (e.g. $4,999)RightMono
"percent"Percent with sign (e.g. +18.3%)RightMono
"badge"Semantic Badge componentLeftBody
"sparkline"Inline Sparkline chartCenterN/A
"status"StatusIndicator with rulesLeftBody
"progress"ProgressBar (0-100)CenterN/A
"bar"Inline horizontal barRightMono
"link"Clickable link with iconLeftBody
"date"Locale-formatted dateLeftBody
Product Overview
ProductPriceGrowthStatusTrend (7d)AdoptionCategory
Analytics Pro$5.0K18%active
SaaS
Data Vault$2.5K-4%churned
Storage
Cloud Suite$7.9K13%active
Platform
ML Pipeline$3.2K1%trial
AI/ML
Edge CDN$1.3K31%active
Infra
const columns: ColumnDef<ProductRow>[] = [
  { key: "product", header: "Product", type: "text", sortable: true, pin: "left" },
  { key: "price", header: "Price", type: "currency", sortable: true },
  { key: "growth", header: "Growth", type: "percent", sortable: true },
  { key: "status", header: "Status", type: "badge", sortable: true },
  { key: "trend", header: "Trend (7d)", type: "sparkline" },
  { key: "progress", header: "Adoption", type: "progress" },
  { key: "category", header: "Category", type: "text" },
];

<DataTable
  data={productData}
  columns={columns}
  title="Product Overview"
  striped
/>

Badge columns automatically map common status strings like "active", "pending", "failed" to semantic colors. For custom mappings, use the badgeVariant or badgeColor column properties. Status columns work with statusRules for rule-based indicator rendering. Link columns use linkHref to generate URLs dynamically.

Sorting & Multi-Sort

Columns with sortable: true cycle through ascending, descending, and none on click. Enable multiSort to allow Shift+click on additional columns for multi-level sorting (up to 3 levels). Active sort columns show numbered priority badges on their headers so users can see the sort hierarchy at a glance.

Sales Leaderboard

Shift+click headers to multi-sort

RepSegmentRevenueDeals
Alice ChenEnterprise$285.0K12
Bob ParkSMB$142.0K28
Carol DiazEnterprise$310.0K8
Dan KimSMB$198.0K35
Eva NovakEnterprise$275.0K15
Frank LiMid-Market$167.0K18
<DataTable
  data={salesReps}
  columns={[
    { key: "name", header: "Rep", type: "text", sortable: true },
    { key: "department", header: "Segment", type: "text", sortable: true },
    { key: "revenue", header: "Revenue", type: "currency", sortable: true },
    { key: "deals", header: "Deals", type: "number", sortable: true },
  ]}
  title="Sales Leaderboard"
  subtitle="Shift+click headers to multi-sort"
  multiSort
/>
// Single sort (default) — click toggles asc → desc → none
<DataTable columns={[{ key: "name", sortable: true }]} ... />

// Multi-sort — Shift+click adds secondary/tertiary sort
<DataTable columns={columns} multiSort />
// Up to 3 sort levels. Priority badges (1, 2, 3) appear on headers.

Expandable Rows

Pass a renderExpanded function to enable row expansion. Each row gets a chevron toggle on the left that reveals a detail panel below. The function receives the row data and index, and can return any React content.

Recent Orders
Order IDCustomerTotalStatusItems
ORD-001Acme Corp$12.5Kcompleted3
ORD-002Globex Inc$8.9Kpending2
ORD-003Initech$23.1Kcompleted5
ORD-004Umbrella Corp$4.5Kfailed1
<DataTable
  data={orders}
  columns={orderColumns}
  title="Recent Orders"
  renderExpanded={(row) => (
    <div className="space-y-2 text-sm text-[var(--muted)]">
      <p><strong>Order:</strong> {row.id}</p>
      <p><strong>Date:</strong> {row.date}</p>
      <p><strong>Items:</strong> {row.items} line items</p>
      <p><strong>Notes:</strong> Click any row to see its details.</p>
    </div>
  )}
/>

Conditional Formatting

Apply color rules at the column level with conditions — the same system used by KpiCard. Rules are evaluated top-to-bottom; first match wins. Supports above, below, between, equals, and compound and/or rules. For row-level highlighting, use rowConditions to apply CSS classes to entire rows based on a predicate function.

SLA Dashboard
MetricCurrentTargetDelta
Uptime1001000
Latency (ms)14510045
Error Rate01-1
Throughput8.5K10K-1.5K
Satisfaction550
const columns = [
  { key: "metric", header: "Metric", type: "text" },
  {
    key: "value",
    header: "Current",
    type: "number",
    conditions: [
      { when: "above", value: 99, color: "emerald" },
      { when: "between", min: 50, max: 99, color: "amber" },
      { when: "below", value: 50, color: "red" },
    ],
  },
  { key: "target", header: "Target", type: "number" },
  {
    key: "delta",
    header: "Delta",
    type: "number",
    conditions: [
      { when: "above", value: 0, color: "emerald" },
      { when: "below", value: 0, color: "red" },
    ],
  },
];

<DataTable data={metrics} columns={columns} title="SLA Dashboard" />
// Row-level conditions — apply a className to entire rows
<DataTable
  data={data}
  columns={columns}
  rowConditions={[
    {
      when: (row) => row.status === "critical",
      className: "bg-red-500/10",
    },
    {
      when: (row) => row.status === "warning",
      className: "bg-amber-500/10",
    },
  ]}
/>

Data States

DataTable handles loading, empty, and error states out of the box. The loading state renders animated skeleton rows that match your column layout.

Loading

Nothing to show — try adjusting your filters

Empty

No data matches your filters

// Loading — renders skeleton rows
<DataTable data={[]} loading />

// Empty state with custom message
<DataTable data={[]} empty={{ message: "No results found" }} />

// Error state
<DataTable data={[]} error={{ message: "Failed to load data" }} />

// Stale data indicator
<DataTable data={staleData} stale={{ message: "Data is 5 minutes old" }} />

Props

PropTypeDescription
data*
T[]

Row data array.

columns
Column<T>[]

Column definitions. When omitted, columns are auto-inferred from the first row.

title
string

Card title.

subtitle
string

Card subtitle.

description
string | React.ReactNode

Description popover content.

footnote
string

Footnote below the table.

action
React.ReactNode

Action slot (top-right).

pageSize
number

Page size for pagination. Set to enable pagination.

pagination
boolean

Enable pagination. Default: true when pageSize is set.

maxRows
number

Max visible rows. Shows "View all" when exceeded.

onViewAll
() => void

Callback when "View all" is clicked.

striped
boolean

Alternating row backgrounds.

dense
boolean

Compact row height. Falls back to config.dense.

onRowClick
(row: T, index: number) => void

Row click handler.

nullDisplay
NullDisplay

What to show when a cell value is null/undefined.

footer
FooterRow

Summary/totals footer row. Object keyed by column key with ReactNode values.

variant
CardVariant

Visual variant (supports custom strings). CSS-variable-driven via [data-variant].

className
string

Additional CSS class names.

loading
boolean

Show skeleton rows.

empty
EmptyState

Empty state configuration.

error
ErrorState

Error state.

stale
StaleState

Stale data indicator.

id
string

HTML id attribute.

data-testid
string

Test id.

stickyHeader
boolean

Sticky table header on scroll.

classNames
{ root?: string; header?: string; table?: string; thead?: string; tbody?: string; row?: string; cell?: string; footer?: string; pagination?: string }

Sub-element class name overrides.

searchable
boolean

Show a search input above the table for client-side filtering.

animate
boolean

Enable/disable animations. Currently reserved for future use.

scrollIndicators
boolean

Show left/right fade indicators when the table is horizontally scrollable.

rowConditions
RowCondition<T>[]

Conditional row styling. Each entry has a `when(row, index) => boolean` predicate and a `className` to apply when true.

multiSort
boolean

Enable Shift+click multi-column sorting. Default: false (single sort).

renderExpanded
(row: T, index: number) => React.ReactNode

Render expanded detail panel below a row. Enables chevron toggle on each row.

crossFilter
boolean | { field: string }

Enable cross-filter signal on row click. true uses the first column key, { field } overrides. Emits selection via CrossFilterProvider — does NOT change table appearance. Dev reads selection with useCrossFilter() and filters their own data.

drillDown
boolean | ((event: { row: T; index: number }) => React.ReactNode)

Enable drill-down on row click. `true` auto-generates a summary KPI row + detail view from the row data. Pass a render function for full control over the panel content. Requires DrillDown.Root wrapper. When both drillDown and crossFilter are set, drillDown wins.

drillDownMode
DrillDownMode

Presentation mode for the drill-down panel. "slide-over" (default) slides from the right, full height. "modal" renders centered and compact.

Data Shape

type ColumnType = "text" | "number" | "currency" | "percent" | "link" | "badge" | "sparkline" | "status" | "progress" | "date" | "bar";

interface Column<T> {
  key: string;                   // Property key on the data object
  header?: string;               // Column header text (preferred)
  label?: string;                // @deprecated — use header
  type?: ColumnType;             // Column type — auto-formats and auto-aligns. See ColumnType docs.
  format?: FormatOption;         // Auto-format numeric values (overrides type-inferred format)
  align?: "left" | "center" | "right";  // Default: left for text, right for numbers, center for sparkline/progress
  width?: string | number;       // Column width (CSS value)
  sortable?: boolean;            // Enable sorting
  render?: (value: any, row: T, index: number) => React.ReactNode;  // Custom cell renderer
  pin?: "left";                  // Sticky column on horizontal scroll
  wrap?: boolean;                // Allow text wrapping in cells (default: truncate)
  conditions?: Condition[];      // Conditional cell coloring based on value
  linkHref?: (value: any, row: T) => string;     // URL resolver for type:"link" columns
  linkTarget?: string;           // Link target attribute (e.g. "_blank")
  badgeColor?: (value: any, row: T) => string | undefined;    // Custom badge color for type:"badge"
  badgeVariant?: (value: any, row: T) => BadgeVariant | undefined;  // Custom badge variant for type:"badge"
  statusRules?: StatusRule[];    // Threshold rules for type:"status" columns
  statusSize?: StatusSize;       // Display size for type:"status" columns
  dateFormat?: Intl.DateTimeFormatOptions;  // Intl format options for type:"date" columns
}

interface RowCondition<T> {
  when: (row: T, index: number) => boolean;  // Predicate — return true to apply className
  className: string;                          // CSS class to apply to matching rows
}

interface FooterRow {
  [key: string]: React.ReactNode;
}

Notes

  • Generic component — TypeScript infers T from the data prop.
  • When columns are omitted, they are auto-inferred from the first row's keys with camelCase-to-Title-Case conversion.
  • Sort cycles through: asc -> desc -> none. With multiSort, Shift+click adds secondary/tertiary sorts.
  • Column `header` is preferred over `label` (which is deprecated).
  • Auto-alignment: numeric types (number, currency, percent, bar) right-align. sparkline/progress center-align. Everything else left-aligns.
  • Column types auto-render: 'currency'/'percent'/'number' auto-format values, 'badge' renders a Badge, 'status' renders StatusIndicator, 'sparkline' renders a Sparkline, 'link' renders a clickable link, 'progress' renders a ProgressBar, 'date' formats via Intl.DateTimeFormat, 'bar' renders inline bar chart.
  • The pagination component shows 'Previous' / 'Next' buttons with row range indicator.
  • Search filters across all column values using case-insensitive string matching.
  • renderExpanded adds a chevron toggle column. Clicking a row expands/collapses the detail panel.
  • rowConditions apply CSS classes to rows matching predicates — useful for highlighting warnings or critical rows.
  • crossFilter prop emits a selection signal on row click — it does NOT change the table's appearance. The dev reads the selection via useCrossFilter() and filters their own data.
  • drillDown={true} auto-generates a summary KPI row + detail view from the clicked row. Pass a render function for custom panel content. Requires DrillDown.Root wrapper.
  • When both drillDown and crossFilter are set on the same component, drillDown wins.

Playground

Experiment with every prop interactively. Adjust the controls on the right to see the component update in real time.

Live Preview

Top Products

By revenue this quarter

ProductRevenueUnits SoldGrowthStatus
Pro Plan$128.4K856+24.5%active
Enterprise Plan$96.2K124+18.2%active
Starter Plan$45.8K1.8K-3.1%active
API Add-on$34.6K412+42.8%active
Storage Add-on$22.1K678+15.4%active
Analytics Add-on$18.9K345+8.7%new
Support Premium$15.4K189-5.2%active
Migration Service$12.8K64+31%new
Training Package$9.6K48-12.4%sunset
Legacy Plan$4.2K210-28.6%sunset
Consulting Hours$3.8K19+5%active
Custom Integration$2.4K8+100%new

Code

<DataTable
  data={topProducts}
  columns={[
    { key: "product", header: "Product", sortable: true, pin: "left" },
    { key: "revenue", header: "Revenue", sortable: true, format: "currency" },
    { key: "units", header: "Units Sold", sortable: true, format: "number" },
    { key: "growth", header: "Growth", sortable: true, align: "right", render: (v, row) => <...> },
    { key: "status", header: "Status", render: (v, row) => <...> },
  ]}
  title="Top Products"
  subtitle="By revenue this quarter"
/>

Props

Adjust props to see the table update in real time

Switch between sample datasets

0 = show all. Otherwise shows "View all" link.