Mosmox UI
Current version
GitHubnpmjs.com
Content & Media

Table

Structured data tables with sorting, selection, editing, pinned columns, resizing, virtualization, and custom row actions.

Table

Table renders structured data as an accessible native HTML table. It supports client-side sorting, controlled sorting for server-side data, row selection, editable cells, pinned columns and rows, resizable columns, virtualization, loading and empty states, custom cell renderers, and action columns.

The component intentionally does not include column filters, global search, export controls, expandable rows, interactive density buttons, or pagination.

Basic usage

import { Table } from '@mosmox/ui';

const rows = [
  { id: 'alpha', name: 'Alpha', team: 'Design', score: 92 },
  { id: 'beta', name: 'Beta', team: 'Engineering', score: 84 },
];

const columns = [
  { key: 'name', header: 'Name', accessor: 'name', sortable: true },
  { key: 'team', header: 'Team', accessor: 'team', sortable: true },
  { key: 'score', header: 'Score', accessor: 'score', sortable: true, align: 'right' },
];

<Table
  ariaLabel="Team scores"
  data={rows}
  columns={columns}
  rowKey="id"
  defaultSort={[{ key: 'score', direction: 'desc' }]}
/>

Data and columns

Row identity

Use rowKey when rows have a stable identifier. It accepts a property name or a function. Stable keys are important for selection, pinned rows, and virtualization.

<Table data={rows} columns={columns} rowKey={(row) => row.id} />

If rowKey is omitted, the component uses row.id when available and otherwise falls back to the row index.

Column values

A column reads its value from accessor first. A function accessor is useful for nested or computed values; a property accessor reads a field; without an accessor, row[column.key] is used.

const columns = [
  { key: 'name', header: 'Project', accessor: 'name' },
  {
    key: 'ownerName',
    header: 'Owner',
    accessor: (project) => `${project.owner.firstName} ${project.owner.lastName}`,
  },
];

Column API

PropertyTypeDescription
keystringUnique column identifier used by sorting, ordering, pinning, and cell keys.
headerReactNodeHeader content.
accessorkeyof T | ((row: T) => unknown)Source value for the cell.
render(value, row, state) => ReactNodeCustom renderer for this column.
editor(value, row, state) => ReactNodeEditor rendered after an editable cell is double-clicked.
sortablebooleanSet to false to disable sorting for this column.
sort(a, b, rowA, rowB) => numberCustom client-side comparator.
widthnumber | stringInitial CSS width, for example 160 or '12rem'.
minWidth / maxWidthnumberResize limits in pixels.
align'left' | 'center' | 'right'Cell and header alignment.
pin'left' | 'right'Pins the column during horizontal scrolling.
resizablebooleanSet to false to disable the resize handle.
editablebooleanEnables the column editor when table editing is enabled.
classNamestringClass applied to body cells.
headerClassNamestringClass applied to header cells.
ariaLabelstringAccessible label for column-specific controls.

The state passed to renderers and editors contains row, rowKey, column, value, rowIndex, selected, and editing.

Sizes, variants, and density

The two table sizes are:

  • default: regular typography and spacing;
  • small: smaller typography and tighter cell padding.
<Table data={rows} columns={columns} size="default" />
<Table data={rows} columns={columns} size="small" />

Variants are plain, bordered, striped, and card. Density is independent from size and can be compact, comfortable, or spacious.

<Table data={rows} columns={columns} variant="striped" density="compact" />
<Table data={rows} columns={columns} variant="card" density="spacious" />

Density is configured through props or ConfigProvider. Table does not render an interactive density switcher.

Sorting

Set sortable: true on a column. Header clicks cycle through ascending, descending, and unsorted states. The default comparator handles numbers, booleans, and text-like values. Use column.sort for domain-specific values.

<Table
  data={rows}
  columns={columns}
  defaultSort={[{ key: 'score', direction: 'desc' }]}
  multiSort
/>

multiSort defaults to true. Set it to false to keep only one active sort rule.

Server-side sorting

Use controlled sort state and call your API from onSortChange. The callback receives an array of TableSortRule values with a column key and direction.

import { useState } from 'react';
import type { TableSortRule } from '@mosmox/ui';

const [sort, setSort] = useState<TableSortRule[]>([]);
const [rows, setRows] = useState<Project[]>([]);

async function loadProjects(nextSort: TableSortRule[]) {
  const query = new URLSearchParams();

  nextSort.forEach((rule, index) => {
    query.set(`sort[${index}][key]`, rule.key);
    query.set(`sort[${index}][direction]`, rule.direction);
  });

  const response = await fetch(`/api/projects?${query}`);
  const result = await response.json();

  setRows(result.data);
  setSort(nextSort);
}

<Table data={rows} columns={columns} sort={sort} onSortChange={loadProjects} />

One rule can be encoded as:

GET /api/projects?sort[0][key]=score&sort[0][direction]=desc

Multiple rules can be encoded as:

GET /api/projects?sort[0][key]=status&sort[0][direction]=asc&sort[1][key]=score&sort[1][direction]=desc

The component receives server-returned rows through data. Keep the API sort keys aligned with column keys.

Row selection

Set selectionMode to single or multiple. The first column renders the shared Checkbox component, including the indeterminate state for select-all.

const [selectedKeys, setSelectedKeys] = useState<(string | number)[]>([]);

<Table
  data={rows}
  columns={columns}
  rowKey="id"
  selectionMode="multiple"
  selectedRowKeys={selectedKeys}
  onSelectionChange={(keys, selectedRows) => {
    setSelectedKeys(keys);
    console.log('Selected rows:', selectedRows);
  }}
/>

Use defaultSelectedRowKeys for uncontrolled initial selection. Use isRowDisabled to prevent selection for specific rows. onRowClick is called when a row is clicked.

Row actions

A final column can render any React content. Use the shared Button component for actions. Stop propagation so an action does not also trigger row selection or onRowClick.

import { Eye, DotsThree } from '@mosmox-ui/icons';
import { Button, Table } from '@mosmox/ui';

const columns = [
  ...baseColumns,
  {
    key: 'actions',
    header: 'Actions',
    accessor: () => '',
    sortable: false,
    resizable: false,
    align: 'right',
    width: 96,
    pin: 'right',
    render: (_value, row) => (
      <div onClick={(event) => event.stopPropagation()}>
        <Button
          aria-label={`View ${row.name}`}
          iconOnly
          iconLeft={<Eye size={14} />}
          onClick={() => openProject(row.id)}
          size="xs"
          variant="text"
        />
        <Button
          aria-label={`More actions for ${row.name}`}
          iconOnly
          iconLeft={<DotsThree size={14} />}
          onClick={() => openMenu(row.id)}
          size="xs"
          variant="text"
        />
      </div>
    ),
  },
];

Add pin: 'right' when actions should stay visible during horizontal scrolling.

Custom rendering

Use a column render function for badges, progress indicators, links, and formatted values. Use renderCell when the same policy should apply to every column.

const columns = [
  {
    key: 'status',
    header: 'Status',
    accessor: 'status',
    render: (value) => <StatusBadge status={String(value)} />,
  },
  {
    key: 'amount',
    header: 'Amount',
    accessor: 'amount',
    align: 'right',
    render: (value) => formatCurrency(Number(value)),
  },
];

Editing cells

Set editable on the table and editable: true plus an editor on a column. A double-click opens the editor. Persist the new value from onCellEdit.

<Table
  data={rows}
  columns={[{
    key: 'name',
    header: 'Name',
    accessor: 'name',
    editable: true,
    editor: (value) => <input defaultValue={String(value ?? '')} />,
  }]}
  editable
  onCellEdit={(row, column, value, rowIndex) => {
    saveCell({ id: row.id, column: column.key, value, rowIndex });
  }}
/>

Pinned columns and rows

Pin columns with column.pin or controlled pinning. defaultPinning provides the initial uncontrolled state.

<Table data={rows} columns={columns} pinning={{ left: ['name'], right: ['actions'] }} />

Use pinnedRows to keep known row keys at the top or bottom of the rendered result.

<Table
  data={rows}
  columns={columns}
  rowKey="id"
  pinnedRows={{ top: ['summary'], bottom: ['totals'] }}
/>

Column order and resizing

Pass columnOrder to control the order explicitly, or defaultColumnOrder for an initial order. The order contains column keys.

<Table
  data={rows}
  columns={columns}
  defaultColumnOrder={['name', 'status', 'score', 'actions']}
  columnOrder={order}
  onColumnOrderChange={setOrder}
/>

Resize handles are enabled by default. Set resizableColumns={false} to disable them globally; set resizable: false on one column; and use minWidth, maxWidth, and onColumnResize to constrain or persist widths.

Use loading while data is fetched. loadingContent and emptyContent customize state content. footer renders below the table and stickyFooter keeps it visible inside a scrollable table.

<Table
  data={rows}
  columns={columns}
  loading={isLoading}
  loadingContent={<Spinner />}
  emptyContent={<Empty description="No projects found" />}
  footer={<Pagination page={page} onChange={setPage} />}
  stickyFooter
/>

Pagination is intentionally composed outside Table.

Scrollable and virtualized tables

Set maxHeight to make the body scrollable and stickyHeader to keep the header visible. For large datasets, enable virtualize and provide a numeric rowHeight matching the rendered rows.

<Table data={largeDataset} columns={columns} maxHeight={520} stickyHeader virtualize rowHeight={48} />

Avoid variable-height rows when virtualization is enabled.

ConfigProvider defaults

Table reads size, variant, density, and stickyHeader from ConfigProvider. Instance props take precedence.

<ConfigProvider
  components={{
    table: {
      size: 'small',
      variant: 'striped',
      density: 'compact',
      stickyHeader: true,
    },
  }}
>
  <Table data={rows} columns={columns} />
</ConfigProvider>

Props reference

General

PropTypeDefaultDescription
dataT[]requiredRows to render.
columnsTableColumn<T>[]requiredColumn definitions.
rowKeykeyof T | ((row, index) => string | number)id or indexStable row identity.
captionReactNode—Optional visible caption.
captionSide'top' | 'bottom''top'Caption placement.
ariaLabelstring—Accessible name for the table.
size'default' | 'small''default'Table typography and spacing.
variant'plain' | 'bordered' | 'striped' | 'card''plain'Visual variant.
density'compact' | 'comfortable' | 'spacious''comfortable'Cell padding density.
stickyHeaderbooleanfalseKeeps header visible in the scroll area.
maxHeightnumber | string—Scroll area maximum height.
borderedbooleanfalseAdds a scroll-area border.
hoverablebooleantrueApplies row hover styling.

Sorting

PropTypeDefaultDescription
sortablebooleantrueEnables sortable headers globally.
multiSortbooleantrueKeeps multiple active rules.
defaultSortTableSortRule[][]Initial uncontrolled sort.
sortTableSortRule[]—Controlled sort state.
onSortChange(sort) => void—Receives next sort rules.

Selection and interaction

PropTypeDefaultDescription
selectionMode'none' | 'single' | 'multiple''none'Enables row selection.
selectedRowKeysTableRowKey[]—Controlled selection.
defaultSelectedRowKeysTableRowKey[][]Initial uncontrolled selection.
onSelectionChange(keys, rows) => void—Receives selected keys and rows.
onRowClick(row, rowIndex) => void—Called when a row is clicked.
isRowDisabled(row, rowIndex) => boolean—Disables selection for a row.
rowClassName(row, rowIndex) => string | undefined—Adds a row class.

Layout, rendering, and state

PropTypeDefaultDescription
editablebooleanfalseEnables editors on double-click.
onCellEdit(row, column, value, rowIndex) => void—Receives edited values.
columnOrderstring[]—Controlled column key order.
defaultColumnOrderstring[]columns orderInitial column order.
onColumnOrderChange(order) => void—Callback for external order controls.
resizableColumnsbooleantrueEnables resize handles.
onColumnResize(column, width) => void—Called while resizing.
pinning{ left?: string[]; right?: string[] }—Controlled pinned columns.
defaultPinningsame as pinning{}Initial pinned columns.
onPinningChange(pinning) => void—Callback for external pinning controls.
pinnedRows{ top?: TableRowKey[]; bottom?: TableRowKey[] }{}Pins known row keys.
rowHeightnumber48Estimated row height.
virtualizebooleanfalseRenders visible body rows only.
renderCell(value, row, column, state) => ReactNode—Table-wide renderer.
showToolbarbooleantrueAllows custom toolbar rendering.
toolbarReactNode—Custom toolbar content.
footerReactNode—Content below the table.
stickyFooterbooleanfalseKeeps footer visible.

Loading and content

PropTypeDefaultDescription
loadingbooleanfalseShows loading content.
loadingContentReactNode'Loading…'Loading state.
emptyContentReactNode'No data to display.'Empty state.

Accessibility

  • The component renders a native <table> with an accessible name from ariaLabel.
  • Sortable headers expose aria-sort.
  • Selection uses the shared Checkbox with accessible labels.
  • Icon-only action buttons need an aria-label.
  • Keep headers meaningful and label custom editors.

Removed functionality

The current API intentionally does not provide column filters, a global search field, CSV/export controls, expandable detail rows, interactive density buttons, or built-in pagination. Compose those features around Table when required.