measureRowsCount
prop will soon be deprecated. Please use new width props instead.
Table is an arrangement of data in rows and columns.
Use Data Tables to display large information in an organized way through rows and columns, so that users can scan, compare and sort data. Also, table automatically truncates long text in header, column and footer cells.
Example
function MyTable() {
const longText =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.";
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Age',
accessor: 'age',
align: 'left'
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title'
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city'
},
{
Header: longText,
accessor: 'long',
width: 200,
Footer: () => longText
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin',
long: longText
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
age: 29,
job: {
title: 'job_title_2'
},
'[user].city': 'Sydney',
long: longText
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
age: 30,
job: {
title: 'job_title_3'
},
'[user].city': 'London',
long: longText
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
age: 28,
job: {
title: 'job_title_4'
},
'[user].city': 'New york',
long: longText
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
age: 35,
job: {
title: 'job_title_5'
},
'[user].city': 'Tokyo',
long: longText
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
age: 32,
job: {
title: 'job_title_6'
},
'[user].city': 'Berlin',
long: longText
}
];
return (
<div style={{ height: 300 }}>
<Table data={data} columns={columns} />
</div>
);
}
Usage
Table component should be placed in a container with a fixed height.
The Table component will fill all the available space of the container.
function YourComponent() {
return (
<div style={{ height: 300 }}>
<Table />
</div>
);
}
The table supports automatic column width measurement according to the longest cell width within the specified number of rows.
Use measureRowsCount
to specify the number of rows in the table to be measured.
The table will include the Header
and Footer
rows in the measurements as well.
If you specify width
or max-width
value for the column, Cells will take the specified width instead of measured width.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
Cell: ({ value }) => <Badge iconName="Edit" label={value} />
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
Footer: () => 100000
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Last name 1',
room: 1
},
{
firstName: 'Very very very long name',
lastName: 'Last name 2',
room: 2
},
{
firstName: 'Random name',
lastName: 'Last name 3',
room: 3
}
];
return (
<div style={{ height: 300 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
);
}
Auto rows height
When autoRowsHeight
prop is used, rows in the table will grow in respect to their content on first render.
To remeasure or recalculate the rows at runtime when a cell height grows, use the measureRows
function passed to Cell
render prop.
In the example below, typing in any of the ComboBox will grow the table row, measureRows
function has been used to remeasure the rows height.
function MyTable() {
const MultiCreatable = ({ onChange }) => {
const initialValues = [
{
value: '714x3w',
label: '714x3w'
},
{
value: 'w2z0t',
label: 'w2z0t'
},
{
value: 'ypen88',
label: 'ypen88'
},
{
value: 'arvkq2',
label: 'arvkq2'
}
];
const [value, setValue] = useState(initialValues);
const [options, setOptions] = useState([]);
const [inputValue, setInputValue] = useState('');
const remeasureRows = useCallback(() => {
if (onChange) onChange();
}, [onChange]);
const handleChange = useCallback(
(value) => {
setValue(value);
remeasureRows();
},
[remeasureRows]
);
const handleInput = useCallback(
(inputValue) => {
setInputValue(inputValue);
remeasureRows();
},
[remeasureRows, inputValue]
);
const onCreateOption = useCallback(
(event, inputValue) => {
const newOption = {
value: `${inputValue}-${value.length}`,
label: inputValue
};
setValue([...value, newOption]);
setOptions([...options, newOption]);
setInputValue('');
remeasureRows();
event.preventDefault();
},
[value, remeasureRows]
);
const handleKeyDown = (event) => {
if (!inputValue) return;
switch (event.key) {
case 'Enter':
onCreateOption(event, inputValue);
return;
}
};
return (
<ComboBox
kind="multi-creatable"
value={value}
inputValue={inputValue}
options={options}
onCreateOption={onCreateOption}
onChange={handleChange}
onInputChange={handleInput}
onKeyDown={handleKeyDown}
placeholder="Type anything and press `Enter`"
aria-label="creatable-example"
/>
);
};
const columns = useMemo(
() => [
{
Header: 'Link',
accessor: 'label'
},
{
Header: 'Keywords',
accessor: 'keywords',
width: 400,
Cell: ({ row: { original, index }, measureRows }) => {
if (index === 0) {
return <MultiCreatable onChange={measureRows} />;
}
if (index === 1) {
return (
<div>
{Array(12)
.fill('Adjust rocks')
.map((v, i) => (
<Badge key={`${v}-${i}`} label={v} />
))}
</div>
);
}
return `keyword ${index}`;
}
}
],
[]
);
const data = useMemo(() => {
const randomStr = () => Math.random().toString(36).substring(7);
return Array(14)
.fill()
.map(() => {
const label = randomStr();
return {
label,
keywords: null
};
});
}, []);
return (
<div style={{ height: 300 }}>
<Table
autoRowsHeight
data={data}
columns={columns}
visualProperties={{ areColumnsBordered: true }}
/>
</div>
);
}
Column Resizing
The Table supports the column resizing function. You can use isResizable
prop for making desired columns resizable. If you want to limit resizable column width value, you can use for the column maxWidth
property.
function MyTable() {
const longText =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
const initialData = [
{
name: 'Just a name',
city: longText,
role: 'User'
},
{
name: 'Just another name',
city: longText,
role: 'Admin'
}
];
const visualProperties = {
areColumnsBordered: true,
hasGreyBackgroundHeader: true
};
const columns = [
{
Header: 'Name',
accessor: 'name',
isResizable: true
},
{
Header: 'Test',
accessor: 'city',
isResizable: true
},
{
Header: longText,
accessor: 'role',
isResizable: true
}
];
const sortData = (data, sorting) => {
const { columnKey, direction } = sorting;
return [...data].sort((prev, next) => {
const prevValue = prev[columnKey];
const nextValue = next[columnKey];
const isAscSortingActive = direction === 'asc';
if (prevValue < nextValue) {
return isAscSortingActive ? -1 : 1;
}
if (prevValue > nextValue) {
return isAscSortingActive ? 1 : -1;
}
return 0;
});
};
const initialSorting = {
direction: 'desc'
};
const [data, setData] = useState(sortData(initialData, initialSorting));
const [activeSorting, setActiveSorting] = useState(initialSorting);
const handleSortingChange = (columnKey) => {
const { direction } = activeSorting;
setActiveSorting({
columnKey,
direction: direction === 'asc' ? 'desc' : 'asc'
});
if (!columnKey) {
setData(initialData);
return;
}
const sortedData = sortData(data, activeSorting);
setData(sortedData);
};
return (
<>
<div
style={{
height: 230
}}
>
<Table
data={data}
columns={columns}
rowSize="medium"
visualProperties={visualProperties}
activeSorting={activeSorting}
onSortingChange={handleSortingChange}
/>
</div>
<div style={{ height: 180 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
</>
);
}
Column Initial Widths
- Initial Min Width
- Initial Max Width
- Initial Min & Max Width
initialMinWidth
is a special property (unlike minWidth
) which allows you to set an initial min-width for a column & resize it below the specified value
Using width
or minWidth
together with this property will override its default behavior
function MyTable() {
const data = [
{
no: 1,
fullName: 'John Doe'
},
{
no: 2,
fullName: 'Jane Doe'
}
];
const columns = [
{
Header: 'No.',
accessor: 'no',
isResizable: true,
initialMinWidth: 100
},
{
Header: 'Full Name',
accessor: 'fullName'
}
];
return (
<div style={{ height: 200 }}>
<Table data={data} columns={columns} measureRowsCount={data.length} />
</div>
);
}
Column Resize Callback
You can use the onColumnResized
callback property to get the new width of the column that was resized. The callback will be passed a column
argument which is of type TableColumn
function MyTable() {
const columns = [
{
Header: 'No.',
accessor: 'no',
isResizable: true
},
{
Header: 'First Name',
accessor: 'firstName',
isResizable: true
},
{
Header: 'Last Name',
accessor: 'lastName'
}
];
const data = [
{
no: 1,
firstName: 'John Doe',
lastName: 'Last name 1'
},
{
no: 2,
firstName: 'Very very very super long name',
lastName: 'Last name 2'
},
{
no: 3,
firstName: 'Jane Doe',
lastName: 'Last name 3'
}
];
const handleColumnResized = (column) => {
console.log(column.Header);
console.log(column.accessor);
console.log(column.isResizable);
console.log(column.width);
};
return (
<div style={{ height: 200 }}>
<Table
data={data}
columns={columns}
onColumnResized={handleColumnResized}
measureRowsCount={2}
/>
</div>
);
}
Custom backgroundColor for the Cells
The prop getCellProps
allows you to apply style conditionally as below.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Activity',
accessor: 'activity'
},
{
Header: 'Age',
accessor: 'age',
align: 'left'
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city'
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
activity: 'created',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin'
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
activity: 'changed',
age: 29,
job: {
title: 'job_title_2'
},
'[user].city': 'Sydney'
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
activity: 'removed',
age: 30,
job: {
title: 'job_title_3'
},
'[user].city': 'London'
}
];
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
getCellProps={(cellInfo) => {
let color = 'unset';
if (cellInfo && cellInfo.column.id === 'activity') {
if (cellInfo.value === 'created') color = ColorPrimary10;
if (cellInfo.value === 'removed') color = ColorNegative20;
if (cellInfo.value === 'changed') color = ColorNeutral20;
}
return {
style: {
backgroundColor: color
}
};
}}
/>
</div>
);
}
Custom Cell Renderer
There are Header
, Footer
, Cell
property within the column configuration.
With them you can define the custom render function for the header, the footer and a table cell accordingly.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 300,
Cell: ({ value }) => <Badge label={value} />
},
{
Header: 'Points',
accessor: 'points',
align: 'right',
Footer: (info) => {
const { rows } = info;
const totalValue = rows.reduce((sum, row) => {
const { values } = row;
const value = values['points'];
if (!value) {
return sum;
}
return sum + parseFloat(value);
}, 0);
return totalValue;
}
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Some lastName',
points: 10
},
{
firstName: 'Another name',
lastName: 'Another lastName',
points: 11
},
{
firstName: 'Random name',
lastName: 'Random lastName',
points: 700
}
];
return (
<div style={{ height: 300 }}>
<Table data={data} columns={columns} />
</div>
);
}
Custom Cell Renderer on Row Hover
If you want to update your custom Cell component when the row it belongs to changes, you can use the getHoveredRowId
method to get the id of the row currently being hovered.
function MyTable() {
const [rowIdHovered, setRowIdHovered] = useState();
const CustomCellComp = ({ value, rowId }) => {
if (rowIdHovered === rowId) {
return <div>row hovered</div>;
} else {
return <div>row not hovered</div>;
}
};
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 300,
Cell: ({ value }) => <Badge label={value} />
},
{
Header: 'Points',
accessor: 'points',
align: 'right',
Cell: ({ value, row: { id } }) => (
<CustomCellComp value={value} rowId={id} />
),
Footer: (info) => {
const { rows } = info;
const totalValue = rows.reduce((sum, row) => {
const { values } = row;
const value = values['points'];
if (!value) {
return sum;
}
return sum + parseFloat(value);
}, 0);
return totalValue;
}
}
];
const data = Array.from({ length: 1000 }, (_, index) => ({
firstName: `firstName_${index + 1}`,
lastName: `lastName_${index + 1}`,
room: index + 1
}));
return (
<div style={{ height: 600 }}>
<Table data={data} columns={columns} getHoveredRowId={setRowIdHovered} />
</div>
);
}
Custom Minimum Width for Columns
The table supports a custom minimum width option for desired columns. You can use the minWidth
property for each column to change the minimum width value.
If you do not use the minWidth property for the column, column will take the minWidth 150px width value by default.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
minWidth: 600
},
{
Header: 'Last Name',
accessor: 'lastName'
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Last name 1'
},
{
firstName: 'Very very very long name',
lastName: 'Last name 2'
},
{
firstName: 'Random name',
lastName: 'Last name 3'
}
];
return (
<div style={{ height: 200 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
);
}
Custom Maximum Width for Columns
The table supports a custom maximum width option for desired columns. You can use the maxWidth
property for each column to change the maximum width value.
If you do not use the maxWidth property for the column, column will take the maxWidth 500px width value by default.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
maxWidth: 300
},
{
Header: 'Last Name',
accessor: 'lastName'
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Last name 1'
},
{
firstName: 'Very very very long name',
lastName: 'Last name 2'
},
{
firstName: 'Random name',
lastName: 'Last name 3'
}
];
return (
<div style={{ height: 200 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
);
}
DND Column Re-ordering
The Table supports the drag-and-drop column re-ordering function. You can use isDraggable
prop for making desired columns draggable for re-ordering.
function MyTable() {
const longText =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
const initialData = [
{
name: 'Just a name',
city: 'A',
role: 'User'
},
{
name: 'Just another name',
city: 'B',
role: 'Admin'
}
];
const visualProperties = {
areColumnsBordered: true,
hasGreyBackgroundHeader: true
};
const columns = [
{
Header: longText,
accessor: 'name',
isDraggable: true,
isResizable: true
},
{
Header: 'Test',
accessor: 'city',
isDraggable: true,
isResizable: true
},
{
Header: 'Role',
accessor: 'role'
}
];
const sortData = (data, sorting) => {
const { columnKey, direction } = sorting;
return [...data].sort((prev, next) => {
const prevValue = prev[columnKey];
const nextValue = next[columnKey];
const isAscSortingActive = direction === 'asc';
if (prevValue < nextValue) {
return isAscSortingActive ? -1 : 1;
}
if (prevValue > nextValue) {
return isAscSortingActive ? 1 : -1;
}
return 0;
});
};
const initialSorting = {
direction: 'desc'
};
const [data, setData] = useState(sortData(initialData, initialSorting));
const [activeSorting, setActiveSorting] = useState(initialSorting);
const handleSortingChange = (columnKey) => {
const { direction } = activeSorting;
setActiveSorting({
columnKey,
direction: direction === 'asc' ? 'desc' : 'asc'
});
if (!columnKey) {
setData(initialData);
return;
}
const sortedData = sortData(data, activeSorting);
setData(sortedData);
};
return (
<>
<div
style={{
height: 230
}}
>
<Table
data={data}
columns={columns}
rowSize="medium"
visualProperties={visualProperties}
activeSorting={activeSorting}
onSortingChange={handleSortingChange}
/>
</div>
<div style={{ height: 180 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
</>
);
}
DND Column Re-ordering Callback
You can use onColumnRearrange
callback property to get an ordered array of column accessors as a first argument and perform any action after column is rearranged.
function MyTable() {
const initialData = [
{
name: 'Just a name',
city: 'A',
role: 'User'
},
{
name: 'Just another name',
city: 'B',
role: 'Admin'
}
];
const visualProperties = {
areColumnsBordered: true,
hasGreyBackgroundHeader: true
};
const columns = [
{
Header: 'Name',
accessor: 'name',
isDraggable: true
},
{
Header: 'Test',
accessor: 'city',
isDraggable: true
},
{
Header: 'Role',
accessor: 'role'
}
];
const sortData = (data, sorting) => {
const { columnKey, direction } = sorting;
return [...data].sort((prev, next) => {
const prevValue = prev[columnKey];
const nextValue = next[columnKey];
const isAscSortingActive = direction === 'asc';
if (prevValue < nextValue) {
return isAscSortingActive ? -1 : 1;
}
if (prevValue > nextValue) {
return isAscSortingActive ? 1 : -1;
}
return 0;
});
};
const initialSorting = {
direction: 'desc'
};
const [data, setData] = useState(sortData(initialData, initialSorting));
const [activeSorting, setActiveSorting] = useState(initialSorting);
const handleSortingChange = (columnKey) => {
const { direction } = activeSorting;
setActiveSorting({
columnKey,
direction: direction === 'asc' ? 'desc' : 'asc'
});
if (!columnKey) {
setData(initialData);
return;
}
const sortedData = sortData(data, activeSorting);
setData(sortedData);
};
const onColumnRearrange = (columnsOrder) => {
console.log('Column Rearranged!', columnsOrder);
};
return (
<>
<div
style={{
height: 230
}}
>
<Table
data={data}
columns={columns}
rowSize="medium"
visualProperties={visualProperties}
activeSorting={activeSorting}
onSortingChange={handleSortingChange}
onColumnRearrange={onColumnRearrange}
/>
</div>
</>
);
}
Empty State
You can add an Empty State by using prop emptyState
. It is shown if there is no data.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Age',
accessor: 'age',
align: 'left'
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title'
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city'
}
];
const emptyStateProps = {
headLine: 'Opps! No data',
bodyText: "Please check your searching or filtering.",
buttonLabel: "Retry",
buttonIconName: "ArrowCircleRight",
buttonIconAlignment: "left",
onClickButton: () => {
console.log('clicked')
}
}
return (
<div style={{ height: 400 }}>
<Table data={[]} columns={columns} emptyState={emptyStateProps} />
</div>
);
}
Expandable row
You can use this feature to have expandable rows. To do this you need to add subRows
in data like the example.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
minWidth: 220
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Age',
accessor: 'age',
align: 'left'
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title'
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city'
}
];
const data = [
{
firstName: 'firstName_0',
lastName: 'lastName_0',
age: 36,
job: {
title: 'job_title_0'
},
'[user].city': 'Berlin'
},
{
firstName: 'firstName_1',
lastName: 'lastName_1',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin',
subRows: [
{
firstName: 'sub firstName_1',
lastName: 'sub lastName_2',
job: {
title: 'sub job_title_2'
}
},
{
firstName: 'sub firstName_2',
lastName: 'sub lastName_2',
age: 306,
job: {
title: 'sub job_title_2'
}
}
]
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
age: 29,
job: {
title: 'job_title_2'
},
'[user].city': 'Sydney'
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
age: 30,
job: {
title: 'job_title_3'
},
'[user].city': 'London'
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
age: 28,
job: {
title: 'job_title_4'
},
'[user].city': 'New york',
subRows: [
{
firstName: 'firstName_4_0',
lastName: 'lastName_4_0',
job: {
title: 'job_title_4_0'
}
},
{
firstName: 'firstName_4_1',
lastName: 'lastName_4_1',
job: {
title: 'job_title_4_1'
}
},
{
firstName: 'firstName_4_2',
lastName: 'lastName_4_2',
age: 306,
job: {
title: 'job_title_4_2'
},
subRows: [
{
firstName: 'sub 4_2_1',
lastName: 'sub 4_2_1',
job: {
title: 'sub 4_2_1'
}
},
{
firstName: 'sub 4_2_2',
lastName: 'sub 4_2_3',
age: 306,
job: {
title: 'sub 4_2_3'
}
}
]
}
]
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
age: 35,
job: {
title: 'job_title_5'
},
'[user].city': 'Tokyo'
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
age: 32,
job: {
title: 'job_title_6'
},
'[user].city': 'Berlin'
}
];
const visualPropertiesStripe = {
isStriped: true,
areColumnsBordered: true
};
return (
<div style={{ height: 500 }}>
<Table data={data} columns={columns} visualProperties={visualPropertiesStripe} />
</div>
);
}
You can use the toggleRowExpanded
or toggleAllRowsExpanded
function passed to the Cell
render prop to toggle one or all expandable table rows respectively.
In the example below, we call toggleRowExpanded(rowId, false)
after deleting a row in the table to fix an issue that occurs when an expanded row is deleted and the next row is automatically expanded.
function MyTable() {
const [data, setData] = useState([
{
firstName: 'firstName_1',
lastName: 'lastName_1',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin',
subRows: [
{
firstName: 'sub firstName_1',
lastName: 'sub lastName_2',
job: {
title: 'sub job_title_2'
}
},
{
firstName: 'sub firstName_2',
lastName: 'sub lastName_2',
age: 306,
job: {
title: 'sub job_title_2'
}
}
]
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
age: 36,
job: {
title: 'job_title_2'
},
'[user].city': 'Berlin',
subRows: [
{
firstName: 'sub firstName_2',
lastName: 'sub lastName_2',
job: {
title: 'sub job_title_2'
}
},
{
firstName: 'sub firstName_2',
lastName: 'sub lastName_2',
age: 306,
job: {
title: 'sub job_title_2'
}
}
]
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
age: 28,
job: {
title: 'job_title_4'
},
'[user].city': 'New york',
subRows: [
{
firstName: 'firstName_4_0',
lastName: 'lastName_4_0',
job: {
title: 'job_title_4_0'
}
},
{
firstName: 'firstName_4_1',
lastName: 'lastName_4_1',
job: {
title: 'job_title_4_1'
}
},
{
firstName: 'firstName_4_2',
lastName: 'lastName_4_2',
age: 306,
job: {
title: 'job_title_4_2'
},
subRows: [
{
firstName: 'sub 4_2_1',
lastName: 'sub 4_2_1',
job: {
title: 'sub 4_2_1'
}
},
{
firstName: 'sub 4_2_2',
lastName: 'sub 4_2_3',
age: 306,
job: {
title: 'sub 4_2_3'
}
}
]
}
]
}
]);
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
minWidth: 220
},
{
Header: 'Action',
accessor: '-',
Cell: ({ row, toggleRowExpanded }) => {
return row.depth === 0 ? (
<Button
label="Delete"
onClick={() => {
setData(data.filter((v, i) => i !== row.index));
toggleRowExpanded(row.id, false);
}}
/>
) : null;
}
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Age',
accessor: 'age',
align: 'left'
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title'
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city'
}
];
const visualPropertiesStripe = {
isStriped: true,
areColumnsBordered: true
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesStripe}
/>
</div>
);
}
First and Last Column padding
You can align first and last columns using the firstColumnLeftPadding
& lastColumnRightPadding
props under visualProperties
.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
Cell: ({ value }) => <Badge iconName="Edit" label={value} />,
width: 300
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
Footer: () => 100000
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Last name 1',
room: 1
},
{
firstName: 'Very very very long name',
lastName: 'Last name 2',
room: 2
},
{
firstName: 'Random name',
lastName: 'Last name 3',
room: 3
}
];
const visualProperties = {
firstColumnLeftPadding: 24,
lastColumnRightPadding: 0,
}
return (
<>
<div style={{ height: 350 }}>
<Text type="headline1" css={{marginLeft: '25px'}}>Unaligned Table</Text>
<Table
data={data}
columns={columns}
/>
</div>
<div style={{ height: 300 }}>
<Text type="headline1" css={{marginLeft: '25px'}}>Aligned Table</Text>
<Table
data={data}
columns={columns}
visualProperties={visualProperties}
/>
</div>
</>
);
}
Global Filter
You can use this feature to globally search through all columns in the table.
You can use this disableGlobalFilter
feature to disable filtering for a specific column
function MyTable() {
const [searchTerm, setSearchTerm] = useState('');
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
disableGlobalFilter: true
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Room',
accessor: 'room',
align: 'right'
}
];
const data = [
{
firstName: 'John',
lastName: 'Doe',
room: 1
},
{
firstName: 'Sarah',
lastName: 'Cooks',
room: 2
},
{
firstName: 'Yaw',
lastName: 'Amstrong',
room: 3
},
{
firstName: 'Kofi',
lastName: 'Ansah',
room: 4
},
{
firstName: 'Ama',
lastName: 'Doe',
room: 5
},
{
firstName: 'Harsh',
lastName: 'Amazi',
room: 6
}
];
const visualPropertiesStripe = {
isStriped: true
};
return (
<>
<Input
label="Search"
type="search"
value={searchTerm}
onClear={() => {
setSearchTerm('');
}}
onChange={(e) => {
setSearchTerm(e.target.value);
}}
/>
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
searchTerm={searchTerm}
getFilteredData={(filteredValues) => console.log({ filteredValues })}
visualProperties={visualPropertiesStripe}
/>
</div>
</>
);
}
Fixed Columns
You can specify sticky
field within the column configuration to make the column "stick" either to the left or to the right side of the table.
Columns with sticky: 'right'
should be placed in the end of the columns
array.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
sticky: 'left',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 500
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
width: 500
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Some lastName',
room: 1234
},
{
firstName: 'Another name',
lastName: 'Another lastName',
room: 1111
}
];
return (
<div style={{ height: 300 }}>
<Table data={data} columns={columns} measureRowsCount={2} />
</div>
);
}
Flex Table
You can use the flexGrow
prop to determine how much space the desired table columns occupy compared to other columns.
If you use flexGrow: 1
for one column, the other columns will take flexGrow: 0
by default. You are also able to use it for multiple columns to specify flex-grow
order for columns. For different use cases you can look at the examples below.
To enable the flex layout feature for the table you need to give flex
prop to the Table.
If you are using Header Groups and its columns need flexGrow
, then Header Group needs flexGrow
as well.
React-Table does not support flex-layout and column resizing features at the same time. Therefore you are not able to use these features together.
We are following the updates on this issue. We will announce it when it will be possible.
measureRowsCount
prop can't be used with the flex-table.
function MyTable() {
const visualPropertiesBordered = {
areColumnsBordered: true
};
const columnsWithSingleFlexGrow = [
{
Header: 'First Name',
accessor: 'firstName',
flexGrow: 1,
align: 'right'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 200
},
{
Header: 'Room',
accessor: 'room'
}
];
const columnsWithGroupHeader = [
{
Header: 'Room',
accessor: 'room'
},
{
Header: 'Name',
align: 'center',
flexGrow: 3,
columns: [
{
Header: 'First Name',
accessor: 'firstName',
flexGrow: 2,
align: 'left'
},
{
Header: 'Last Name',
accessor: 'lastName',
flexGrow: 1
}
]
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Some lastName',
room: 1234
},
{
firstName: 'Another name',
lastName: 'Another lastName',
room: 1111
}
];
return (
<>
<div style={{ height: 200 }}>
<Table
data={data}
columns={columnsWithSingleFlexGrow}
visualProperties={visualPropertiesBordered}
flex
/>
</div>
<div style={{ height: 200 }}>
<Table
data={data}
columns={columnsWithGroupHeader}
visualProperties={visualPropertiesBordered}
flex
/>
</div>
</>
);
}
By default, header cell and column cells are aligned according to align
property but there're use-cases when header cell needs a different alignment (e.g. RTL text direction). To align header-cell, alignHeader
property can be specified for any column within columns
array.
The alignHeader
property supports the following options (please refer to the table below which shows all of supported options):
left
- defaultleft-reversed
right
right-reversed
center
center-reversed
space-between
space-between-reversed
function MyTable() {
const longText =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
const width = 250;
const popOverProps = {
isOpen: false,
content: <p>Popover content</p>,
iconButtonProps: {
type: 'button',
isHighlighted: true,
iconName: 'Star',
'aria-label': 'test label'
},
onApply: () => console.log('onApply'),
onCancel: () => console.log('onCancel'),
parentOnClickHandler: () => console.log('parentOnClickHandler')
};
const columns = [
{
popOverProps,
width,
sortable: true,
Header: 'Default',
accessor: 'default'
},
{
popOverProps,
width,
sortable: true,
Header: 'Left',
accessor: 'left',
align: 'right',
alignHeader: 'left'
},
{
popOverProps,
width,
sortable: true,
Header: 'Left reversed',
accessor: 'left-reversed',
align: 'right',
alignHeader: 'left-reversed'
},
{
popOverProps,
width,
sortable: true,
Header: 'Right',
accessor: 'right',
align: 'right',
alignHeader: 'right'
},
{
popOverProps,
width,
sortable: true,
Header: 'Right reversed',
accessor: 'right-reversed',
align: 'right',
alignHeader: 'right-reversed'
},
{
popOverProps,
width,
sortable: true,
Header: 'Center',
accessor: 'center',
align: 'center',
alignHeader: 'center'
},
{
popOverProps,
width,
sortable: true,
Header: 'Center reversed',
accessor: 'center-reversed',
align: 'center',
alignHeader: 'center-reversed'
},
{
popOverProps,
width,
sortable: true,
Header: 'Space between',
accessor: 'space-between',
align: 'right',
alignHeader: 'space-between'
},
{
popOverProps,
width: 350,
sortable: true,
Header: 'Space between reversed',
accessor: 'space-between-reversed',
align: 'right',
alignHeader: 'space-between-reversed'
}
];
const data = [
{
default: 'A',
left: 'A',
'left-reversed': 'A',
right: 'A',
'right-reversed': 'A',
center: 'A',
'center-reversed': 'A',
'space-between': 'A',
'space-between-reversed': 'A'
},
{
default: 'B',
left: 'B',
'left-reversed': 'B',
right: 'B',
'right-reversed': 'B',
center: 'B',
'center-reversed': 'B',
'space-between': 'B',
'space-between-reversed': 'B'
},
{
default: longText,
left: longText,
'left-reversed': longText,
right: longText,
'right-reversed': longText,
center: longText,
'center-reversed': longText,
'space-between': longText,
'space-between-reversed': longText
}
];
return (
<div style={{ height: 250 }}>
<Table
data={data}
columns={columns}
activeSorting={{ columnKey: 'default', direction: 'asc' }}
visualProperties={{
areColumnsBordered: true,
hasGreyBackgroundHeader: true
}}
onSortingChange={(columnKey) =>
console.log(`onSortingChange: ${columnKey}`)
}
/>
</div>
);
}
The example below shows a table with grouped headers.
function MyTable() {
const visualPropertiesBordered = {
areColumnsBordered: true
};
const columns = [
{
Header: 'Name',
columns: [
{
Header: 'First',
accessor: 'firstName',
isDraggable: true,
isResizable: true
},
{
Header: 'Last',
accessor: 'lastName',
isDraggable: true,
sortable: false
},
{
Header: 'Given name',
accessor: 'givenName',
isDraggable: true,
sortable: false
}
]
},
{
Header: 'Age',
accessor: 'age',
isDraggable: true,
sortable: false
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title',
isDraggable: true,
sortable: false
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city',
isDraggable: true,
sortable: false
}
];
const initialData = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
givenName: 'givenName_1',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin'
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
givenName: 'givenName_2',
age: 29,
job: {
title: 'job_title_2'
},
'[user].city': 'Sydney'
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
age: 30,
job: {
title: 'job_title_3'
},
'[user].city': 'London'
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
age: 28,
job: {
title: 'job_title_4'
},
'[user].city': 'New york'
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
age: 35,
job: {
title: 'job_title_5'
},
'[user].city': 'Tokyo'
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
age: 32,
job: {
title: 'job_title_6'
},
'[user].city': 'Berlin'
}
];
const sortData = (data, sorting) => {
const { columnKey, direction } = sorting;
return [...data].sort((prev, next) => {
const prevValue = prev[columnKey];
const nextValue = next[columnKey];
const isAscSortingActive = direction === 'asc';
if (prevValue < nextValue) {
return isAscSortingActive ? -1 : 1;
}
if (prevValue > nextValue) {
return isAscSortingActive ? 1 : -1;
}
return 0;
});
};
const initialSorting = {
direction: 'desc'
};
const [data, setData] = useState(sortData(initialData, initialSorting));
const [activeSorting, setActiveSorting] = useState(initialSorting);
const handleSortingChange = (columnKey) => {
const { direction } = activeSorting;
setActiveSorting({
columnKey,
direction: direction === 'asc' ? 'desc' : 'asc'
});
if (!columnKey) {
setData(initialData);
return;
}
const sortedData = sortData(data, activeSorting);
setData(sortedData);
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesBordered}
activeSorting={activeSorting}
onSortingChange={handleSortingChange}
/>
</div>
);
}
You can also hide the header group title by using prop hideHeaderGroupTitle
.
function MyTable() {
const visualPropertiesBordered = {
areColumnsBordered: true
};
const columns = [
{
Header: 'Name',
columns: [
{
Header: 'First',
accessor: 'firstName',
isDraggable: true,
isResizable: true
},
{
Header: 'Last',
accessor: 'lastName',
isDraggable: true,
sortable: false
},
{
Header: 'Given name',
accessor: 'givenName',
isDraggable: true,
sortable: false
}
]
},
{
Header: 'Age',
accessor: 'age',
isDraggable: true,
sortable: false
},
{
Header: 'Job',
accessor: (rows) => rows.job.title,
id: 'title',
isDraggable: true,
sortable: false
},
{
Header: 'City',
accessor: (rows) => rows['[user].city'],
id: '[user].city',
isDraggable: true,
sortable: false
}
];
const initialData = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
givenName: 'givenName_1',
age: 36,
job: {
title: 'job_title_1'
},
'[user].city': 'Berlin'
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
givenName: 'givenName_2',
age: 29,
job: {
title: 'job_title_2'
},
'[user].city': 'Sydney'
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
age: 30,
job: {
title: 'job_title_3'
},
'[user].city': 'London'
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
age: 28,
job: {
title: 'job_title_4'
},
'[user].city': 'New york'
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
age: 35,
job: {
title: 'job_title_5'
},
'[user].city': 'Tokyo'
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
age: 32,
job: {
title: 'job_title_6'
},
'[user].city': 'Berlin'
}
];
const sortData = (data, sorting) => {
const { columnKey, direction } = sorting;
return [...data].sort((prev, next) => {
const prevValue = prev[columnKey];
const nextValue = next[columnKey];
const isAscSortingActive = direction === 'asc';
if (prevValue < nextValue) {
return isAscSortingActive ? -1 : 1;
}
if (prevValue > nextValue) {
return isAscSortingActive ? 1 : -1;
}
return 0;
});
};
const initialSorting = {
direction: 'desc'
};
const [data, setData] = useState(sortData(initialData, initialSorting));
const [activeSorting, setActiveSorting] = useState(initialSorting);
const handleSortingChange = (columnKey) => {
const { direction } = activeSorting;
setActiveSorting({
columnKey,
direction: direction === 'asc' ? 'desc' : 'asc'
});
if (!columnKey) {
setData(initialData);
return;
}
const sortedData = sortData(data, activeSorting);
setData(sortedData);
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesBordered}
activeSorting={activeSorting}
onSortingChange={handleSortingChange}
hideHeaderGroupTitle
/>
</div>
);
}
The prop headerMenuProps
allows you to render a menu that opens on click on the table header title.
function MyTable() {
const testFn = (t) => console.log(t);
const [ispopOverOpen, setispopOverOpen] = useState({});
const handleClick = (id) => (openState) => {
setispopOverOpen({ [id]: openState });
};
ComboBoxRenderer = () => {
const openSnackbar = useContext(SnackbarContext);
const options = [
{ value: 'melon', label: 'Melon' },
{ value: 'honeydew-melon', label: 'Honeydew melon' },
{ value: 'watermelon', label: 'Watermelon' },
{ value: 'apple', label: 'Apple' },
{ value: 'mango', label: 'Mango' }
];
const handleOnChange = (e) => {
openSnackbar({
title: `onChange happened. Value selected is ${e.value}`,
kind: 'positive'
});
};
return (
<ComboBox
options={options}
label="Fruits"
placeholder="Select some fruit"
onChange={handleOnChange}
/>
);
};
const headerMenuProps = {
id: 'abc',
menuItems: [
{
label: 'Duplicate',
onClick: () => {},
iconName: 'Copy'
},
{
label: 'Edit',
onClick: () => {},
iconName: 'Edit'
},
{
type: 'submenu',
label: 'Export',
iconName: 'Download',
submenuItems: [
{ label: 'PNG', onClick: () => {} },
{ label: 'PDF', onClick: () => {} },
{ label: 'CSV', onClick: () => {} }
]
},
{
label: 'Delete',
onClick: () => {},
iconName: 'Trash'
}
]
};
const columns = [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName',
headerMenuProps,
popOverProps: {
content: (
<>
<ComboBoxRenderer />
<Button
onClick={() => handleClick('lastName')(false)}
label="test isOpen prop on Room column"
/>
</>
),
iconButtonProps: {
type: 'button',
isHighlighted: true,
iconName: 'Star',
'aria-label': 'test label'
},
onApply: () => {
testFn('apply');
},
onCancel: () => {
testFn('cancel');
},
isOpen: ispopOverOpen['lastName'],
parentOnClickHandler: handleClick('lastName')
}
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
headerMenuProps
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
room: 1
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
room: 2
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
room: 3
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
room: 4
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
room: 5
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
room: 6
}
];
return (
<SnackbarProvider offsetTop={76}>
<div style={{ height: 300 }}>
<Table data={data} columns={columns} measureRowsCount={6} />
</div>
</SnackbarProvider>
);
}
The prop isWithoutHeader
is used for hiding table headers.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 200
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
width: 500
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Some lastName',
room: 1234
},
{
firstName: 'Another name',
lastName: 'Another lastName',
room: 1111
}
];
return (
<div style={{ height: 160 }}>
<Table data={data} columns={columns} isHeaderless={true} />
</div>
);
}
Hover Highlighting
isHoverable
can be used to toggle hover highlighting on table rows. By default, it is set to false.
function MyTable() {
const data = [
{
name: 'Just a name',
role: 'User'
},
{
name: 'Just another name',
role: 'Admin'
}
];
const columns = [
{
Header: 'Name',
accessor: 'name'
},
{
Header: 'Role',
accessor: 'role'
}
];
return (
<div
style={{
height: 300
}}
>
<Table data={data} columns={columns} rowSize="small" isHoverable />
</div>
);
}
Inline editing in Table
You can have inline editing in Table by using updateData
prop and defining custom cell which includes needed editable component such as Input
, ComboBox
, Checkbox
, etc.
function MyTable() {
const InputCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
updateData(index, id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<Input
aria-label="age"
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
const columns = [
{
Header: 'Name',
accessor: 'name',
Cell: InputCell,
isEditable: true
},
{
Header: 'Age',
accessor: 'age'
},
{
Header: 'Team',
accessor: 'team',
Cell: InputCell,
isEditable: true
}
];
const initData = [
{
name: 'Name_1',
age: 36,
team: 'team_1'
},
{
name: 'Name_2',
age: 29,
team: 'team_2'
},
{
name: 'Name_3',
age: 30,
team: 'team_3'
},
{
name: 'Name_4',
age: 28,
team: 'team_4'
},
{
name: 'Name_5',
age: 35,
team: 'team_5'
},
{
name: 'Name_6',
age: 32,
team: 'team_6'
}
];
const [data, setData] = React.useState(initData);
const updateData = (rowIndex, columnId, value) => {
setData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
return (
<div style={{ height: 300 }}>
<Table data={data} updateData={updateData} columns={columns} />
</div>
);
}
Here is another example with more editable cells with ComboBox, Checkbox, SegmentedButton and nested Objects:
function MyTable() {
const InputCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
updateData(index, id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<Input
aria-label="age"
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
const SegmentedButtonCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const cities = [
{ value: 'shanghai', label: 'Shanghai' },
{ value: 'moscow', label: 'Moscow' },
{ value: 'berlin', label: 'Berlin' },
{ value: 'paris', label: 'Paris' }
];
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
const updatedValue = e.target.value;
setValue(updatedValue);
updateData(index, id, updatedValue);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <SegmentedButton items={cities} value={value} onChange={onChange} />;
};
const JobCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
const { name, value } = e.target;
setValue((prevState) => ({
...prevState,
[name]: value
}));
};
const onBlur = () => {
updateData(index, id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<div style={{ display: 'flex' }}>
<Input
css={{ marginRight: 8 }}
aria-label="age"
name="title"
value={value.title}
onChange={onChange}
onBlur={onBlur}
/>
<Input
aria-label="age"
name="team"
value={value.team}
onChange={onChange}
onBlur={onBlur}
/>
</div>
);
};
const EditableCheckboxCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.checked);
updateData(index, id, e.target.checked);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <Checkbox aria-label="job" checked={value} onChange={onChange} />;
};
const EditableComboBoxCell = ({
value: initialValue,
row: { index },
column: { id },
updateData
}) => {
const options = [
{ value: 'notDefined', label: 'Not defined' },
{ value: 'female', label: 'Female' },
{ value: 'male', label: 'Male' }
];
const [value, setValue] = React.useState({
label: initialValue,
value: initialValue
});
const onBlur = () => {
updateData(index, id, value.value);
};
React.useEffect(() => {
const updatedValue = { label: initialValue, value: initialValue };
setValue(updatedValue);
}, [initialValue]);
return (
<ComboBox
aria-label="Gender"
options={options}
value={value}
placeholder="Gender"
onChange={(value) => setValue(value)}
onBlur={onBlur}
/>
);
};
const columns = [
{
Header: 'Name',
accessor: 'name',
Cell: InputCell,
isEditable: true
},
{
Header: 'Gender',
accessor: 'gender',
Cell: EditableComboBoxCell,
isEditable: true
},
{
Header: 'Job',
accessor: (row) => row.job,
id: 'job',
Cell: JobCell,
isEditable: true,
width: 300
},
{
Header: 'City',
accessor: 'city',
Cell: SegmentedButtonCell,
isEditable: true,
width: 320
},
{
Header: 'Is Active',
accessor: 'isActive',
Cell: EditableCheckboxCell,
isEditable: true
}
];
const initData = [
{
name: 'Name_1',
age: 36,
gender: 'notDefined',
city: 'berlin',
job: {
title: 'job_title_1',
team: 'team_1'
},
isActive: true
},
{
name: 'Name_2',
age: 29,
gender: 'male',
city: 'paris',
job: {
title: 'job_title_2',
team: 'team_2'
},
isActive: false
},
{
name: 'Name_3',
age: 30,
gender: 'female',
city: 'moscow',
job: {
title: 'job_title_3',
team: 'team_3'
},
isActive: true
},
{
name: 'Name_4',
age: 28,
gender: 'notDefined',
city: 'berlin',
job: {
title: 'job_title_4',
team: 'team_4'
},
isActive: false
},
{
name: 'Name_5',
age: 35,
gender: 'female',
city: 'paris',
job: {
title: 'job_title_5',
team: 'team_5'
},
isActive: true
},
{
name: 'Name_6',
age: 32,
gender: 'male',
city: 'berlin',
job: {
title: 'job_title_6',
team: 'team_6'
},
isActive: true
}
];
const [data, setData] = React.useState(initData);
const updateData = (rowIndex, columnId, value) => {
setData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
return (
<div style={{ height: 300 }}>
<Table data={data} updateData={updateData} columns={columns} />
</div>
);
}
Loading More Data (aka Infinite loading)
Table
supports infinite loading by calling onLoadMoreData
callback when Table gets scrolled to the end. To integrate infinite loading with React Query please refer to useInfiniteQuery hook and this example.
function MyTable() {
const generateMoreData = useCallback(
(offset = 0, chunkSize = 25) =>
Array.from({ length: chunkSize }, (_, index) => {
const id = offset + index + 1;
return {
firstName: `firstName_${id}`,
lastName: `lastName_${id}`,
points: id
};
}),
[]
);
const initialData = useMemo(() => generateMoreData(), [generateMoreData]);
const [data, setData] = useState(initialData);
const dataLength = data.length;
const [isLoadingMoreData, setIsLoadingMoreData] = useState(false);
const columns = useMemo(
() => [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 300,
Cell: ({ value }) => <Badge label={value} />
},
{
Header: 'Points',
accessor: 'points',
align: 'right'
}
],
[]
);
const onLoadMoreData = useCallback(() => {
setIsLoadingMoreData(true);
setTimeout(() => {
const generatedData = generateMoreData(dataLength);
setData((prevData) => [...prevData, ...generatedData]);
setIsLoadingMoreData(false);
}, 3000);
}, [dataLength, generateMoreData]);
const maxNumberOrRows = 200;
return (
<div style={{ height: 600 }}>
<Table
data={data}
columns={columns}
hasMoreData={dataLength <= maxNumberOrRows}
isLoadingMoreData={isLoadingMoreData}
onLoadMoreData={onLoadMoreData}
/>
</div>
);
}
Loading State
You can active a loading state for the table by passing isLoading
to be true. This helps users to see a skeleton cell when data is loading.
Also the loader height adapts to the typeScales of cellTypeScale and width is random, and also adapts the column alignments.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName',
align: 'center'
},
{
Header: 'Room',
accessor: 'room',
align: 'right'
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
room: 1
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
room: 2
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
room: 3
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
room: 4
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
room: 5
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
room: 6
}
];
const visualPropertiesStripe = {
isStriped: true
};
const typeScales = {
cellTypeScale: 2
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
isLoading
visualProperties={visualPropertiesStripe}
typeScales={typeScales}
/>
</div>
);
}
Page level Scrolling
Use pageLevelScroll
prop to let the table manage the window scroll to scroll through the items.
align
on Table columns
prop can't be used with flex
and pageLevelScroll
at the same time unless the table header takes the full width
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Footer: () => 'This is a footer'
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Room',
accessor: 'room',
align: 'right'
},
{
Header: 'points',
accessor: 'points',
align: 'right'
},
{
Header: 'cats',
accessor: 'cats',
align: 'right'
},
{
Header: 'birds',
accessor: 'birds',
align: 'right'
},
{
Header: 'fruits',
accessor: 'fruits',
align: 'right'
}
];
const data = Array.from({ length: 20 }, (_, index) => ({
firstName: `firstName_${index + 1}`,
lastName: `lastName_${index + 1}`,
room: index + 1
}));
return (
<div style={{ width: 600 }}>
<Table data={data} columns={columns} pageLevelScroll topOffset={60} />
</div>
);
}
The prop popOverProps
renders an button that opens a popOver on the table header. When the methods onClear
, onApply
and onCancel
are passed, the popOver renders a footer with button actions that onClick also close the popOver. You can also pass some of the IconButton
props in order to customize it.
function MyTable() {
const testFn = (t) => console.log(t);
const [ispopOverOpen, setispopOverOpen] = useState({});
const handleClick = (id) => (openState) => {
setispopOverOpen({ [id]: openState });
};
ComboBoxRenderer = () => {
const openSnackbar = useContext(SnackbarContext);
const options = [
{ value: 'melon', label: 'Melon' },
{ value: 'honeydew-melon', label: 'Honeydew melon' },
{ value: 'watermelon', label: 'Watermelon' },
{ value: 'apple', label: 'Apple' },
{ value: 'mango', label: 'Mango' }
];
const handleOnChange = (e) => {
openSnackbar({
title: `onChange happened. Value selected is ${e.value}`,
kind: 'positive'
});
};
return (
<ComboBox
options={options}
label="Fruits"
placeholder="Select some fruit"
onChange={handleOnChange}
/>
);
};
const columns = [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName',
popOverProps: {
content: (
<>
<ComboBoxRenderer />
<Button
onClick={() => handleClick('lastName')(false)}
label="test isOpen prop on Room column"
/>
</>
),
iconButtonProps: {
type: 'button',
isHighlighted: true,
iconName: 'Star',
'aria-label': 'test label'
},
onApply: () => {
testFn('apply');
},
onCancel: () => {
testFn('cancel');
},
isOpen: ispopOverOpen['lastName'],
parentOnClickHandler: handleClick('lastName')
}
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
popOverProps: {
content: (
<>
<ComboBoxRenderer />
<Button
onClick={() => handleClick('room')(false)}
label="test isOpen prop on Room column"
/>
</>
),
iconButtonProps: {
type: 'button',
isHighlighted: true,
iconName: 'Star',
'aria-label': 'test label'
},
onApply: () => {
testFn('apply');
},
onCancel: () => {
testFn('cancel');
},
isOpen: ispopOverOpen['room'],
parentOnClickHandler: handleClick('room')
}
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
room: 1
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
room: 2
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
room: 3
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
room: 4
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
room: 5
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
room: 6
}
];
return (
<SnackbarProvider offsetTop={76}>
<div style={{ height: 300 }}>
<Table data={data} columns={columns} measureRowsCount={6} />
</div>
</SnackbarProvider>
);
}
Row onClick Handler
You can pass a method to the table that would return the Row object on click of a row.
Be mindful of onClick events on the cell content when using this feature.
Check the code for an example of a cell with a button that includes a `e.stopPropagation` call to avoid triggering the row method when the button is clicked.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
Cell: ({ value }) => (
<div onClick={(e) => e.stopPropagation()}>
<Button kind="primary" label="Button" />
</div>
)
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Room',
accessor: 'room',
align: 'right'
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
room: 1
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
room: 2
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
room: 3
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
room: 4
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
room: 5
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
room: 6,
country: 'France'
}
];
const rowOnClickHandler = (rowData) => {
console.log(rowData);
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
rowOnClickHandler={rowOnClickHandler}
/>
</div>
);
}
RowSize
The table component is available in two sizes: small
and medium
.
function MyTable() {
const longText =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
const data = [
{
name: 'Just a name',
role: 'User'
},
{
name: longText,
role: 'Admin'
}
];
const columns = [
{
Header: 'Name',
accessor: 'name'
},
{
Header: longText,
accessor: 'role'
}
];
return (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: Spacing20,
height: 300
}}
>
<Table data={data} columns={columns} rowSize="small" />
<Table data={data} columns={columns} rowSize="medium" />
</div>
);
}
Sorting
The table always shows data in the provided order and the actual sorting functionality should be implemented on your side.
You can specify the activeSorting
to visually highlight the sorting for a column and onSortingChange
to handle clicks within the header cell.
onSortingChange
will return the columnKey and sorting direction will be determined by the activeSorting you provide.
You can disable sorting for a specific column by specifying sortable
in the columns
array.
You can also provide multiple values with activeSorting
prop to visually highlight the active sorting of multiple columns. Please check initialSortingMultiple
in the below example.
function MyTable() {
const columns = [
{
Header: 'Level',
accessor: 'level'
},
{
Header: 'Count',
accessor: 'count'
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
sortable: false
}
];
const data = [
{
count: 2,
level: 'A',
room: 1
},
{
count: 5,
level: 'B',
room: 3
},
{
count: 3,
level: 'C',
room: 2
}
];
const dataMultiSort = [
{
count: 2,
level: 'C',
room: 1
},
{
count: 3,
level: 'B',
room: 3
},
{
count: 5,
level: 'A',
room: 2
}
];
const initialSorting = {
columnKey: 'level',
direction: 'asc'
};
const initialSortingMultiple = [
{
columnKey: 'count',
direction: 'asc'
},
{
columnKey: 'level',
direction: 'desc'
}
];
const handleSortingChange = (columnKey) => {
};
return (
<>
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
activeSorting={initialSorting}
onSortingChange={handleSortingChange}
/>
</div>
<div style={{ height: 300 }}>
<Table
data={dataMultiSort}
columns={columns}
activeSorting={initialSortingMultiple}
onSortingChange={handleSortingChange}
/>
</div>
</>
);
}
Type Scales
Typescale is the combination of font-size, line-height, and letter-spacing. By default, the type scale of all cells is defined by the size variant of the table.
Adding typeScales
enforces a distinct type scale for the specific table cell(header, body or footer).
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
sticky: 'left',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 500
},
{
Header: 'Room',
accessor: 'room',
align: 'right',
width: 500
}
];
const data = [
{
firstName: 'Just a name',
lastName: 'Some lastName',
room: 1234
},
{
firstName: 'Another name',
lastName: 'Another lastName',
room: 1111
}
];
const typeScales = {
headerTypeScale: 1,
cellTypeScale: 2,
footerTypeScale: 2
};
return (
<div
style={{
height: 300
}}
>
<Table data={data} columns={columns} typeScales={typeScales} />
</div>
);
}
UnstickThreshold
This component supports the automatic limitation of fixed columns based on its width. You may need to use a more scrollable area for non-fixed columns when having both fixed and non-fixed columns together in the Table.
Use unstickThreshold
to specify the percentage of the limitation for fixed columns. For instance, if unstickThreshold
is 30
, the Table will change fixed columns to non-fixed when the total width of fixed columns is greater than 30%
of the table width.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName',
sticky: 'left',
Footer: () => 'Total'
},
{
Header: 'Last Name',
accessor: 'lastName',
sticky: 'left',
width: 200
},
{
Header: 'Job',
accessor: 'job',
width: 200
},
{
Header: 'Age',
accessor: 'age',
width: 200
},
{
Header: 'City',
accessor: 'city',
width: 200
},
{
Header: 'Gender',
accessor: 'gender',
width: 200
},
{
Header: 'Birthdate',
accessor: 'birthdate',
align: 'right',
width: 500
}
];
const data = [
{
firstName: 'name',
lastName: 'lastName',
job: 'engineer',
age: 34,
city: 'Berlin',
gender: 'male',
birthdate: 'November'
},
{
firstName: 'name two',
lastName: 'lastName two',
job: 'developer',
age: 28,
city: 'Berlin',
gender: 'female',
birthdate: 'December'
}
];
return (
<div style={{ height: 200 }}>
<Table
data={data}
columns={columns}
unstickThreshold={50}
/>
</div>
);
}
Updating Table with Different Instances
The example below shows a table that re renders with updated columns and data
function MyTable() {
const [tableData, setTableData] = useState([]);
const [tableColumns, setTableColumns] = useState([]);
const options = [
{
label: 'Foo',
value: 'foo'
},
{
label: 'Bar',
value: 'bar'
}
];
const columnsFoo = [
{
Header: 'First Name',
accessor: 'firstName',
isResizable: true
},
{
Header: 'Last Name',
accessor: 'lastName',
width: 200
}
];
const columnsBar = [
{
Header: 'Description',
accessor: 'description'
},
{
Header: 'Author',
accessor: 'author',
width: 300
},
{
Header: 'City',
accessor: 'city'
}
];
const dataFoo = [
{
firstName: 'Just a name',
lastName: 'Some lastName'
},
{
firstName: 'Another name',
lastName: 'Another lastName'
}
];
const dataBar = [
{
description:
'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...',
author: 'Quiche Hollandaise',
city: 'LA'
},
{
description: 'Lorem Ipsum is not simply random text',
author: 'Ravi O`Leigh',
city: 'NYC'
}
];
useEffect(() => {
setTableData(dataFoo);
setTableColumns(columnsFoo);
}, []);
const onChangeTableData = (selectedOption) => {
const { value } = selectedOption;
if (value === 'bar') {
setTableColumns(columnsBar);
setTableData(dataBar);
} else {
setTableColumns(columnsFoo);
setTableData(dataFoo);
}
};
return (
<>
<div
style={{
display: 'grid',
gridTemplateColumns: '.5fr 1fr',
gap: Spacing20,
height: 60
}}
>
<ComboBox
options={options}
onChange={onChangeTableData}
placeholder="Change Table Data"
aria-label="change-table-data"
/>
</div>
<div style={{ height: 250 }}>
<Table data={tableData} columns={tableColumns} measureRowsCount={1} />
</div>
</>
);
}
VisualProperties
VisualProperties are used to show visual representation of records. For instance, showing bordered
columns or greyBackgroundHeader
.
function MyTable() {
const data = [
{
name: 'Just a name',
role: 'User'
},
{
name: 'Just another name',
role: 'Admin'
}
];
const columns = [
{
Header: 'Name',
accessor: 'name'
},
{
Header: 'Role',
accessor: 'role',
isDraggable: true
}
];
const visualPropertiesBordered = {
areColumnsBordered: true
};
const visualPropertiesGreyHeader = {
hasGreyBackgroundHeader: true
};
const visualPropertiesGreyHeaderAndRow = {
hasGreyBackgroundHeader: true,
hasGreyBackgroundRow: true
};
const visualPropertiesDisabledRowBorder = {
disableRowBorder: true
};
return (
<div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: Spacing20,
height: 320
}}
>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesBordered}
/>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesGreyHeader}
/>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesGreyHeaderAndRow}
/>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesDisabledRowBorder}
/>
</div>
</div>
);
}
Zebra Stripe
You can activate zebra-striping of rows by adding isStriped
to the visualProperties. This helps users orient themselves in long and complex tables.
function MyTable() {
const columns = [
{
Header: 'First Name',
accessor: 'firstName'
},
{
Header: 'Last Name',
accessor: 'lastName'
},
{
Header: 'Room',
accessor: 'room',
align: 'right'
}
];
const data = [
{
firstName: 'firstName_1',
lastName: 'lastName_1',
room: 1
},
{
firstName: 'firstName_2',
lastName: 'lastName_2',
room: 2
},
{
firstName: 'firstName_3',
lastName: 'lastName_3',
room: 3
},
{
firstName: 'firstName_4',
lastName: 'lastName_4',
room: 4
},
{
firstName: 'firstName_5',
lastName: 'lastName_5',
room: 5
},
{
firstName: 'firstName_6',
lastName: 'lastName_6',
room: 6
}
];
const visualPropertiesStripe = {
isStriped: true
};
return (
<div style={{ height: 300 }}>
<Table
data={data}
columns={columns}
visualProperties={visualPropertiesStripe}
/>
</div>
);
}
Props
|
data * The data array that you want to display on the Table . It should be memoized. | Record<string, unknown>[]
| — |
columns * The core columns configuration object for the entire Table . It should be memoized. | | — |
updateData The callback to use for updating table data. the use case is inline editing mode of Table which requires updateData callback | Record<string, unknown>[]
| — |
measureRowsCount If measureRowsCount is provided, columns will be automatically adjusting their width based on the longest cell within the range of the provided number.
The Table will include the `Header` and `Footer` rows in the measurements as well. | | — |
activeSorting The sorting applied to the Table . | ColumnSorting | ColumnSorting[]
| — |
onSortingChange The handler will be called when a user clicks on a header cell | ((columnName: string) => void)
| — |
rowSize The size of rows | | "small" |
visualProperties Provide visual properties to Table | | — |
pageLevelScroll If set to true the content of the table will be scrolled by the whole page scrollbar | | false |
topOffset The offset at the top of the page that needs to be considered when page level scrolling is enabled | | 0 |
unstickThreshold The maximum percentage value that fixed columns can occupy over the table width. If the total fixed columns width exceeds this value, they will automatically turn into unfixed columns | | — |
isHoverable If set to true, the table body rows can show hover state | | false |
isHeaderless If set to true table headers will be hidden | | — |
hideHeaderGroupTitle If set to true table group header title will be hidden | | false |
getHoveredRowId pass this method to access the id of the row being hovered | | — |
onColumnRearrange Callback prop onColumnRearrange when dropping column on rearrange | ((columnsOrder: string[]) => void)
| — |
onColumnResized Callback prop onColumnResized when column is resized | ((column: TableColumn) => void)
| — |
flex If set to true table will use flex layout | | — |
rowOnClickHandler Handler that returns row data on click | ((rowData: RowData) => void)
| — |
isLoading Show loading state of the table | | false |
typeScales Provide type scales for the table head, body and footer | | — |
searchTerm Search term to be used to do global filtering of data | | — |
getFilteredData callback to get filtered data after global filter is applied | ((filteredData?: RowData[]) => void)
| — |
getCellProps callback to get cell props | | "() => ({})" |
loadingMoreDataLabel Custom label to be shown while loading more data | | — |
emptyState The props of Empty State component, it will be shown if there is no data | Omit<EmptyStateProps, "size" | "background" | "iconKey">
| — |
isLoadingMoreData More data is being loaded | | — |
hasMoreData There's still some data to be loaded | | — |
onLoadMoreData It's called when user scrolls to the end of table in order to load more data | | — |
autoRowsHeight If set to true, table body rows height will grow respectively to content height | | — |
data-{foo} Data attributes can be used by testing libraries to retrieve components or assert their existence | | — |
* - the prop is required. |