Moul UIBeta
Components

Table

Zero-runtime semantic HTML data table primitives styled with StyleX, tailored for TanStack Table, TanStack Virtual, TanStack Query, TanStack Store, and TanStack Pacer.

Table

The Table component is a collection of composable, zero-runtime semantic HTML table primitives styled with StyleX. Built to pair seamlessly with headless data engines like TanStack Table, it provides accessible table markup, customizable visual variants (dense, striped, hoverable, stickyHeader), column pinning, sort indicators, numeric alignment, and built-in loading and empty states.

Preview

Interactive
Service
Role
Status
Latency
Auth GatewaySecurityOperational24ms
Global Edge CDNEdgeDegraded142ms
PostgreSQL PrimaryDatabaseOperational4ms
Worker QueueComputeOperational18ms

Properties

Live Code Snippet
import {
  Table,
  TableHeader,
  Column,
  TableBody,
  Row,
  Cell,
  Badge,
} from '@moul-dev/ui';
import { useState } from 'react';

export default function Example() {
  const [sortDescriptor, setSortDescriptor] = useState({
    column: 'name',
    direction: 'ascending',
  });

  const services = [
    { id: '1', name: 'Auth Gateway', role: 'Security', status: 'Operational', variant: 'success' },
    { id: '2', name: 'PostgreSQL Primary', role: 'Database', status: 'Operational', variant: 'success' },
    { id: '3', name: 'Global Edge CDN', role: 'Edge', status: 'Degraded', variant: 'warning' },
    { id: '4', name: 'Worker Queue', role: 'Compute', status: 'Operational', variant: 'success' },
  ];

  return (
    <Table
      aria-label="Services Table" selectionMode="undefined"
      sortDescriptor={sortDescriptor}
      onSortChange={setSortDescriptor}
    >
      <TableHeader>
        <Column id="name" isRowHeader allowsSorting>Service</Column>
        <Column id="role" allowsSorting>Role</Column>
        <Column id="status" allowsSorting>Status</Column>
        <Column id="latency" allowsSorting>Latency</Column>
      </TableHeader>
      <TableBody items={services}>
        {(item) => (
          <Row key={item.id} id={item.id}>
            <Cell>{item.name}</Cell>
            <Cell>{item.role}</Cell>
            <Cell>
              <Badge variant={item.variant} size="sm" dot>
                {item.status}
              </Badge>
            </Cell>
            <Cell>{item.latency}</Cell>
          </Row>
        )}
      </TableBody>
    </Table>
  );
}

Import

import {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableRow,
  TableHead,
  TableCell,
  TableCaption,
  TableEmpty,
  TableSkeleton,
} from '@moul-dev/ui';

Interactive TanStack Table Showcase

Experience a full-featured data table powered by TanStack Table, TanStack Store (reactive view preferences), and TanStack Pacer (debounced search filtering):

1 service selected across table
Service Name
Team
Region
Health
CPU Load
Memory
Latency
Throughput
Uptime
Action
Auth Gatewaysrv-1
Securityus-east-1Operational18%1.4 GB24ms1.2M req/s99.99%
Edge CDN Gatewaysrv-3
NetworkingglobalDegraded78%3.8 GB142ms3.8M req/s99.85%
Media Transcodersrv-7
Mediaus-east-2Operational64%5.2 GB85ms140k req/s99.90%
Notification Hubsrv-8
Messagingeu-west-1Operational22%1.8 GB12ms2.1M req/s99.99%
Payment Processingsrv-5
Billingus-east-1Incident94%8.6 GB520ms95k req/s98.90%
Showing 5 of 8 total servicesCluster Health: 100% Verified
Page 1 of 2

Basic Semantic Usage

Construct a table declaratively with standard semantic primitives without any external state dependencies:

Active production services across clusters
Service NameRegionStatusThroughput
Auth Gatewayus-east-1Operational1.2M req/s
PostgreSQL Primaryus-west-2Operational450k req/s
Edge CDN GatewayglobalDegraded3.8M req/s
Cluster Summary3 Nodes Active
import {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableRow,
  TableHead,
  TableCell,
  TableCaption,
  Badge,
} from '@moul-dev/ui';

export default function BasicTableExample() {
  return (
    <Table aria-label="Cluster Services Table">
      <TableCaption>Active production services across clusters</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Service Name</TableHead>
          <TableHead>Region</TableHead>
          <TableHead>Status</TableHead>
          <TableHead align="numeric">Throughput</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell className="font-semibold">Auth Gateway</TableCell>
          <TableCell>us-east-1</TableCell>
          <TableCell><Badge variant="success" size="sm" dot>Operational</Badge></TableCell>
          <TableCell align="numeric">1.2M req/s</TableCell>
        </TableRow>
        <TableRow>
          <TableCell className="font-semibold">PostgreSQL Primary</TableCell>
          <TableCell>us-west-2</TableCell>
          <TableCell><Badge variant="success" size="sm" dot>Operational</Badge></TableCell>
          <TableCell align="numeric">450k req/s</TableCell>
        </TableRow>
        <TableRow>
          <TableCell className="font-semibold">Edge CDN Gateway</TableCell>
          <TableCell>global</TableCell>
          <TableCell><Badge variant="warning" size="sm" dot>Degraded</Badge></TableCell>
          <TableCell align="numeric">3.8M req/s</TableCell>
        </TableRow>
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Cluster Summary</TableCell>
          <TableCell align="numeric">3 Nodes Active</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  );
}

Visual Variants

The Table component provides built-in visual variants for dense layouts, zebra striping, borders, and sticky headers:

// Compact / Dense layout for data-dense dashboards
<Table dense>...</Table>

// Zebra striping for alternating row backgrounds
<Table striped>...</Table>

// Sticky pinned thead while scrolling
<Table stickyHeader>...</Table>

// Fixed table column layout
<Table layout="fixed">...</Table>

// Sticky column pinning (left / right / start / end)
<TableHead pinned="left" pinOffset={0}>Pinned Service</TableHead>
<TableCell pinned="left" pinOffset={0}>Auth Service</TableCell>

TanStack Table Integration

Connect @moul-dev/ui table primitives with @tanstack/react-table for multi-column sorting, row selection, filtering, and pagination:

import {
  Table,
  TableHeader,
  TableBody,
  TableRow,
  TableHead,
  TableCell,
  Checkbox,
} from '@moul-dev/ui';
import {
  useTable,
  createColumnHelper,
  createCoreRowModel,
  createSortedRowModel,
  createFilteredRowModel,
  createPaginatedRowModel,
  flexRender,
  type SortingState,
  type RowSelectionState,
} from '@tanstack/react-table';
import { useState, useMemo } from 'react';

interface Service {
  id: string;
  name: string;
  region: string;
  status: string;
  latency: string;
}

const columnHelper = createColumnHelper<Service>();

export default function TanStackTableExample({ data }: { data: Service[] }) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

  const columns = useMemo(
    () => [
      columnHelper.display({
        id: 'select',
        header: ({ table }) => (
          <Checkbox
            aria-label="Select all"
            isSelected={table.getIsAllPageRowsSelected()}
            isIndeterminate={table.getIsSomePageRowsSelected()}
            onChange={(val) => table.toggleAllPageRowsSelected(val)}
          />
        ),
        cell: ({ row }) => (
          <Checkbox
            aria-label="Select row"
            isSelected={row.getIsSelected()}
            onChange={(val) => row.toggleSelected(val)}
          />
        ),
      }),
      columnHelper.accessor('name', {
        header: 'Service Name',
        cell: (info) => info.getValue(),
      }),
      columnHelper.accessor('region', {
        header: 'Region',
        cell: (info) => info.getValue(),
      }),
      columnHelper.accessor('status', {
        header: 'Status',
        cell: (info) => info.getValue(),
      }),
      columnHelper.accessor('latency', {
        header: 'Latency',
        cell: (info) => info.getValue(),
      }),
    ],
    []
  );

  const table = useTable({
    data,
    columns,
    state: { sorting, rowSelection },
    onSortingChange: setSorting,
    onRowSelectionChange: setRowSelection,
    getCoreRowModel: createCoreRowModel(),
    getSortedRowModel: createSortedRowModel(),
    getFilteredRowModel: createFilteredRowModel(),
    getPaginationRowModel: createPaginatedRowModel(),
  });

  return (
    <Table stickyHeader>
      <TableHeader>
        {table.getHeaderGroups().map((headerGroup) => (
          <TableRow key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <TableHead
                key={header.id}
                sortDirection={header.column.getIsSorted()}
                onSort={
                  header.column.getCanSort()
                    ? header.column.getToggleSortingHandler()
                    : undefined
                }
              >
                {header.isPlaceholder
                  ? null
                  : flexRender(
                      header.column.columnDef.header,
                      header.getContext()
                    )}
              </TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id} selected={row.getIsSelected()}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Large Dataset Virtualization (TanStack Virtual)

Render thousands of table rows with smooth 60fps windowing using @tanstack/react-virtual:

Rendering 2,000 virtualized rows via @tanstack/react-virtualDOM Rows in View: 0
Node HostnamePrivate IPCPU LoadMemoryStatus
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@moul-dev/ui';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

export function VirtualTableExample({ data }: { data: any[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: data.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,
    overscan: 10,
  });

  const virtualRows = rowVirtualizer.getVirtualItems();
  const totalSize = rowVirtualizer.getTotalSize();
  const paddingTop = virtualRows.length > 0 ? virtualRows[0].start || 0 : 0;
  const paddingBottom =
    virtualRows.length > 0
      ? totalSize - (virtualRows[virtualRows.length - 1].end || 0)
      : 0;

  return (
    <div ref={parentRef} style={{ maxHeight: '360px', overflowY: 'auto' }}>
      <Table stickyHeader dense wrapInContainer={false}>
        <TableHeader>
          <TableRow>
            <TableHead>Hostname</TableHead>
            <TableHead>IP Address</TableHead>
            <TableHead align="numeric">CPU</TableHead>
            <TableHead>Status</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {paddingTop > 0 && (
            <tr>
              <td style={{ height: `${paddingTop}px` }} colSpan={4} />
            </tr>
          )}
          {virtualRows.map((virtualRow) => {
            const item = data[virtualRow.index];
            return (
              <TableRow key={item.id}>
                <TableCell>{item.hostname}</TableCell>
                <TableCell>{item.ip}</TableCell>
                <TableCell align="numeric">{item.cpu}</TableCell>
                <TableCell>{item.status}</TableCell>
              </TableRow>
            );
          })}
          {paddingBottom > 0 && (
            <tr>
              <td style={{ height: `${paddingBottom}px` }} colSpan={4} />
            </tr>
          )}
        </TableBody>
      </Table>
    </div>
  );
}

Server-Side Pagination with TanStack Query & Pacer

Handle asynchronous server data fetching with cached background refetches and loading skeletons:

Server Query State: Page 1 of 4• Fetching from cloud...
Event IDTimestampLevelLog Payload
import {
  Table,
  TableHeader,
  TableBody,
  TableRow,
  TableHead,
  TableCell,
  TableSkeleton,
  Button,
} from '@moul-dev/ui';
import { useQuery } from '@tanstack/react-query';
import { useDebouncedValue } from '@tanstack/react-pacer';
import { useState } from 'react';

export function QueryTableExample() {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');
  const [debouncedSearch] = useDebouncedValue(search, 300);

  const { data, isFetching } = useQuery({
    queryKey: ['cluster-nodes', page, debouncedSearch],
    queryFn: () => fetchNodes({ page, search: debouncedSearch }),
  });

  return (
    <Table dense>
      <TableHeader>
        <TableRow>
          <TableHead>ID</TableHead>
          <TableHead>Node Name</TableHead>
          <TableHead>Cluster</TableHead>
          <TableHead align="numeric">Health Score</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {isFetching && !data ? (
          <TableSkeleton rows={5} columns={4} />
        ) : (
          data?.records.map((node: any) => (
            <TableRow key={node.id}>
              <TableCell>{node.id}</TableCell>
              <TableCell>{node.name}</TableCell>
              <TableCell>{node.cluster}</TableCell>
              <TableCell align="numeric">{node.score}</TableCell>
            </TableRow>
          ))
        )}
      </TableBody>
    </Table>
  );
}

Loading and Empty State Components

Display animated skeleton pulse rows while data is fetching, or render custom empty states when queries return 0 results:

// 1. Loading Skeleton Rows
<TableBody>
  <TableSkeleton rows={5} columns={4} />
</TableBody>

// 2. Empty State Row
<TableBody>
  <TableEmpty colSpan={4}>
    <EmptyState
      title="No cluster nodes found"
      description="Try clearing active query filters."
    />
  </TableEmpty>
</TableBody>

Props Reference

Table Props

Prop

Type

TableHeader Props

Prop

Type

TableRow Props

Prop

Type

TableHead Props

Prop

Type

TableCell Props

Prop

Type

TableEmpty Props

Prop

Type

TableSkeleton Props

Prop

Type

On this page