DataTable

Figma
Github
Storybook

DataTable enables users to act upon complex datasets with a variety of features including filtering and sorting.

Importing

The component can be imported via:

import { DataTable } from '@customerio/pluma-components/react';

When composing with individual components, the following are also available:

  • DataTableHeader - the header section above the table, where controls like column visibility are placed
  • DataTableHeaderLeftSection - the header section on the left side
  • DataTableHeaderRightSection - the header section on the right side
  • DataTableHeaderColumnsMenu - the dropdown menu for column order and visibility
  • DataTableHeaderSortMenu - the dropdown menu for choosing a sort order
  • DataTableTable - the actual table
  • DataTablePagination - the pagination controls, which by default are placed below the table. It will only render its contents when pagination is enabled.

Usage

The DataTable component is based on the TanStack Table library.

To use the component, at a minimum you'll need to pass in the following props:

  • caption - an accessible caption for the table
  • data - an array of objects representing the rows of the table
  • columns - column configuration, an array of TanStack Table's Column Def objects

A simple example might look like this:

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
	/>
}

Cell formatting

Cells can receive custom content, by defining a cell property in the column definition.

Birds of Australia
Name
Population count
Australian Magpie5,000
Galah7,000
Rainbow lorikeet41,234
Golden-shouldered parrot123
Tasmanian Emu0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
		cell: (info) => formatNumber(info.getValue()),
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
	/>
}

In React, the cell property can be a function that returns React components:

Birds of Australia
Name
Conservation status
Australian MagpieLeast concern
GalahLeast concern
Rainbow lorikeetLeast concern
Golden-shouldered parrotEndangered
Tasmanian EmuExtinct
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('conservationStatus', {
		header: 'Conservation status',
		cell: (info) => {
			const status = info.getValue();
			const label = conservationStatusLabels[status];
			const color = conservationStatusColors[status];

			return <Label color={color}>{label}</Label>;
		},
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
	/>
}

Column sizing

By default, DataTable will divide the available space equally between all columns.

It's possible to set a column's width by extending the column definition with a meta object, containing one, or multiple, of the following properties:

  • width - the desired width of the column. This can be:
    • a number, which will be interpreted as a pixel value (e.g., 123)
    • a string ending in px, which will be interpreted as a pixel value (e.g., '10px')
    • a string ending in %, which will be interpreted as a percentage of the table's total width (e.g., '40%')
    • a string ending in fr, which will behave like the fr grid layout unit - a fraction of the available space (e.g., '2fr')
  • minWidth - a minimum width of the column. This can be a pixel number, a pixel string (e.g., '10px'), or a percentage string (e.g., '20%'). fr values aren't supported.
  • maxWidth - a maximum width of the column. This can be a pixel number, a pixel string (e.g., '10px'), or a percentage string (e.g., '80%'). fr values aren't supported.

A column without a defined width acts as if it had a width of 1fr.

Column sizing example
Column A (40%, min 200px)
Column B (100 number)
Column C (100px string)
Column D (2fr)
Column E (1fr default)
Lorem ipsumdolorsitamet
consecteturadipiscingelitseddo
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const data = [
	{ a: 'Lorem ', b: 'ipsum', c: 'dolor', d: 'sit', e: 'amet' },
	{ a: 'consectetur', b: 'adipiscing', c: 'elit', d: 'sed', e: 'do' },
];

const columns = [
	columnHelper.accessor('a', {
		header: 'Column A (40%, min 200px)',
		meta: {
			width: '40%',
			minWidth: 200
		}
	}),
	columnHelper.accessor('b', {
		header: 'Column B (100 number)',
		meta: {
			width: 100
		}
	}),
	columnHelper.accessor('c', {
		header: 'Column C (100px string)',
		meta: {
			width: '100px'
		}
	}),
	columnHelper.accessor('d', {
		header: 'Column D (2fr)',
		meta: {
			width: '2fr'
		}
	}),
	columnHelper.accessor('e', {
		header: 'Column E (1fr default)'
	}),
];

export default function Example() {
	return <DataTable
		caption="Column sizing example"
		data={data}
		columns={columns}
	/>
}

Column alignment

You can control the text alignment of columns by setting the align property in the column definition's meta object.

The property accepts one of the following values:

  • start - left-aligns text (default)
  • center - centers text horizontally
  • end - right-aligns text (good for numeric values)
Column Alignment Example
Left-aligned (default)
Center-aligned
Right-aligned
Numbers (right)
Left-alignedCenter-alignedRight-aligned12,345
Default is leftThis is centeredRight-aligned content67,890
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const data = [
  {
    id: 1,
    start: 'Left-aligned',
    center: 'Center-aligned',
    end: 'Right-aligned',
    number: 12345,
  },
  {
    id: 2,
    start: 'Default is left',
    center: 'This is centered',
    end: 'Right-aligned content',
    number: 67890,
  },
];

const columns = [
  { accessorKey: 'start', header: 'Left-aligned (default)' },
  { 
    accessorKey: 'center', 
    header: 'Center-aligned',
    meta: { align: 'center' }
  },
  { 
    accessorKey: 'end', 
    header: 'Right-aligned', 
    meta: { align: 'end' }
  },
  { 
    accessorKey: 'number', 
    header: 'Numbers (right)',
    cell: (info) => formatNumber(info.getValue()),
    meta: { align: 'end' }
  },
];

export default function Example() {
  return <DataTable
    caption="Column Alignment Example"
    data={data}
    columns={columns}
  />
}

Cell colspan

Cells can be configured to span multiple columns by defining a getCellColSpan function in the column definition's meta object. The function receives the cell instance and should return the number of columns to span, or undefined for the default behavior (spanning 1 column).

When a cell spans N columns, the next N - 1 cells in that row are hidden. If a colspan exceeds the number of remaining columns in the row, it is limited to the available space.

Users
Name
Email
Role
Alicealice@example.comAdmin
Bob
Charliecharlie@example.comViewer
import { DataTable, Banner } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const data = [
	{ 
		name: 'Alice', 
		email: 'alice@example.com', 
		role: 'Admin',
		 },
	{ 
		name: 'Bob',
		email: 'bob@example.com',
		role: 'Editor',
		isInvalid: true,
	},
	{ 
		name: 'Charlie',
		email: 'charlie@example.com',
		role: 'Viewer',
	},
];

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('email', {
		header: 'Email',
		meta: {
			getCellColSpan: (cell) => {
				if (cell.row.original.isInvalid === true) {
					return 2;
				}
			},
		},
		cell: (info) => {
			if (info.row.original.isInvalid) {
				return <Banner variant="error" type="inline" description="This user's account has been suspended." />;
			}

			return info.getValue();
		},
	}),
	columnHelper.accessor('role', {
		header: 'Role',
	}),
];

export default function Example() {
	return <DataTable
		caption="Users"
		data={data}
		columns={columns}
	/>
}

Column order and visibility

Enabling the withColumnsSettings prop will show a dropdown menu, allowing users to show/hide columns and change their order. Selected (visible) columns are grouped at the top of the dropdown, with unselected columns below.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
	/>
}

It is also possible to set the initial order and visibility of columns through the following props:

  • defaultColumnOrder - an array of column IDs, in the order they should be displayed
  • defaultColumnVisibility - an object mapping column IDs to booleans, indicating whether the column should be visible or not
Birds of Australia
Species
Name
tibicenAustralian Magpie
roseicapillaGalah
moluccanusRainbow lorikeet
chrysopterygiusGolden-shouldered parrot
novaehollandiaeTasmanian Emu
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const defaultColumnOrder = ['species', 'name', 'genus'];
const defaultColumnVisibility = {
	genus: false,
	species: true,
};

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		defaultColumnOrder={defaultColumnOrder}
		defaultColumnVisibility={defaultColumnVisibility}
	/>
}

Finally, you can also fully control the column order and visibility states with the following props:

  • columnOrder - an array of column IDs, in the order they should be displayed
  • onColumnOrderChange - a function that will be called when the column order changes
  • columnVisibility - an object mapping column IDs to booleans, indicating whether the column should be visible or not
  • onColumnVisibilityChange - a function that will be called when the column visibility changes
Birds of Australia
Species
Name
tibicenAustralian Magpie
roseicapillaGalah
moluccanusRainbow lorikeet
chrysopterygiusGolden-shouldered parrot
novaehollandiaeTasmanian Emu
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [order, setOrder] = useState(['species', 'name', 'genus']);
	const [visibility, setVisibility] = useState({
		genus: false,
		species: true,
	});

	const handleOrderChange = (newOrder) => {
		setOrder(newOrder);
	};
	const handleVisibilityChange = (newVisibility) => {
		setVisibility(newVisibility);
	};

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		columnOrder={order}
		onColumnOrderChange={handleOrderChange}
		columnVisibility={visibility}
		onColumnVisibilityChange={handleVisibilityChange}
	/>
}

Searching columns

When there are a lot of columns, it can be useful to allow users to search inside the dropdown for a column by name. A search input can be enabled by the columnsSettings prop to an object with a withSearch: true property.

The items within the search dropdown render as a virtualized list, to make sure performance doesn't suffer when there are a lot of columns.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const extraColumns = new Array(60).fill(null).map((_, i) =>
	columnHelper.display({
		id: `extra_column_${i + 1}`,
		header: `${i + 1} Lorem`,
		cell: `lorem ${i + 1}`,
	}),
);

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
	...extraColumns
];

const defaultColumnVisibility = extraColumns.reduce((acc, c) => {
	acc[c.id] = false;
	return acc;
}, {});

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		columnsSettings={{
			withSearch: true,
		}}
		defaultColumnVisibility={defaultColumnVisibility}
	/>
}

Loading more columns

When there are too many columns to load at once (e.g., a workspace with 300+ attributes), the columns dropdown can load them incrementally as the user scrolls. This is controlled via the columnsSettings prop:

  • onLoadMore - a callback triggered when the user scrolls near the bottom of the dropdown. Also fires automatically if the initial content is short enough that no scrolling is needed.
  • shouldLoadMore - whether there are more columns available to load. Set to false when all columns have been loaded.
  • isLoadingMore - whether more columns are currently being loaded (shows a spinner at the bottom of the list)
Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState, useCallback } from 'react';

const columnHelper = createColumnHelper();

const extraColumns = new Array(200).fill(null).map((_, i) =>
	columnHelper.display({
		id: `extra_column_${i + 1}`,
		header: `${i + 1} Lorem`,
		cell: `lorem ${i + 1}`,
	}),
);

const allColumns = [
	columnHelper.accessor('name', { header: 'Name' }),
	columnHelper.accessor('genus', { header: 'Genus' }),
	columnHelper.accessor('species', { header: 'Species' }),
	...extraColumns,
];

const defaultColumnVisibility = extraColumns.reduce((acc, c) => {
	acc[c.id] = false;
	return acc;
}, {});

export default function Example() {
	const PAGE_SIZE = 50;
	const [columns, setColumns] = useState(allColumns.slice(0, 3 + PAGE_SIZE));
	const [isLoadingMore, setIsLoadingMore] = useState(false);

	const onLoadMore = useCallback(() => {
		setIsLoadingMore(true);
		setTimeout(() => {
			setColumns((prev) => allColumns.slice(0, Math.min(prev.length + PAGE_SIZE, allColumns.length)));
			setIsLoadingMore(false);
		}, 500);
	}, []);

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		defaultColumnVisibility={defaultColumnVisibility}
		withColumnsSettings={true}
		columnsSettings={{
			withSearch: true,
			onLoadMore,
			shouldLoadMore: columns.length < allColumns.length,
			isLoadingMore,
		}}
	/>
}

When using onLoadMore with server-side data, client-side search won't find columns that haven't been loaded yet. The onSearch callback delegates search to the consumer, who can query a server API and return matching column IDs.

  • onSearch - called when the user types in the search input. When provided, client-side filtering is disabled and the consumer controls what appears in the dropdown.
  • searchResultColumnIds - an array of column IDs to show in the dropdown. Set to null to show all columns (e.g., when search is cleared). Only the dropdown list is filtered — the table's visible columns are unaffected.
  • isSearching - suppresses the "No columns found" message while a search is in progress

The consumer is responsible for ensuring that columns returned by search exist as column definitions in the table (typically as hidden columns via columnVisibility).

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState, useCallback } from 'react';

const columnHelper = createColumnHelper();

const extraColumns = new Array(200).fill(null).map((_, i) =>
	columnHelper.display({
		id: `extra_column_${i + 1}`,
		header: `${i + 1} Lorem`,
		cell: `lorem ${i + 1}`,
	}),
);

const allColumns = [
	columnHelper.accessor('name', { header: 'Name' }),
	columnHelper.accessor('genus', { header: 'Genus' }),
	columnHelper.accessor('species', { header: 'Species' }),
	...extraColumns,
];

const defaultColumnVisibility = extraColumns.reduce((acc, c) => {
	acc[c.id] = false;
	return acc;
}, {});

export default function Example() {
	const [searchResultIds, setSearchResultIds] = useState(null);
	const [isSearching, setIsSearching] = useState(false);

	const onSearch = useCallback((searchValue) => {
		if (searchValue === '') {
			setSearchResultIds(null);
			setIsSearching(false);
			return;
		}
		setIsSearching(true);
		// Simulate server-side search with a delay
		setTimeout(() => {
			const lower = searchValue.toLowerCase();
			const ids = allColumns
				.filter((col) => {
					const header = typeof col.header === 'string' ? col.header : col.id;
					return header.toLowerCase().includes(lower);
				})
				.map((col) => col.id)
				.filter(Boolean);
			setSearchResultIds(ids);
			setIsSearching(false);
		}, 300);
	}, []);

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={allColumns}
		defaultColumnVisibility={defaultColumnVisibility}
		withColumnsSettings={true}
		columnsSettings={{
			withSearch: true,
			onSearch,
			searchResultColumnIds: searchResultIds,
			isSearching,
		}}
	/>
}

Pagination

DataTable supports two types of pagination:

  • uncontrolled - the component manages pagination internally
  • controlled - the component receives pagination state, and data is expected to be the current page of data (you control the pagination)

Uncontrolled pagination

To enable uncontrolled pagination, set the withPagination prop to true:

Paginated table
id
foo
bar
baz
1foo 1bar 1baz 1
2foo 2bar 2baz 2
3foo 3bar 3baz 3
4foo 4bar 4baz 4
5foo 5bar 5baz 5
6foo 6bar 6baz 6
7foo 7bar 7baz 7
8foo 8bar 8baz 8
9foo 9bar 9baz 9
10foo 10bar 10baz 10
11foo 11bar 11baz 11
12foo 12bar 12baz 12
13foo 13bar 13baz 13
14foo 14bar 14baz 14
15foo 15bar 15baz 15
16foo 16bar 16baz 16
17foo 17bar 17baz 17
18foo 18bar 18baz 18
19foo 19bar 19baz 19
20foo 20bar 20baz 20
120 of 237
import { DataTable } from '@customerio/pluma-components/react';

const columns = [
	{ accessorKey: 'id', meta: { width: 100 } },
	{ accessorKey: 'foo' },
	{ accessorKey: 'bar' },
	{ accessorKey: 'baz' },
];

const data = [...new Array(237)].map((_, i) => ({
	id: i + 1,
	foo: 'foo ' + (i + 1),
	bar: 'bar ' + (i + 1),
	baz: 'baz ' + (i + 1),
}));

export default function Example() {
	return <DataTable
		caption="Paginated table"
		data={data}
		columns={columns}
		withPagination={true}
	/>
}

Additionally, the pagination prop accepts an object with the same properties as the PlumaPagination component.

This allows you to configure the page size, available page sizes, etc.:

Paginated table
id
foo
bar
baz
1foo 1bar 1baz 1
2foo 2bar 2baz 2
3foo 3bar 3baz 3
4foo 4bar 4baz 4
5foo 5bar 5baz 5
6foo 6bar 6baz 6
7foo 7bar 7baz 7
17 of 237
import { DataTable } from '@customerio/pluma-components/react';

const columns = [
	{ accessorKey: 'id', meta: { width: 100 } },
	{ accessorKey: 'foo' },
	{ accessorKey: 'bar' },
	{ accessorKey: 'baz' },
];

const data = [...new Array(237)].map((_, i) => ({
	id: i + 1,
	foo: 'foo ' + (i + 1),
	bar: 'bar ' + (i + 1),
	baz: 'baz ' + (i + 1),
}));

const paginationConfig = {
	pageSizes: [3, 7, 20, 60],
	pageSize: 7,
};

export default function Example() {
	return <DataTable
		caption="Paginated table"
		data={data}
		columns={columns}
		withPagination={true}
		pagination={paginationConfig}
	/>
}

Manual pagination

To enable controlled pagination, set the pagination prop to an object with at least one of the following properties:

  • page - the current page number (1-indexed)
  • onPageChange - a callback function that will be called when the page changes
  • hrefBuilder - a function for building URLs for the pagination buttons, when pagination is navigation-based

Additionally, you should pass in the same properties as you would for PlumaPagination, like totalItemCount, so that the component can calculate the number of pages available.

Here's an example that simulates a controlled pagination. In a real app, pagination would usually be performed against an API.

Paginated table
id
foo
bar
baz
1foo 1bar 1baz 1
2foo 2bar 2baz 2
3foo 3bar 3baz 3
4foo 4bar 4baz 4
5foo 5bar 5baz 5
6foo 6bar 6baz 6
7foo 7bar 7baz 7
8foo 8bar 8baz 8
9foo 9bar 9baz 9
10foo 10bar 10baz 10
110 of 237
import { DataTable } from '@customerio/pluma-components/react';

const columns = [
	{ accessorKey: 'id', meta: { width: 100 } },
	{ accessorKey: 'foo' },
	{ accessorKey: 'bar' },
	{ accessorKey: 'baz' },
];

const data = [...new Array(237)].map((_, i) => ({
	id: i + 1,
	foo: 'foo ' + (i + 1),
	bar: 'bar ' + (i + 1),
	baz: 'baz ' + (i + 1),
}));

const paginationConfig = {
	pageSizes: [3, 7, 20, 60],
	pageSize: 7,
};

export default function Example() {
	const [page, setPage] = useState(1);
	const [pageSize, setPageSize] = useState(10);

	const paginateData = (data, page, pageSize) => {
		const pageIndex = page - 1;
		let pageOfData;

		if (!pageIndex && !pageSize) {
			pageOfData = data;
		} else {
			const startIndex = pageIndex * pageSize;
			pageOfData = data.slice(startIndex, Math.min(startIndex + pageSize, data.length));
		}

		return {
			pageOfData,
			totalItemCount: data.length,
		};
	};

	const { pageOfData, totalItemCount } = paginateData(data, page, pageSize);

	return <DataTable
		caption="Paginated table"
		data={pageOfData}
		columns={columns}
		withPagination={true}
		pagination={{
			page,
			onPageChange: setPage,
			pageSize,
			onPageSizeChange: setPageSize,
			totalItemCount,
		}}
	/>
}

Controlled, not manual, pagination

In some cases, you may want to control the state of the pagination props (the current page etc.), but still have the component perform the actual pagination. For example, you might want to reflect the pagination state in URL query params.

To enable this, set the pagination object as you would in the manual pagination example, but additionally pass an isManual: false property. This will tell the table to paginate based on the external props.

Current page: 1

Current page size: 10

Paginated table
id
foo
bar
baz
1foo 1bar 1baz 1
2foo 2bar 2baz 2
3foo 3bar 3baz 3
4foo 4bar 4baz 4
5foo 5bar 5baz 5
6foo 6bar 6baz 6
7foo 7bar 7baz 7
8foo 8bar 8baz 8
9foo 9bar 9baz 9
10foo 10bar 10baz 10
110 of 237
import { DataTable, Paragraph } from '@customerio/pluma-components/react';

const columns = [
	{ accessorKey: 'id', meta: { width: 100 } },
	{ accessorKey: 'foo' },
	{ accessorKey: 'bar' },
	{ accessorKey: 'baz' },
];

const data = [...new Array(237)].map((_, i) => ({
	id: i + 1,
	foo: 'foo ' + (i + 1),
	bar: 'bar ' + (i + 1),
	baz: 'baz ' + (i + 1),
}));

const paginationConfig = {
	pageSizes: [3, 7, 20, 60],
	pageSize: 7,
};

export default function Example() {
	const [page, setPage] = useState(1);
	const [pageSize, setPageSize] = useState(10);

	return <>
		<Paragraph>Current page: {page}</Paragraph>
		<Paragraph>Current page size: {pageSize}</Paragraph>

		<DataTable
			caption="Paginated table"
			data={data}
			columns={columns}
			withPagination={true}
			pagination={{
				page,
				onPageChange: setPage,
				pageSize,
				onPageSizeChange: setPageSize,
				isManual: false,
			}}
		/>
	</>;
}

Sorting

DataTable supports two types of sorting:

  • uncontrolled - the component manages sorting internally
  • controlled - the component receives sorting state, and data is expected to already be sorted

Uncontrolled sorting

To enable uncontrolled sorting, set the withSorting prop to true. Initially, no sorting will be applied, but a column header can be clicked to sort by that column.

Sorted table
Australian MagpieGymnorhina5000
GalahEolophus7000
Rainbow lorikeetTrichoglossus41234
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
	}),
];

export default function Example() {
	return <DataTable
		caption="Sorted table"
		data={defaultData}
		columns={columns}
		withSorting={true}
	/>
}

An initial sorting column and direction can be set through the sorting prop. It accepts a configuration object, where value is the current sorting state, consisting of:

  • id - the id of the column to sort by
  • desc - a boolean indicating whether the sorting should be descending or not
Sorted table
Rainbow lorikeetTrichoglossus41234
GalahEolophus7000
Australian MagpieGymnorhina5000
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
	}),
];

const initialSorting = {
	id: 'population',
	desc: true,
};

export default function Example() {
	return <DataTable
		caption="Sorted table"
		data={defaultData}
		columns={columns}
		withSorting={true}
		sorting={{
			value: initialSorting
		}}
	/>;
}

Manual sorting

To enable controlled sorting, set the sorting prop to an object with the properties:

  • value - the current sorting state (with id and desc properties)
  • onChange - a callback function that is called when the sorting state changes

Typically, manual sorting might involve calling an API to fetch sorted data.

Sorted table
Rainbow lorikeetTrichoglossus41234
GalahEolophus7000
Australian MagpieGymnorhina5000
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
	}),
];

const sortData = (data, sortColumnId, isDesc) => {
	return data.slice().sort((a, b) => {
		const aValue = a[sortColumnId];
		const bValue = b[sortColumnId];

		let sortResult = 0;

		if (typeof aValue === 'string' && typeof bValue === 'string') {
			sortResult = aValue.toLowerCase().localeCompare(bValue.toLowerCase());
		}

		if (typeof aValue === 'number' && typeof bValue === 'string') {
			sortResult = 1;
		}

		if (typeof aValue === 'string' && typeof bValue === 'number') {
			sortResult = -1;
		}

		if (typeof aValue === 'number' && typeof bValue === 'number') {
			if (aValue > bValue) {
				sortResult = 1;
			}
			if (aValue < bValue) {
				sortResult = -1;
			}
		}

		if (isDesc) {
			sortResult *= -1;
		}

		return sortResult;
	});
};

export default function Example() {
	const [sorting, setSorting] = useState({
		id: 'population',
		desc: true,
	});

	const sortedData = sortData(defaultData, sorting.id, sorting.desc);

	return <DataTable
		caption="Sorted table"
		data={sortedData}
		columns={columns}
		withSorting={true}
		sorting={{
			value: sorting,
			onChange: setSorting,
		}}
	/>;
}

Controlled, not manual, sorting

In some cases, you may want to control the state of the sorting props (the current sort column and direction), but still have the component perform the actual sorting. For example, you might want to reflect the sorting state in URL query params.

To enable this, set the sorting object as you would in the manual sorting example, but additionally pass an isManual: false property. This will tell the table to sort based on the external props.

Current sorting: population, desc

Sorted table
Rainbow lorikeetTrichoglossus41234
GalahEolophus7000
Australian MagpieGymnorhina5000
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable, Paragraph } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
	}),
];

export default function Example() {
	const [sorting, setSorting] = useState({
		id: 'population',
		desc: true,
	});

	return <>
		<Paragraph>Current sorting: {sorting.id}, {sorting.desc ? 'desc' : 'asc'}</Paragraph>
		<DataTable
			caption="Sorted table"
			data={defaultData}
			columns={columns}
			withSorting={true}
			sorting={{
				value: sorting,
				onChange: setSorting,
				isManual: false,
			}}
		/>
	</>;
}

Additional sort options

DataTable's column definitions accept the same configuration options as TanStack Table's columns.

By default, when sorting is enabled, all columns will be sortable. To disable sorting on specific columns, set enableSorting to false in the column definition:

Sorted table
Genus
Australian MagpieGymnorhina5000
GalahEolophus7000
Rainbow lorikeetTrichoglossus41234
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
		enableSorting: false,
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
	}),
];

export default function Example() {
	return <DataTable
		caption="Sorted table"
		data={defaultData}
		columns={columns}
		withSorting={true}
	/>
}

Sort dropdown button labels

The sort dropdown above the table will display Ascending/Descending or A-Z/Z-A for the sort direction buttons by inspecting the provided data.

If you would like to explicitly define the text shown in the dropdown, you can extend the column definitions with a meta object, containing a sortDirectionLabelType property.

It can be set to either:

  • default - this sets the button labels to Ascending/Descending for this column
  • text - this sets the button labels to A-Z/Z-A for this column
Sorted table
Australian MagpieGymnorhina5000
GalahEolophus7000
Rainbow lorikeetTrichoglossus41234
Golden-shouldered parrotPsephotellus123
Tasmanian EmuDromaius0
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
		meta: {
			sortDirectionLabelType: 'text',
		}
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
		meta: {
			sortDirectionLabelType: 'text',
		}
	}),
	columnHelper.accessor('population', {
		header: 'Population count',
		meta: {
			sortDirectionLabelType: 'default',
		}
	}),
];

export default function Example() {
	return <DataTable
		caption="Sorted table"
		data={defaultData}
		columns={columns}
		withSorting={true}
	/>
}

Nested rows

DataTable supports rendering nested rows: showing/hiding additional rows with data related to the parent row.

To enable this feature, first, set the withRowExpanding prop to true. This adds a column at the start of the table, which will contain an expand/collapse button for each row.

Next, define the child rows. By default, DataTable will see if there is a children array property on the data elements, and use them as data for the child rows. The children elements must have the same structure as the parent data.

If it's necessary to customize how child rows are retrieved, a getSubRows callback can be passed to DataTable, which should return an array of child rows.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const nestedData = [
	{
		id: 'australian_magpie',
		name: 'Australian Magpie',
		genus: 'Gymnorhina',
		species: 'tibicen',
		children: [
			{
				id: 'australian_magpie_nested_1',
				name: 'Australian Magpie (1)',
				genus: 'Gymnorhina',
				species: 'tibicen',
			},
			{
				id: 'australian_magpie_nested_2',
				name: 'Australian Magpie (2)',
				genus: 'Gymnorhina',
				species: 'tibicen',
			},
			{
				id: 'australian_magpie_nested_3',
				name: 'Australian Magpie (3)',
				genus: 'Gymnorhina',
				species: 'tibicen',
			},
		]
	},
	{
		id: 'galah',
		name: 'Galah',
		genus: 'Eolophus',
		species: 'roseicapilla',
		children: [
			{
				id: 'galah_nested_1',
				name: 'Galah (1)',
				genus: 'Eolophus',
				species: 'roseicapilla',
			},
		]
	},
	{
		id: 'rainbow_lorikeet',
		name: 'Rainbow lorikeet',
		genus: 'Trichoglossus',
		species: 'moluccanus',
	},
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={nestedData}
		columns={columns}
		withRowExpanding={true}
	/>
}

Expanding rows

DataTable also supports expanding rows with custom content, which can be used to show additional information about a row.

To enable this feature, first, set the withRowExpanding prop to true. This adds a column at the start of the table, which will contain an expand/collapse button for each row.

Next, define what content should be shown when a row is expanded, by passing an expandedRowComponent prop to the DataTable. This prop is a component, which will be rendered by DataTable when a row is expanded. The component will receive a single row prop, which is a TanStack Table row object.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withRowExpanding={true}
		expandedRowComponent={(props) => {
			return <Paragraph>{birdDetails[props.row.original.id]}</Paragraph>;
		}}
	/>
}

Row selection

DataTable supports row selection, allowing users to select one or more rows in the table via checkboxes.

To enable this feature, set the withRowSelection prop to true. This adds a column at the start of the table, which will contain a checkbox for each row.

The component will internally keep manage the state of the currently selected rows. The rowSelection prop accepts an object of row IDs and booleans, allowing you to control the state instead. Additionally, onRowSelectionChange will be called whenever the selection state changes, allowing you to update the state accordingly.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [rowSelection, setRowSelection] = useState({});

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withRowSelection={true}
		rowSelection={rowSelection}
		onRowSelectionChange={setRowSelection}
	/>
}

Conditional row selection

By default, when withRowSelection is enabled, all rows will be selectable. Selection can be disabled for certain rows, and there are two elements to control this:

First, withRowSelection can be a function, which will be called for each row with the TanStack Table row object as an argument. The function should return a boolean. If it returns false, the row won't render a checkbox at all.

Second, a getRowCanSelect callback can also be passed to DataTable. It receives the same row argument as withRowSelection, and should return a boolean. When this is false, but withRowSelection is true, the row will render a checkbox, but in a disabled state.

Essentially, withRowSelection controls whether the row will render a checkbox at all, while getRowCanSelect controls whether the checkbox will be enabled or disabled.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [rowSelection, setRowSelection] = useState({});

	const withRowSelection = (row) => {
		// Disable selection for extinct birds
		return row.original.conservationStatus !== 'ex';
	};

	const getRowCanSelect = (row) => {
		// Disable selection for a specific bird
		return row.original.id !== 'galah';
	};

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withRowSelection={withRowSelection}
		getRowCanSelect={getRowCanSelect}
		rowSelection={rowSelection}
		onRowSelectionChange={setRowSelection}
	/>
}

Single row selection

By default, row selection allows selecting multiple rows at once. To restrict selection to a single row at a time, set enableMultiRowSelection to false. This follows the TanStack Table configuration.

When single row selection is enabled:

  • Selecting a new row will automatically deselect any previously selected row
  • The header checkbox is hidden when no rows are selected (to prevent "select all")
  • The header checkbox appears when a row is selected, allowing quick deselection

enableMultiRowSelection can also be a function, which will be called for each row to disable multi-row selection conditionally for a row's sub-rows.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [rowSelection, setRowSelection] = useState({});

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withRowSelection={true}
		enableMultiRowSelection={false}
		rowSelection={rowSelection}
		onRowSelectionChange={setRowSelection}
	/>
}

Bulk actions

When selecting rows, the typical use case is to apply an action to the selected items. DataTable supports this through the bulkActions prop. It accepts a configuration object with the following properties:

  • dropdownActions: an array of bulk actions to be rendered in a dropdown menu. The bulk action definitions are objects of the DataTableDropdownBulkAction shape
  • dropdownTrigger: an object with configuration options for the dropdown trigger. Usually you won't need to use this, but it's available to customize the trigger's label or appearance. The shape is DataTableBulkActionDropdownTriggerConfig
  • promotedActions: an array of actions that will render as buttons in the selection header (instead of inside the dropdown). Think of this as the "primary" bulk actions. The shape of these objects is a little different from the dropdown actions, and is defined via DataTablePromotedBulkAction. Note: it is recommended to keep the number of "promoted" actions low, to avoid showing too many primary choices

The onClick handlers for bulk actions are called with the following arguments:

  • selection: the current selection state
  • table: the instance of Tanstack Table
Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const bulkActions = {
	promotedActions: [
		{
			label: 'Main action',
			onClick: (selection, table) => {
				console.log('Main action clicked', { selection, table });
			}
		},
	],
	dropdownActions: [
		{
			label: 'Copy',
			onClick: (selection, table) => {
				console.log('Copy clicked', selection, table);
			}
		},
		{
			label: 'Delete',
			isDanger: true,
			onClick: (selection, table) => {
				console.log('Delete clicked', { selection, table });
			}
		}
	]
};

export default function Example() {
	const [rowSelection, setRowSelection] = useState({});

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withRowSelection={true}
		rowSelection={rowSelection}
		onRowSelectionChange={setRowSelection}
		bulkActions={bulkActions}
	/>
}

Row actions

DataTable supports row actions: actions that can be performed on an individual row, by rendering a dropdown menu with options in each row.

This is controlled via the rowActions prop. It accepts a configuration object with the following properties:

  • dropdownActions - an array of actions or groups of actions to be rendered in the dropdown menu. They can either be:
    • a group definition, with the keys:
      • label - the label of the group. This is optional - if not provided, a label-less group will be rendered (which can be useful for rendering sections of actions separated by a line)
      • actions - an array of action items to be rendered within this group
    • an action item, defined by the DataTableRowActionItem shape
  • dropdownMenu - optionally, additional configuration for the DropdownMenu component
  • dropdownTrigger - optionally, additional configuration for the dropdown menu's button trigger

Alternatively, rowActions can also be a function, which will be called with the Row instance as an argument, and is expected to return the configuration object as described above. This allows creating dynamic row actions, or disabling actions for certain rows.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const rowActions = {
	dropdownActions: [
		{
			label: 'Copy',
			onClick: (row) => {
				console.log('Copy clicked', row);
			}
		},
		{
			label: 'Delete',
			isDanger: true,
			onClick: (row) => {
				console.log('Delete clicked', row);
			}
		}
	]
};

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		rowActions={rowActions}
	/>
}

Searching

DataTable supports searching the table contents with a search input. This can be enabled by enabling the withSearch prop.

Two types of search are supported:

  • uncontrolled - the component manages search internally
  • controlled - the component receives search state, and data is expected to already be filtered accordingly

Additional search configuration can be passed in via the search prop. For example, globalFilterFn can be used to configure how TanStack Table should filter the contents.

The search prop also accepts most of the PlumaSearch component's options. This allows you to configure when the search should be triggered (on each key input, on enter, etc.), what the aria-label, placeholder, should be, and more.

Uncontrolled search will use TanStack Table's global filtering feature. By default, all columns are searched, but this can be disabled on individual columns by setting their enableGlobalFilter key to false.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withSearch={true}
		search={{
			shouldSearchOnInput: true,
			shouldSearchOnClear: true,
		}}
	/>
}

Controlled search is when the search state is managed by the invoker of the component. To enable it, the search configuration prop should include the following keys:

  • value - the current value of the search input
  • onSearchInput - a callback when the value of the search input changes
  • onSearch - called when the search should be triggered (depending on settings such as shouldSearchOnEnter etc.)
Searchable table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const searchData = (data, value) => {
	const lowerCaseValue = value.toLowerCase();

	return data.slice().filter((row) => {
		const rowValues = [row.name, row.genus, row.species];
		return rowValues.some((rowValue) => rowValue.toLowerCase().includes(lowerCaseValue));
	});
};

export default function Example() {
	const [searchValue, setSearchValue] = useState('');
	const [filteredData, setFilteredData] = useState(defaultData);

	return <DataTable
		caption="Searchable table"
		data={filteredData}
		columns={columns}
		withSearch={true}
		search={{
			value: searchValue,
			onSearchInput: setSearchValue,
			onSearch: (value) => {
				setSearchValue(value);
				setFilteredData(searchData(defaultData, value));
			},
			shouldSearchOnEnter: true,
			shouldSearchOnClear: true,
		}}
	/>;
}

In some cases, you may want to control the state of the search props (the current search value), but still have the component perform the actual search. For example, you might want to reflect the search state in URL query params.

To enable this, set the search object as you would in the manual search example, but additionally pass an isManual: false property. This will tell the table to search based on the external props.

Note: the value passed into the component is the value that'll instantly apply for filtering, regardless of the shouldSearch setting. Internally, the component maintains separate state for the value of the search input, versus the value that's currently applied for filtering. In this mode, make sure to only update the value state in the onSearch callback. If you wish to also know what the current search input value is, you can use the onSearchInput callback.

Current search value:

Current search input value:

Searchable table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable, Paragraph } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [searchInputValue, setSearchInputValue] = useState('');
	const [searchValue, setSearchValue] = useState('');

	return <>
		<Paragraph>Current search value: {searchValue}</Paragraph>
		<Paragraph>Current search input value: {searchInputValue}</Paragraph>
		<DataTable
			caption="Searchable table"
			data={defaultData}
			columns={columns}
			withSearch={true}
			search={{
				value: searchValue,
				onSearchInput: setSearchInputValue,
				onSearch: setSearchValue,
				shouldSearchOnEnter: true,
				shouldSearchOnClear: true,
				isManual: false,
			}}
		/>
	</>;
}

Decoupled input value

By default, when search is controlled, the value prop drives both the search input's displayed text and the filter applied to the table. This means the table rerenders on every keystroke, which can be slow for large datasets.

The inputValue prop allows decoupling the input's display value from the filter value. When provided:

  • inputValue controls what the search input displays
  • value controls the filter applied to the table

This way, the table only rerenders when value changes (e.g. on submit), while the input remains responsive as the user types.

Current search value:

Current input value:

Searchable table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable, Paragraph } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const [inputValue, setInputValue] = useState('');
	const [searchValue, setSearchValue] = useState('');

	return <>
		<Paragraph>Current search value: {searchValue}</Paragraph>
		<Paragraph>Current input value: {inputValue}</Paragraph>
		<DataTable
			caption="Searchable table"
			data={defaultData}
			columns={columns}
			withSearch={true}
			search={{
				value: searchValue,
				inputValue: inputValue,
				onSearchInput: setInputValue,
				onSearch: (value) => {
					setInputValue(value);
					setSearchValue(value);
				},
				shouldSearchOnEnter: true,
				shouldSearchOnClear: true,
				isManual: false,
			}}
		/>
	</>;
}

Filtering

In addition to text-input-based search, DataTable also supports filtering the table contents with the Filters component. Due to the flexibility the Filters component provides, this filtering is always "controlled": it is up to you to implement the filtering logic (typically through an API), and pass in the filtered data.

To enable filtering, set withFilters to true. Additionally, the filters prop expects the same props as the Filters component itself.

This will render a Filters button in the header section of the table, with the filter selection dropdown. When filters are added, a section is added below the header, with the entire Filters component embedded.

Filtered table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const filters = [
	{
		key: 'genus',
		label: 'Genus',
		type: 'checkbox',
		options: [
			{ label: 'Gymnorhina', value: 'gymnorhina' },
			{ label: 'Eolophus', value: 'eolophus' },
			{ label: 'Trichoglossus', value: 'trichoglossus' },
			{ label: 'Psephotellus', value: 'psephotellus' },
			{ label: 'Dromaius', value: 'dromaius' },
		],
	},
	{
		key: 'species',
		label: 'Species',
		type: 'radio',
		options: [
			{ label: 'Tibicen', value: 'tibicen' },
			{ label: 'Roseicapilla', value: 'roseicapilla' },
			{ label: 'Moluccanus', value: 'moluccanus' },
			{ label: 'Chrysopterygius', value: 'chrysopterygius' },
			{ label: 'Novaehollandiae', value: 'novaehollandiae' },
		],
	},
];

const filterData = (data, filterValues) => {
	let filteredData = data.slice();

	filterValues.forEach((filter) => {
		const { key, value } = filter;
		const matchingConfig = filters.find((f) => f.key === key);

		if (matchingConfig.key === 'genus') {
			// "value" in an array of selected option values (checkboxes)
			if (value.length === 0) {
				return;
			}

			filteredData = filteredData.filter((row) => {
				return value.includes(row.genus.toLowerCase());
			});
		}

		if (matchingConfig.key === 'species') {
			// "value" is a single selected option value (radio)
			filteredData = filteredData.filter((row) => {
				return row.species === value;
			});
		}
	});

	return filteredData;
};

export default function Example() {
	const [filterValues, setFilterValues] = useState([]);

	const handleFilterChange = (key, value) => {
		const exists = filterValues.some(v => v.key === key);
		const updated = exists
			? filterValues.map(v => v.key === key ? { ...v, value } : v)
			: [...filterValues, { key, value }];
		setFilterValues(updated);
	};

	const handleFilterClear = (key) => {
		setFilterValues(filterValues.filter(v => v.key !== key));
	};

	const handleFilterClearAll = () => {
		setFilterValues([]);
	};

	const filteredData = filterData(defaultData, filterValues);

	return <DataTable
		caption="Filtered table"
		data={filteredData}
		columns={columns}
		withFilters={true}
		filters={{
			filters,
			values: filterValues,
			onChange: handleFilterChange,
			onClear: handleFilterClear,
			onClearAll: handleFilterClearAll,
		}}
	/>;
}

Empty state

When the table has no data to show (either the data prop is empty, or the result of search or filters returns empty results), the table will show an empty state.

This behavior can be disabled by setting the withEmptyState prop to false. The empty state can be customized by passing a emptyState prop, which is an object with the following properties:

  • title - The title of the empty state
  • icon - The icon to show above the title
  • description - The description of the empty state
  • actions - An array of buttons to show in the empty state. The shape of these objects is the same as the Button component's props.

The table will render the EmptyState component with the provided configuration.

Birds of Australia
Name
Genus
Species

No matching results found

Try adjusting your search or filters.

import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={[]}
		columns={columns}
		emptyState={{
			title: 'No matching results found',
			icon: 'person',
			description: 'Try adjusting your search or filters.',
			actions: [
				{
					label: 'Cancel',
					variant: 'secondary',
					onClick: () => console.log('Cancel clicked'),
				},
				{
					label: 'Refresh',
					variant: 'primary',
					onClick: () => console.log('Refresh clicked'),
				},
			],
		}}
	/>
}

Custom empty state

If the empty state configuration object isn't enough to suit your use case, you can pass a custom component to the emptyStateComponent prop, which will be rendered in place of the default. This component will be called with a table prop, which is the table instance.

Birds of Australia
Name
Genus
Species

Custom empty state

import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={[]}
		columns={columns}
		emptyStateComponent={() => {
			return <Paragraph textAlign="center">Custom empty state</Paragraph>;
		}}
	/>
}

Loading state

While data is loading, the table can show a loading state via the isLoading prop. When the current data is empty, the table will show a default number of placeholder rows with SkeletonText components. A loading spinner will also be shown in the header section, if it's visible (when settings such as column visibility or order are enabled).

Birds of Australia
Name
Genus
Species
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={[]}
		columns={columns}
		withColumnsSettings={true}
		isLoading={true}
	/>
}

If there is data, the table will show a number of placeholders equal to how many data rows would currently be visible.

Birds of Australia
Name
Genus
Species
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		isLoading={true}
	/>
}

Custom number of placeholder rows

The number of placeholder rows can be customized via the loadingState prop. It accepts an object, where the rowCount property is the number of placeholder rows you'd like to show.

Birds of Australia
Name
Genus
Species
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		isLoading={true}
		loadingState={{
			rowCount: 10,
		}}
	/>
}

Custom placeholder cell content

It is also possible to override the placeholder content for each column's cells. This can be done by adding a meta property to the column definition, with a loadingPlaceholder property which is a function that follows the same rules as the standard cell rendering definitions.

The function will be called with an object argument containing the following properties:

  • table - the instance of the table
  • column - the current column
  • rowIndex - the index of this placeholder row
Birds of Australia
Name
Genus
Species
Placeholder 0
Placeholder 1
Placeholder 2
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
		meta: {
			loadingPlaceholder: (info) => `Placeholder ${info.rowIndex}`,
		},
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={[]}
		columns={columns}
		withColumnsSettings={true}
		isLoading={true}
	/>
}

Loading more rows

When there is more data to load, the table can be configure to show a "Load more" row, which - when clicked - will trigger a callback to load more data. Such a row can be inserted at the start and/or end of the table.

Typically, you might insert a "load more" row at the top when there is new data to be loaded. You would place one at the bottom when there are more pages to be loaded - like in continuation-based paging - or when there's more "older" data.

The props are loadNew and loadMore respectively, and they accept an object of the shape:

  • canLoad - a boolean indicating whether there is more data to load - this controls whether the row is shown
  • onClick - a callback triggered when the row is clicked. If this returns a promise, the button will switch to a loading state while the promise is pending
  • isLoading - a boolean to control the loading state of the button (if the onClick promise isn't appropriate)
  • label - (optional) the text to display in the button

Once new data is loaded, it's up to the developer to update the passed in data state accordingly.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	const onClick = async () => {
		await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate async fetch
	};

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		loadNew={{
			canLoad: true,
			onClick,
			label: 'Load new (3 events)'
		}}
		loadMore={{
			canLoad: true,
			onClick,
		}}
	/>
}

Saved views

The table supports saved views, which are a way to save and share table configurations, or provide preset filters.

To enable this functionality, you must enable the withSavedViews prop. Additionally, a savedViews prop must be provided, which is a configuration object for the feature. At the very least, the following properties should be provided:

  • views - an array of view objects to show
    • each view object must at last have a unique id string property, and a name string property
  • activeViewId - the id of the currently active view
  • onActiveViewChange - a callback that is called when the active view changes (when the user clicks on a view)

The DataTable component doesn't handle any state management for saved views, it is up to the consumer to manage active view state, any filters or search queries it might include, and the saving/editing/retrieving of views.

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const views = [
	{
		id: 'lorem',
		name: 'Lorem',
	},
	{
		id: 'ipsum',
		name: 'Ipsum',
	},
	{
		id: 'dolor',
		name: 'Dolor',
	},
];

export default function Example() {
	const [activeViewId, setActiveViewId] = useState(null);

	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withSavedViews={true}
		savedViews={{
			views,
			activeViewId,
			onActiveViewChange: setActiveViewId,
		}}
	/>
}

Usage with filters

For convenience, a view object may include a meta property, which is an object that can be used to store any type of additional data.

This can, for example, be used to store filter values for each view, allowing views to apply any filter state.

Filtered table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const filters = [
	{
		key: 'genus',
		label: 'Genus',
		type: 'checkbox',
		options: [
			{ label: 'Gymnorhina', value: 'gymnorhina' },
			{ label: 'Eolophus', value: 'eolophus' },
			{ label: 'Trichoglossus', value: 'trichoglossus' },
			{ label: 'Psephotellus', value: 'psephotellus' },
			{ label: 'Dromaius', value: 'dromaius' },
		],
	},
	{
		key: 'species',
		label: 'Species',
		type: 'radio',
		options: [
			{ label: 'Tibicen', value: 'tibicen' },
			{ label: 'Roseicapilla', value: 'roseicapilla' },
			{ label: 'Moluccanus', value: 'moluccanus' },
			{ label: 'Chrysopterygius', value: 'chrysopterygius' },
			{ label: 'Novaehollandiae', value: 'novaehollandiae' },
		],
	},
];

const filterData = (data, filterValues) => {
	let filteredData = data.slice();

	filterValues.forEach((filter) => {
		const { key, value } = filter;
		const matchingConfig = filters.find((f) => f.key === key);

		if (matchingConfig.key === 'genus') {
			// "value" in an array of selected option values (checkboxes)
			if (value.length === 0) {
				return;
			}

			filteredData = filteredData.filter((row) => {
				return value.includes(row.genus.toLowerCase());
			});
		}

		if (matchingConfig.key === 'species') {
			// "value" is a single selected option value (radio)
			filteredData = filteredData.filter((row) => {
				return row.species === value;
			});
		}
	});

	return filteredData;
};

const views = [
	{
		id: 'gymnorhina',
		name: 'Gymnorhina',
		meta: {
			filters: [
				{
					key: 'genus',
					value: 'gymnorhina',
				}
			]
		}
	},
	{
		id: 'mixed',
		name: 'Mixed',
		meta: {
			filters: [
				{
					key: 'genus',
					value: 'eolophus',
				},
				{
					key: 'species',
					value: ['tibicen', 'moluccanus']
				}
			]
		}
	},
];

export default function Example() {
	const [activeViewId, setActiveViewId] = useState(null);
	const [filterValues, setFilterValues] = useState([]);

	const handleFilterChange = (key, value) => {
		const exists = filterValues.some(v => v.key === key);
		const updated = exists
			? filterValues.map(v => v.key === key ? { ...v, value } : v)
			: [...filterValues, { key, value }];
		setFilterValues(updated);
	};

	const handleFilterClear = (key) => {
		setFilterValues(filterValues.filter(v => v.key !== key));
	};

	const handleFilterClearAll = () => {
		setFilterValues([]);
	};

	const filteredData = filterData(defaultData, filterValues);

	const handleActiveViewChange = (id) => {
		setActiveViewId(id);

		// Reset filters when "Default" is clicked
		if (id == null) {
			setFilterValues([]);
		} else {
			const maybeView = views.find((view) => view.id === id);

			if (maybeView != null) {
				if ((maybeView.meta?.['filters'] ?? []).length > 0) {
					setFilterValues(maybeView.meta?.['filters']);
				}
			}
		}
	};


	return <DataTable
		caption="Filtered table"
		data={filteredData}
		columns={columns}
		withFilters={true}
		filters={{
			filters,
			values: filterValues,
			onChange: handleFilterChange,
			onClear: handleFilterClear,
			onClearAll: handleFilterClearAll,
		}}
		withSavedViews={true}
		savedViews={{
			views,
			activeViewId,
			onActiveViewChange: handleActiveViewChange,
		}}
	/>;
}

Creating, editing, and deleting saved views

To enable creating new views, and/or editing/deleting existing ones, the savedViews config accepts the following properties:

  • onSave - a callback that is called when a new view is saved
  • onEdit - a callback that is called when an existing view is edited
  • onDelete - a callback that is called when an existing view is deleted

The callbacks will be called with the following arguments:

  • canCreateView - a boolean indicating whether the user can create new views
  • onCreateView - a callback that is called when a new view is created
    • the callback receives an object with the following properties:
      • name - the name of the new view
      • table - the instance of the table
      • columnOrder - the current state of the column order
      • columnVisibility - the current state of the column visibility settings
      • searchValue - the current search value (if any)
      • filters - the current filter values (if any)
  • canEditView - a boolean indicating whether the user can edit existing views
  • onEditView - a callback that is called when an existing view is edited
    • the callback receives an object with the same properties as onCreateView, plus an id property - the id of the edited view
    • when a saved view is renamed, only the id, name, and table properties are passed to the callback
  • canDeleteView - a boolean indicating whether the user can delete existing views
  • onDeleteView - a callback that is called when an existing view is deleted
    • the callback is called with the id of the view to be deleted

Additionally, for more fine-grained control, each individual view object may also include the following properties:

  • canEdit - a boolean indicating whether the user can edit this view
  • canDelete - a boolean indicating whether the user can delete this view

It is up to the developer to implement the logic for saving and deleting views.

Filtered table
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';
import { useState } from 'react';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

const filters = [
	{
		key: 'genus',
		label: 'Genus',
		type: 'checkbox',
		options: [
			{ label: 'Gymnorhina', value: 'gymnorhina' },
			{ label: 'Eolophus', value: 'eolophus' },
			{ label: 'Trichoglossus', value: 'trichoglossus' },
			{ label: 'Psephotellus', value: 'psephotellus' },
			{ label: 'Dromaius', value: 'dromaius' },
		],
	},
	{
		key: 'species',
		label: 'Species',
		type: 'radio',
		options: [
			{ label: 'Tibicen', value: 'tibicen' },
			{ label: 'Roseicapilla', value: 'roseicapilla' },
			{ label: 'Moluccanus', value: 'moluccanus' },
			{ label: 'Chrysopterygius', value: 'chrysopterygius' },
			{ label: 'Novaehollandiae', value: 'novaehollandiae' },
		],
	},
];

const filterData = (data, filterValues) => {
	let filteredData = data.slice();

	filterValues.forEach((filter) => {
		const { key, value } = filter;
		const matchingConfig = filters.find((f) => f.key === key);

		if (matchingConfig.key === 'genus') {
			// "value" in an array of selected option values (checkboxes)
			if (value.length === 0) {
				return;
			}

			filteredData = filteredData.filter((row) => {
				return value.includes(row.genus.toLowerCase());
			});
		}

		if (matchingConfig.key === 'species') {
			// "value" is a single selected option value (radio)
			filteredData = filteredData.filter((row) => {
				return row.species === value;
			});
		}
	});

	return filteredData;
};

let viewId = 1;

export default function Example() {
	const [activeViewId, setActiveViewId] = useState(null);
	const [filterValues, setFilterValues] = useState([]);
	const [views, setViews] = useState([
		{
			id: '0',
			name: 'Gymnorhina',
			meta: {
				filters: [
					{
						key: 'genus',
						value: 'gymnorhina',
					}
				]
			}
		},
	]);

	const handleFilterChange = (key, value) => {
		const exists = filterValues.some(v => v.key === key);
		const updated = exists
			? filterValues.map(v => v.key === key ? { ...v, value } : v)
			: [...filterValues, { key, value }];
		setFilterValues(updated);
	};

	const handleFilterClear = (key) => {
		setFilterValues(filterValues.filter(v => v.key !== key));
	};

	const handleFilterClearAll = () => {
		setFilterValues([]);
	};

	const filteredData = filterData(defaultData, filterValues);

	const handleActiveViewChange = (id) => {
		setActiveViewId(id);

		// Reset filters when "Default" is clicked
		if (id == null) {
			setFilterValues([]);
		} else {
			const maybeView = views.find((view) => view.id === id);

			if (maybeView != null) {
				if ((maybeView.meta?.['filters'] ?? []).length > 0) {
					setFilterValues(maybeView.meta?.['filters']);
				}
			}
		}
	};

	const handleCreateView = async (view) => {
		const id = `${viewId++}`;

		const newView = {
			id,
			name: view.name,
			meta: {
				filters: view.filters ?? [],
			},
		};

		await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate async save

		setViews((prevViews) => {
			return [...prevViews, newView];
		});
		setActiveViewId(newView.id);
		setFilterValues(newView.meta.filters);
	};

	const handleEditView = async (viewData) => {
		const view = views.find((v) => v.id === viewData.id);

		const updatedView = {
			...view,
			name: viewData.name ?? view.name,
			meta: {
				filters: viewData.filters ?? view.meta?.['filters'] ?? [],
			},
		};

		await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate async save

		setViews((prevViews) => {
			return prevViews.map((v) => (v.id === updatedView.id ? updatedView : v));
		});
	};

	const handleDeleteView = async (id) => {
		await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate async save

		setViews((prevViews) => {
			return prevViews.filter((v) => v.id !== id);
		});
		setActiveViewId(null);
		setFilterValues([]);
	};


	return <DataTable
		caption="Filtered table"
		data={filteredData}
		columns={columns}
		withFilters={true}
		filters={{
			filters,
			values: filterValues,
			onChange: handleFilterChange,
			onClear: handleFilterClear,
			onClearAll: handleFilterClearAll,
		}}
		withSavedViews={true}
		savedViews={{
			views,
			activeViewId,
			onActiveViewChange: handleActiveViewChange,
			canCreateView: true,
			canEditView: true,
			canDeleteView: true,
			onCreateView: handleCreateView,
			onEditView: handleEditView,
			onDeleteView: handleDeleteView,
		}}
	/>;
}

Virtualized rendering

DataTable supports rendering "virtualized" table rows, where only a subset of rows is actually rendered at a time (only as much as is visible in the viewport). This can be used to render very large tables without performance issues, when pagination is not desired.

For virtualization we use the TanStack Virtual library (both in Ember and React).

To enable it, set the withVirtualizer prop to true Optionally, the virtualizerOptions argument accepts the same options as the library's Virtualizer class, allowing you to customize the behavior if necessary.

DataTable will attempt to find the nearest scrollable parent container, but it is recommended to pass in a getScrollElement function to ensure the virtualizer references the correct container.

Additionally, it may be necessary to pass in an estimateSize function to allow the virtualizer to better estimate the size of the rows, and the total height.

Virtualized table
id
foo
bar
baz
import { useRef } from 'react';
import { DataTable } from '@customerio/pluma-components/react';

const columns = [
	{ accessorKey: 'id', meta: { width: 100 } },
	{ accessorKey: 'foo' },
	{ accessorKey: 'bar' },
	{ accessorKey: 'baz' },
];

const data = [...new Array(237)].map((_, i) => ({
	id: i + 1,
	foo: 'foo ' + (i + 1),
	bar: 'bar ' + (i + 1),
	baz: 'baz ' + (i + 1),
}));

export default function Example() {
	const scrollElement = useRef(null);

	return <Box p="400" overflowY="auto" ref={scrollElement} style={{ height: '500px', margin: '-32px' }}>
			<DataTable
			caption="Virtualized table"
			data={data}
			columns={columns}
			withVirtualizer={true}
			virtualizerOptions={{
				getScrollElement: () => scrollElement.current,
			}}
		/>
	</Box>;
}

Composition

By default, DataTable is used by invoking the one component and passing configuration props. The table is styled to render in a Panel component.

In some use cases you may want to render the table without the Panel styling, or perhaps insert additional components into the various header sections.

The DataTable component, and the individual header row and section components, behave in the following way:

  • when rendered as a self-closing tag (no children content), they render theri default contents
  • when children content is present, no defaults are rendered to allow for customization/composition

The default layout of the table is as follows:

  • a header section, consisting of multiple rows and their contents. In the default panel layout, each section is rendered inside a DataTableHeaderRow wrapper component, which is a PanelSection and ensures consistent padding and borders. The sections of the header are:
    • a DataTableHeaderSavedViews section, containing:
      • DataTableHeaderSavedViewsLeftSection:
        • DataTableHeaderSavedViewsList
      • DataTableHeaderSavedViewsRightSection:
        • by default, empty
    • a DataTableHeaderActions section, containing:
      • DataTableHeaderActionsLeftSection:
        • DataTableHeaderSearchInput (if search is enabled)
        • DataTableHeaderFiltersButton (if filters are enabled)
      • DataTableHeaderActionsRightSection:
        • a PlumaSpinner, is isLoading is true
        • DataTableHeaderSortMenu (if sorting is enabled)
        • DataTableHeaderColumnMenu (if column settings are enabled)
    • a DataTableHeaderFilters section, containing:
      • DataTableHeaderFiltersLeftSection:
        • DataTableHeaderFiltersList (which wraps a Filters component)
      • DataTableHeaderFiltersRightSection:
        • DataTableHeaderSaveViewButton (if saved views are enabled)
  • the actual table (wrapped in a PanelInset)
  • DataTablePagination, rendered outside of the panel, if pagination is enabled

Simple composition

The most simple use case would be to render the table without the Panel styling, which would look like this:

Birds of Australia
Name
Genus
Species
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import { DataTable, DataTableHeader, DataTableTable, DataTablePagination } from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
	>
		<DataTableHeader />
		<DataTableTable />
		<DataTablePagination />
	</DataTable>;
}

Header customization

You may want to customize the header sections of the table, for example to add a custom control next to the existing action controls (like the column ordering and sorting dropdowns).

DataTableHeader yields an object with the following properties:

  • shouldShowSavedViews - a boolean indicating whether the saved views section should be shown. It's based on whether saved views is enabled, and whether there are any saved views to show.
  • shouldShowActions - a boolean indicating whether the actions section should be shown (based on whether search, filters, sorting, or column settings are enabled)
  • shouldShowFilters - a boolean indicating whether the filters section should be shown
Birds of Australia
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import {
	Button,
  DataTable,
  DataTableHeader,
  DataTableHeaderActions,
  DataTableHeaderActionsLeftSection,
	DataTableHeaderActionsRightSection,
  DataTableHeaderColumnsMenu,
  DataTableHeaderSortMenu,
  DataTableTable,
  DataTablePagination,
} from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		withSorting={true}
	>
		<DataTableHeader>
			{(header) => (
				<>
					{header.shouldShowActions ? (
						<DataTableHeaderActions>
							<DataTableHeaderActionsLeftSection>
								<Button icon="chat" variant="subtle" size="sm">Custom control</Button>
							</DataTableHeaderActionsLeftSection>

							<DataTableHeaderActionsRightSection />
						</DataTableHeaderActions>
					) : null}
				</>
			)}
		</DataTableHeader>

		<DataTableTable />
		<DataTablePagination />
	</DataTable>;
}

Customizing with Panel

If you would like to keep the Panel styling while composing, surround the table contents (except for pagination, which is meant to live outside of the panel), with a Panel component.

DataTableHeader should not be wrapped in a PanelSection or PanelInset component, as it doesn't render any surrounding tags itself. The header sections, however, should be wrapped in a DataTableHeaderRow component, if you want to reuse the DataTable's default section paddings and borders.

DataTableTable should be placed in a PanelInset component, so that it renders from edge to edge of the Panel.

Birds of Australia
Australian MagpieGymnorhinatibicen
GalahEolophusroseicapilla
Rainbow lorikeetTrichoglossusmoluccanus
Golden-shouldered parrotPsephotelluschrysopterygius
Tasmanian EmuDromaiusnovaehollandiae
import {
	Button,
  DataTable,
  DataTableHeader,
  DataTableHeaderRow,
  DataTableHeaderSavedViews,
  DataTableHeaderActions,
  DataTableHeaderActionsLeftSection,
	DataTableHeaderActionsRightSection,
  DataTableHeaderColumnsMenu,
  DataTableHeaderSortMenu,
  DataTableHeaderFilters,
  DataTableTable,
  DataTablePagination,
	Panel,
	PanelInset,
} from '@customerio/pluma-components/react';
import { createColumnHelper } from '@tanstack/table-core';

const columnHelper = createColumnHelper();

const columns = [
	columnHelper.accessor('name', {
		header: 'Name',
	}),
	columnHelper.accessor('genus', {
		header: 'Genus',
	}),
	columnHelper.accessor('species', {
		header: 'Species',
	}),
];

export default function Example() {
	return <DataTable
		caption="Birds of Australia"
		data={defaultData}
		columns={columns}
		withColumnsSettings={true}
		withSorting={true}
	>
		<Panel>
			<DataTableHeader>
				{(header) => (
					<>
						{header.shouldShowSavedViews ? (
							<DataTableHeaderRow>
								<DataTableHeaderSavedViews />
							</DataTableHeaderRow>
						) : null}

						{header.shouldShowActions ? (
							<DataTableHeaderRow>
								<DataTableHeaderActions>
									<DataTableHeaderActionsLeftSection />

									<DataTableHeaderActionsRightSection>
										<Button icon="chat" variant="subtle" size="sm">Custom control</Button>
										<DataTableHeaderColumnsMenu />
										<DataTableHeaderSortMenu />
									</DataTableHeaderActionsRightSection>
								</DataTableHeaderActions>
							</DataTableHeaderRow>
						) : null}

						{header.shouldShowFilters ? (
							<DataTableHeaderRow>
								<DataTableHeaderFilters />
							</DataTableHeaderRow>
						) : null}
					</>
				)}
			</DataTableHeader>

			<PanelInset>
				<DataTableTable />
			</PanelInset>
		</Panel>

		<DataTablePagination />
	</DataTable>;
}

API

Bulk actions to show when at least one row is selected. dropdownActions will be shown in a dropdown menu. dropdownTrigger is a configuration object for the dropdown trigger button. promotedActions are action buttons that render in the header, next to the dropdown if there is one.

A caption for the table for assistive technologies, but hidden visually.

The column order, when you want to control it externally.

Additional configuration for the column settings dropdown.

The column visibility, when you want to control it externally.

The data to render in the table.

The initial order of columns, as an array of column IDs. Ignored when columnOrder is set.

The initial visibility of columns, where the keys are column IDs. Ignored when columnVisibility is set.

A configuration object for the empty state.

A custom component to render for the empty state, instead of the default one configured in emptyState. It receives a table prop, which is the current instance of TanStack Table.

Default:true

Whether to allow selecting multiple rows, or only one row at a time. If the value is a boolean, it applies to the whole table. If a function is provided, it can be used to control whether multiple selection is allowed for a row's children. https://tanstack.com/table/latest/docs/guide/row-selection#single-row-selection

A custom component to render when a row is expanded. This is different from rendering sub-rows: the custom component can render anything, while "nested rows" are more rows of the same type.

A configuration object for filters. These extend from the PlumaFilters component.

A function to determine whether a row can be expanded. By default, a row can be expanded either when:

  • it has sub-rows, in which case expanding it will render the sub-rows
  • there is an expandedRowComponent, which will be rendered as the expansion

Whether to enable selection on individual rows. When withRowSelection is true, this argument can be used to disable selection for all or specific rows. If this is a boolean of false, all rows render a disabled checkbox. If it's a function that returns false, the false rows will render a disabled checkbox

A function to get the ID of a row. By default, id will be used - if present. Otherwise, the row's index will be used.

A function to determine a row's sub-rows (nested rows of the same type). By default, we check whether a row has a children array property and use that. Override this if you'd like to determine nested rows differently.

An object to control the initial expanded state of rows, where the keys are row IDs.

Whether to disable the border style between table rows

Whether data is currently being loaded, making the table show a loading state.

Whether to render table rows with alternating background colors

Default:auto

The layout of the table

Additional configuration for the loading state.

Configuration for the "Load more" row, shown at the bottom of the table. For example, when there is more data to load that should be appended to the current data.

Configuration for the "Load new" row, shown at the top of the table. For example, when there is new data available to load.

A callback for when the column order changes.

A configuration object for pagination within the table. When withPagination is enabled and no configuration is provided, the table will perform pagination internally with default settings, based on the provided data. When an object is provided without the page property, the table will also perform pagination internally, but with the provided settings. When page is provided, the table assumes pagination is controlled externally, and data is already paginated.

Configuration object for row actions. dropdownActions are the actions shown within the dropdown menu. dropdownMenu can contain additional configuration for the DropdownMenu component. dropdownTrigger can contain additional configuration for the dropdown menu's trigger button.

This can also be a function that receives the current Row instance, and returns the configuration object, all (which allows for dynamic actions, customizing them per row).

Row selection state object, where the keys are row IDs.

Whether hovering over the rows should highlight them (change their background color)

A configuration object for sorting. The { id, desc } object form is deprecated, please use the { value: { id, desc }, onChange } form instead.

Default:false

Whether to show the Columns button for controlling column order and visibility.

Default:true

Whether to enable showing the empty state when data is empty.

Default:false

Whether to enable filters.

Whether to enable pagination.

Whether to enable row expansion. This will add an "expander" column to the left of the table, and allow expanding expandable rows.

Default:false

Whether to enable row selecting. This will add a column at the start of the table to render selection checkboxes in. Can be a boolean to enable selection for all rows, or a function to conditionally enable selection for certain rows. When this returns false for a row, the row won't render a checkbox at all.

Default:false

Whether to enable the saved views feature.

Default:false

Whether to enable searching via a search input.

Default:false

Whether to enable sorting rows. If no sorting state is provided, the table will sort the rows internally.

Whether the table rows should render virtualized.

Whether more columns are currently being loaded.

Whether a server-side search is currently in progress. When true, the dropdown shows a loading indicator instead of "No columns found".

Callback for when the end of the columns list is reached and more columns should be loaded.

Callback for server-side search. When provided, the component delegates search to the consumer instead of filtering columns client-side. The consumer should set searchResultColumnIds with the matching column IDs.

Column IDs to show in the dropdown when using server-side search. Only these columns appear in the dropdown list. The table's columns are unaffected. Set to null/undefined to show all columns (e.g. when search is cleared).

Whether the onLoadMore callback should be called when the end of the list is reached. If no more columns are available, this should be set to false.

Whether to show a search input for filtering columns within the dropdown.

If provided, the component will render as a link instead

The name of an icon (from Pluma Icons) to render in the button

Default:leading

Which side of the text the icon should be rendered on. leading renders the icon before the text, trailing renders it after

Whether the button should be rendered in an danger state

Disables the button. Use this instead of the native disabled prop, to make sure non-button elements (when used with as) get the correct styles

When this flag is true, an a tag will be used instead of the provider's linkComponent, even if it exists. Additionally, target="_blank" rel="noopener noreferrer" will be added automatically

When this is set to true, the button won't render any text, and will reduce its side paddings

The text to show in the button.

Function called when the action is selected. The arguments are the current selection state, and the table instance.

This is passed into the provider's linkComponent. It can be used by the link component implementation to handle replaceState instead of pushState

Default:md

The size of the button. medium and small are deprecated, use md and sm instead.

Optional tooltip text to display when hovering over the button

Default:secondary

The visual style variant of the button.

This allows turning off the automatic addition of target="_blank" rel="noopener noreferrer". This can be used for links to other protocols like mailto:

Optional description text to display below the menu item

The URL for the link, if the item should render as a link instead of a button

Optional icon to display alongside the menu item text

Whether the item should be displayed with critical/danger styling

Whether the item is disabled

Whether the link is external. When this flag is true, an a tag will be used, and target="_blank" rel="noopener noreferrer" will be added automatically

Whether this item is also a trigger for another sub-menu.

The text to show in the menu item.

Function called when the item is clicked. The arguments are the current selection state, and the table instance.

This is passed into the provider's linkComponent when used with href. It can be used by the link component implementation to handle replaceState instead of pushState

Default:true

Whether clicking the item should close the popover

Optional tooltip text to display when hovering over the menu item

Optional icon to display on the trailing side of the menu item. Typically used for a sub-menu trigger indicator (e.g. a right chevron).

Default:true

Whether to add safe external attributes (target="_blank" rel="noopener noreferrer") for external links. This can be turned off for special cases like mailto: links

The name of an icon (from Pluma Icons) to render in the button

Default:leading

Which side of the text the icon should be rendered on. leading renders the icon before the text, trailing renders it after

Disables the button. Use this instead of the native disabled prop, to make sure non-button elements (when used with as) get the correct styles

When this is set to true, the button won't render any text, and will reduce its side paddings

Shows a spinner in the button overlaid on top of the button's content. The button will act as if disabled while in the loading state.

Default:"Actions"

The label to show in the dropdown trigger.

Default:md

The size of the button. medium and small are deprecated, use md and sm instead.

Optional tooltip text to display when hovering over the button

Default:secondary

The visual style variant of the button.

Actions to show in a dropdown menu for each row. Can be a static array of actions, or a function that receives the current row and returns an array of actions (allowing for dynamic actions, customizing them per row). The array items can either be groups of actions (with optional labels), or plain actions.

Additional configuration options for the DropdownMenu component.

The actions to show in this group.

An option label for the group. If not provided, the group won't render a header, but will still render a divider to separate action items from other groups.

Optional description text to display below the menu item

The URL for the link, if the item should render as a link instead of a button

Optional icon to display alongside the menu item text

Whether the item should be displayed with critical/danger styling

Whether the item is disabled

Whether the link is external. When this flag is true, an a tag will be used, and target="_blank" rel="noopener noreferrer" will be added automatically

Whether this item is also a trigger for another sub-menu.

The text to show in the menu item.

Function called when the item is clicked. Receives the row instance in which the action was clicked as an argument.

This is passed into the provider's linkComponent when used with href. It can be used by the link component implementation to handle replaceState instead of pushState

Default:true

Whether clicking the item should close the popover

Optional tooltip text to display when hovering over the menu item

Optional icon to display on the trailing side of the menu item. Typically used for a sub-menu trigger indicator (e.g. a right chevron).

Default:true

Whether to add safe external attributes (target="_blank" rel="noopener noreferrer") for external links. This can be turned off for special cases like mailto: links

Whether sorting is controlled externally. By default, when withSorting is enabled and sorting is provided, DataTable assumes that the consumer controls sorting the data as well. Setting isSortingManual to false allows only controlling the sorting prop, while still letting the table handle sorting internally.

The current sorting state, when sorting is done externally (e.g. via an API). If onChange isn't provided, this will be the initial sorting state, and the table will perform sorting internally. This is an object with the following properties:

  • id - a column ID
  • desc - a boolean indicating whether the column is sorted in descending order

(Optional) The number of items on the current page. When the total number of items is unknown (totalItemCount is -1), this count will be used to display the count of items on the current page.

If provided, this function will be used to generate links for the page navigation buttons. The buttons will be rendered as a tags, and the strings returned by this function will be used as the href attribute.

If page is not provided to the component (continuation-based paging), the first argument will be -1.

Whether the current page is the first page. Useful in continuation-based paging where the current page number is unknown.

Whether the current page is the last page. Useful in continuation-based paging where the current page number is unknown.

Whether the pagination is controlled externally. By default, when page, onPageChange, or hrefBuilder are provided, DataTable assumes that the consumer controls paginating the data as well. Setting isManual to false allows only controlling the page prop, while still letting the table handle pagination internally.

Called when a navigation button is clicked, with the new page number (if available), and the control that was clicked.

If page is not provided to the component (continuation-based paging), the first argument will be -1.

When pagination is changed programmatically (not through a button click), the control argument will be custom.

Called when the page size is changed, with the new page size.

The current page number.

Default:20

The size of the page (how many items per page). This, along with totalItemCount, is used to calculate the total number of pages.

Default:[10, 20, 50, 100]

The page sizes to display in the page size selector.

The total number of items (if known). This, along with pageSize, is used to calculate the total number of pages. If a total is unknown, provide -1 to have the component reflect that in the page count.

Element (or elements) that describe the form field. Also used for error messages, as aria-errormessage isn't supported in all assistive technologies: https://a11ysupport.io/tech/aria/aria-errormessage_attribute

String value that labels the form field.

Element (or elements) that label the form field.

Additional classes to be applied to the top-level container. This is necessary as an argument in Ember, where we apply splattributes on the "input" itself, which means we can't pass a custom class to the containing element the classic way.

Default:true

Whether the search input is immediately visible by default. When false, we will first show a button, which reveals the search input when clicked.

A description for the field, providing additional context or hints.

An error message associated with the form field. Boolean: Ember-only. If the error named block is used, this prop can be set to true to indicate an error is present, which will make the error block render.

A custom filter function to use for uncontrolled search. See TanStack Table docs for more details.

The name of an icon (from Pluma Icons) to be rendered on the left side of the input.

An optional id to be assigned to the input element. Also used to generate ids for labels and error messages. If one isn't provided, it will be generated.

When provided, this controls the search value for the search input, separately from the value prop, which will then be used for filtering. This allows decoupling the input state from the actual search value, which can improve DataTable performance when typing in the input (otherwise, the table might rerender on every keystroke).

Whether the input should render in an "active" styled state.

Whether the form field is disabled.

Whether the form field is in an invalid state.

Whether the input should show a loading spinner.

Whether filtering via the search input is controlled externally. By default, when value and onSearch or onSearchInput are provided, DataTable assumes that the consumer controls filtering the data as well. Setting isManual to false allows only controlling the value prop, while still letting the table handle filtering internally.

The label text to show with the form field.

The name attribute is used for HTML forms

Called when the clear button is clicked.

The function called when the search should be done

Any changes to the value for the search input

Placeholder text to show inside the field.

Whether to allow browser autofill and password managers. When false (default), adds attributes to disable password managers (1Password, LastPass, Bitwarden, Dashlane). Set to true for fields where autofill is desired (e.g., email, password, address fields).

Whether the generated description id should be included in the input's aria-describedby attribute. Disable this when a label`` tag surrounds the input as well as it's description text, for example in an OptionCard` component.

Whether the generated label id should be included in the input's aria-labelledby attribute. Disable this when the label tag surrounds the input as well as it's label text, for example in an OptionCard component.

Whether to search when the search input is blurred.

Whether to search when the clear button is pressed.

Whether to search on a specific debounced interval, must be greater than 0 to trigger a search.

Whether to search when Enter is pressed.

Whether to search when the value changes.

By default, form elements will throw an error if no label or aria-label is provided. Disable this to suppress the error, for example when building a custom form element, and you want to handle labeling yourself.

The type of input that should be rendered. Defaults to text.

Pluma internal property to set a component name used for logging. Only use if building a custom component that wraps this one and need to make it more obvious where errors, for example, come from.

Additional classes to be applied to the description element.

Additional classes to be applied to the error element.

Additional classes to be applied to the field node.

Additional classes to be applied to the footer element.

Ember-only: this is used internally to detect whether a label named block is present, and to ignore empty label props if so.

Additional classes to be applied to the input node.

Sets the size attribute on the input element. This controls the visible width of the input in characters.

Additional classes to be applied to the div that wraps the input element.

Whether the component should render with "legacy" styles.

Additional classes to be applied to the label node.

Whether to apply the center-baseline variant.

Ember only: a modifier to apply on the input's wrapper element.

React only: a ref to the input's wrapper element.

Whether the "add button" dropdown should render in an open state by default.

Whether the filters are disabled

Called when a filter is selected within the dropdown

The values of the filters

The description of the EmptyState.

The icon to show above the title.

The title of the EmptyState.

How many placeholder rows to show while loading.

The id of the currently active saved view, if any.

Whether creating new views is allowed.

Whether deleting views is allowed.

Whether editing views is allowed.

Default:"Default"

The name of the "default" view.

Called when the active view changes (usually by clicking a saved view button).

An array of saved views to show in the table header.

Whether deleting this view is allowed.

Whether editing this view is allowed.

A link to navigate to when the saved view is clicked. Can be used instead of onClick, which will make the component render a link, triggering navigation events. Can be used when saved views are driven by query parameters.

A unique identifier for the saved view.

Whether this view is currently disabled.

Any custom data to keep in the view object.

The text to show in the saved view button.

A click handler for when the saved view is clicked.

Optional tooltip text to display when hovering over the button.

Whether more data is available to load, which shows the row.

Whether more data is currently being loaded, which puts the row into a loading state.

Optionally overrides the default label shown within the row.

Callback for when the load more row button is clicked.

A function that returns a custom colspan for a cell, which can be used to make cells that span multiple columns. When a cell spans multiple columns, the next N cells will be hidden, where N is the colspan - 1.

A component to use for this column's cells when the table is in the loading state. This follows the same rules as defining cell or header renderers for the column.

The maximum possible width for the column. Can be a number (pixels), a % string, or a px string.

The minimum possible width for the column. Can be a number (pixels), a % string, or a px string.

The name of a column. Used as a fallback for column headers when the header property isn't a string, and when you don't want the column's id to be the visible name.

Default:false

Whether cell content should wrap when it overflows the cell. By default, cell content will be truncated with an ellipsis.

This setting determines what the Sort dropdown shows for the ascending/descending sort options. Depending on data type, the buttons will say either:

  • Ascending and Descending
  • A-Z and Z-A The table will try and guess the best option depending on the data provided, but this setting allows explicit control.

The options are:

  • default for Ascending and Descending
  • text for A-Z and Z-A

The width of a column.

  • when it's a string ending in %, it will be interpreted as a percentage of the table width.
  • when it's a string ending in fr, it will act like a fraction unit, similar to a grid layout.
  • when it's a string ending in px, it will be interpreted as a pixel value.
  • when it's a number, it's interpreted as a pixel value.