Table

free

Table gives dense records a scannable grid you compose from styled row and cell primitives.

Preview
Dark
MemberRoleStatus
Aria Patel
Aria Patel
aria@acme.io
OwnerActive
Diego Ramos
Diego Ramos
diego@acme.io
AdminActive
Mei Lin
Mei Lin
mei@acme.io
EditorInvited
Sam Turner
Sam Turner
sam@acme.io
ViewerActive
import Table from '@/components/ui/Table';
import Avatar from '@/components/ui/Avatar';
import Tag from '@/components/ui/Tag';

const { THead, TBody, Tr, Th, Td } = Table;

const members = [
  {
    id: 1,
    name: 'Aria Patel',
    email: 'aria@acme.io',
    avatar: '/img/avatars/thumb-1.jpg',
    role: 'Owner',
    status: 'Active',
  },
  {
    id: 2,
    name: 'Diego Ramos',
    email: 'diego@acme.io',
    avatar: '/img/avatars/thumb-2.jpg',
    role: 'Admin',
    status: 'Active',
  },
  {
    id: 3,
    name: 'Mei Lin',
    email: 'mei@acme.io',
    avatar: '/img/avatars/thumb-3.jpg',
    role: 'Editor',
    status: 'Invited',
  },
  {
    id: 4,
    name: 'Sam Turner',
    email: 'sam@acme.io',
    avatar: '/img/avatars/thumb-4.jpg',
    role: 'Viewer',
    status: 'Active',
  },
];

export default function UsageDemo() {
  return (
    <Table>
      <THead>
        <Tr>
          <Th>Member</Th>
          <Th>Role</Th>
          <Th>Status</Th>
        </Tr>
      </THead>
      <TBody>
        {members.map((member) => (
          <Tr key={member.id}>
            <Td>
              <div className="flex items-center gap-2">
                <Avatar
                  size={32}
                  src={member.avatar}
                  alt={member.name}
                />
                <div className="min-w-0">
                  <div className="font-semibold text-foreground">
                    {member.name}
                  </div>
                  <div className="text-muted-foreground">
                    {member.email}
                  </div>
                </div>
              </div>
            </Td>
            <Td>{member.role}</Td>
            <Td>
              <Tag className="gap-1 bg-card">
                <span 
                  className={`h-2 w-2 rounded-full ${
                    member.status === 'Active' ? 'bg-success' : 
                    member.status === 'Invited' ? 'bg-warning' : ''
                  }`}
                />
                {member.status}
              </Tag>
            </Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Table

Examples

Basic

Preview
Dark
InvoiceClientAmount
INV-1024Northwind Traders$1,280.00
INV-1025Globex Corp$3,540.00
INV-1026Soylent Industries$920.00
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td } = Table;

const invoices = [
  { id: 'INV-1024', client: 'Northwind Traders', amount: '$1,280.00' },
  { id: 'INV-1025', client: 'Globex Corp', amount: '$3,540.00' },
  { id: 'INV-1026', client: 'Soylent Industries', amount: '$920.00' },
];

export default function BasicDemo() {
  return (
    <Table>
      <THead>
        <Tr>
          <Th>Invoice</Th>
          <Th>Client</Th>
          <Th>Amount</Th>
        </Tr>
      </THead>
      <TBody>
        {invoices.map((invoice) => (
          <Tr key={invoice.id}>
            <Td className="font-medium text-foreground">{invoice.id}</Td>
            <Td>{invoice.client}</Td>
            <Td className="tabular-nums">{invoice.amount}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Compact

Preview
Dark
SKUProductIn stock
KB-01Mechanical Keyboard132
MS-04Wireless Mouse58
HS-02Studio Headset9
WC-06HD Webcam0
DK-03Docking Station24
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td } = Table;

const rows = [
  { sku: 'KB-01', name: 'Mechanical Keyboard', stock: 132 },
  { sku: 'MS-04', name: 'Wireless Mouse', stock: 58 },
  { sku: 'HS-02', name: 'Studio Headset', stock: 9 },
  { sku: 'WC-06', name: 'HD Webcam', stock: 0 },
  { sku: 'DK-03', name: 'Docking Station', stock: 24 },
];

export default function CompactDemo() {
  return (
    <Table compact>
      <THead>
        <Tr>
          <Th>SKU</Th>
          <Th>Product</Th>
          <Th>In stock</Th>
        </Tr>
      </THead>
      <TBody>
        {rows.map((row) => (
          <Tr key={row.sku}>
            <Td className="font-mono text-xs">{row.sku}</Td>
            <Td className="font-medium text-foreground">{row.name}</Td>
            <Td className="tabular-nums">{row.stock}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Bordered

Preview
Dark
ProjectLeadProgress
Website relaunchAria Patel80%
Mobile app v2Diego Ramos45%
Billing migrationMei Lin12%
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td } = Table;

const rows = [
  { project: 'Website relaunch', lead: 'Aria Patel', progress: '80%' },
  { project: 'Mobile app v2', lead: 'Diego Ramos', progress: '45%' },
  { project: 'Billing migration', lead: 'Mei Lin', progress: '12%' },
];

export default function BorderedDemo() {
  return (
    <Table overflow={false} bordered>
      <THead>
        <Tr>
          <Th>Project</Th>
          <Th>Lead</Th>
          <Th>Progress</Th>
        </Tr>
      </THead>
      <TBody>
        {rows.map((row) => (
          <Tr key={row.project}>
            <Td className="font-medium text-foreground">{row.project}</Td>
            <Td>{row.lead}</Td>
            <Td className="tabular-nums">{row.progress}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Vertical Divider

Preview
Dark
RegionDealsRevenue
North America42$128k
Europe31$96k
Asia Pacific27$74k
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td } = Table;

const rows = [
  { region: 'North America', deals: 42, revenue: '$128k' },
  { region: 'Europe', deals: 31, revenue: '$96k' },
  { region: 'Asia Pacific', deals: 27, revenue: '$74k' },
];

export default function VerticalDividerDemo() {
  return (
    <Table verticalDivider={{ head: true, body: true }}>
      <THead>
        <Tr>
          <Th>Region</Th>
          <Th>Deals</Th>
          <Th>Revenue</Th>
        </Tr>
      </THead>
      <TBody>
        {rows.map((row) => (
          <Tr key={row.region}>
            <Td className="font-medium text-foreground">{row.region}</Td>
            <Td className="tabular-nums">{row.deals}</Td>
            <Td className="tabular-nums">{row.revenue}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Grouped Header

Preview
Dark
RepMeetingsOutcomes
Q1Q2WonLost
Aria Patel2431124
Diego Ramos182297
Mei Lin3027153
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td } = Table;

const rows = [
  { name: 'Aria Patel', q1: 24, q2: 31, won: 12, lost: 4 },
  { name: 'Diego Ramos', q1: 18, q2: 22, won: 9, lost: 7 },
  { name: 'Mei Lin', q1: 30, q2: 27, won: 15, lost: 3 },
];

export default function GroupedHeaderDemo() {
  return (
    <Table verticalDivider={{ head: true }}>
      <THead>
        <Tr>
          <Th rowSpan={2}>Rep</Th>
          <Th colSpan={2} className="text-center">
            Meetings
          </Th>
          <Th colSpan={2} className="text-center">
            Outcomes
          </Th>
        </Tr>
        <Tr>
          <Th>Q1</Th>
          <Th>Q2</Th>
          <Th>Won</Th>
          <Th>Lost</Th>
        </Tr>
      </THead>
      <TBody>
        {rows.map((row) => (
          <Tr key={row.name}>
            <Td className="font-medium text-foreground">{row.name}</Td>
            <Td className="tabular-nums">{row.q1}</Td>
            <Td className="tabular-nums">{row.q2}</Td>
            <Td className="tabular-nums">{row.won}</Td>
            <Td className="tabular-nums">{row.lost}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Sorting

Preview
Dark
ORD-3014$9607
ORD-3012$4203
ORD-3015$2752
ORD-3013$1801
import { useState } from 'react';
import Table from '@/components/ui/Table';

const { THead, TBody, Tr, Th, Td, Sorter } = Table;

type Order = { id: string; total: number; items: number };

const orders: Order[] = [
  { id: 'ORD-3012', total: 420, items: 3 },
  { id: 'ORD-3013', total: 180, items: 1 },
  { id: 'ORD-3014', total: 960, items: 7 },
  { id: 'ORD-3015', total: 275, items: 2 },
];

type SortKey = keyof Order;
type SortState = { key: SortKey; order: 'asc' | 'desc' } | null;

export default function SortingDemo() {
  const [sort, setSort] = useState<SortState>({ key: 'total', order: 'desc' });

  const sorted = [...orders].sort((a, b) => {
    if (!sort) return 0;
    const first = a[sort.key];
    const second = b[sort.key];
    const diff =
      typeof first === 'number' && typeof second === 'number'
        ? first - second
        : String(first).localeCompare(String(second));
    return sort.order === 'asc' ? diff : -diff;
  });

  const toggleSort = (key: SortKey) => {
    setSort((prev) =>
      prev && prev.key === key
        ? { key, order: prev.order === 'asc' ? 'desc' : 'asc' }
        : { key, order: 'asc' },
    );
  };

  const sortValue = (key: SortKey) =>
    sort && sort.key === key ? sort.order : true;

  const columns: [SortKey, string][] = [
    ['id', 'Order'],
    ['total', 'Total'],
    ['items', 'Items'],
  ];

  return (
    <Table>
      <THead>
        <Tr>
          {columns.map(([key, label]) => (
            <Th key={key}>
              <button
                type="button"
                className="inline-flex cursor-pointer items-center gap-2 uppercase"
                onClick={() => toggleSort(key)}
              >
                {label}
                <Sorter sort={sortValue(key)} />
              </button>
            </Th>
          ))}
        </Tr>
      </THead>
      <TBody>
        {sorted.map((order) => (
          <Tr key={order.id}>
            <Td className="font-medium text-foreground">{order.id}</Td>
            <Td className="tabular-nums">${order.total}</Td>
            <Td className="tabular-nums">{order.items}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Filtering

Preview
Dark
NameTeam
Aria PatelDesign
Diego RamosEngineering
Mei LinEngineering
Sam TurnerSupport
Priya ShahDesign
import { useState } from 'react';
import Table from '@/components/ui/Table';
import Input from '@/components/ui/Input';

const { THead, TBody, Tr, Th, Td } = Table;

const people = [
  { id: 1, name: 'Aria Patel', team: 'Design' },
  { id: 2, name: 'Diego Ramos', team: 'Engineering' },
  { id: 3, name: 'Mei Lin', team: 'Engineering' },
  { id: 4, name: 'Sam Turner', team: 'Support' },
  { id: 5, name: 'Priya Shah', team: 'Design' },
];

export default function FilteringDemo() {
  const [query, setQuery] = useState('');
  const term = query.trim().toLowerCase();

  const filtered = people.filter((person) => {
    const haystack = (person.name + ' ' + person.team).toLowerCase();
    return haystack.includes(term);
  });

  return (
    <div className="flex w-full flex-col gap-4">
      <div className="px-4 pt-4">
        <Input
          value={query}
          placeholder="Search people..."
          onChange={(event) => setQuery(event.target.value)}
        />
      </div>
      <Table>
        <THead>
          <Tr>
            <Th>Name</Th>
            <Th>Team</Th>
          </Tr>
        </THead>
        <TBody>
          {filtered.length > 0 ? (
            filtered.map((person) => (
              <Tr key={person.id}>
                <Td className="font-medium text-foreground">{person.name}</Td>
                <Td>{person.team}</Td>
              </Tr>
            ))
          ) : (
            <Tr>
              <Td colSpan={2} className="text-center text-muted-foreground">
                No people match the search.
              </Td>
            </Tr>
          )}
        </TBody>
      </Table>
    </div>
  );
}

Row Selection

Preview
Dark
FileSize
Q3-report.pdf2.4 MB
brand-assets.zip18 MB
roadmap.png640 KB
contract.docx82 KB
import { useState } from 'react';
import Table from '@/components/ui/Table';
import Checkbox from '@/components/ui/Checkbox';

const { THead, TBody, Tr, Th, Td } = Table;

const files = [
  { id: 'f1', name: 'Q3-report.pdf', size: '2.4 MB' },
  { id: 'f2', name: 'brand-assets.zip', size: '18 MB' },
  { id: 'f3', name: 'roadmap.png', size: '640 KB' },
  { id: 'f4', name: 'contract.docx', size: '82 KB' },
];

export default function RowSelectionDemo() {
  const [selected, setSelected] = useState<Set<string>>(new Set(['f2']));

  const allSelected = selected.size === files.length;
  const someSelected = selected.size > 0 && !allSelected;

  const toggleAll = () => {
    if (allSelected) {
      setSelected(new Set());
    } else {
      setSelected(new Set(files.map((file) => file.id)));
    }
  };

  const toggleRow = (id: string) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  return (
    <Table>
      <THead>
        <Tr>
          <Th className="w-10">
            <Checkbox
              checked={allSelected}
              indeterminate={someSelected}
              onChange={toggleAll}
            />
          </Th>
          <Th>File</Th>
          <Th>Size</Th>
        </Tr>
      </THead>
      <TBody>
        {files.map((file) => (
          <Tr
            key={file.id}
            className={selected.has(file.id) ? 'bg-primary-soft' : undefined}
          >
            <Td>
              <Checkbox
                checked={selected.has(file.id)}
                onChange={() => toggleRow(file.id)}
              />
            </Td>
            <Td className="font-medium text-foreground">{file.name}</Td>
            <Td className="tabular-nums text-muted-foreground">{file.size}</Td>
          </Tr>
        ))}
      </TBody>
    </Table>
  );
}

Pagination

Preview
Dark
ReferenceAmount
TXN-5000$42.00
TXN-5001$79.00
TXN-5002$116.00
TXN-5003$153.00
TXN-5004$190.00
23 transactions
import { useState } from 'react';
import Table from '@/components/ui/Table';
import Pagination from '@/components/ui/Pagination';

const { THead, TBody, Tr, Th, Td } = Table;

const transactions = Array.from({ length: 23 }, (_, index) => ({
  id: 5000 + index,
  reference: 'TXN-' + (5000 + index),
  amount: '$' + (index * 37 + 42).toFixed(2),
}));

const PAGE_SIZE = 5;

export default function PaginationDemo() {
  const [page, setPage] = useState(1);

  const start = (page - 1) * PAGE_SIZE;
  const pageRows = transactions.slice(start, start + PAGE_SIZE);

  return (
    <div className="flex w-full flex-col gap-4">
      <Table>
        <THead>
          <Tr>
            <Th>Reference</Th>
            <Th>Amount</Th>
          </Tr>
        </THead>
        <TBody>
          {pageRows.map((transaction) => (
            <Tr key={transaction.id}>
              <Td className="font-medium text-foreground">
                {transaction.reference}
              </Td>
              <Td className="tabular-nums">{transaction.amount}</Td>
            </Tr>
          ))}
        </TBody>
      </Table>
      <div className="flex items-center justify-between px-4 pb-4">
        <span className="text-sm text-muted-foreground">
          {transactions.length} transactions
        </span>
        <Pagination
          currentPage={page}
          total={transactions.length}
          pageSize={PAGE_SIZE}
          onChange={setPage}
        />
      </div>
    </div>
  );
}

Expandable Rows

Preview
Dark
ShipmentCarrierStatus
SHP-01FedExIn transit
  • Picked up - Austin, TX
  • Departed - Memphis, TN
SHP-02UPSDelivered
SHP-03DHLPending
import { Fragment, useState } from 'react';
import Table from '@/components/ui/Table';
import { PiCaretRight } from 'react-icons/pi'

const { THead, TBody, Tr, Th, Td } = Table;

const shipments = [
  {
    id: 'SHP-01',
    carrier: 'FedEx',
    status: 'In transit',
    events: ['Picked up - Austin, TX', 'Departed - Memphis, TN'],
  },
  {
    id: 'SHP-02',
    carrier: 'UPS',
    status: 'Delivered',
    events: ['Picked up - Denver, CO', 'Delivered - Boulder, CO'],
  },
  {
    id: 'SHP-03',
    carrier: 'DHL',
    status: 'Pending',
    events: ['Label created - Newark, NJ'],
  },
];

export default function ExpandableRowsDemo() {
  const [open, setOpen] = useState<Set<string>>(new Set(['SHP-01']));

  const toggle = (id: string) => {
    setOpen((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  return (
    <Table>
      <THead>
        <Tr>
          <Th className="w-10" />
          <Th>Shipment</Th>
          <Th>Carrier</Th>
          <Th>Status</Th>
        </Tr>
      </THead>
      <TBody>
        {shipments.map((shipment) => {
          const isOpen = open.has(shipment.id);
          return (
            <Fragment key={shipment.id}>
              <Tr>
                <Td>
                  <button
                    type="button"
                    aria-label={isOpen ? 'Collapse row' : 'Expand row'}
                    className="inline-flex cursor-pointer text-muted-foreground"
                    onClick={() => toggle(shipment.id)}
                  >
                    <PiCaretRight
                      className={isOpen ? 'rotate-90 transition' : 'transition'}
                    />
                  </button>
                </Td>
                <Td className="font-medium text-foreground">{shipment.id}</Td>
                <Td>{shipment.carrier}</Td>
                <Td className="text-muted-foreground">{shipment.status}</Td>
              </Tr>
              {isOpen && (
                <Tr>
                  <Td></Td>
                  <Td colSpan={3}>
                    <ul className="flex flex-col gap-2">
                      {shipment.events.map((event) => (
                        <li key={event}>{event}</li>
                      ))}
                    </ul>
                  </Td>
                </Tr>
              )}
            </Fragment>
          );
        })}
      </TBody>
    </Table>
  );
}

TanStack Table

Use Table as the styled shell around a headless table model when you need richer data behavior. This example pairs the table primitives with @tanstack/react-table: TanStack owns the column definitions, row model, sorting, and pagination state, while NateUI renders the native table structure, Sorter, Pagination, and Select controls.

Preview
Dark
Ticket
Subject
Priority
Updated
TCK-201Checkout button unresponsiveHigh2h ago
TCK-198Export CSV missing a columnMedium5h ago
TCK-195Typo in confirmation emailLow1d ago
TCK-190Slow load on reports pageMedium2d ago
TCK-187Dark mode toggle resets on reloadMedium3d ago
    import { useMemo, useState } from 'react';
    import {
      columnVisibilityFeature,
      createPaginatedRowModel,
      createSortedRowModel,
      flexRender,
      rowPaginationFeature,
      rowSortingFeature,
      tableFeatures,
      useTable,
    } from '@tanstack/react-table';
    import type { ColumnDef, ColumnSort } from '@tanstack/react-table';
    import Table from '@/components/ui/Table';
    import Pagination from '@/components/ui/Pagination';
    import Select from '@/components/ui/Select';
    import Tag from '@/components/ui/Tag';
    
    const { THead, TBody, Tr, Th, Td, Sorter } = Table;
    
    type Ticket = {
      id: string;
      subject: string;
      priority: 'Low' | 'Medium' | 'High';
      updated: string;
    };
    
    const tickets: Ticket[] = [
      { id: 'TCK-201', subject: 'Checkout button unresponsive', priority: 'High', updated: '2h ago' },
      { id: 'TCK-198', subject: 'Export CSV missing a column', priority: 'Medium', updated: '5h ago' },
      { id: 'TCK-195', subject: 'Typo in confirmation email', priority: 'Low', updated: '1d ago' },
      { id: 'TCK-190', subject: 'Slow load on reports page', priority: 'Medium', updated: '2d ago' },
      { id: 'TCK-187', subject: 'Dark mode toggle resets on reload', priority: 'Medium', updated: '3d ago' },
      { id: 'TCK-183', subject: 'API rate limit hit during import', priority: 'High', updated: '4d ago' },
      { id: 'TCK-179', subject: 'Invite link expires too early', priority: 'Low', updated: '5d ago' },
      { id: 'TCK-174', subject: 'Duplicate rows after CSV export', priority: 'High', updated: '6d ago' },
      { id: 'TCK-168', subject: 'Missing empty state on filters', priority: 'Low', updated: '1w ago' },
      { id: 'TCK-161', subject: 'Timezone off by one hour', priority: 'Medium', updated: '1w ago' },
      { id: 'TCK-155', subject: 'Search ignores accented characters', priority: 'Low', updated: '2w ago' },
      { id: 'TCK-149', subject: 'Webhook retries never stop', priority: 'High', updated: '2w ago' },
    ];
    
    const features = tableFeatures({
      columnVisibilityFeature,
      rowPaginationFeature,
      rowSortingFeature,
      paginatedRowModel: createPaginatedRowModel(),
      sortedRowModel: createSortedRowModel(),
    });
    
    const columns: ColumnDef<typeof features, Ticket>[] = [
      { accessorKey: 'id', header: 'Ticket' },
      { accessorKey: 'subject', header: 'Subject' },
      {
        accessorKey: 'priority',
        header: 'Priority',
        cell: ({ row }) => {
          const { priority } = row.original;
          return (
            <Tag
              prefix
              prefixClass={
                priority === 'High'
                  ? 'bg-destructive'
                  : priority === 'Medium'
                    ? 'bg-warning'
                    : undefined
              }
              className="bg-card"
            >
              {priority}
            </Tag>
          );
        },
      },
      { accessorKey: 'updated', header: 'Updated', enableSorting: false },
    ];
    
    const pageSizeOptions = [5, 10, 20].map((value) => ({
      value,
      label: `${value} / page`,
    }));
    
    export default function TanStackTableDemo() {
      const [sorting, setSorting] = useState<ColumnSort[]>([]);
      const [pageIndex, setPageIndex] = useState(1);
      const [pageSize, setPageSize] = useState(5);
    
      const sorted = useMemo(() => {
        if (sorting.length === 0) return tickets;
        const { id, desc } = sorting[0];
        return [...tickets].sort((a, b) => {
          const diff = String(a[id as keyof Ticket]).localeCompare(
            String(b[id as keyof Ticket]),
          );
          return desc ? -diff : diff;
        });
      }, [sorting]);
    
      const start = (pageIndex - 1) * pageSize;
      const pageRows = sorted.slice(start, start + pageSize);
    
      const table = useTable({
        features,
        data: pageRows,
        columns,
        manualSorting: true,
        manualPagination: true,
        state: { sorting },
        onSortingChange: (updater) => {
          const next = typeof updater === 'function' ? updater(sorting) : updater;
          setSorting(next);
          setPageIndex(1);
        },
      });
    
      return (
        <div className="flex flex-col gap-4 w-full">
          <Table>
            <THead>
              {table.getHeaderGroups().map((headerGroup) => (
                <Tr key={headerGroup.id}>
                  {headerGroup.headers.map((header) => (
                    <Th
                      key={header.id}
                      className={
                        header.column.getCanSort()
                          ? 'cursor-pointer select-none'
                          : undefined
                      }
                      onClick={header.column.getToggleSortingHandler()}
                    >
                      <div className="inline-flex items-center gap-1">
                        {flexRender(
                          header.column.columnDef.header,
                          header.getContext(),
                        )}
                        {header.column.getCanSort() && (
                          <Sorter sort={header.column.getIsSorted()} />
                        )}
                      </div>
                    </Th>
                  ))}
                </Tr>
              ))}
            </THead>
            <TBody>
              {table.getRowModel().rows.map((row) => (
                <Tr key={row.id}>
                  {row.getVisibleCells().map((cell) => (
                    <Td key={cell.id}>
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </Td>
                  ))}
                </Tr>
              ))}
            </TBody>
          </Table>
          <div className="flex items-center justify-between px-4 pb-4">
            <Pagination
              currentPage={pageIndex}
              total={tickets.length}
              pageSize={pageSize}
              onChange={setPageIndex}
            />
            <Select
              size="sm"
              className="min-w-30"
              placement="top"
              isSearchable={false}
              value={pageSizeOptions.find((option) => option.value === pageSize)}
              options={pageSizeOptions}
              onChange={(option) => {
                setPageSize(option?.value ?? pageSize);
                setPageIndex(1);
              }}
            />
          </div>
        </div>
      );
    }

    API

    Table is a compound component: the default export is the styling wrapper, and the section, row, and cell primitives hang off it as Table.THead, Table.TBody, Table.TFoot, Table.Tr, Table.Th, Table.Td, and Table.Sorter. Each primitive renders its matching native element and forwards native props, so you own the data and behavior yourself.

    Table

    PropDescriptionTypeDefault
    borderedWrap the table in a bordered card surface. Only applies when overflow is false.booleanfalse
    compactReduce cell padding for dense data.booleanfalse
    hoverableHighlight body rows on hover.booleantrue
    overflowWrap the table in a horizontal scroll container.booleantrue
    overflowClassExtra classes for the overflow container.string-
    verticalDividerShow vertical column dividers per section.TableVerticalDivider{ head: false, body: false, footer: false }
    asElementRender the table as a different element, for example a flex layout.ElementType'table'
    ...nativePropsNative table element props.ComponentPropsWithRef<'table'>-

    Table.THead / Table.TBody / Table.TFoot

    PropDescriptionTypeDefault
    asElementRender the section as a different element.ElementType'thead' / 'tbody' / 'tfoot'
    ...nativePropsNative section element props.ComponentPropsWithRef<'thead'>-

    Table.Tr

    PropDescriptionTypeDefault
    asElementRender the row as a different element.ElementType'tr'
    ...nativePropsNative tr props, including className for selected or active rows.ComponentPropsWithRef<'tr'>-

    Table.Th / Table.Td

    PropDescriptionTypeDefault
    asElementRender the cell as a different element.ElementType'th' / 'td'
    ...nativePropsNative cell props, including colSpan, rowSpan, and className.ComponentPropsWithRef<'th'>-

    Table.Sorter

    PropDescriptionTypeDefault
    sortCurrent sort direction. true shows the neutral indicator; 'asc' and 'desc' show the active arrows.boolean | 'asc' | 'desc'-
    classNameExtra classes for the sorter wrapper.string-

    Types

    type TableVerticalDivider = {
      head?: boolean
      body?: boolean
      footer?: boolean
    }