Skip to content
lab components / Tables and lists

Table

Data tables are used to present a set of highly structured data that is easy for the user to scan, compare and analyze.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { Table } from "@siteimprove/fancylab";

# (Data) table v.s List table

To clarify the differences between Table and ListTable, a short description is provided.

Component nameUsage
(Data) tableUsed to organize and present data in a way that helps the user compare and analyze it.
List tableUsed to list a collection of objects of the same type, such as policies, to help the user find an object and navigate to a full-page representation of it.

#Examples

#Basic usage

You can use a Table to structure both static and interactive data, arranged in rows and columns. Since the Table is designed for use cases that are focused on handling large amounts of tabular data, it must contain a Column header for sorting. The component provides tools such as buttons, filters, search and export to ensure that UI elements remain consistent and the user is given the flexibility to gain relevant insights from the data set.

A Table component contains four elements:

  • Column header: the labels for each column in the table. The Column header can sort the data in ascending or descending order. However, the default order of the columns should reflect the importance of the data to the user, and related columns should be adjacent. See Column header.
  • Rows: each row contains the same number of cells and the content related to the corresponding Column header. The rows can be expanded and highlighted to emphasize specific data.
  • Table Toolbar (optional): a flexible container that provides access to several table related functions, such as actions, search, filtering and export. See Table Toolbar.
  • Pagination (optional): allow the user to navigate data as pages when the amount of data is too large to be displayed at once. See Pagination.
Basic usage table
Cook Time
Servings
Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", "data-observe-key": "table-header-dish", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { content: "Cook Time", tooltip: "in minutes", "data-observe-key": "table-header-cook-time", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", tooltip: "in persons", notSortable: true, "data-observe-key": "table-header-servings", }, render: (dto) => dto.servings, }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} caption="Basic usage table" /> );

#Usage with pagination

A Pagination allows the user to navigate through a collection of items split across multiple pages of data, and is displayed below the bottom row.

A Pagination includes:

  • Text to show the total number of items, including the currently displayed items.
  • Button groups to navigate to the first, previous, next and last page.
  • A Select to specify a page number.
  • A Select to specify the number of items per page.
Recipe 1
5
1
Recipe 2
10
1
Recipe 3
15
1
Recipe 4
20
1
Recipe 5
25
1
1 - 5 of 15 items
const lotsOfData: { id: number; title: string; cookTime: number; servings: number }[] = []; for (let i = 1; i < 16; i++) { lotsOfData.push({ id: i, title: `Recipe ${i}`, cookTime: 5 * i, servings: Math.floor(i / 25) + 1, }); } const [sort, setSort] = useState<SortField<typeof lotsOfData[0]>>({ property: "title", direction: "asc", }); const [items, setItems] = useState(sortItems(lotsOfData, sort)); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const pagedItems = items.slice((page - 1) * pageSize, page * pageSize); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={pagedItems} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} pagination={{ total: items.length, count: pagedItems.length, page: page, setPage: setPage, pageSize: pageSize, setPageSize: setPageSize, cancelLabel: "Cancel", confirmLabel: "Confirm", firstLabel: "First", prevLabel: "Previous", nextLabel: "Next", lastLabel: "Last", pagingInfoLabel: (startIdx: number, endIdx: number, total: number) => `${startIdx} - ${endIdx} of ${total} items`, pageLabel: "Page", pageXofYLabel: (current: number, total: number) => `Page ${current} of ${total}`, pageSizeSelectionLabel: (pageSize: number) => `${pageSize} items`, pageSizeSelectorPrefix: "Show", pageSizeSelectorPostfix: "per page", pageSizeLabel: "Items per page", defaultError: "Default pagination error", wholeNumberError: "Must be a whole number", outOfBoundsError: (total: number) => `Enter a number between 1 and ${total}`, }} /> );

Allow the user to search for specific data that meets specific criteria. To ensure consistency, use an input field within the Table Toolbar component.

The user can perform a table-wide keyword search on all cell values that have been indexed for the search. The search function works with the default typeahead functionality of the standard search component. When you perform a search query, the table is updated to show only those rows whose values match the search term.

Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [query, setQuery] = useState(""); const displayedItems = items.filter((x) => x.title.toLowerCase().startsWith(query.toLowerCase())); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <> <TableToolbar search={ <InputFieldWithSlug value={query} onChange={setQuery} placeholder="Search by dish title" rightSlug={ <Button onClick={() => console.log(query)} aria-label="Submit search"> <Icon> <IconSearch /> </Icon> </Button> } /> } /> <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={displayedItems} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} /> </> );

#Usage with filters

In addition to searching, the user can use the filter function to narrow and refine the rows displayed in the Table. To ensure consistency, use filters within the Table Toolbar component.

Depending on the configuration chosen during implementation, filtering can be applied as follows:

  • Table-wide filtering: allows the user to filter the data by all attributes present in any of the data columns.
  • Custom column filtering: allows the user to focus the filtering on the attributes within a single column of data.

Filters that control how Table data is displayed should be placed directly over a Table. The syntax of filters should be transparent to users, and they should easily recognize that they are seeing filtered data. Always make sure that filters have a clear visual indication of their active state.

Cooking time:

All cooking times

Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
type CookingTime = { id: number; name: string }; const cookingTimes: CookingTime[] = [ { id: 1, name: "All cooking times" }, { id: 2, name: "Max. 30 min" }, { id: 3, name: "Max. 60 min" }, ]; const [cookingTime, setCookingTime] = useState<CookingTime | undefined>(cookingTimes[0]); const onChange = (newValue: CookingTime | undefined) => { console.log("Filter changed, calling API with new cooking time", newValue); setCookingTime(newValue); }; const [filterButton, activeFilters] = useSingleFilter(cookingTime, onChange, { label: "Cooking time", stringify: (cookingTime) => cookingTime?.name, items: cookingTimes.map((cookingTime) => ({ title: cookingTime.name, value: cookingTime })), compareFn: (a, b) => a.id === b.id, defaultOption: cookingTimes[0], }); const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const displayedItems = cookingTime?.id === 1 ? items : items.filter((x) => (cookingTime?.id === 2 ? x.cookTime <= 30 : x.cookTime <= 60)); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <> <TableToolbar filter={filterButton} activeFilters={activeFilters} /> <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={displayedItems} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} /> </> );

#Usage with expandable content

Use expandable content to reduce visual noise and make it easier to read the content that is most important for the task at hand. Clicking on the cell expands the row to fill the width of the Table, and if needed, the contents of the expanded row can be accessed further by scrolling. Avoid a lot of interaction and do not take up more than 50% of the screen. If you need to expand the page to display dense, highly interactive content, use a new page for that purpose instead.

Expandable rows serve two purposes for users:

  • Allow users to view simple and additional information.
  • Users can reduce the width of the cells for content they consider less important.

Note that data loading can occur the moment the row is expanded. In this case, you can use a Spinner to tell the user that the content is loading.

Lasagna
2
Pancakes
20
4
Sushi
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, expandOptions: { canCellExpand: (item, pos) => pos.rowNum === 0 || item.cookTime > 30, cellExpandRenderer: (item) => ( <span>Expanded {item.title}. You can place graphs, tables, etc here!</span> ), }, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} /> );

#Usage with highlight

The highlighted effect allows the user to focus on a single row at a time, especially when you have multiple columns and data points. The highlighted row uses the background color $color--background--interactive--selected (#EBF6FF). You can use this effect to highlight a selected row if the row contains an interactive element, such as Checkbox.

Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} highlightRow={(i) => i.title === "Lasagna"} /> );

#Usage with data-observe-keys on table cells

Use data-observe-key on table cells to create an identifier, which is useful for tracking user interactivity, for example.

Cook Time
Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); const observeKeyPrefix = "observe-key-example"; return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, dataObserveKeys: (item) => { return `${observeKeyPrefix}-dish-column-${item.title}`; }, }, }, { header: { content: "Cook Time", }, render: (dto) => dto.cookTime, options: { dataObserveKeys: (item, pos) => { return `${observeKeyPrefix}-cook-column-${pos.columnNum}-row-${pos.rowNum}`; }, }, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, options: { dataObserveKeys: (item, pos) => { return pos.rowNum === 0 ? `${observeKeyPrefix}-servings-column-row-${pos.rowNum}` : undefined; }, }, }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} /> );

#Usage with summary

Include a summary row that displays column totals and averages. This helps the user to get a quick overview of certain data. For example, the Analytics site statistics overview displays the percentage of total page views per device/page and the average bounce rate for each site.

Basic usage table
Cook Time
Servings

46.25

average

20

total

Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); const cookTimeAverage = useMemo<number>(() => { const cookTimeSum = items.reduce((sum, item) => sum + item.cookTime, 0); return cookTimeSum / items.length; }, [items]); const servingsTotal = useMemo<number>( () => items.reduce((sum, item) => sum + item.servings, 0), [items] ); return ( <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { content: "Cook Time", tooltip: "in minutes", }, summary: { value: cookTimeAverage, label: "average", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", tooltip: "in persons", notSortable: true, }, summary: { value: servingsTotal, label: "total", }, render: (dto) => dto.servings, }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} caption="Basic usage table" /> );

#Usage with exporter

Using our TableExporter component or the useTableExport hook, developers have access to a Modal that allows the user to decide which table pages he wants to export (current page or all pages). The user can also choose whether or not to export subtables (expanded content). With these user settings, the developer can proceed and call his own export routine.

If you have a Table within a Card, please display the export button explicitly. Do not place the export button inside the ActionMenu on the right edge of a Card. This is because the export button is for downloading the table data and not all the information on the Card. Sometimes, there might be other UI elements in the Card, such as Tab or ContentSwitcher that export data that the user may not initially need.

Recipe 1
1
Recipe 2
1
Recipe 3
1
Recipe 4
1
Recipe 5
1
1 - 5 of 15 items
const lotsOfData: { id: number; title: string; cookTime: number; servings: number }[] = []; for (let i = 1; i < 16; i++) { lotsOfData.push({ id: i, title: `Recipe ${i}`, cookTime: 5 * i, servings: Math.floor(i / 25) + 1, }); } const [loading, setLoading] = useState(true); const [sort, setSort] = useState<SortField<typeof lotsOfData[0]>>({ property: "title", direction: "asc", }); const [items, setItems] = useState(sortItems(lotsOfData, sort)); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const pagedItems = items.slice((page - 1) * pageSize, page * pageSize); const getCookTimeData = (item: typeof lotsOfData[0]) => { return [ { cookMode: "Baked", cookTime: item.cookTime }, { cookMode: "Boiled", cookTime: item.cookTime + 5 }, { cookMode: "Grilled", cookTime: item.cookTime + 10 }, ]; }; const MyExporter = () => ( <TableExporter onExport={(opt) => { console.log( "Call your export routine (front-end or back-end) with the following options", opt ); }} buttonContent={ <> <Icon> <IconDownload /> </Icon> <InlineText>Export</InlineText> </> } modalTitle="Export" pageSize={pageSize} total={lotsOfData.length} expandableProperties={[{ propertyName: "cookTime", displayName: "Cook time" }]} /> ); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <> <TableToolbar exports={<MyExporter />} /> <Table columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, expandOptions: { canCellExpand: () => true, cellExpandRenderer: (item) => ( <CookTimeSubTable item={item} data={getCookTimeData(item)} /> ), }, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, }, ]} items={pagedItems} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} pagination={{ total: items.length, count: pagedItems.length, page: page, setPage: setPage, pageSize: pageSize, setPageSize: setPageSize, cancelLabel: "Cancel", confirmLabel: "Confirm", firstLabel: "First", prevLabel: "Previous", nextLabel: "Next", lastLabel: "Last", pagingInfoLabel: (startIdx: number, endIdx: number, total: number) => `${startIdx} - ${endIdx} of ${total} items`, pageLabel: "Page", pageXofYLabel: (current: number, total: number) => `Page ${current} of ${total}`, pageSizeSelectionLabel: (pageSize: number) => `${pageSize} items`, pageSizeSelectorPrefix: "Show", pageSizeSelectorPostfix: "per page", pageSizeLabel: "Items per page", defaultError: "Default pagination error", wholeNumberError: "Must be a whole number", outOfBoundsError: (total: number) => `Enter a number between 1 and ${total}`, }} /> </> );

#Usage with CSV exporter

Raw tabular data is data that can be exported to a CSV file. It contains no formatting and is identical to the source file.

The "Export to CSV" button is the most common secondary action for the user. Thus we offer a built-in CSV exporter for free through the TableCsvExporter component, which supports exporting subtables, custom filename, custom header and footer, custom delimiter, etc. The CSV representation of each column will be inferred automatically if possible, otherwise, developers should define it by using the csv property on each column config, as we can see in the example code. Subtable settings must always be defined by the developer.

Recipe 1
1
Recipe 2
1
Recipe 3
1
Recipe 4
1
Recipe 5
1
1 - 5 of 15 items
const lotsOfData: { id: number; title: string; cookTime: number; servings: number }[] = []; for (let i = 1; i < 16; i++) { lotsOfData.push({ id: i, title: `Recipe ${i}`, cookTime: 5 * i, servings: Math.floor(i / 25) + 1, }); } const [loading, setLoading] = useState(true); const [sort, setSort] = useState<SortField<typeof lotsOfData[0]>>({ property: "title", direction: "asc", }); const [items, setItems] = useState(sortItems(lotsOfData, sort)); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const pagedItems = items.slice((page - 1) * pageSize, page * pageSize); type CookTimeData = { cookMode: string; cookTime: number }; const getCookTimeData = (item: typeof lotsOfData[0]): CookTimeData[] => { return [ { cookMode: "Baked", cookTime: item.cookTime }, { cookMode: "Boiled", cookTime: item.cookTime + 5 }, { cookMode: "Grilled", cookTime: item.cookTime + 10 }, ]; }; const columns: ColumnsArray<typeof lotsOfData[0]> = [ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { property: "cookTime", content: "Cook Time", }, render: (dto) => dto.cookTime, expandOptions: { canCellExpand: () => true, cellExpandRenderer: (item) => <CookTimeSubTable item={item} data={getCookTimeData(item)} />, }, csv: { subTable: { dataProvider: async (item) => getCookTimeData(item), property: "cookTime", columns: [ { header: "Cook Mode", render: (dto) => dto.cookMode, }, { header: "Cook Time", render: (dto) => dto.cookTime, }, ], }, } as CsvColumnWithSubTableConfig<typeof lotsOfData[0], CookTimeData>, }, { header: { property: "servings", content: "Servings", }, render: (dto) => dto.servings, csv: { header: "Servings (per person)", // you can override the header for the CSV export render: (dto) => dto.servings, // you can override the render function for the CSV export }, }, ]; useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <> <TableToolbar exports={ <TableCsvExporter columns={columns} pageNumber={page} pageSize={pageSize} total={lotsOfData.length} expandableProperties={[{ propertyName: "cookTime", displayName: "Cook time" }]} fileName="table-example.csv" contentPre={`Table Example\n${escapeCsvContent(new Date().toLocaleString())}\n\n`} contentPos={"\n\nSiteimprove ©"} dataProvider={async ({ pageNumber, pageSize }) => items.slice((pageNumber - 1) * pageSize, pageNumber * pageSize) } /> } /> <Table columns={columns} items={pagedItems} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} pagination={{ total: items.length, count: pagedItems.length, page: page, setPage: setPage, pageSize: pageSize, setPageSize: setPageSize, cancelLabel: "Cancel", confirmLabel: "Confirm", firstLabel: "First", prevLabel: "Previous", nextLabel: "Next", lastLabel: "Last", pagingInfoLabel: (startIdx: number, endIdx: number, total: number) => `${startIdx} - ${endIdx} of ${total} items`, pageLabel: "Page", pageXofYLabel: (current: number, total: number) => `Page ${current} of ${total}`, pageSizeSelectionLabel: (pageSize: number) => `${pageSize} items`, pageSizeSelectorPrefix: "Show", pageSizeSelectorPostfix: "per page", pageSizeLabel: "Items per page", defaultError: "Default pagination error", wholeNumberError: "Must be a whole number", outOfBoundsError: (total: number) => `Enter a number between 1 and ${total}`, }} /> </> );

#Combined columns

You can combine columns to create a new column that contains the combined data of the original columns. This is useful when you want to display related data in a single column, but still want to sort and filter the data based on the original columns.

Basic usage table
1 - Lasagna

45 min

2 persons

2 - Pancakes

20 min

4 persons

3 - Sushi

90 min

6 persons

4 - Cake

30 min

8 persons

const [items, setItems] = useState(someData); const [sort, setSort] = useState<SortField<typeof items[0]>>({ property: "title", direction: "asc", }); const [loading, setLoading] = useState(true); useEffect(() => { setItems(sortItems(items, sort)); setLoading(false); }, [sort]); return ( <Table columns={[ { header: [ { property: "id", content: "ID", defaultSortDirection: "asc", "data-observe-key": "table-header-id", }, { property: "title", content: "Dish", defaultSortDirection: "asc", "data-observe-key": "table-header-dish", }, ], render: (dto) => `${dto.id} - ${dto.title}`, options: { isKeyColumn: true, }, }, { header: [ { property: "cookTime", content: "Cook Time", tooltip: "in minutes", "data-observe-key": "table-header-cook-time", }, { property: "servings", content: "Servings", tooltip: "in persons", notSortable: true, "data-observe-key": "table-header-servings", }, ], render: (dto) => ( <BigSmall big={`${dto.cookTime} min`} small={`${dto.servings} persons`} /> ), }, ]} items={items} sort={sort} setSort={(property, direction) => { setLoading(true); setSort({ property: property, direction: property === sort.property ? invertDirection(sort.direction) : direction, }); }} loading={loading} caption="Basic usage table" /> );

#Loading state

The loading state is used to indicate that the data of the table is still being loaded.

Table with loading state enabled
Cook Time
Servings

Loading

<Table loading={true} items={[] as typeof someData} caption="Table with loading state enabled" columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { content: "Cook Time", tooltip: "in minutes", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", tooltip: "in persons", notSortable: true, "data-observe-key": "table-header-servings", }, render: (dto) => dto.servings, }, ]} sort={{ property: "title", direction: "asc" }} setSort={() => {}} />

#No data state

The noDataState is used to indicate that no data is available for display. An Empty State component with the type "reassure" and default heading is shown, but it can be overridden by another type with custom text. For guidelines please refer to the Empty State component.

To avoid confusion, in cases where the user has resolved the issues, explain why the data cannot be displayed. E.g. "Accessibility issues have been resolved".

Table with no data
Cook Time
Servings

No data to display

<Table loading={false} items={[] as typeof someData} caption="Table with no data" columns={[ { header: { property: "title", content: "Dish", defaultSortDirection: "asc", }, render: (dto) => dto.title, options: { isKeyColumn: true, }, }, { header: { content: "Cook Time", tooltip: "in minutes", }, render: (dto) => dto.cookTime, }, { header: { property: "servings", content: "Servings", tooltip: "in persons", notSortable: true, "data-observe-key": "table-header-servings", }, render: (dto) => dto.servings, }, ]} sort={{ property: "title", direction: "asc" }} setSort={() => {}} />

#Properties

1
Number 1
2
Number 2
3
Number 3
4
Number 4
PropertyDescriptionDefinedValue
columnsRequired
type-union[]Configurations for the columns displayed in the table
itemsRequired
unknown[]Items displayed in the table
sortRequired
objectProperty and direction by which the table is sorted
setSortRequired
functionCallback for updating sorting
loadingRequired
booleanIs the table in a loading state?
loadingTextOptional
stringOptional text to be displayed when the table is loading
elementOptional footer to be displayed below the table
paginationOptional
objectPagination
topAlignedOptional
booleanSets the data inside the table to be top-aligned
highlightRowOptional
functionCompare function for highlight row
rowKeyOptional
functionFunction to generate a key for rows
captionOptional
stringCaption for the table
noDataStateOptional
elementContent to be shown when there's no data. An Empty State component is shown by default, but it can be overridden by another type with custom text.
withoutKeyColumnOptional
booleanDoes this table not have a key column? Be sure this is the case otherwise the table is less accessible
data-observe-keyOptional
stringUnique string, used by external script e.g. for event tracking
classNameOptional
stringCustom className that's applied to the outermost element (only intended for special cases)
styleOptional
objectStyle object to apply custom inline styles (only intended for special cases)

#Guidelines

#Best practices

#General

Use Table when

  • organizing and displaying data that fills one or more rows.
  • comparing information from an entire set of data.
  • a task requires the user to navigate to a specific piece of data.

#Usage

Table is typically used for the following purposes:

Finding data that meet specific criteria

In this type of task, the user searches for a specific item or items that meet certain criteria that the user has in mind. In doing so, the user may need to filter, sort, search, or simply visually walk through the table. Provide appropriate tools to help the user find what they are looking for as easily as possible. Examples of this can be found throughout the base data, such as the occurrence of issues, broken links, misspellings etc.

Comparing data

Tables are most effective when users can compare data, but they can be difficult to understand at times. The content in each body cell should be meaningful and clear. Make sure that the most relevant columns are close to each other rather than forcing them to scroll back and forth memorizing data. Examples can be found in the features catering to expert users, such as customizable tables, Ads Data Explorer, SEO Keyword Monitoring etc.

#Rows and columns

  • Ideally, all values have the same visual weight and are emphasized equally. For important information that needs to be emphasized more, consider the order in which the data is presented.
  • If a Table is larger than the screen, consider freezing the Column header to make it easier to find the relevant information.
  • Make sure there is a clear visual cue when a column is hidden.

#Body cells

  • Minimize clutter by including only values that support the purpose of the data. Use visual elements such as icons and illustrations sparingly.
  • A Table can contain interactive elements, such as Checkbox that provide multiple selection and bulk action functions. However, you should use only one interactive element in each cell.
  • For textual or numeric values, each cell can contain only 1 piece of information.
  • Cells can contain the Link component, but avoid linking entire table rows. This navigation feature does not serve the main purpose of a Table. If you need to link a whole row, use the List table instead.

#Alignment

Alignment is determined by the information within each column. Column header should always align to the corresponding content.

  • Numeric data = Right aligned.
  • Textual data = Left aligned.
  • If there is an action, it should be on the far right of the table row.
  • Do not center align data.

#Style

  • Do not truncate content that the user needs to examine in relation to other content in a Table.
  • Avoid extensively wrapping the table content in a narrow column.
  • Avoid embedding Form element wrapper within table rows.
  • Avoid leaving cells blank so that it is not clear whether all data has been loaded. Include a visual indicator for cells that have no content.

#Do not use when

  • there's less than 1 row of tabular data. For a small amount of data, use List table or Card instead.
  • the content does not follow a consistent pattern and cannot be divided into columns.
  • data is provided that does not fit into a tabular format. If you want to display a more complex data relationship, use data visualization, such as Chart instead.
  • you want an action-oriented list of items that link to detail pages. For this functionality, use List table instead.

#Accessibility

#For designers

  • Avoid including more than one interactive element in as single cell.
  • For people using screen magnification, the selected Checkbox may appear outside the magnified screen area. Adding a background color provides an additional way to indicate that a row has been selected. See Usage with highlight.
  • The focus state should be displayed when keyboard users tab through interactive components, such as Checkbox.

#For developers

  • If possible, avoid refreshing the entire page when making changes to a table (e.g. sorting, filtering, removing rows). This returns the focus of the keyboard or screen reader to the top of the page, making it very tedious for the users.
  • Make sure all columns have a meaningful Column header, even if it is visually hidden.
  • Be sure to give a Table a short, meaningful caption. For example, use the same text as a Table heading element. This helps assistive technology users understand the purpose and context of the table.

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • All content should be informative, clear, and concise.
  • Leave enough space for content to accommodate localization, if possible.
  • Include symbols for units of measure (%) in the Column header so they are not repeated in the body cells.
  • Use sentence case.
  • Keep decimal numbers consistent, preferably use 2 decimal places. Do not use 1 decimal in one row and 2 in another.
  • In most cases, punctuation is not required within tables unless the data contains numbers and units. Follow Grammar and mechanics.