Skip to main content

Table

Result
Loading...
Live Editor
<Table>
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
			<TableCell>Header 2</TableCell>
			<TableCell>Header 3</TableCell>
			<TableCell>Header 4</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>1:1</TableCell>
			<TableCell>2:1</TableCell>
			<TableCell>3:1</TableCell>
			<TableCell>4:1</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>1:2</TableCell>
			<TableCell>2:2</TableCell>
			<TableCell>3:2</TableCell>
			<TableCell>4:2</TableCell>
		</TableRow>
	</TableBody>
</Table>

Variants

The table-<variant name> classes can be applied to the table, row, or cell to change the background color of the elements.

Tables

Result
Loading...
Live Editor
const variants = ['primary', 'secondary', 'success', 'warning', 'danger', 'info', 'light', 'dark'];

render(
	<>
		{variants.map((variant) => (
			<Table className={`table-${variant} mb-3`} key={variant}>
				<TableHeader>
					<TableRow>
						<TableCell>Header {`.table-${variant}`}</TableCell>
					</TableRow>
				</TableHeader>
				<TableBody>
					<TableRow>
						<TableCell>1</TableCell>
					</TableRow>
				</TableBody>
			</Table>
		))}
	</>,
);

Rows

Result
Loading...
Live Editor
const variants = ['primary', 'secondary', 'success', 'warning', 'danger', 'info', 'light', 'dark'];

render(
	<Table className="mb-3">
		<TableHeader>
			<TableRow>
				<TableCell>Header 1</TableCell>
			</TableRow>
		</TableHeader>
		<TableBody>
			{variants.map((variant, index) => (
				<TableRow key={variant} className={`table-${variant}`}>
					<TableCell>Cell {`.table-${variant}`}</TableCell>
				</TableRow>
			))}
		</TableBody>
	</Table>,
);

Cells

Result
Loading...
Live Editor
const variants = ['primary', 'secondary', 'success', 'warning', 'danger', 'info', 'light', 'dark'];

render(
	<Table className="mb-3">
		<TableHeader>
			<TableRow>
				<TableCell>Header 1</TableCell>
				<TableCell>Header 2</TableCell>
				<TableCell>Header 3</TableCell>
				<TableCell>Header 4</TableCell>
				<TableCell>Header 5</TableCell>
				<TableCell>Header 6</TableCell>
				<TableCell>Header 7</TableCell>
				<TableCell>Header 8</TableCell>
			</TableRow>
		</TableHeader>
		<TableBody>
			<TableRow>
				{variants.map((variant, index) => (
					<TableCell key={variant} className={`table-${variant}`}>
						{index + 1}
					</TableCell>
				))}
			</TableRow>
		</TableBody>
	</Table>,
);

Header Radii

Result
Loading...
Live Editor
<>
	{['square', 'rounded'].map((radius) => (
		<Table className="mb-3" key={radius}>
			<TableHeader radius={radius}>
				<TableRow>
					<TableCell>Header 1</TableCell>
					<TableCell>Header 2</TableCell>
					<TableCell>Header 3</TableCell>
					<TableCell>Header 4</TableCell>
				</TableRow>
			</TableHeader>
			<TableBody>
				<TableRow>
					<TableCell>1</TableCell>
					<TableCell>2</TableCell>
					<TableCell>3</TableCell>
					<TableCell>4</TableCell>
				</TableRow>
				<TableRow>
					<TableCell>1</TableCell>
					<TableCell>2</TableCell>
					<TableCell>3</TableCell>
					<TableCell>4</TableCell>
				</TableRow>
			</TableBody>
		</Table>
	))}
</>

Sizes

Result
Loading...
Live Editor
<>
	{['print', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].map((size) => (
		<div key={size}>
			<h3>{size}</h3>
			<Table className="mb-3" size={size} key={size}>
				<TableHeader>
					<TableRow>
						<TableCell>Header 1</TableCell>
						<TableCell>Header 2</TableCell>
						<TableCell>Header 3</TableCell>
						<TableCell>Header 4</TableCell>
						<TableCell>Header 5</TableCell>
						<TableCell>Header 6</TableCell>
						<TableCell>Header 7</TableCell>
					</TableRow>
				</TableHeader>
				<TableBody>
					<TableRow>
						<TableCell>1</TableCell>
						<TableCell>2</TableCell>
						<TableCell>3</TableCell>
						<TableCell>4</TableCell>
						<TableCell>5</TableCell>
						<TableCell>6</TableCell>
						<TableCell>7</TableCell>
					</TableRow>
				</TableBody>
			</Table>
		</div>
	))}
</>

Striped

Rows

Result
Loading...
Live Editor
<Table striped="rows">
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>1</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>2</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>3</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>4</TableCell>
		</TableRow>
	</TableBody>
</Table>

Columns

Result
Loading...
Live Editor
<Table striped="columns">
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
			<TableCell>Header 2</TableCell>
			<TableCell>Header 3</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>1</TableCell>
			<TableCell>2</TableCell>
			<TableCell>3</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>1</TableCell>
			<TableCell>2</TableCell>
			<TableCell>3</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>1</TableCell>
			<TableCell>2</TableCell>
			<TableCell>3</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>1</TableCell>
			<TableCell>2</TableCell>
			<TableCell>3</TableCell>
		</TableRow>
	</TableBody>
</Table>

Collapsible Tables

Kyber Tables provides stateful expand/collapse functionality through the collapsible prop and the TableRowToggle component. Collapsible rows are managed by the row's ids and the TableRowToggle's forRows prop.

For detailed props of collapsible tables see the withCollapsibleState HOC.

When creating collapsible rows considering the following best practices:

  1. A <TableRow /> that contains a <TableRowToggle /> should be adjacent to its collapsible row. There's no requirement that a <TableRow /> with a <TableRowToggle /> be followed by its managed collapsible rows, however, we consider it a best practice to keep them together.

If a row is intended to be collapsible, it MUST receive the collapsible prop.

Result
Loading...
Live Editor
<Table collapsible>
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
			<TableCell>Header 2</TableCell>
			<TableCell>Header 3</TableCell>
			<TableCell>Header 4</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>Cell 1</TableCell>
			<TableCell>Cell 2</TableCell>
			<TableCell>Cell 3</TableCell>
			<TableCell>
				<TableRowToggle id="toggle-1" forRows={['collapsible-area-1']} />
			</TableCell>
		</TableRow>
		<TableRow id="collapsible-area-1" collapsible>
			<TableCell colSpan="4">Collapsible area</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>Cell 5</TableCell>
			<TableCell>Cell 6</TableCell>
			<TableCell>Cell 7</TableCell>
			<TableCell>
				<TableRowToggle id="toggle-2" forRows={['collapsible-area-2']} />
			</TableCell>
		</TableRow>
		<TableRow id="collapsible-area-2" collapsible>
			<TableCell colSpan="4">Collapsible area</TableCell>
		</TableRow>
	</TableBody>
</Table>

Controlling Multiple Collapsible Rows

It's also possible to use the <TableRowToggle/>'s forRows prop to toggle multiple collapsible areas within a table.

Result
Loading...
Live Editor
<Table collapsible>
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
			<TableCell>Header 2</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>Cell 1</TableCell>
			<TableCell>
				<TableRowToggle
					id="mutli-area-toggle"
					forRows={['multi-area-1', 'multi-area-2', 'multi-area-3', 'multi-area-4']}
				/>
			</TableCell>
		</TableRow>
		<TableRow id="multi-area-1" collapsible>
			<TableCell colSpan="2">Collapsible area 1</TableCell>
		</TableRow>
		<TableRow id="multi-area-2" collapsible>
			<TableCell colSpan="2">Collapsible area 2</TableCell>
		</TableRow>
		<TableRow id="multi-area-3" collapsible>
			<TableCell colSpan="2">Collapsible area 3</TableCell>
		</TableRow>
		<TableRow id="multi-area-4" collapsible>
			<TableCell colSpan="2">Collapsible area 4</TableCell>
		</TableRow>
	</TableBody>
</Table>

Sortable Tables

Kyber Tables provides stateful sort UI controls through the sortable props on <Table /> and <TableCell /> components. Kyber only maintains UI control over the presentation of <TableCell />s within a <TableHeader />. The individual rows of the <Table /> must be sorted by the user.

For detailed props of sortable tables see the withSortableState HOC.

Result
Loading...
Live Editor
const columns = [
	{
		title: 'Name',
		dataKey: 'name',
	},
	{
		title: 'Age',
		dataKey: 'age',
	},
];

const rows = [
	{
		name: 'Ada Lovelace',
		age: 36,
	},
	{
		name: 'Lord Byron',
		age: 36,
	},
	{
		name: 'Charles Babbage',
		age: 79,
	},
	{
		name: 'Marie Curie',
		age: 66,
	},
];

function sortById(id) {
	return (a, b) => {
		let x = a[id];
		let y = b[id];

		if (typeof x === 'string') {
			x = x.toLowerCase();
		}
		if (typeof y === 'string') {
			y = y.toLowerCase();
		}

		if (x < y) {
			return -1;
		} else if (x > y) {
			return 1;
		} else {
			return 0;
		}
	};
}

function sort(data, id, direction) {
	const sorted = data.sort(sortById(id));

	if (direction === 'desc') {
		sorted.reverse();
	}

	return sorted;
}

function Example() {
	const [sortedData, setSortedData] = React.useState(rows);

	return (
		<Table
			sortable
			onClickSortLabel={(event, { sortId, sortDirection }) => {
				setSortedData([...sort(sortedData, sortId, sortDirection)]);
			}}
		>
			<TableHeader>
				<TableRow>
					<TableCell id="demo-component-sortable-name" sortable>
						Name
					</TableCell>
					<TableCell id="demo-component-sortable-age" sortable>
						Age
					</TableCell>
				</TableRow>
			</TableHeader>
			<TableBody>
				{sortedData.map((row) => (
					<TableRow key={`row-${row.name}`}>
						{columns.map((column) => (
							<TableCell key={`row-${row.name}-${column.dataKey}`}>{row[column.dataKey]}</TableCell>
						))}
					</TableRow>
				))}
			</TableBody>
		</Table>
	);
}

render(<Example />);

Sortable and Collapsible Tables

Result
Loading...
Live Editor
const columns = [
	{
		title: 'Name',
		dataKey: 'name',
	},
	{
		title: 'Age',
		dataKey: 'age',
	},
];

const rows = [
	{
		name: 'Ada Lovelace',
		age: 36,
	},
	{
		name: 'Lord Byron',
		age: 36,
	},
	{
		name: 'Charles Babbage',
		age: 79,
	},
	{
		name: 'Marie Curie',
		age: 66,
	},
];

function sortById(id) {
	return (a, b) => {
		let x = a[id];
		let y = b[id];

		if (typeof x === 'string') {
			x = x.toLowerCase();
		}
		if (typeof y === 'string') {
			y = y.toLowerCase();
		}

		if (x < y) {
			return -1;
		} else if (x > y) {
			return 1;
		} else {
			return 0;
		}
	};
}

function sort(data, id, direction) {
	const sorted = data.sort(sortById(id));

	if (direction === 'desc') {
		sorted.reverse();
	}

	return sorted;
}

function Example() {
	const [sortedData, setSortedData] = React.useState(rows);

	return (
		<Table
			sortable
			collapsible
			onClickSortLabel={(event, { sortId, sortDirection }) => {
				setSortedData([...sort(sortedData, sortId, sortDirection)]);
			}}
		>
			<TableHeader>
				<TableRow>
					<TableCell id="name" sortable>
						Name
					</TableCell>
					<TableCell id="age" sortable>
						Age
					</TableCell>
					<TableCell id="collapsible" />
				</TableRow>
			</TableHeader>
			<TableBody>
				{sortedData.map((row) => (
					<>
						<TableRow key={`row-${row.name}`}>
							{columns.map((column) => (
								<TableCell key={`row-${row.name}-${column.dataKey}`}>
									{row[column.dataKey]}
								</TableCell>
							))}
							<TableCell>
								<TableRowToggle
									id={`toggle-${row.name}`}
									forRows={[`collapsible-row-${row.name}`]}
								/>
							</TableCell>
						</TableRow>
						<TableRow collapsible id={`collapsible-row-${row.name}`}>
							<TableCell colSpan="3">{row.name}</TableCell>
						</TableRow>
					</>
				))}
			</TableBody>
		</Table>
	);
}

render(<Example />);

Fixed Tables

The Kyber Table's Table component provides the fixedHeader and fixedFooter props that can be used to create a table with fixed headers, footers.

Result
Loading...
Live Editor
<div style={{ maxHeight: 500, overflowY: 'auto' }}>
	<Table fixedHeader fixedFooter>
		<TableHeader variant="light">
			<TableRow>
				<TableCell>Header 1</TableCell>
				<TableCell>Header 2</TableCell>
				<TableCell>Header 3</TableCell>
			</TableRow>
		</TableHeader>
		<TableBody>
			{Array.from(Array(100).keys()).map((i) => (
				<TableRow key={`row-${i}`}>
					<TableCell>Cell 1</TableCell>
					<TableCell>Cell 2</TableCell>
					<TableCell>Cell 3</TableCell>
				</TableRow>
			))}
		</TableBody>
		<TableFooter>
			<TableRow>
				<TableCell>Footer 1</TableCell>
				<TableCell>Footer 2</TableCell>
				<TableCell>Footer 3</TableCell>
			</TableRow>
		</TableFooter>
	</Table>
</div>

Fixed columns

Kyber tables support any number of fixed columns. The fixedColumns prop can be used to specify the number of columns to fix to the left edge of the table.

Result
Loading...
Live Editor
<>
	<div style={{ maxWidth: '100%', overflowX: 'auto' }} className="k-mb-3">
		<Table fixedColumns={1}>
			<TableHeader>
				<TableRow>
					{Array.from(Array(25).keys()).map((i) => (
						<TableCell key={`header-${i}`} className="text-nowrap">
							Header {i}
						</TableCell>
					))}
				</TableRow>
			</TableHeader>
			<TableBody>
				{Array.from(Array(3).keys()).map((i) => (
					<TableRow key={`row-${i}`}>
						{Array.from(Array(25).keys()).map((k) => (
							<TableCell key={`cell-${k}`}>Cell {k}</TableCell>
						))}
					</TableRow>
				))}
			</TableBody>
		</Table>
	</div>
	<div style={{ maxWidth: '100%', overflowX: 'auto' }}>
		<Table fixedColumns={5}>
			<TableHeader>
				<TableRow>
					{Array.from(Array(25).keys()).map((i) => (
						<TableCell key={`header-${i}`} className="text-nowrap">
							Header {i}
						</TableCell>
					))}
				</TableRow>
			</TableHeader>
			<TableBody>
				{Array.from(Array(3).keys()).map((i) => (
					<TableRow key={`row-${i}`}>
						{Array.from(Array(25).keys()).map((k) => (
							<TableCell key={`cell-${k}`}>Cell {k}</TableCell>
						))}
					</TableRow>
				))}
			</TableBody>
		</Table>
	</div>
</>
Result
Loading...
Live Editor
<div style={{ maxWidth: '100%', maxHeight: 500, overflow: 'auto' }}>
	<Table fixedColumns={2} fixedFooter fixedHeader>
		<TableHeader>
			<TableRow>
				{Array.from(Array(50).keys()).map((i) => (
					<TableCell key={`header-${i}`} className="text-nowrap">
						Header {i}
					</TableCell>
				))}
			</TableRow>
		</TableHeader>
		<TableBody>
			{Array.from(Array(100).keys()).map((i) => (
				<TableRow key={`row-${i}`}>
					{Array.from(Array(50).keys()).map((k) => (
						<TableCell key={`cell-${k}`}>Cell {k}</TableCell>
					))}
				</TableRow>
			))}
		</TableBody>
		<TableFooter>
			<TableRow>
				{Array.from(Array(50).keys()).map((i) => (
					<TableCell key={`footer-${i}`} className="text-nowrap">
						Footer {i}
					</TableCell>
				))}
			</TableRow>
		</TableFooter>
	</Table>
</div>

Stacked Tables

Resize the viewport to be narrower than 768px to view the stacked table.

Result
Loading...
Live Editor
<Table stacked id="stacked">
	<TableHeader>
		<TableRow>
			<TableCell>Header 1</TableCell>
			<TableCell>Header 2</TableCell>
			<TableCell>Header 3</TableCell>
			<TableCell>Header 4</TableCell>
		</TableRow>
	</TableHeader>
	<TableBody>
		<TableRow>
			<TableCell>Cell 1</TableCell>
			<TableCell>Cell 2</TableCell>
			<TableCell>Cell 3</TableCell>
			<TableCell>Cell 4</TableCell>
		</TableRow>
		<TableRow>
			<TableCell>Cell 5</TableCell>
			<TableCell>Cell 6</TableCell>
			<TableCell>Cell 7</TableCell>
			<TableCell>Cell 8</TableCell>
		</TableRow>
	</TableBody>
</Table>

Common Mobile Patterns

Horizontal Scrolling

Placing an overflow container around the table is often an easy way to handle data dense tables that may not condense well on mobile viewports.

Result
Loading...
Live Editor
const data = [
	{
		id: 1,
		name: 'John Doe',
		age: 28,
		dateOfBirth: '1995-01-01',
		favoriteCheese: 'Cheddar',
		favoriteBerry: 'Blueberry',
	},
	{
		id: 2,
		name: 'Jane Smith',
		age: 34,
		dateOfBirth: '1989-02-02',
		favoriteCheese: 'Gouda',
		favoriteBerry: 'Strawberry',
	},
	{
		id: 3,
		name: 'Alice Johnson',
		age: 45,
		dateOfBirth: '1978-03-03',
		favoriteCheese: 'Brie',
		favoriteBerry: 'Raspberry',
	},
	{
		id: 4,
		name: 'Bob Brown',
		age: 29,
		dateOfBirth: '1994-04-04',
		favoriteCheese: 'Swiss',
		favoriteBerry: 'Blackberry',
	},
	{
		id: 5,
		name: 'Charlie Davis',
		age: 31,
		dateOfBirth: '1992-05-05',
		favoriteCheese: 'Feta',
		favoriteBerry: 'Cranberry',
	},
	{
		id: 6,
		name: 'Diana Evans',
		age: 27,
		dateOfBirth: '1996-06-06',
		favoriteCheese: 'Blue Cheese',
		favoriteBerry: 'Gooseberry',
	},
	{
		id: 7,
		name: 'Ethan Foster',
		age: 40,
		dateOfBirth: '1983-07-07',
		favoriteCheese: 'Parmesan',
		favoriteBerry: 'Elderberry',
	},
	{
		id: 8,
		name: 'Fiona Green',
		age: 36,
		dateOfBirth: '1987-08-08',
		favoriteCheese: 'Mozzarella',
		favoriteBerry: 'Huckleberry',
	},
	{
		id: 9,
		name: 'George Harris',
		age: 50,
		dateOfBirth: '1973-09-09',
		favoriteCheese: 'Ricotta',
		favoriteBerry: 'Mulberry',
	},
	{
		id: 10,
		name: 'Hannah Ives',
		age: 22,
		dateOfBirth: '2001-10-10',
		favoriteCheese: 'Gorgonzola',
		favoriteBerry: 'Boysenberry',
	},
];
function Example() {
	return (
		<div className="overflow-x-auto" style={{ maxWidth: '50%' }}>
			<Table>
				<TableHeader>
					<TableRow>
						{Object.keys(data[0]).map((item) => (
							<TableCell key={item} className="text-nowrap">
								{item}
							</TableCell>
						))}
					</TableRow>
				</TableHeader>
				<TableBody>
					{data.map((item) => (
						<TableRow key={item.id}>
							<TableCell className="text-nowrap">{item.id}</TableCell>
							<TableCell className="text-nowrap">{item.name}</TableCell>
							<TableCell className="text-nowrap">{item.age}</TableCell>
							<TableCell className="text-nowrap">{item.dateOfBirth}</TableCell>
							<TableCell className="text-nowrap">{item.favoriteCheese}</TableCell>
							<TableCell className="text-nowrap">{item.favoriteBerry}</TableCell>
						</TableRow>
					))}
				</TableBody>
			</Table>
		</div>
	);
}

render(<Example />);

Hiding Columns

If certain columns contain data that is supplemental or not essential to understanding the table, you might opt to hide those columns at mobile sizes. This can be achieved with Bootstrap's utility classes.

Result
Loading...
Live Editor
const data = [
	{
		name: 'Account 1',
		description: 'First account',
		updated: '01/01/2020',
		value: 500,
	},
	{
		name: 'Account 2',
		description: 'Second account',
		updated: '04/01/2022',
		value: 1000,
	},
	{
		name: 'Account 3',
		description: 'Third account',
		updated: '01/15/2020',
		value: 750,
	},
	{
		name: 'Account 4',
		description: 'Fourth account',
		updated: '01/01/2020',
		value: 375,
	},
	{
		name: 'Account 5',
		description: 'Fifth account',
		updated: '01/01/2020',
		value: 100,
	},
];

function Example() {
	return (
		<Table>
			<TableHeader>
				<TableRow>
					<TableCell>Name</TableCell>
					<TableCell className="d-none d-lg-table-cell">Description</TableCell>
					<TableCell className="d-none d-lg-table-cell">Updated</TableCell>
					<TableCell>Value</TableCell>
				</TableRow>
			</TableHeader>
			<TableBody>
				{data.map((item) => (
					<TableRow>
						<TableCell>{item.name}</TableCell>
						<TableCell className="d-none d-lg-table-cell">{item.description}</TableCell>
						<TableCell className="d-none d-lg-table-cell">{item.updated}</TableCell>
						<TableCell>{item.value}</TableCell>
					</TableRow>
				))}
			</TableBody>
		</Table>
	);
}

render(<Example />);

Props

Table

TableRow

TableCell

TableFooter