Skip to main content

Table

Table is a data display that organizes records into rows and columns, such as team members, invoices, or inventory.

Import

import { Table } from '@vortexlabs/vortex';
import type { TableProps } from '@vortexlabs/vortex';

Other links

ProjectStatus
WebsiteIn progress
Mobile appPlanned
"use client";

import { Table } from "@vortexlabs/vortex";

<Table.Root ariaLabel="Project status">
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Project</Table.HeaderCell>
      <Table.HeaderCell>Status</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell rowHeader>Website</Table.Cell>
      <Table.Cell>In progress</Table.Cell>
    </Table.Row>
    <Table.Row>
      <Table.Cell rowHeader>Mobile app</Table.Cell>
      <Table.Cell>Planned</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table.Root>

Features

  • Displays records with native table semantics.
  • Supports controlled sorting and row selection.
  • Pins the first column and checkbox column during horizontal scrolling.
  • Supports responsive row sizes, wrapped text, and truncated values.
  • Shows loading rows, empty states, and a custom footer.

Anatomy

<Table.Root ariaLabel="Team members">
  <Table.Header>
    <Table.Row>
      <Table.SelectionCell ariaLabel="Select all members" checked={false} />
      <Table.HeaderCell>Name</Table.HeaderCell>
      <Table.HeaderCell>Role</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.SelectionCell ariaLabel="Select Alex Chen" checked={false} />
      <Table.Cell rowHeader>Alex Chen</Table.Cell>
      <Table.Cell>Designer</Table.Cell>
    </Table.Row>
    {/* With no records */}
    <Table.Empty>No members yet.</Table.Empty>
  </Table.Body>
</Table.Root>
Component Part
Description
Names the table and manages scrolling and loading.
Groups the column headings.
Groups a record's cells or column headings.
Selects a row or a group of rows.
Names a column and optionally controls its sort direction.
Groups the records or their replacement state.
Displays a value or names its row.
Displays a message across all columns when there are no records.

Sizes

size sets row height and cell padding and accepts responsive values. The default is sm.

InvoiceAmount
INV-001$120.00
<Table.Root ariaLabel="Invoices" size={{ xs: "sm", md: "md" }}>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Invoice</Table.HeaderCell>
      <Table.HeaderCell align="end">Amount</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell rowHeader>INV-001</Table.Cell>
      <Table.Cell align="end">$120.00</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table.Root>

Sorting and Selection

Table doesn't sort or select data. Reorder records using the direction passed to onSort, and store selected row IDs so selection survives sorting and paging.

Role
Alex ChenDesigner
Mia RossiEngineer
Jordan ParkProduct manager
"use client";

import { Table, type TableSortDirection } from "@vortexlabs/vortex";
import { type ReactElement, useState } from "react";

const members = [
	{ id: "alex", name: "Alex Chen", role: "Designer" },
	{ id: "mia", name: "Mia Rossi", role: "Engineer" },
	{ id: "jordan", name: "Jordan Park", role: "Product manager" },
];

export const TableSelectionExample = (): ReactElement => {
	const [direction, setDirection] = useState<TableSortDirection | false>(false);
	const [selected, setSelected] = useState<string[]>([]);
	const selectedIds = new Set(selected);
	const rows = [...members].sort((a, b) =>
		direction
			? a.name.localeCompare(b.name) * (direction === "asc" ? 1 : -1)
			: 0,
	);

	return (
		<Table.Root ariaLabel="Team members">
			<Table.Header>
				<Table.Row>
					<Table.SelectionCell
						ariaLabel="Select all members"
						checked={selected.length === members.length}
						indeterminate={
							selected.length > 0 && selected.length < members.length
						}
						onChange={({ checked }): void =>
							setSelected(checked ? members.map((member) => member.id) : [])
						}
					/>
					<Table.HeaderCell
						sortDirection={direction}
						onSort={({ direction }): void => setDirection(direction)}
					>
						Name
					</Table.HeaderCell>
					<Table.HeaderCell>Role</Table.HeaderCell>
				</Table.Row>
			</Table.Header>
			<Table.Body>
				{rows.map((member) => (
					<Table.Row key={member.id} selected={selectedIds.has(member.id)}>
						<Table.SelectionCell
							ariaLabel={`Select ${member.name}`}
							checked={selectedIds.has(member.id)}
							onChange={({ checked }): void =>
								setSelected((current) =>
									checked
										? [...current, member.id]
										: current.filter((id) => id !== member.id),
								)
							}
						/>
						<Table.Cell rowHeader>{member.name}</Table.Cell>
						<Table.Cell>{member.role}</Table.Cell>
					</Table.Row>
				))}
			</Table.Body>
		</Table.Root>
	);
};

Text Overflow and Scrolling

truncate shows overflowing values in a tooltip and needs width on the column header. wrap lets text span lines. Set a container height to scroll rows vertically.

ProjectContactNotes
Websitealex.chen@long-company-name.example.comReview the updated designs before the next release.
Mobile appjordan.park@long-company-name.example.comConfirm the onboarding copy with the product team.
<Stack width="100%" maxWidth="32rem" height="10rem">
  <Table.Root ariaLabel="Project contacts" attributes={{ style: { height: "100%" } }}>
    <Table.Header>
      <Table.Row>
        <Table.HeaderCell width="10rem">Project</Table.HeaderCell>
        <Table.HeaderCell width="12rem">Contact</Table.HeaderCell>
        <Table.HeaderCell>Notes</Table.HeaderCell>
      </Table.Row>
    </Table.Header>
    <Table.Body>
      <Table.Row>
        <Table.Cell rowHeader>Website</Table.Cell>
        <Table.Cell truncate>alex.chen@long-company-name.example.com</Table.Cell>
        <Table.Cell wrap>Review the updated designs before the next release.</Table.Cell>
      </Table.Row>
      <Table.Row>
        <Table.Cell rowHeader>Mobile app</Table.Cell>
        <Table.Cell truncate>jordan.park@long-company-name.example.com</Table.Cell>
        <Table.Cell wrap>Confirm the onboarding copy with the product team.</Table.Cell>
      </Table.Row>
    </Table.Body>
  </Table.Root>
</Stack>

loading replaces body rows with skeletons and hides the footer. footer holds summaries or pagination controls.

NameRole
<Table.Root ariaLabel="Team members" loading loadingRows={3} footer={<Text>3 members</Text>}>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Name</Table.HeaderCell>
      <Table.HeaderCell>Role</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body />
</Table.Root>

Empty State

Table.Empty shows a message across all columns. Place it in Table.Body when there are no records.

NameRole
No results
Try a different search or clear the filters.
<Table.Root ariaLabel="Search results">
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Name</Table.HeaderCell>
      <Table.HeaderCell>Role</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Empty>
      <Icon src={Search} size={6} color="subtle" />
      <Stack gap={1}>
        <Text size="body-sm-desktop" weight="medium">No results</Text>
        <Text size="body-sm-desktop" color="subtle">Try a different search or clear the filters.</Text>
      </Stack>
      <Button variant="secondary" size="sm">Clear search</Button>
    </Table.Empty>
  </Table.Body>
</Table.Root>

Use the TanStack Table integration for column definitions, sorting, selection, and pagination managed together.

Accessibility

Key
Description
TabShiftTab
Move between sort buttons, checkboxes, truncated values, and an overflowing scroll region.
EnterSpace
Sort the focused column.
Left arrowRight arrowUp arrowDown arrow
Scroll the focused scroll region.
Name the table with ariaLabel or ariaLabelledby.
Use rowHeader on the cell that names each record.
Use hideLabel instead of an empty heading for action columns.