Marble UI
← Components

Table

Preview: Selectable

Data table on React Aria Table: sorting, row selection, keyboard grid navigation. Wrap in a bordered container for the card look.

Import

import { Table, TableHeader, Column, TableBody, Row, TableCell, … } from '@marbleui/ui'

Props

<Table>

PropTypeDefaultDescription
densityDensity'regular'
stripedbooleanfalse

Also accepts React Aria props (98).

<TableHeader>

No props of its own.

Also accepts React Aria props (79).

<Column>

No props of its own.

Also accepts React Aria props (84).

<TableBody>

No props of its own.

Also accepts React Aria props (77).

<Row>

No props of its own.

Also accepts React Aria props (99).

<TableCell>

No props of its own.

Also accepts React Aria props (79).

<Cell>alias of TableCell

No props of its own.

Also accepts React Aria props (79).

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Cell · React Aria Collection · React Aria Column · React Aria Row · React Aria Table · React Aria TableBody · React Aria TableHeader. Follow the primitive's docs for the full keyboard map.

Usage notes

Static or lightly interactive table on React Aria Table: sorting, selection and grid keyboard navigation (arrows move between cells).

<Table aria-label="Invoices" sortDescriptor={sort} onSortChange={setSort}>
  <TableHeader><Column id="number" isRowHeader allowsSorting>Number</Column><Column id="total">Total</Column></TableHeader>
  <TableBody items={rows}>{(r) => <Row id={r.id}><Cell>{r.number}</Cell><Cell>{r.total}</Cell></Row>}</TableBody>
</Table>
  • Sorting is controlled: you sort rows yourself from sortDescriptor.
  • Every column header needs text; for an actions column use visually hidden text.
  • Need search, pagination or column toggles? Use DataGrid.
Example source · show
import { useMemo, useState } from 'react'
import type { SortDescriptor } from 'react-aria-components'
import { Badge } from '../badge'
import { Cell, Column, Row, Table, TableBody, TableHeader } from './table'

type Invoice = { id: string; customer: string; status: 'Paid' | 'Pending' | 'Overdue'; amount: number; date: string }
const invoices: Invoice[] = [
  { id: 'INV-1042', customer: 'Northwind', status: 'Paid', amount: 1280, date: '2026-10-02' },
  { id: 'INV-1043', customer: 'Globex', status: 'Pending', amount: 640, date: '2026-10-05' },
  { id: 'INV-1044', customer: 'Initech', status: 'Overdue', amount: 2300, date: '2026-09-21' },
  { id: 'INV-1045', customer: 'Umbrella', status: 'Paid', amount: 410, date: '2026-10-11' },
  { id: 'INV-1046', customer: 'Hooli', status: 'Pending', amount: 980, date: '2026-10-14' },
]
const tone = { Paid: 'success', Pending: 'warning', Overdue: 'danger' } as const
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })

const Frame = ({ children }: { children: React.ReactNode }) => (
  <div className="w-[36rem] max-w-full overflow-auto rounded-lg border border-border bg-surface">{children}</div>
)

function Sortable() {
  const [sort, setSort] = useState<SortDescriptor>({ column: 'date', direction: 'descending' })
  const rows = useMemo(
    () =>
      [...invoices].sort((a, b) => {
        const k = sort.column as keyof Invoice
        const d = a[k] < b[k] ? -1 : a[k] > b[k] ? 1 : 0
        return sort.direction === 'descending' ? -d : d
      }),
    [sort],
  )
  return (
    <Frame>
      <Table aria-label="Invoices" sortDescriptor={sort} onSortChange={setSort} selectionMode="multiple">
        <TableHeader>
          <Column id="id" isRowHeader allowsSorting>Invoice</Column>
          <Column id="customer" allowsSorting>Customer</Column>
          <Column id="status">Status</Column>
          <Column id="date" allowsSorting>Date</Column>
          <Column id="amount" allowsSorting className="text-right">Amount</Column>
        </TableHeader>
        <TableBody items={rows}>
          {(r) => (
            <Row id={r.id}>
              <Cell className="font-medium">{r.id}</Cell>
              <Cell>{r.customer}</Cell>
              <Cell><Badge tone={tone[r.status]} dot>{r.status}</Badge></Cell>
              <Cell className="text-fg-muted">{r.date}</Cell>
              <Cell className="text-right">{money.format(r.amount)}</Cell>
            </Row>
          )}
        </TableBody>
      </Table>
    </Frame>
  )
}

export const styles = {
  Selectable: Sortable,
  Compact: () => (
    <Frame>
      <Table aria-label="Invoices" density="compact" striped>
        <TableHeader>
          <Column isRowHeader>Invoice</Column>
          <Column>Customer</Column>
          <Column className="text-right">Amount</Column>
        </TableHeader>
        <TableBody items={invoices}>
          {(r) => (
            <Row id={r.id}>
              <Cell className="font-medium">{r.id}</Cell>
              <Cell>{r.customer}</Cell>
              <Cell className="text-right">{money.format(r.amount)}</Cell>
            </Row>
          )}
        </TableBody>
      </Table>
    </Frame>
  ),
  Empty: () => (
    <Frame>
      <Table aria-label="Invoices">
        <TableHeader>
          <Column isRowHeader>Invoice</Column>
          <Column>Customer</Column>
          <Column>Amount</Column>
        </TableHeader>
        <TableBody renderEmptyState={() => <div className="py-10 text-center text-sm text-fg-muted">No invoices yet.</div>}>
          {[]}
        </TableBody>
      </Table>
    </Frame>
  ),
}

Source: packages/ui/src/table/table.tsx

Catalog

Category
collections
Priority
Foundation
Primitive
react-aria
React Aria
Table