List table
Present structured data for quick scanning and essential details, ideal for related items with concise summaries.
#Examples
Composition:
- Background: A subtle background color can visually separate the list table from surrounding content. Use a neutral color that doesn't compete with the content.
- Item Count (Optional): Display the total number of items in the list, especially for filtered or large datasets. Add context when applicable (e.g., "Filtered by: Active").
- Sorting Indicator (Optional): Clearly indicate the active sorting criteria (e.g., "Sorted by Name (Ascending)"). This helps users understand how the list is organized.
- Rows and Cells (Optional): Rows represent individual data items, while cells hold specific pieces of information within a row.
#Basic usage
Use Cases:
- Surface essential information quickly (e.g., site names, scores, policy summaries).
- Offer an overview of data, acting as an entry point to more details. (e.g., Core wins).
- Enable users to scan for patterns and insights.
Best Practices:
- Focus on the most important data points, keeping information density low to avoid overwhelming users.
- Consider embedding list tables within cards or other primary content areas to provide context.
- Keep additional actions (shortcuts, CTAs) to a maximum of two per row to maintain visual clarity.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
{...translations}
/>
#Usage with pagination
Use for large datasets. Provide clear controls and indicate the total number of pages. Consider offering options to adjust items per page.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(2);
const visibleItems = sortItems(items, { property: "dish", direction: "asc" }).slice(
(page - 1) * pageSize,
page * pageSize
);
return (
<ListTable
items={visibleItems}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
pagination={{
total: items.length,
count: pageSize,
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}`,
}}
{...translations}
/>
);
#Usage with load more buttons
Alternative to pagination when space is limited or the total item count is unknown. Ensure the button is visible and provide loading feedback.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
<ListTable
sort={{ property: "dish", direction: "asc" }}
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreCount={1}
showLoadAll
{...translations}
/>
#Usage with sorting
Allow users to reorder the list. Use a Select for options and indicate the current sorting state.
When using sorted lists, you must provide the sortSelect
and sort
properties as well as the colums[].header.property
value of sortable columns.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
const [sort, setSort] = useState(sortOptions[0]);
return (
<ListTable
items={sortItems(items, sort)}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
sort={sort}
sortSelect={sortSelectPropsHelper(
"Some a11y label",
sortOptions,
sort,
(property, direction) =>
setSort(
sortOptions.find((x) => x.property === property && x.direction === direction) ||
sortOptions[0]
)
)}
caption="Some table data in a list"
{...translations}
/>
);
#Usage with table toolbar
Provide a centralized space for actions that apply to the entire table (filtering, bulk actions, customization). Keep it uncluttered and visually distinct.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
type Calories = { id: number; name: string };
const caloriesAmount: Calories[] = [
{ id: 1, name: "All calories" },
{ id: 2, name: "Less than 500 calories" },
{ id: 3, name: "Less than 400 calories" },
];
const [calories, setCalories] = useState<Calories | undefined>(caloriesAmount[0]);
const onChange = (newValue: Calories | undefined) => {
console.log("Filter changed, calling API with new calories count", newValue);
setCalories(newValue);
};
const [filterButton, activeFilters] = useSingleFilter(calories, onChange, {
label: "Calories",
stringify: (calories) => calories?.name,
items: caloriesAmount.map((calories) => ({ title: calories.name, value: calories })),
compareFn: (a, b) => a.id === b.id,
defaultOption: caloriesAmount[0],
});
const sort: SortField<typeof items[0]> = {
property: "dish",
direction: "asc",
};
const displayedItems =
calories?.id === 1
? items
: items.filter((x) => {
const caloriesMatch = x.calories.match(/\d+/);
if (!caloriesMatch) return false;
const caloriesValue = Number(caloriesMatch[0]);
return calories?.id === 2 ? caloriesValue < 500 : caloriesValue < 400;
});
return (
<>
<TableToolbar filter={filterButton} activeFilters={activeFilters} />
<ListTable
items={sortItems(displayedItems, { property: sort.property, direction: sort.direction })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (displayedItems) => <div>{displayedItems.dish}</div>,
options: { isKeyColumn: true },
},
{
header: { content: "Calories" },
render: (displayedItems) => <div>{displayedItems.calories}</div>,
},
{
header: { content: "Protein" },
render: (displayedItems) => <div>{displayedItems.protein}</div>,
},
]}
sort={sort}
loading={false}
caption="Some table data in a list"
{...translations}
/>
</>
);
#Usage without filter, sort or count header
For simple, static lists where these features aren't needed. Ensure the list is self-explanatory and visually organized. Consider a card header or title for context.
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
return (
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreLabel={(count: number) => `Load ${count} more`}
loadAllLabel="Load all"
// leave out the "itemsFoundLabel", "sortByLabel" & "sortedByLabel" props to avoid the header
/>
);
#Loading state
Use a Spinner and/or brief message to indicate data is being fetched.
Best Practices:
- Use a Spinner component to indicate the loading state.
- Place the loading indicator in the center of the list table area.
- Consider adding a brief message explaining the delay.
Dish | Calories | Protein |
---|---|---|
Loading |
<ListTable
items={sortItems([] as typeof items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={true}
caption="Some table data in a list"
{...translations}
/>
#No data state
Communicate clearly that no data matches the criteria. Offer suggestions or actions to help the user.
Best Practices:
- Use a visually distinct "empty state" design, read more in Empty State.
- Provide a clear, concise message explaining why no data is available.
- Offer relevant actions, such as "Try a different search" or "Create a new item".
- Avoid blaming the user or using negative language.
Consideration:
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".
Dish | Calories | Protein |
---|---|---|
No data to display |
<ListTable
items={sortItems([] as typeof items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
{...translations}
/>
#Properties
Dish | Calories | Protein |
---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
Property | Description | Defined | Value |
---|---|---|---|
columnsRequired | type-union[] Column configurations | ||
itemsRequired | unknown[] Items to be displayed | ||
loadingRequired | boolean Is the table in a loading state? | ||
noDataStateOptional | element Content 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. | ||
string Caption for the list table | |||
withoutKeyColumnOptional | boolean Does this table not have a key column? Be sure this is the case otherwise the table is less accessible | ||
itemsFoundLabelOptional | function Label describing how many items are in the table | ||
sortByLabelOptional | string Label describing the dropdown with sort options | ||
sortedByLabelOptional | string Label describing by which property the table is sorted | ||
data-observe-keyOptional | string Unique string, used by external script e.g. for event tracking | ||
classNameOptional | string Custom className that's applied to the outermost element (only intended for special cases) | ||
styleOptional | object Style object to apply custom inline styles (only intended for special cases) |
#Guidelines
#Best practices
#Do not use when
#Accessibility
Explore detailed guidelines for this component: Accessibility Specifications