Skip to content
lab components / Tables and lists

List table

Present structured data for quick scanning and essential details, ideal for related items with concise summaries.

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 { ListTable } from "@siteimprove/fancylab";

#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.

3 items found

Sorted by Dish (ascending)

Some table data in a list
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.

3 items found

Sorted by Dish (ascending)

Some table data in a list
Dish
Calories
Protein
Beef Stir-Fry
450 calories
25 g protein
Grilled Salmon
500 calories
30 g protein
1 - 2 of 3 items
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.

3 items found

Sorted by Dish (ascending)

Some table data in a list
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.

3 items found

Sort by
Some table data in a list
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.

Calories:

All calories

3 items found

Sorted by Dish (ascending)

Some table data in a list
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.

Some table data in a list
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.

0 items found

Sorted by Dish (ascending)

Some table data in a list
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".

0 items found

Sorted by Dish (ascending)

Some table data in a list
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

3 items found

Sorted by Dish (ascending)

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
PropertyDescriptionDefinedValue
columnsRequired
type-union[]Column configurations
itemsRequired
unknown[]Items to be displayed
loadingRequired
booleanIs the table in a loading state?
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.
captionOptional
stringCaption for the list table
withoutKeyColumnOptional
booleanDoes this table not have a key column? Be sure this is the case otherwise the table is less accessible
itemsFoundLabelOptional
functionLabel describing how many items are in the table
sortByLabelOptional
stringLabel describing the dropdown with sort options
sortedByLabelOptional
stringLabel describing by which property the table is sorted
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 ListTable when

  • Display structured overview of data with clear item relationships.
  • Navigate to specific items within a hierarchy.
  • Quick access to individual items.
  • Visual elements to enhance presentation (charts, icons, illustrations).

#Placement

ListTable is typically used in the following places:

#Style

  • Siteimprove Design System: Adhere to Siteimprove's guidelines for color, typography, and spacing. If you are not using a component from Fancy, match the styling of your ListTable to existing components for visual consistency.
  • Always use a header when embedding a ListTable within a Card.
  • Anchor supporting visuals (thumbnails, etc.) along the row's edge for better scalability.
  • Incorporate bulk actions like checkboxes or a "mark as favorite" starred button (first column).
  • Labels:
    • Avoid long text in expandable containers.
    • Prioritize key information over metadata.
    • Ensure each cell's content is self-explanatory to eliminate the need for column headers.

#Do not use when

  • Highly detailed information or complex relationships. Use Table instead.
  • Primarily numerical data requiring calculations. Use Table instead.
  • Extremely large datasets. Use Table instead.
  • Identifying trends. Use Chart instead.

#Accessibility

#For designers

  • Ensure sufficient contrast, clear focus indicators, and descriptive labels/alt text for visual elements.

#For developers

This component comes with built-in accessibility, no extra work required.

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • Use concise, descriptive labels for cells.
  • Follow sentence case conventions. Be mindful of localization.
  • Maintain consistency in decimal usage (e.g., one decimal places). Do not use 1 decimal in one row and 2 in another.
  • Avoid punctuation within list unless data contains numbers and units. Follow Grammar and mechanics.