Table

Tables display information in an easily scannable format, helping users identify patterns and insights.This component utilizes the TanStack Table API for its core functionalities.

Available from eds-core/1.5.0

Updated in eds-core/1.15.0. See changelog for more details.

Quick Start

Installation
npm install @adaptavant/eds-core
Import
import { Table } from '@adaptavant/eds-core';

Basic usage

To ensure that the columns and data are not recreated on every render, you should memoize them using React.useMemo. This improves performance by preventing unnecessary re-renders.

When dealing with dynamic data that can change over time, useState should be used to manage the data state. This allows the table to update correctly when the data changes.

1TorieRustman
2KordulaGecks
3VikkiSimoens
4BurnabyCowern
5TeddieTraice
const basicColumnDef = [
  {
    header: "Id",
    accessorKey: "id",
  },
  {
    header: "First Name",
    accessorKey: "first_name",
  },
  {
    header: "Last Name",
    accessorKey: "last_name",
  },
];
const userList = [
  {
    id: 1,
    first_name: "Torie",
    last_name: "Rustman",
  },
  {
    id: 2,
    first_name: "Kordula",
    last_name: "Gecks",
  },
  {
    id: 3,
    first_name: "Vikki",
    last_name: "Simoens",
  },
  {
    id: 4,
    first_name: "Burnaby",
    last_name: "Cowern",
  },
  {
    id: 5,
    first_name: "Teddie",
    last_name: "Traice",
  },
];

const columns = React.useMemo(() => basicColumnDef, []);
const data = React.useMemo(() => userList, []);

return <Table columns={columns} data={data} />;

Data examples

Supports a variety of data types, including arrays of any type and objects. Column definition accessorKey differs for each type Refer below example,

1) Array of Objects

When your data is an array of objects, you can use accessorKey to specify the key for each column.

JohnDoe28
JaneSmith34
const userList = [
  { firstName: "John", lastName: "Doe", age: 28 },
  { firstName: "Jane", lastName: "Smith", age: 34 },
];
const columnsDef = [
  { header: "First Name", accessorKey: "firstName" },
  { header: "Last Name", accessorKey: "lastName" },
  { header: "Age", accessorKey: "age" },
];

const columns = React.useMemo(() => columnsDef, []);
const data = React.useMemo(() => userList, []);

return <Table columns={columns} data={data} />;

2) Array of Arrays

When your data is an array of arrays, each inner array typically represents a row in the table. You have two approaches to define columns when dealing with an array of arrays:

Using accessorFn to Access Index

Specify each column's header and use accessorFn to access the corresponding index in each row.

const data = [
  ["John", "Doe", 28],
  ["Jane", "Smith", 34],
];
const columns = [
  { header: "First Name", accessorFn: (row) => row[0] },
  { header: "Last Name", accessorFn: (row) => row[1] },
  { header: "Age", accessorFn: (row) => row[2] },
];

Using accessorKey with Numeric Strings

Alternatively, you can use accessorKey directly with numeric strings to specify the index.

const data = [
['John', 'Doe', 28],
['Jane', 'Smith', 34],
];

const columns = [
	{
		accessorKey: '0',
		header: 'First Name',
	},
	{
		accessorKey: '1',
		header: 'Last Name',
	},
	{
		accessorKey: '2',
		header: 'Age',
	},
];

Note: When using accessorKey with numeric strings, ensure each string corresponds to the index of the data array correctly.

3) Nested Objects

When your data includes nested objects, there are two ways to access nested keys.

Using dot notation

Use accessorKey with dot notation to directly access nested properties within your data objects. This method is simple and concise, allowing you to specify the exact path to the nested property as a string.

// Using dot notations
const data = [
{ name: { first: 'John', last: 'Doe' }, age: 28 },
{ name: { first: 'Jane', last: 'Smith' }, age: 34 },
];

const columns = [
{ header: 'First Name', accessorKey: 'name.first' },
{ header: 'Last Name', accessorKey: 'name.last' },
{ header: 'Age', accessorKey: 'age' },
];

Using accessorFn Function

Use accessorFn to define a function that accesses the nested properties within your data objects. This method offers more flexibility, allowing for complex data transformations or calculations if needed.

// Alternatively use accessorFn to access the row data
const data = [
  { name: { first: "John", last: "Doe" }, age: 28 },
  { name: { first: "Jane", last: "Smith" }, age: 34 },
];

const columns = [
  { header: "First Name", accessorKey: "name.first" }, // Accesses the 'first' property within the 'name' object
  { header: "Last Name", accessorKey: "name.last" }, // Accesses the 'last' property within the 'name' object
  { header: "Age", accessorKey: "age" }, // Accesses the 'age' property directly
];

Custom cell content

Custom cell content can be rendered by defining a cell function (or component) in the column definition. The function receives the TanStack cell context, so you can read the cell value with getValue() and the full row with row.original.

The example below demonstrates how to customize cell content in different ways within a single table. Each column highlights a custom pattern of interactivity and formatting as listed below:

  • Email: The email address is displayed inside a Tooltip, letting users view the value on hover.
  • Date of birth: The raw date value is transformed into a localized, human-readable format.
  • Country: The country name is wrapped within a Badge component.
  • Action: Provides an interactive cell containing a button that opens a confirmation Modal, showcasing how to embed stateful UI controls within table cells.
Name
Email
Date of Birth
Country
Action
Johnjohn.doe@example.comJan 1, 1990USA
Janejane.smith@example.comMay 15, 1985Canada
Samsam.brown@example.comJul 22, 1992UK
Alicealice.johnson@example.comMar 12, 1988Australia
Bobbob.davis@example.comNov 30, 1995New Zealand
const userList = [
  {
    id: 1,
    name: "John",
    email: "john.doe@example.com",
    date_of_birth: "1990-01-01",
    country: "USA",
  },
  {
    id: 2,
    name: "Jane",
    email: "jane.smith@example.com",
    date_of_birth: "1985-05-15",
    country: "Canada",
  },
  {
    id: 3,
    name: "Sam",
    email: "sam.brown@example.com",
    date_of_birth: "1992-07-22",
    country: "UK",
  },
  {
    id: 4,
    name: "Alice",
    email: "alice.johnson@example.com",
    date_of_birth: "1988-03-12",
    country: "Australia",
  },
  {
    id: 5,
    name: "Bob",
    date_of_birth: "1995-11-30",
    email: "bob.davis@example.com",
    country: "New Zealand",
  },
];

// wraps the value in a tooltip
const cellWithTooltip = ({ getValue }) => (
  <Tooltip content={getValue()} placement="bottom-start">
    {({ triggerProps }) => <span {...triggerProps}>{getValue()}</span>}
  </Tooltip>
);

// formats the raw value for display
const dateFormatter = ({ getValue }) =>
  new Date(getValue()).toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });

// renders the value inside a badge
const cellWithBadge = ({ getValue }) => (
  <Badge size="small" tone="positive">
    {getValue()}
  </Badge>
);

// interactive cell with its own state and a confirmation modal
const DeleteUserCell = ({ row }) => {
  const info = row.original;
  const [openModal, setOpenModal] = React.useState(false);

  return (
    <Fragment>
      <IconButton
        aria-label="Delete"
        icon={DeleteIcon}
        onClick={() => setOpenModal(true)}
        size="small"
        variant="accentPrimary"
      />
      <Modal
        closeOnEsc={false}
        closeOnOverlayClick={false}
        descriptionId="modal-description"
        onClose={() => setOpenModal(false)}
        open={openModal}
        role="alertdialog"
        titleId="modal-title"
      >
        <ModalHeader>
          <Heading as="h3" className="text-heading-16 font-stronger" id="modal-title">
            Delete the user?
          </Heading>
        </ModalHeader>
        <ModalContent>
          <p className="text-body-14" id="modal-description">
            Delete {info.name} from {info.country}?
          </p>
        </ModalContent>
        <ModalFooter>
          <Button onClick={() => setOpenModal(false)} variant="neutralTertiary">
            No
          </Button>
          <Button onClick={() => setOpenModal(false)}>Yes</Button>
        </ModalFooter>
      </Modal>
    </Fragment>
  );
};

const columnsDef = [
  { header: "Name", accessorKey: "name" },
  { header: "Email", accessorKey: "email", cell: cellWithTooltip },
  { header: "Date of Birth", accessorKey: "date_of_birth", cell: dateFormatter },
  { header: "Country", accessorKey: "country", cell: cellWithBadge },
  { header: "Action", accessorKey: "delete", cell: DeleteUserCell },
];

const columns = React.useMemo(() => columnsDef, []);
const data = React.useMemo(() => userList, []);

return <Table columns={columns} data={data} role="grid" enableSorting={false} />;

Header groups

Header groups allow you to create nested column headers in your table component, which helps organize and display related columns under a common header. To achieve header groups, define your columns with a nested structure where each group has its own header and a columns array for its child columns.

Name
Info
1JohnDoe1990-01-01USA123-456-7890
2JaneSmith1985-05-15Canada987-654-3210
3SamBrown1992-07-22UK456-789-1230
4AliceJohnson1988-03-12Australia321-654-9870
5BobDavis1995-11-30New Zealand654-321-9876
const userList = [
  {
    id: 1,
    first_name: "John",
    last_name: "Doe",
    date_of_birth: "1990-01-01",
    country: "USA",
    phone: "123-456-7890",
  },
  {
    id: 2,
    first_name: "Jane",
    last_name: "Smith",
    date_of_birth: "1985-05-15",
    country: "Canada",
    phone: "987-654-3210",
  },
  {
    id: 3,
    first_name: "Sam",
    last_name: "Brown",
    date_of_birth: "1992-07-22",
    country: "UK",
    phone: "456-789-1230",
  },
  {
    id: 4,
    first_name: "Alice",
    last_name: "Johnson",
    date_of_birth: "1988-03-12",
    country: "Australia",
    phone: "321-654-9870",
  },
  {
    id: 5,
    first_name: "Bob",
    last_name: "Davis",
    date_of_birth: "1995-11-30",
    country: "New Zealand",
    phone: "654-321-9876",
  },
];

// Column definitions with header groups
const columnsDef = [
  {
    header: "Id",
    accessorKey: "id",
  },
  {
    header: "Name",
    columns: [
      {
        header: "First Name",
        accessorKey: "first_name",
      },
      {
        header: "Last Name",
        accessorKey: "last_name",
      },
    ],
  },
  {
    header: "Info",
    columns: [
      {
        header: "Date of Birth",
        accessorKey: "date_of_birth",
      },
      {
        header: "Country",
        accessorKey: "country",
      },
      {
        header: "Phone",
        accessorKey: "phone",
      },
    ],
  },
];

const columns = React.useMemo(() => columnsDef, []);
const data = React.useMemo(() => userList, []);

return <Table columns={columns} data={data} />;

The table component supports both client-side and server-side pagination. To enable and configure pagination, utilize the pagination prop when integrating the table component into your application. This prop allows you to specify pagination settings.

Clientside Pagination

This example showcases client-side pagination, ideal for efficiently handling small to moderate amounts of data.

Note: This section shows the table's built-in default pagination (and the deprecated displayFormat prop). The recommended approach is to explicitly render SimplePagination or NumberedPagination through the renderPagination prop, covered in the sections below.

const pagination = { pageSize: 5 };
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const basicColumnDef = [
  {
    header: "FirstName",
    accessorKey: "name.first",
  },
  {
    header: "LastName",
    accessorKey: "name.last",
  },
  {
    header: "Age",
    accessorKey: "dob.age",
  },
  {
    header: "Phone",
    accessorKey: "phone",
  },
  {
    header: "Country",
    accessorKey: "location.country",
  },
];
React.useEffect(() => {
  fetch("https://randomuser.me/api/?results=30")
    .then((response) => response.json())
    .then((data) => {
      setData(data.results);
      setLoading(false);
    })
    .catch((error) => {
      console.log(error);
      setLoading(false);
    });
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
if (loading) {
  return (
    <Box className="flex justify-center w-full p-4 h-[200px]">
      <Loading size="48" />
    </Box>
  );
}
return <Table columns={columns} data={data} pagination={pagination} />;

Pagination adornmentStart

Add any content to the start of the pagination by passing the adornmentStart to the pagination config object. It accepts a React node and will be rendered at the start of the pagination.

This example shows how to add a button to the start of the pagination component.

const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const adornmentStart = <Button variant="accentPrimary">Download as CSV</Button>;
const pagination = { pageSize: 6, adornmentStart };
const basicColumnDef = [
  {
    header: "FirstName",
    accessorKey: "name.first",
  },
  {
    header: "LastName",
    accessorKey: "name.last",
  },
  {
    header: "Age",
    accessorKey: "dob.age",
  },
  {
    header: "Phone",
    accessorKey: "phone",
  },
  {
    header: "Country",
    accessorKey: "location.country",
  },
];
React.useEffect(() => {
  fetch("https://randomuser.me/api/?results=20")
    .then((response) => response.json())
    .then((data) => {
      setData(data.results);
      setLoading(false);
    })
    .catch((error) => {
      console.log(error);
      setLoading(false);
    });
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
if (loading) {
  return (
    <Box className="flex justify-center w-full p-4 h-[200px]">
      <Loading size="48" />
    </Box>
  );
}
return <Table columns={columns} data={data} pagination={pagination} />;

Serverside Pagination

For large datasets, use server-side pagination by setting manualPagination: true. This gives you full control over data fetching and pagination logic.

Pagination Props

  • pageSize - Number of rows per page
  • pageIndex - Current page (0-based, optional)
  • manualPagination - Enable server-side pagination
  • rowCount - Total rows across all pages (optional, enables "X of Y" display)
  • onPaginationChange - Callback when page changes: (pageIndex, pageSize) => void
  • renderPagination - Render function for the pagination UI. Receives { tableInstance, config } and returns <SimplePagination /> or <NumberedPagination /> (spread {...paginationProps}). Replaces the deprecated displayFormat prop.
  • displayFormat (deprecated) - Return <SimplePagination variant="…" /> from renderPagination instead. Still accepted for back-compat when renderPagination is not provided.

Loading State Props

  • isLoading - Shows loading indicator inside the table body while maintaining header and pagination

No posts found

1/0
const [posts, setPosts] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const [pageIndex, setPageIndex] = React.useState(0);
const [totalRows, setTotalRows] = React.useState(0);
const [pageSize, setPageSize] = React.useState(6);

const columns = React.useMemo(() => [
  {
    header: "ID",
    accessorKey: "id",
    size: 60,
  },
  {
    header: "Title",
    accessorKey: "title",
    cell: ({ getValue }) => {
      const title = getValue();
      return (
        <Box className="max-w-[300px]">
          <p className="truncate" title={title}>
            {title}
          </p>
        </Box>
      );
    },
  },
  {
    header: "Body",
    accessorKey: "body", 
    cell: ({ getValue }) => {
      const body = getValue();
      return (
        <Box className="max-w-[200px]">
          <p className="truncate" title={body}>
            {body}
          </p>
        </Box>
      );
    },
  },
  {
    header: "User ID",
    accessorKey: "userId",
    size: 80,
  },
], []);

const fetchPosts = React.useCallback(async (page, limit) => {
  setLoading(true);
  try {
    const skip = page * limit;
    const response = await fetch(
      `https://dummyjson.com/posts?skip=${skip}&limit=${limit}`
    );
    const data = await response.json();
    setPosts(data.posts || []);
    setTotalRows(data.total || 0);
  } catch (err) {
    console.error('Error fetching posts:', err);
    setPosts([]);
    setTotalRows(0);
  } finally {
    setLoading(false);
  }
}, []);

React.useEffect(() => {
  fetchPosts(pageIndex, pageSize);
}, [fetchPosts, pageIndex, pageSize]);

const handlePaginationChange = React.useCallback((newPageIndex, newPageSize) => {
  if (newPageSize !== pageSize) {
    setPageSize(newPageSize);
    setPageIndex(0); // Reset to first page when page size changes
  } else {
    setPageIndex(newPageIndex);
  }
}, [pageSize]);

const EmptyTemplate = () => (
  <Box className="flex justify-center text-center p-4 h-[200px]">
    <Box className="flex flex-col items-center justify-center">
      <InformationIcon size="16" />
      <p className="text-primary">No posts found</p>
    </Box>
  </Box>
);

return (
  <Box className="w-full min-w-0">
    <Table
      columns={columns}
      data={posts}
      isLoading={loading} // Loading state inside table body
      emptyTemplate={<EmptyTemplate />}
      pagination={{
        pageSize,
        pageIndex,
        rowCount: totalRows, // Enables "Showing X to Y out of Z" format
        manualPagination: true, // Enable server-side pagination
        renderPagination: (paginationProps) => (
          <SimplePagination {...paginationProps} variant="compact" />
        ), // Shows "X/Y" format instead of detailed counts
        onPaginationChange: handlePaginationChange,
      }}
      role="table"
    />
  </Box>
);

Advanced server-side Pagination

This example builds on server-side pagination by adding error handling and dynamic page sizing.

  • Error handling — when the request fails, a StatusPanel with a Retry action is rendered instead of the table.
  • Dynamic page sizing — the buttons above the table change pageSize and reset to the first page, triggering a fresh fetch.
  • isLoading keeps the header and pagination in place while the body shows a loading indicator.
ID
Name
Email
Age
Company

No users found

Showing 0 to 0 out of 0
const [users, setUsers] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [pageIndex, setPageIndex] = React.useState(0);
const [pageSize, setPageSize] = React.useState(5);
const [totalRows, setTotalRows] = React.useState(0);

const fetchUsers = React.useCallback(async (page, limit) => {
  setLoading(true);
  setError(null);
  try {
    const skip = page * limit;
    const response = await fetch(
      `https://dummyjson.com/users?skip=${skip}&limit=${limit}`
    );
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const result = await response.json();
    setUsers(result.users || []);
    setTotalRows(result.total || 0);
  } catch (err) {
    setError(err instanceof Error ? err.message : "Failed to fetch users");
    setUsers([]);
    setTotalRows(0);
  } finally {
    setLoading(false);
  }
}, []);

React.useEffect(() => {
  fetchUsers(pageIndex, pageSize);
}, [fetchUsers, pageIndex, pageSize]);

const handlePaginationChange = React.useCallback(
  (newPageIndex, newPageSize) => {
    if (newPageSize !== pageSize) {
      setPageSize(newPageSize);
      setPageIndex(0); // Reset to first page when page size changes
    } else {
      setPageIndex(newPageIndex);
    }
  },
  [pageSize]
);

const columns = React.useMemo(
  () => [
    { header: "ID", accessorKey: "id", size: 60 },
    {
      header: "Name",
      id: "fullName",
      accessorFn: (row) => `${row.firstName} ${row.lastName}`,
    },
    {
      header: "Email",
      accessorKey: "email",
      cell: ({ getValue }) => (
        <Box className="max-w-[200px]">
          <p className="truncate" title={getValue()}>
            {getValue()}
          </p>
        </Box>
      ),
    },
    { header: "Age", accessorKey: "age", size: 80 },
    {
      header: "Company",
      accessorKey: "company.name",
      cell: ({ getValue }) => (
        <Box className="max-w-[150px]">
          <p className="truncate" title={getValue()}>
            {getValue()}
          </p>
        </Box>
      ),
    },
  ],
  []
);

if (error) {
  return (
    <Box className="flex justify-center items-center h-64 w-full">
      <StatusPanel
        action={
          <Button
            onClick={() => fetchUsers(pageIndex, pageSize)}
            size="small"
            variant="criticalPrimary"
          >
            Retry
          </Button>
        }
        description={error}
        title="Failed to load users"
        variant="error"
      />
    </Box>
  );
}

return (
  <Box className="w-full min-w-0 flex flex-col gap-3">
    <Box className="flex gap-2">
      {[5, 10].map((size) => (
        <Button
          key={size}
          onClick={() => {
            setPageSize(size);
            setPageIndex(0);
          }}
          size="small"
          variant={pageSize === size ? "accentPrimary" : "neutralSecondary"}
        >
          {size} per page
        </Button>
      ))}
    </Box>
    <Table
      columns={columns}
      data={users}
      emptyTemplate={
        <Box className="flex justify-center text-center p-4 h-[200px]">
          <Box className="flex flex-col items-center justify-center">
            <InformationIcon size="16" />
            <p className="text-primary">No users found</p>
          </Box>
        </Box>
      }
      enableSorting={false}
      isLoading={loading}
      pagination={{
        pageSize,
        pageIndex,
        rowCount: totalRows,
        manualPagination: true,
        onPaginationChange: handlePaginationChange,
      }}
      role="table"
    />
  </Box>
);

SimplePagination

Return <SimplePagination /> from the pagination.renderPagination render prop for a compact text summary of the current range, with previous/next controls. It is also what the table renders by default when you do not provide renderPagination.

Its only prop is variant: 'detailed' (default) shows the full "Showing X to Y out of Z" summary, while 'compact' shows a condensed "X/Y" (current page / total pages).

const basicColumnDef = [
  { header: "FirstName", accessorKey: "name.first" },
  { header: "LastName", accessorKey: "name.last" },
  { header: "Age", accessorKey: "dob.age" },
];
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
  fetch("https://randomuser.me/api/?results=30")
    .then((response) => response.json())
    .then((data) => {
      setData(data.results);
      setLoading(false);
    })
    .catch(() => setLoading(false));
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
if (loading) {
  return (
    <Box className="flex justify-center w-full p-4 h-[200px]">
      <Loading size="48" />
    </Box>
  );
}
return (
  <Table
    role="table"
    columns={columns}
    data={data}
    pagination={{
      pageSize: 5,
      renderPagination: (paginationProps) => (
        <SimplePagination {...paginationProps} />
      ),
    }}
  />
);

Compact variant

Use variant="compact" to show a condensed "X/Y" summary instead of the detailed count, useful for tight layouts.

const basicColumnDef = [
  { header: "FirstName", accessorKey: "name.first" },
  { header: "LastName", accessorKey: "name.last" },
  { header: "Age", accessorKey: "dob.age" },
];
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
  fetch("https://randomuser.me/api/?results=30")
    .then((response) => response.json())
    .then((data) => {
      setData(data.results);
      setLoading(false);
    })
    .catch(() => setLoading(false));
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
if (loading) {
  return (
    <Box className="flex justify-center w-full p-4 h-[200px]">
      <Loading size="48" />
    </Box>
  );
}
return (
  <Table
    role="table"
    columns={columns}
    data={data}
    pagination={{
      pageSize: 5,
      renderPagination: (paginationProps) => (
        <SimplePagination {...paginationProps} variant="compact" />
      ),
    }}
  />
);

NumberedPagination

Return <NumberedPagination /> from the pagination.renderPagination render prop to replace the simple text pagination with numbered page buttons, an ellipsis overflow, and a go-to-page popover.

1AliceSmithuser1@example.comUSA
2BobJonesuser2@example.comUK
3CarolBrownuser3@example.comCanada
4DavidWilsonuser4@example.comAustralia
5EvaDavisuser5@example.comGermany
const basicColumnDef = [
  { header: "ID", accessorKey: "id", size: 60 },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
  { header: "Email", accessorKey: "email" },
  { header: "Country", accessorKey: "country" },
];

const userList = React.useMemo(() => Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  first_name: ["Alice", "Bob", "Carol", "David", "Eva"][i % 5],
  last_name: ["Smith", "Jones", "Brown", "Wilson", "Davis"][i % 5],
  email: `user${i + 1}@example.com`,
  country: ["USA", "UK", "Canada", "Australia", "Germany"][i % 5],
})), []);

const columns = React.useMemo(() => basicColumnDef, []);

return (
  <Table
    role="table"
    columns={columns}
    data={userList}
    pagination={{
      pageSize: 5,
      renderPagination: (paginationProps) => (
        <NumberedPagination {...paginationProps} />
      ),
    }}
  />
);

Compact variant

Use variant="compact" for tighter layouts, showing up to 3 page buttons with icon-only previous/next controls.

1AliceSmithuser1@example.com
2BobJonesuser2@example.com
3CarolBrownuser3@example.com
4DavidWilsonuser4@example.com
5EvaDavisuser5@example.com
const basicColumnDef = [
  { header: "ID", accessorKey: "id", size: 60 },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
  { header: "Email", accessorKey: "email" },
];

const userList = React.useMemo(() => Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  first_name: ["Alice", "Bob", "Carol", "David", "Eva"][i % 5],
  last_name: ["Smith", "Jones", "Brown", "Wilson", "Davis"][i % 5],
  email: `user${i + 1}@example.com`,
})), []);

const columns = React.useMemo(() => basicColumnDef, []);

return (
  <Table
    role="table"
    columns={columns}
    data={userList}
    pagination={{
      pageSize: 5,
      renderPagination: (paginationProps) => (
        <NumberedPagination {...paginationProps} variant="compact" />
      ),
    }}
  />
);

Large size

Use size="large" for tables where the pagination controls need to match a larger UI scale.

1AliceSmithuser1@example.com
2BobJonesuser2@example.com
3CarolBrownuser3@example.com
4DavidWilsonuser4@example.com
5EvaDavisuser5@example.com
const basicColumnDef = [
  { header: "ID", accessorKey: "id", size: 60 },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
  { header: "Email", accessorKey: "email" },
];

const userList = React.useMemo(() => Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  first_name: ["Alice", "Bob", "Carol", "David", "Eva"][i % 5],
  last_name: ["Smith", "Jones", "Brown", "Wilson", "Davis"][i % 5],
  email: `user${i + 1}@example.com`,
})), []);

const columns = React.useMemo(() => basicColumnDef, []);

return (
  <Table
    role="table"
    columns={columns}
    data={userList}
    pagination={{
      pageSize: 5,
      renderPagination: (paginationProps) => (
        <NumberedPagination {...paginationProps} size="large" />
      ),
    }}
  />
);

The larger hit targets are also useful on mobile viewports. Pair it with Responsive Layout to switch between variant/size (or between SimplePagination and NumberedPagination) based on the current breakpoint.

With adornment

Use adornmentStart to add content to the start of the pagination. This example combines an action button with compact numbered pagination.

1AliceSmithuser1@example.com
2BobJonesuser2@example.com
3CarolBrownuser3@example.com
4DavidWilsonuser4@example.com
5EvaDavisuser5@example.com
const basicColumnDef = [
  { header: "ID", accessorKey: "id", size: 60 },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
  { header: "Email", accessorKey: "email" },
];

const userList = React.useMemo(() => Array.from({ length: 30 }, (_, i) => ({
  id: i + 1,
  first_name: ["Alice", "Bob", "Carol", "David", "Eva"][i % 5],
  last_name: ["Smith", "Jones", "Brown", "Wilson", "Davis"][i % 5],
  email: `user${i + 1}@example.com`,
})), []);

const columns = React.useMemo(() => basicColumnDef, []);

return (
  <Table
    role="table"
    columns={columns}
    data={userList}
    pagination={{
      pageSize: 5,
      adornmentStart: <Button variant="accentPrimary">Download as CSV</Button>,
      renderPagination: (paginationProps) => (
        <NumberedPagination {...paginationProps} variant="compact" />
      ),
    }}
  />
);

Empty template

Use emptyTemplate prop In cases where your table encounters empty datasets. The empty template functionality enhances user experience by displaying custom content or messages. This ensures users receive informative feedback instead of encountering a blank or confusing interface.

No data to display

// Define custom fallBackTemplate 
const FallBackTemplate = () => {
  return (
    <Box className="flex justify-center text-center p-4 h-[200px]">
      <Box className="flex flex-col items-center justify-center">
        <InformationIcon size="16" />
        <p className="text-primary">No data to display</p>
      </Box>
    </Box>
  );
};

//Empty data
const userList = [];

const columnsDef = [
  { header: "First Name", accessorKey: "firstName" },
  { header: "Last Name", accessorKey: "lastName" },
  { header: "Age", accessorKey: "age" },
];

const columns = React.useMemo(() => columnsDef, []);
const data = React.useMemo(() => userList, []);

return <Table columns={columns} data={data} emptyTemplate={<FallBackTemplate/>} />;

Column definition

Use columns prop to specify how each column should behave, including how data should be accessed, displayed, and optionally, how it should be formatted or customized. By defining columns explicitly, you gain fine-grained control over the presentation and functionality of your tables.

Below is a detailed breakdown of the fields used in a column definition.

FieldTypeDefinitionExample
headerstring | React.ReactNodeThe display name of the column. This can be a string or a React node for custom rendering.'Name' or <CustomHeaderComponent />
accessorKeystringThe key used to access the value from the row data. This should match the key in the data object.firstName
accessorFn?(originalRow: TData, index: number) => anyA function to access the value from the row data. This can be used when the data requires transformation or computation.(row) => row.name.first + ' ' + row.name.last

cell?

function | string

A custom cell formatter. This can be a React component or a string for basic rendering.

The function callback has a context object , which provides more access to the table instance.

({ table,column,row,cell,getValue,renderValue }) => <span>{getValue()}</span>

columns?ColumnDef[]An array of column definitions. This is used for nested columns to create column groups.{ header: 'Name', columns: [{ header: 'First Name', accessorKey: 'firstName', size: 100 }, { header: 'Last Name', accessorKey: 'lastName', size: 100 }] }

Sorting

By default, sorting is enabled on the table. You can set the enableSorting prop to false to disable sorting, and the sortDescFirst prop to define the initial sorting order (ascending or descending).

Note: Refer Tanstack table sorting API guide for more context on table and column props.

First Name
1TorieRustmanmale
2KordulaGecksfemale
3Vikkifemale
4BurnabyCowernmale
5TeddieTraicefemale
6BenWilliammale
const basicColumnDef = [
  {
    header: "Id",
    accessorKey: "id",
  },
  {
    header: "First Name",
    accessorKey: "first_name",
    // disable sorting for firstName column
    enableSorting: false,
  },
  {
    header: "Last Name",
    accessorKey: "last_name",
    sortingFn: "basic", // Built-in sorting
    sortUndefined: "last", // sorts undefined columns to the last
  },
  {
    header: "Gender",
    accessorKey: "gender",
    // custom sorting function for gender
    sortingFn: (rowA, rowB) => {
      const genderOrder = ["male", "female"];
      return (
        genderOrder.indexOf(rowA.getValue("gender")) -
        genderOrder.indexOf(rowB.getValue("gender"))
      );
    },
  },
];
const userList = [
  {
    id: 1,
    first_name: "Torie",
    last_name: "Rustman",
    gender: "male",
  },
  {
    id: 2,
    first_name: "Kordula",
    last_name: "Gecks",
    gender: "female",
  },
  {
    id: 3,
    first_name: "Vikki",
    last_name: undefined,
    gender: "female",
  },
  {
    id: 4,
    first_name: "Burnaby",
    last_name: "Cowern",
    gender: "male",
  },
  {
    id: 5,
    first_name: "Teddie",
    last_name: "Traice",
    gender: "female",
  },
  {
    id: 6,
    first_name: "Ben",
    last_name: "William",
    gender: "male",
  },
];

const columns = React.useMemo(() => basicColumnDef, []);
const data = React.useMemo(() => userList, []);

// By default first sortingDirection for string datatype is ascending , set sortDescFirst to true to change it to DESC for the entire table.

return <Table columns={columns} data={data} sortDescFirst={true} />;

Single Row Selection

Enable single row selection by setting rowSelection.mode: 'single'.

Use rowSelection.selectedRowId to control which row is highlighted, and rowSelection.onRowSelectionChange to sync state when the user clicks a row.

Make sure getRowId is used alongside rowSelection.selectedRowId to ensure IDs match correctly. Without it, TanStack Table defaults to string-based array indices. Note: See the TanStack Table Rows API guide for more on row and column props.

No row selected
1TorieRustman
2KordulaGecks
3VikkiSimoens
4BurnabyCowern
5TeddieTraice
const userList = [
  { id: 1, first_name: "Torie", last_name: "Rustman" },
  { id: 2, first_name: "Kordula", last_name: "Gecks" },
  { id: 3, first_name: "Vikki", last_name: "Simoens" },
  { id: 4, first_name: "Burnaby", last_name: "Cowern" },
  { id: 5, first_name: "Teddie", last_name: "Traice" },
];

const basicColumnDef = [
  { header: "Id", accessorKey: "id" },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
];

const [selectedRowId, setSelectedRowId] = React.useState(undefined);

const selectedUser = userList.find((u) => String(u.id) === selectedRowId);
const columns = React.useMemo(() => basicColumnDef, []);
const data = React.useMemo(() => userList, []);

return (
  <Box className="flex flex-col gap-2">
    <Box className="flex gap-2">
      <Button size="small" onClick={() => setSelectedRowId("1")}>Select Torie</Button>
      <Button size="small" onClick={() => setSelectedRowId("3")}>Select Vikki</Button>
      <Button size="small" onClick={() => setSelectedRowId(undefined)}>Clear</Button>
    </Box>
    <Text className="text-body-14">
      {selectedUser
        ? `Selected: ID ${selectedUser.id} - ${selectedUser.first_name} ${selectedUser.last_name}`
        : "No row selected"}
    </Text>
    <Table
      columns={columns}
      data={data}
      role="grid"
      getRowId={(row) => String(row.id)}
      rowSelection={{
        mode: "single",
        selectedRowId: selectedRowId,
        onRowSelectionChange: setSelectedRowId,
      }}
    />
  </Box>
);

Deselecting a selected row

Set rowSelection.shouldDeSelect: true to allow clicking an already-selected row to deselect it. Rows are not deselected by default.

Column size

Column width is controlled per column via the column definition.

The Table uses TanStack Table's column sizing. You can set size (width calculated in pixels) on each column to set the width of the column.

1Widget AW-001Best seller
2Widget BW-002New arrival
3Widget CW-003Limited stock
const productList = [
  { id: 1, name: "Widget A", sku: "W-001", notes: "Best seller" },
  { id: 2, name: "Widget B", sku: "W-002", notes: "New arrival" },
  { id: 3, name: "Widget C", sku: "W-003", notes: "Limited stock" },
];

// Add `size` to each column to set the width of the column.
const columnsDef = [
  { header: "Id", accessorKey: "id", size: 60 },
  { header: "Name", accessorKey: "name", size: 120 },
  { header: "SKU", accessorKey: "sku", size: 100 },
  { header: "Notes", accessorKey: "notes", size: 200 },
];

const columns = React.useMemo(() => columnsDef, []);
const data = React.useMemo(() => productList, []);

return <Table columns={columns} data={data} />;

Column visibility

Control which columns are visible via the columnVisibility prop.

Table column visibility is fully controlled using the columnVisibility prop. Pass your visibility state as columnVisibility.state, which should be an object mapping each column's ID (accessorKey or id from your column definitions) to a boolean — true to show the column, false to hide it. To respond to user changes (such as toggling columns), provide a columnVisibility.onChange callback to update your state accordingly.

Columns omitted from state default to visible. When onChange is omitted, the visibility is fixed to the provided state.

1TorieRustmantorie.rustman@example.com
2KordulaGeckskordula.gecks@example.com
3VikkiSimoensvikki.simoens@example.com
4EdwardCowernedward.cowern@example.com
5TedMosbyted.mosby@example.com
const userList = [
  { id: 1, first_name: "Torie", last_name: "Rustman", age: 32, email: "torie.rustman@example.com" },
  { id: 2, first_name: "Kordula", last_name: "Gecks", age: 27, email: "kordula.gecks@example.com" },
  { id: 3, first_name: "Vikki", last_name: "Simoens", age: 4, email: "vikki.simoens@example.com" },
  { id: 4, first_name: "Edward", last_name: "Cowern", age: 4, email: "edward.cowern@example.com" },
  { id: 5, first_name: "Ted", last_name: "Mosby", age: 4, email: "ted.mosby@example.com" },
];

const basicColumnDef = [
  { header: "Id", accessorKey: "id" },
  { header: "First Name", accessorKey: "first_name" },
  { header: "Last Name", accessorKey: "last_name" },
  { header: "Age", accessorKey: "age" },
  { header: "Email", accessorKey: "email" },
];

// Columns omitted default to true(visible)
const [columnVisibility, setColumnVisibility] = React.useState({
  id: true,
  first_name: true,
  last_name: true,
  age: false,
});

const toggleColumn = (columnId) =>
  setColumnVisibility((prev) => ({ ...prev, [columnId]: prev[columnId] === false }));

const columns = React.useMemo(() => basicColumnDef, []);
const data = React.useMemo(() => userList, []);
const hideableColumns = basicColumnDef.filter((col) => col.accessorKey !== 'id' ) // id column can't be hidden

return (
  <Box className="flex flex-col gap-2">
    <Stack className="gap-4">
      {hideableColumns.map((column) => (
        <Toggle
          key={column.accessorKey}
          label={`Hide "${column.header}" column`}
          checked={columnVisibility[column.accessorKey] === false}
          onChange={() => toggleColumn(column.accessorKey)}
        />
      ))}
    </Stack>
    <Table
      columns={columns}
      data={data}
      columnVisibility={{
        state: columnVisibility,
        onChange: setColumnVisibility,
      }}
    />
  </Box>
);

Column visibility with header groups

Column visibility is managed per column using its accessorKey, making it compatible even with nested header groups. When you toggle a grouped column, only that specific column is shown or hidden, while the overall group header remains visible if at least one child column is still displayed.

To enable toggling for grouped columns, first flatten your column definitions to extract all the columns. In this example, the "id" column cannot be hidden, but all other columns can be toggled individually, letting you control which data is visible without affecting the header group structure.

// Column definitions with header groups
const columnsDef = [
  {
    header: "Id",
    accessorKey: "id",
  },
  {
    header: "Name",
    columns: [
      { header: "First Name", accessorKey: "first_name" },
      { header: "Last Name", accessorKey: "last_name" },
    ],
  },
  {
    header: "Info",
    columns: [
      { header: "Date of Birth", accessorKey: "date_of_birth" },
      { header: "Country", accessorKey: "country" },
      { header: "Phone", accessorKey: "phone" },
    ],
  },
];

const hideableColumns = columnsDef
  .flatMap((col) => col.columns ?? col)
  .filter((col) => col.accessorKey !== 'id');

By flattening the column definitions this way, you can easily generate toggle controls for each column in your UI, allowing users to show or hide individual columns even within header groups.

Editable cells

Set editMode to render editable inputs and provide onCellValueUpdate to receive committed values.
Configure each columnDef via meta:

  • editable: false - opt out of edit mode
  • validate - return an error string to block the commit (runs on blur and on the initial value)
  • onChange - runs on every keystroke; return a value to apply it to the cell (e.g. sanitized/filtered), or return nothing to keep the raw input
  • inputType - switch the input mode
  • label - override the accessible label

Keyboard: Enter commits the cell and advances focus to the same column in the next row. Escape reverts the cell to its original value and clears any error.

⚠️ Editable tables require role="grid".

⚠️ Columns with their own interactivity (icon button, tooltip, links) should not be editable - set meta.editable: false.

1
2
3
const [data, setData] = React.useState([
  { id: 1, first_name: "Torie", email: "torie.example.com", age: 45 },
  { id: 2, first_name: "Kordula", email: "kordula@gecks.io", age: 32 },
  { id: 3, first_name: "Vikki", email: "vikki@simoens.io", age: 28 },
]);

const columnsDef = [
  // Read-only column: stays plain text even in edit mode.
  { header: "Id", accessorKey: "id", meta: { editable: false } },
  {
    header: "First Name",
    accessorKey: "first_name",
    meta: {
      // Blocks the commit until valid; announced to assistive tech.
      validate: (value) =>
        String(value).trim().length < 2 ? "Min 2 characters" : undefined,
      // Strip anything that is not a letter, space, or hyphen as it is typed.
      onChange: (event) => event.target.value.replace(/[^A-Za-z\s-]/g, ""),
      // Accessible label override for the input.
      label: "First name",
    },
  },
  {
    header: "Email",
    accessorKey: "email",
    meta: {
      validate: (value) =>
        String(value).includes("@") ? undefined : "Must be a valid email",
    },
  },
  {
    header: "Age",
    accessorKey: "age",
    meta: {
      inputType: "number",
      // Digits only.
      onChange: (event) => event.target.value.replace(/\D/g, ""),
      validate: (value) =>
        Number(value) > 0 ? undefined : "Must be greater than 0",
    },
  },
];

const columns = React.useMemo(() => columnsDef, []);

return (
  <Table
    columns={columns}
    data={data}
    editMode
    getRowId={(row) => String(row.id)}
    role="grid"
    onCellValueUpdate={(rowId, columnId, value) => {
      setData((prev) =>
        prev.map((row) =>
          String(row.id) === rowId ? { ...row, [columnId]: value } : row
        )
      );
    }}
  />
);

API Reference

Table

PropDefaultDescription
roletable'table' | 'grid'
The role attribute of the table. Use 'grid' for interactive tables that have buttons, links, or other interactive elements.
data_Array
The dataset for the table. Each item in the array represents a row in the table and can be either an object or a primitive value.
columns_Array
The columns of the table. Each column is represented by a ColumnDef object, which specifies properties such as the header, accessor, and other column configurations.
pagination?_PaginationConfig
Configuration to control the pagination behavior of the table. See Pagination Configuration section below for detailed properties.
rowSelection?_RowSelectionConfig
Configuration to control single-row selection behavior of the table. The Table is purely controlled — the consumer owns selection state via selectedRowId. See Row Selection Configuration section below for detailed properties.
rowSelection.mode_'single'
The mode of row selection. Currently only single-row selection is supported.
rowSelection.selectedRowId_string
The ID of the currently selected row. Must match the value returned by getRowId for the target row. This prop is the source of truth for which row is selected — to move selection, update this prop (typically in response to onRowSelectionChange).
rowSelection.shouldDeSelect?falseboolean
Controls whether the user can deselect the currently selected row by clicking it again. When true, clicking the selected row fires onRowSelectionChange(undefined).
rowSelection.onRowSelectionChange_(selectedRowId: string | undefined) => void
Called when the user clicks or presses Enter on a row. Receives the new selected row ID, or undefined when deselecting (only fires with undefined when shouldDeSelect is true). When omitted, rows are non-interactive (no click/keyboard handling, no focus ring).
getRowId?_(row: TData) => string
Maps each row's data object to a stable string ID. When using rowSelection.selectedRowId, the value passed must match the string returned by this function for the target row. When omitted, the row's array index is used ('0', '1', …).
emptyTemplate?_ReactNode
Fallback template to render inside the table body when there is no data available.
enableSorting?_boolean
Sorting is enabled by default for the table. To disable sorting for the entire table, set this prop to false.
Note: You can also disable sorting for specific columns by setting this in the column definition for the required columns.
sortDescFirst?falseboolean
For numbers, the default sorting direction is DESC, while for strings, it is ASC. To apply the same sorting order (DESC) to strings as well, set this prop to true. This will make the first sorting direction DESC for all columns on the first click.
sortingFns?_Record<string, SortingFn>
Custom sorting functions for the table. The key is the column accessor and the value is the sorting function.
isLoading?falseboolean
Shows a loading indicator in the table body while keeping header and pagination visible. Useful for server-side data fetching scenarios to maintain visual stability.
editMode?falseboolean
When true, every eligible cell renders as an editable input. Opt a column out with meta.editable: false in its ColumnDef. Requires role="grid" for accessibility (the Table warns in development when used with role="table"). See Editable Column Configuration section below for per-column options.
onCellValueUpdate?_(rowId: string, columnId: string, value: string) => void
Called when a cell value changes and passes validation (on blur). Receives the row ID, column ID, and the new value. The value is always a string (number columns commit strings too), so consumers coerce it themselves — e.g. Number(value). The consumer owns the data and is responsible for updating it. Invalid edits are never committed, so this fires only for valid values.
columnVisibility?_ColumnVisibilityConfig
Configuration to control which columns are visible. The consumer owns the visibility state via state.
columnVisibility.state_Record<string, boolean>
A map of column ID to a boolean indicating each column's visibility (true shows the column, false hides it). Column IDs are the accessorKey (or id) from the column definitions. Columns omitted from the map default to visible.
columnVisibility.onChange?_(visibility: Record<string, boolean>) => void
Called when the column visibility state changes. When omitted, the visibility is fixed to the provided state.

Pagination configuration

When the pagination prop is provided, it accepts a PaginationConfig object with the following properties.

Use the renderPagination render prop to control the pagination presentation. It receives { tableInstance, config } and returns the pagination component. When omitted, the table renders <SimplePagination variant="detailed" /> which shows "Showing X to Y out of Z". Return <SimplePagination {...paginationProps} variant="compact" /> for an "X/Y" compact view, or <NumberedPagination {...paginationProps} /> for numbered page buttons.

PropDefaultDescription
pageSize?10number
Number of rows to be displayed per page.
pageIndex?0number
Initial page index (0-based).
manualPagination?falseboolean
When true, the table will not automatically paginate data. You will need to manually control pagination via onPaginationChange callback. This is useful for server-side pagination.
rowCount?_number
The total number of rows across all pages (for server-side pagination). When provided, enables "Showing X to Y out of Z" format. When not provided, shows "Showing X to Y" format.
renderPagination?_(paginationProps) => React.ReactElement
Render function for the pagination UI. Called with { tableInstance, config }; return <SimplePagination /> or <NumberedPagination /> (spread {...paginationProps} to forward them). When omitted, <SimplePagination variant="detailed" /> is rendered.
displayFormat?'detailed''detailed' | 'compact'
Display format for pagination info. 'detailed' shows "Showing X to Y out of Z", 'compact' shows "X/Y" (current page / total pages).\

⚠️ Deprecated. Return <SimplePagination variant="…" /> from renderPagination instead. Honoured only when renderPagination is not provided.

onPaginationChange?_(pageIndex: number, pageSize: number) => void
Callback function called when pagination state changes. Required for server-side pagination when manualPagination is true.
adornmentStart?_React.ReactNode
Content to be inserted at the start slot of the pagination.

SimplePagination

When returning <SimplePagination /> from renderPagination (spread the injected paginationProps to forward tableInstance/config), it accepts the following prop:

PropDefaultDescription
variant?'detailed''detailed' | 'compact'
Controls the pagination summary format. 'detailed' shows "Showing X to Y out of Z". 'compact' shows "X/Y" (current page / total pages).

NumberedPagination

When returning <NumberedPagination /> from renderPagination (spread the injected paginationProps to forward tableInstance/config), it accepts the following props:

PropDefaultDescription
variant?'detailed''detailed' | 'compact'
Controls how many page-number buttons render and how previous/next controls appear. 'detailed' shows up to 7 page buttons with labelled previous/next controls. 'compact' shows up to 3 page buttons with icon-only previous/next controls.
size?'standard''standard' | 'large'
Size of the page-number buttons.
previousLabel?translationstring
Visible label for the previous-page control. Defaults to the previousPageButtonLabel translation.
nextLabel?translationstring
Visible label for the next-page control. Defaults to the nextPageButtonLabel translation.

Editable column configuration

When editMode is enabled, each column can be configured for editing through the meta object on its ColumnDef. These fields augment TanStack Table's ColumnMeta.

PropDefaultDescription
meta.editable?trueboolean
Whether this column is editable while the table is in edit mode. Set to false to keep the column read-only (it renders as plain text even when editMode is on).
meta.validate?_(value: unknown) => string | undefined
Validation function run on blur (and on the initial value). Return an error string to block the commit and show the error state inline; return undefined to allow the commit. A cell that starts with an invalid value shows the error state immediately.
meta.inputType?'text''text' | 'number'
Input type for the editable cell. 'number' renders the input with a numeric input mode.
meta.onChange?_(event: React.ChangeEvent<HTMLInputElement>) => string | void
Called on every keystroke, before the value is applied. Receives the change event; return the (possibly sanitized) string to apply, or return nothing to keep the raw value. Use it to filter input — e.g. digits only, or stripping special characters — so disallowed input never enters the cell and never reaches the commit (onCellValueUpdate). Runs independently of meta.validate, which still runs on commit.
meta.label?_string
Accessible label override for the editable input. Falls back to the column header when it is a string.

Style API

Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.

Please refer to the Style API documentation for more insights.

Table parts

Showing 0 to 0 out of 0

const pagination = { pageSize: 10 };
const [data, setData] = React.useState([]);
const basicColumnDef = [
	{
		header: 'FirstName',
		accessorKey: 'name.first',
	},
	{
		header: 'LastName',
		accessorKey: 'name.last',
	},
	{
		header: 'Age',
		accessorKey: 'dob.age',
	},
	{
		header: 'Phone',
		accessorKey: 'phone',
	},
	{
		header: 'Country',
		accessorKey: 'location.country',
	},
];
React.useEffect(() => {
	fetch('https://randomuser.me/api/?results=50')
		.then((response) => response.json())
		.then((data) => {
			setData(data.results);
		})
		.catch((error) => console.log(error));
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
return (
	<Table
		classNames={{
			baseContainer: 'border-4 border-input-critical-pressed',
			tableContainer: 'max-h-96 overflow-auto',
			table: 'bg-caution-secondary',
			// Styles the table header
			headerRow: 'sticky top-0 bg-gray-400 ',
			headerCell: 'bg-palette-violet-background',
			headerContentWrapper: 'p-1 pl-2 bg-palette-blue-background-active',
			headerContent: 'font-stronger text-constant-white',
			sortIconsWrapper: 'bg-accent-secondary border-2',
			sortIcon: 'fill-positive',
			// Styles the table body row
			row: 'border-input-active',
			cell: 'font-stronger text-caution-secondary',
			// Styles the footer pagination wrapper
			footerContainer: 'bg-positive-secondary',
		}}
		columns={columns}
		data={data}
		pagination={pagination}
	/>
);
Stylable PartsDescription
baseContainerWrapper for the table component which holds table and pagination.
tableContainerWrapper that holds the entire table, ensuring proper layout, scrolling, and responsiveness
tableComponent for displaying data in rows and columns with a consistent design and interactions
headerSection that contains the header row with all header cells, defining column titles and interactions.
headerRowContains all header cells, defining column titles, sorting, actions etc.,
headerCellContainer that holds header content and aligns with the table’s structure.
headerContentWrapperContainer that holds the column header content, ensuring proper alignment, spacing, and interaction
headerContentMain content inside a column header, typically including text, icons, or actions
sortIconsWrapperContainer that holds sorting icons.
sortIconVisually indicate and enable sorting of column data.
bodyThe main section that holds the data rows. It follows structured spacing, typography.
rowRow represents a single record or data entry, structured across column. It typically includes text, icons etc.,
cellCell is a single unit of content within a row and column.
editableCellWrapper around the input rendered inside an editable cell (when editMode is on).
editableInputThe text input rendered inside an editable cell.
footerContainerAn optional row at the bottom that provides summary information, pagination controls, actions etc.,

Pagination parts

Showing 0 to 0 out of 0
const adornmentStart = <Button variant="accentPrimary">Download data</Button>
const pagination = { pageSize: 5, adornmentStart };
const [data, setData] = React.useState([]);
const basicColumnDef = [
	{
		header: 'FirstName',
		accessorKey: 'name.first',
	},
	{
		header: 'LastName',
		accessorKey: 'name.last',
	},
	{
		header: 'Age',
		accessorKey: 'dob.age',
	},
	{
		header: 'Phone',
		accessorKey: 'phone',
	},
	{
		header: 'Country',
		accessorKey: 'location.country',
	},
];
React.useEffect(() => {
	fetch('https://randomuser.me/api/?results=20')
		.then((response) => response.json())
		.then((data) => {
			setData(data.results);
		})
		.catch((error) => console.log(error));
}, []);
const columns = React.useMemo(() => basicColumnDef, []);
return (
	<Table
		classNames={{
			pagination: 'border-2 border-input-critical bg-gray-400',
			paginationAdornmentStart: 'bg-caution-secondary',
			paginationTrack: 'bg-amber-100',
			paginationText: 'bg-palette-violet-background',
			paginationPreviousPageButton: 'p-1 pl-2 bg-palette-blue-background-active',
			paginationNextPageButton: 'bg-positive-secondary',
		}}
		columns={columns}
		data={data}
		pagination={pagination}
	/>
);
Stylable PartsDescription
paginationContainer of the whole pagination component.
paginationTrackWrapper that holds the pagination text and the next and previous page buttons
paginationAdornmentStartStart slot of the pagination component
paginationTextText which shows the information on the pagination like the current page and total number of pages.
paginationPreviousPageButtonButton which navigates to the previous page
paginationNextPageButtonButton which navigates to the next page

Editable cell parts

The editable cell exposes its own stylable parts, available only when editMode is on. Use editableCellWrapper to style the editable <td>, editableCell for the wrapper around the input, editableInput for the input itself, editableInputFocusContainer and editableInputFocusIndicator for the input's background and border (including hover/focus).

The first row below starts with an invalid First Name (a single character), so its cell renders in the error state on load — letting you see the error styling without interacting.

Id
First Name
Age
1
2
3
const [data, setData] = React.useState([
	{ id: 1, first_name: 'V', last_name: 'Rustman', age: 45 },
	{ id: 2, first_name: 'Kordula', last_name: 'Gecks', age: 32 },
	{ id: 3, first_name: 'Vikki', last_name: 'Simoens', age: 28 },
]);

const basicColumnDef = [
	{ header: 'Id', accessorKey: 'id', meta: { editable: false } },
	{
		header: 'First Name',
		accessorKey: 'first_name',
		meta: {
			validate: (value) =>
				String(value).trim().length < 2 ? 'Min 2 characters' : undefined,
		},
	},
	{ header: 'Age', accessorKey: 'age', meta: { inputType: 'number' } },
];

const columns = React.useMemo(() => basicColumnDef, []);

return (
	<Table
		classNames={{
			editableCellWrapper: 'bg-palette-blue-background',
			editableInput: 'font-stronger text-accent',
			editableInputFocusIndicator: 'group-hover:border-accent',
		}}
		columns={columns}
		data={data}
		editMode
		enableSorting={false}
		getRowId={(row) => String(row.id)}
		role="grid"
		onCellValueUpdate={(rowId, columnId, value) => {
			setData((prev) =>
				prev.map((row) =>
					String(row.id) === rowId ? { ...row, [columnId]: value } : row
				)
			);
		}}
	/>
);
Stylable PartsDescription
editableCellWrapperThe <td> element for an editable cell (full row height, no padding so the input fills the cell).
editableCellWrapper around the input rendered inside an editable cell.
editableInputThe text input rendered inside an editable cell.
editableInputFocusContainerThe input's focus container — controls background and corner radius.
editableInputFocusIndicatorThe input's border indicator — controls the border shown on hover and focus.

Usage guidelines

Do

  1. Presenting tabular information: Present tabular information with a clear, repeating structure.
  2. Scanning structured data: Help the user scan structured data to make informed decisions.
  3. Paginating resources: If dealing with larger datasets, consider implementing pagination to display all of the user’s resources effectively.

Don’t

  1. Simple tables: Do not use table to present simple information that isn't naturally tabular.
  2. Emphasizing Fewer Sections: Do not use table when you have only a few sections and you want to emphasize each section more.

Best practices

Do

Introduce a simple hierarchy that enables users to find specific data faster.

Don’t

Avoid introducing a complex, difficult-to-scan hierarchy.

Do

Keep table headings short and precise.

Don’t

Avoid long table headings that include unnecessary details.

Do

Ensure there’s a clear link between the column heading and associated data.

Don’t

Avoid displaying data that’s unrelated to the column heading.

Do

Where there are gaps in data, display ‘N/A’ (Not applicable) as the value.

Don’t

Avoid using an en dash (–) or blank cell to represent gaps in data.

Do

The height of components inside cells must match the slot height of 24 px.

Don’t

Avoid using component variants that exceed the slot height of 24 px. These variants increase individual row height in tables.

Do

Display an associated CTA above the upper-right corner of the table. Use the less prominent button variant (secondary).

Don’t

Avoid using the prominent button variant (primary) and displaying a CTA in other areas of the screen.

Do

An empty state message should include a concise description of why data isn’t available. Provide a solution if possible.

Don’t

Avoid less descriptive messages that don’t offer the user unique information or a solution.

Do

Base column width on the average length of associated data.

Don’t

Avoid setting a column width that creates inconsistent spacing between data in one column and the next.

Do

If the data in a cell exceeds the column width, truncate the data and use a tooltip to show it in full.

Don’t

Avoid wrapping data to multiple lines and increasing individual row height.