The Advanced Table is for complex datasets with features like sticky headers, keyboard navigation, and expandable rows. Don't mix Table and Advanced Table components; they aren't interchangeable.
Usage
When to use
- When large datasets benefit from being viewed in a scrollable container instead of with pagination.
- When an expandable table is needed for hierarchical data.
- When users would benefit from more efficient keyboard navigation, such as when there are many rows or columns.
When not to use
- If your dataset requires only basic interactions, such as simple sorting or pagination, and does not require features like nested rows, advanced keyboard navigation, or sticky headers, the standard Table is a more suitable choice.
- When visual representations like charts or graphs better convey the data.
- As a layout mechanism for structuring content that isn’t tabular data.
- To replicate spreadsheet-like functionality with extensive in-cell editing or calculations.
Columns
Sorting
- Sorting is not relevant for all content, so thoughtfully consider when to apply sorting.
- An Advanced Table allows end-users to sort by one column at a time. While multiple columns may offer sorting options, users can only apply sorting to one column at any given moment.
Tooltips
Labels within the Advanced Table column should be clear, concise, and straightforward. In the case that more context or details are necessary, a Tooltip can be used in conjunction with the label but should be used sparingly and as a last resort.
Some examples where it may be useful to include additional context in a tooltip include:
- When the label contains a product or HashiCorp-specific term.
- When the label refers to a setting that can be changed elsewhere in the application.
Width
Column width is determined by manually resizing the header column and cells within Figma. As a best practice, column width should be adjusted to fit the longest data type within the cell.
Placement
Column placement determines the visual styling based on where the column is placed relative to other columns in the Advanced Table.
Alignment
The alignment of text and content within an Advanced Table impacts the readability and speed at which users can effectively parse the information. The proper alignment method depends on the content within the cell, and relative position within the advanced table.
Use consistent alignment between the header label and the cell content.
Avoid misaligned header labels and content.
Left alignment
Align content to the left of the cell by default. This ensures readability across different content types, consistency in content of varying lengths, and alignment between the column header label and the content within the cell.
Use left alignment for:
- String and text-based content (unique identifiers or IDs, names and naming conventions, etc).
- Numerical values that do not contain decimals or floating point numbers.
- Numerical values that contain periods or other delimiter characters (IP addresses).
- Nested components that display a string or text value, e.g., a Badge.
Right alignment
Right alignment can be used when expressing numerical values with decimals as this aligns the decimal places vertically.
Common examples of right alignment include:
- Financial information and currency amounts.
- Fractional and floating point values represented with decimals.
Right alignment can also be used in the last column of an advanced table to:
- Highlight a "more options" function.
- As a means to visually "bookend" the row with content that is of a similar length, e.g., timestamps, TTL (time-to-live) values, dates.
Don’t right align content that is variable in length. This can make the content more difficult to read by forcing an unnatural reading pattern.
Other alignment methods
We don’t recommend center or justified alignment of content within Advanced Table cells. These alignment methods can result in the content being difficult to read, especially if it is variable in length.
Don’t center header labels or cell content within a table.
Column and row span
- Supports combining multiple columns or rows into a single cell.
- Apply column and row spans carefully to maintain alignment, accessibility, and smooth table interactions.
- Multi-span cells should use the same alignment for readability.
Rows
Headers
- Labels in headers should be clear, concise, and straightforward.
- The label should clearly indicate what type of content is contained within the cell (string, number, status, etc).
- Labels should use sentence-case capitalization, not all-caps.
Sticky headers
Sticky headers keep column labels visible while scrolling, aiding navigation in large data sets or nested rows.
Expandable rows
Expandable rows let users show or hide more content without navigating away from the table. The expanded content should align with the header labels, even if the parent row includes minimal data.
Avoid using expandable rows when data is not structured in parent-child relationships.
Avoid using different density settings for parent and child rows.
Striping
Striping enhances readability by alternating row colors, making it easier to scan tabular data.
- Non-Nested Advanced Tables: Striping starts with the second row, distinguishing it from the header.
- Nested Advanced Tables: Child rows are automatically striped, while parent rows remain unstriped to visually reinforce hierarchy. This behavior cannot be disabled.
Placement
Row placement determines the visual styling based on where the row is placed relative to other rows within the Advanced Table. Only cells with a column placement that is either start or end use the row placement property; column position middle does not use this property.
Cells
Density
- Use medium density by default for balanced readability and display.
- Choose short density for text-heavy tables to fit more rows on a page.
- Dense content can make tables harder to read and scan, so use it thoughtfully.
Horizontal scrolling
Use horizontal scrolling when the number of columns expands beyond the viewport or container.
Multi-Select
Multi-select allows users to select multiple rows to perform bulk actions, such as deleting or exporting data. The Advanced Table maintains selection states across pagination and filtering, ensuring consistency when interacting with large datasets. For more details, check out the Multi-Select Table Pattern.
A multi-select pattern consists of:
- A select all in the table's header row. This acts as the parent checkbox, allowing the simultaneous selection or deselection of all child rows in a single table.
- Row level select allowing for the selection of an individual row.
How to use this component
The Advanced Table is a component meant to display tabular data to overcome limitations with the HTML <table>
elements and increase the accessibility for complex features, like nested rows and a sticky header.
Instead of using the <table>
elements, the Advanced Table uses <div>
s with explicitly set roles (for example, instead of <tr>
, it uses <div role="row">
). This allows the Advanced Table to use CSS Grid for styling.
Basic Advanced Table
To use an Advanced Table, first define the data model in your route or model:
import Route from '@ember/routing/route';
export default class ComponentsAdvancedTableRoute extends Route {
async model() {
// example of data retrieved:
//[
// {
// id: '1',
// attributes: {
// artist: 'Nick Drake',
// album: 'Pink Moon',
// year: '1972'
// },
// },
// {
// id: '2',
// attributes: {
// artist: 'The Beatles',
// album: 'Abbey Road',
// year: '1969'
// },
// },
// ...
let response = await fetch('/api/demo.json');
let { data } = await response.json();
return { myDemoData: data };
}
}
For documentation purposes, we’re imitating fetching data from an API and working with that as data model. Depending on your context and needs, you may want to manipulate and adapt the structure of your data to better suit your needs in the template code.
You can insert your own content into the :body
block and the component will take care of looping over the @model
provided:
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array (hash label="Artist") (hash label="Album") (hash label="Year")}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Nested rows
For complex data sets where there is a parent row with several children, you can render them as nested rows. By default, the Advanced Table uses the children
key on the @model
argument to render the nested rows. To change the key used, set the @childrenKey
argument on the Advanced Table.
To ensure the Advanced Table is accessible, the columns in the nested rows must match the columns of the parent rows. Otherwise the relationship between the parent and nested rows will not be clear to users.
// example of data retrieved for the model:
[
{
id: '1',
name: 'Policy set 1',
status: 'PASS',
children: [
{
name: 'test-advisory-pass.sentinel',
status: 'PASS',
description: 'Sample description for this thing.'
},
{
name: 'test-hard-mandatory-pass.sentinel',
status: 'PASS',
description: 'Sample description for this thing.'
}
]
},
{
id: '2',
name: 'Policy set 2',
status: 'FAIL',
children: [
{
name: 'test-advisory-pass.sentinel',
status: 'PASS',
description: 'Sample description for this thing.'
},
// ...
]
},
]
Similar to the basic Advanced Table, you can insert your own content into the :body
block and the component will take care of looping over the @model
provided for the parent and nested rows. The component adds the expand/collapse button to the [B].Th
component in each row that has children.
<Hds::AdvancedTable
@model={{this.demoDataWithNestedRows}}
@columns={{array
(hash key="name" label="Name")
(hash key="status" label="Status")
(hash key="description" label="Description")
}}
>
<:body as |B|>
<B.Tr>
<B.Th>{{B.data.name}}</B.Th>
<B.Td>
{{#if (eq B.data.status "FAIL")}}
<Hds::Badge @text="Fail" @color="critical" @icon="x" />
{{else}}
<Hds::Badge @text="Pass" @color="success" @icon="check" />
{{/if}}
</B.Td>
<B.Td>{{B.data.description}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Sortable Advanced Table
Add isSortable=true
to the hash for each column that should be sortable.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Release Year")
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Pre-sorting columns
To indicate that a specific column should be pre-sorted, add @sortBy
, where the value is the column’s key.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Release Year")
}}
@sortBy="artist"
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Pre-sorting direction
By default, the sort order is set to ascending. To indicate that the column defined in @sortBy
should be pre-sorted in descending order, pass in @sortOrder="desc"
.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Release Year")
}}
@sortBy="artist"
@sortOrder="desc"
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Custom sort callback
To implement a custom sort callback on a column:
- add a custom function as the value for
sortingFunction
in the column hash. - include a custom
onSort
action in your Table invocation to track the sorting order and use it in the custom sorting function.
This is useful for cases where the key might not be A-Z or 0-9 sortable by default, e.g., status, and you’re otherwise unable to influence the shape of the data in the model.
The code has been truncated for clarity.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash
key='status'
label='Status'
isSortable=true
sortingFunction=this.myCustomSortingFunction
)
(hash key='album' label='Album')
(hash key='year' label='Year')
}}
@onSort={{this.myCustomOnSort}}
>
<!-- <:body> here -->
</Hds::AdvancedTable>
Here’s an example of what a custom sort function could look like. In this example, we are indicating that we want to sort on a status, which takes its order based on the position in the array:
// we use an array to declare the custom sorting order for the "status" column
const customSortingCriteriaArray = [
'failing',
'active',
'establishing',
'pending',
];
// we track the sorting order, so it can be used in the custom sorting function
@tracked customSortOrderForStatus = 'asc';
// we define a "getter" that returns a custom sorting function ("s1" and "s2" are data records)
get customSortingMethodForStatus() {
return (s1, s2) => {
const index1 = customSortingCriteriaArray.indexOf(s1['status']);
const index2 = customSortingCriteriaArray.indexOf(s2['status']);
if (index1 < index2) {
return this.customSortOrderForStatus === 'asc' ? -1 : 1;
} else if (index1 > index2) {
return this.customSortOrderForStatus === 'asc' ? 1 : -1;
} else {
return 0;
}
};
}
// we define a callback function that listens to the `onSort` event in the table,
// and updates the tracked sort order values accordingly
@action
customOnSort(_sortBy, sortOrder) {
this.customSortOrderForStatus = sortOrder;
}
Density
To create a condensed or spacious Advanced Table, add @density
to the Advanced Table’s invocation. Note that it only affects the table body, not the table header.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Release Year")
}}
@density="short"
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Horizontal alignment
To create a column that has right-aligned content, set @align
to right
on both the column’s header and cell (the cell’s horizontal content alignment should be the same as the column’s horizontal content alignment).
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash label="Actions" align="right")
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td @align="right">
<Hds::Dropdown @isInline={{true}} as |dd|>
<dd.ToggleIcon @icon="more-horizontal" @text="Overflow Options" @hasChevron={{false}} @size="small" />
<dd.Interactive @route="components">Create</dd.Interactive>
<dd.Interactive @route="components">Read</dd.Interactive>
<dd.Interactive @route="components">Update</dd.Interactive>
<dd.Separator />
<dd.Interactive @route="components" @color="critical" @icon="trash">Delete</dd.Interactive>
</Hds::Dropdown>
</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Tooltip
Header cells should be clear, concise, and straightforward whenever possible. However, there could be cases where the label is insufficient by itself and extra information is required. In this case, it’s possible to show a tooltip next to the label in the header:
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist")
(hash key="album" label="Album" tooltip="Title of the album (in its first release)")
(hash key="vinyl-cost" label="Vinyl Cost (USD)" isSortable=true tooltip="Cost of the vinyl (adjusted for inflation)" align="right")
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td @align="right">{{B.data.vinyl-cost}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Scrollable table
Consuming a large amount of data in a tabular format can lead to an intense cognitive load for the user. As a general principle, care should be taken to simplify the information within a table as much as possible.
We recommend using functionalities like pagination, sorting, and filtering to reduce this load.
Vertical scrolling
For situations where the default number of rows visible may be high, it can be difficult for users to track which column is which once they scroll. In this case, the hasStickyHeader
argument can be used to make the column headers persist as the user scrolls.
<!-- this is an element with "overflow: auto" and "max-height: 500px" -->
<div class="doc-advanced-table-vertical-scrollable-wrapper">
<Hds::AdvancedTable
@model={{this.demoDataWithLargeNumberOfRows}}
@columns={{array
(hash key="id" label="ID")
(hash key="name" label="Name" isSortable=true)
(hash key="email" label="Email")
(hash key="role" label="Role" isSortable=true)
}}
@hasStickyHeader={{true}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.id}}</B.Td>
<B.Td>{{B.data.name}}</B.Td>
<B.Td>{{B.data.email}}</B.Td>
<B.Td>{{B.data.role}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
</div>
Horizontal scrolling
There may be cases when it’s necessary to show an Advanced Table with a large number of columns and allow the user to scroll horizontally. In this case the consumer can place the Advanced Table inside a container with overflow: auto
.
<!-- this is an element with "overflow: auto" -->
<div class="doc-advanced-table-scrollable-wrapper">
<Hds::AdvancedTable
@model={{this.demoDataWithLargeNumberOfColumns}}
@columns={{array
(hash key="first_name" label="First Name" isSortable=true)
(hash key="last_name" label="Last Name" isSortable=true)
(hash key="age" label="Age" isSortable=true)
(hash key="email" label="Email")
(hash key="phone" label="Phone")
(hash key="bio" label="Biography" width="350px")
(hash key="education" label="Education Degree")
(hash key="occupation" label="Occupation")
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.first_name}}</B.Td>
<B.Td>{{B.data.last_name}}</B.Td>
<B.Td>{{B.data.age}}</B.Td>
<B.Td>{{B.data.email}}</B.Td>
<B.Td>{{B.data.phone}}</B.Td>
<B.Td>{{B.data.bio}}</B.Td>
<B.Td>{{B.data.education}}</B.Td>
<B.Td>{{B.data.occupation}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
</div>
Multi-select Advanced Table
A multi-select Advanced Table includes checkboxes enabling users to select multiple rows for purposes of performing bulk operations. Checking or unchecking the checkbox in the Advanced Table header either selects or deselects the checkboxes on each row in the body. Individual checkboxes in the rows can also be selected or deselected.
Add isSelectable=true
to create a multi-select Advanced Table. The onSelectionChange
argument can be used to pass a callback function to receive selection keys when the selected rows change. You must also pass a selectionKey
to each row which gets passed back through the onSelectionChange
callback which maps the row selection on the Advanced Table to an item in your data model.
Simple multi-select
This is a simple example of an Advanced Table with multi-selection. Notice the @selectionKey
argument provided to the rows, used by the @onSelectionChange
callback to provide the list of selected/deselected rows as argument(s) for the invoked function:
<Hds::AdvancedTable
@isSelectable={{true}}
@onSelectionChange={{this.demoOnSelectionChange}}
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist")
(hash key="album" label="Album")
(hash key="year" label="Year")
}}
>
<:body as |B|>
<B.Tr @selectionKey={{B.data.id}} @selectionAriaLabelSuffix="row {{B.data.artist}} / {{B.data.album}}">
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Here’s an example of what a @onSelectionChange
callback function could look like.
@action
demoOnSelectionChange({
selectionKey, // the `selectionKey` value for the selected row or "all" if the "select all" has been toggled
selectionCheckboxElement, // the checkbox DOM element toggled by the user
selectableRowsStates, // an array of objects describing each displayed "row" state (its `selectionKey` value and its `isSelected` state)
selectedRowsKeys // an array of all the `selectionKey` values of the currently selected rows
}) {
// here we use the `selectedRowsKeys` to execute some action on each of the data records associated (via the `@selectionKey` argument) to the selected rows
selectedRowsKeys.forEach((rowSelectionKey) => {
// do something using the row’s `selectionKey` value
// ...
// ...
// ...
});
}
For details about the arguments provided to the @onSelectionChange
callback function, refer to the Component API section.
Multi-select with sorting by selection state
To enable sorting by selected rows in an Advanced Table, you need to set @selectableColumnKey
to the key in each row that tracks its selection state. This allows you to sort based on whether rows are selected or not.
In the demo below, we set up a multi-select Advanced Table that can be sorted based on the selection state of its rows.
<Hds::AdvancedTable
@isSelectable={{true}}
@selectableColumnKey="isSelected"
@onSelectionChange={{this.demoOnSelectionChangeSortBySelected}}
@model={{this.demoSortBySelectedData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Year" isSortable=true)
(hash key="selection" label="Selected" isSortable=true)
}}
@sortBy="isSelected"
@sortOrder="desc"
>
<:body as |B|>
<B.Tr
@selectionKey={{B.data.id}}
@isSelected={{B.data.isSelected}}
@selectionAriaLabelSuffix="row {{B.data.artist}} / {{B.data.album}}"
>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
<B.Td>{{if B.data.isSelected "Yes" "No"}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Multi-select with pagination and persisted selection status
This is a more complex example, where an Advanced Table with multi-selection is associated with a Pagination element (a similar use case would apply if a filter is applied to the data used to populate the Advanced Table). In this case, a subset of rows is displayed on screen.
When a user selects a row, if the displayed rows are replaced with other ones (e.g., when the user clicks on the “next” button or on a different page number) there’s the question of what happens to the previous selection: is it persisted in the data/model underlying the table? Or is it lost?
In the demo below, we are persisting the selection in the data/model, so that when navigating to different pages, the row selections persist across table re-renderings.
<div class="doc-advanced-table-multiselect-with-pagination-demo">
<Hds::AdvancedTable
@isSelectable={{true}}
@onSelectionChange={{this.demoOnSelectionChangeWithPagination}}
@model={{this.demoPaginatedData}}
@columns={{array
(hash key="artist" label="Artist")
(hash key="album" label="Album")
(hash key="year" label="Year")
}}
>
<:body as |B|>
<B.Tr @selectionKey={{B.data.id}} @isSelected={{B.data.isSelected}} @selectionAriaLabelSuffix="row {{B.data.artist}} / {{B.data.album}}">
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
<Hds::Pagination::Numbered
@totalItems={{this.demoTotalItems}}
@currentPage={{this.demoCurrentPage}}
@pageSizes={{array 2 4}}
@currentPageSize={{this.demoCurrentPageSize}}
@onPageChange={{this.demoOnPageChange}}
@onPageSizeChange={{this.demoOnPageSizeChange}}
@ariaLabel="Pagination for multi-select table"
/>
</div>
Depending on the expected behavior, you will need to implement the consumer-side logic that handles the persistence (or not) using the @onSelectionChange
callback function. For the example above, something like this:
@action
demoOnSelectionChangeWithPagination({ selectableRowsStates }) {
// we loop over all the displayed table rows (a subset of the dataset)
selectableRowsStates.forEach((row) => {
// we find the record in the dataset corresponding to the current row
const recordToUpdate = this.demoSourceData.find(
(modelRow) => modelRow.id === row.selectionKey
);
if (recordToUpdate) {
// we update the record `isSelected` state based on the row (checkbox) state
recordToUpdate.isSelected = row.isSelected;
}
});
}
For details about the arguments provided to the @onSelectionChange
callback function, refer to the Component API section.
Usability and accessibility considerations
Since the “selected” state of a row is communicated with the checkbox selection, there are some important considerations to keep in mind when implementing a multi-select Advanced Table.
If the selection status of the rows is persisted even when a row is not displayed in the UI, consider what the expectations of the user might be: how are they made aware that the action they are going to perform may involve rows that were previously selected but not displayed in the current view?
Even more complex is the case of the “Select all” checkbox in the Advanced Table header. While the expected behavior might seem straightforward when all rows are displayed, it may not be obvious what the expected behavior is when the rows are paginated or have been filtered.
Consider the experience of a user intending to select all or a subset of all possible rows:
If a user interacts with a “Select all” function or button, is the expectation that only displayed rows are selected (what happens in the example above), or that all of the rows in the data set/model are selected, even if not displayed in the current view?
In the first scenario, the “Select all” state changes depending on what rows are in view and can be confusing.
In the second scenario it might not be obvious that all of the rows have been selected and may result in the user unintentionally performing a destructive action under the assumption that they have only selected the rows in the current view.
Whatever functionality you decide to implement, be mindful of all these possible subtleties and complexities.
At a bare minimum we recommend clearly communicating to the user if they have selected rows outside of their current view and how many out of the total data set are selected. We're working to document these scenarios as they arise, in the meantime contact the Design Systems Team for assistance.
More examples
Visually hidden headers
Labels within the header cells are intended to provide contextual information about the column’s content to the end user. There may be special cases in which that label is redundant from a visual perspective, because the kind of content can be inferred by looking at it (eg. a contextual dropdown).
In this example we’re visually hiding the label in the last column by passing isVisuallyHidden=true
to it:
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash key="album" label="Album" isSortable=true)
(hash key="year" label="Year" isSortable=true)
(hash key="other" label="Select an action from the menu" isVisuallyHidden=true width="60px")
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
<B.Td>
<Hds::Dropdown as |D|>
<D.ToggleIcon
@icon="more-horizontal"
@text="Overflow Options"
@hasChevron={{false}}
@size="small"
/>
<D.Interactive
@href="#"
@color="critical"
@icon="trash"
>Delete</D.Interactive>
</Hds::Dropdown>
</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Notice: only non-sortable headers can be visually hidden.
Internationalized column headers, overflow menu dropdown
Here’s an Advanced Table implementation that uses an array hash with localized strings for the column headers. It indicates which columns should be sortable, and adds an overflow menu.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@columns={{array
(hash key="artist" label=(t "components.table.headers.artist") isSortable=true)
(hash key="album" label=(t "components.table.headers.album") isSortable=true)
(hash key="year" label=(t "components.table.headers.year") isSortable=true)
(hash key="other" label=(t "global.titles.other"))
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{B.data.artist}}</B.Td>
<B.Td>{{B.data.album}}</B.Td>
<B.Td>{{B.data.year}}</B.Td>
<B.Td>
<Hds::Dropdown as |D|>
<D.ToggleIcon
@icon="more-horizontal"
@text="Overflow Options"
@hasChevron={{false}}
@size="small"
/>
<D.Interactive @href="#">Create</D.Interactive>
<D.Interactive @href="#">Read</D.Interactive>
<D.Interactive @href="#">Update</D.Interactive>
<D.Separator />
<D.Interactive @href="#" @color="critical" @icon="trash">Delete</D.Interactive>
</Hds::Dropdown>
</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
Component API
The Advanced Table component itself is where most of the options will be applied. However, the APIs for the child components are also documented here in case a custom implementation is desired.
AdvancedTable
<:body>
named block
[B].rowIndex
number | string
@each
loop. Returns a number when there are no nested rows. Returns a string in the form ${parentIndex}.${childIndex}
when there are nested rows.
[B].sortBy
string
sortBy
tracked variable.
[B].sortOrder
string
sortOrder
tracked variable.
[B].isExpanded
boolean
isExpanded
tracked variable from the row if it has nested rows; otherwise returns undefined
.
model
array
children
array
children
key in the model to render the child content. The key can be changed by setting childrenKey
argument on the Hds::AdvancedTable
.
isExpanded
boolean
isExpanded
to the row in the model.
columns
array
hash
that defines each column with key-value properties that describe each column. Options:
label
string
key
string
isSortable
boolean
- false (default)
true
, indicates that a column should be sortable.Important: Advanced Table does not support setting
isSelectable
to true when there are nested rows.
align
enum
- left (default)
- center
- right
width
string
isVisuallyHidden
boolean
- false (default)
true
, it visually hides the column’s text content (it will still be available to screen readers for accessibility). Only available for non-sortable columns.
sortingFunction
function
tooltip
string
Tooltip
. May contain basic HTML tags for formatting text such as strong
and em
tags. Not intended for multi-paragraph text or other more complex content. May not contain interactive content such as links or buttons. The placement
and offset
are automatically set and can’t be overwritten.
sortBy
string
sortOrder
string
- asc (default)
- desc
sortBy
. If defined, indicates which direction the column should be pre-sorted in. If not defined, asc
is applied by default.
isSelectable
boolean
- false (default)
true
, creates a “multi-select” table which renders checkboxes in the table header and on the table rows enabling bulk interaction. Use in conjunction with onSelectionChange
on the Table
and selectionKey
on each Table::Tr
.Important: Advanced Table does not support having
isSelectable
true when there are nested rows.
onSelectionChange
function
isSelectable
to pass a callback function to know the selection state. Must be used in conjunction with setting a selectionKey
on each Table::Tr
.
When called, this function receives an object as argument, with different keys corresponding to different information:
selectionKey
: the value of the@selectionKey
argument associated with the row selected/deselected by the user orall
if the “select all” checkbox has been toggledselectionCheckboxElement
: the checkbox (DOM element) that has been toggled by the user.selectedRowsKeys
: an array containing all the@selectionKey
s of the selected rows in the table (an empty array is returned if no row is selected).selectableRowsStates
: an array of objects corresponding to all the rows displayed in the table when the user changed a selection; each object contains the@selectionKey
value for the associated row and itsisSelected
boolean state (if the checkbox is checked or not).
Important: the order of the rows in the array doesn’t necessarily follow the order of the rows in the table/DOM.
isStriped
boolean
- false (default)
Important: Advanced Table does not support setting
isStriped
to true when there are nested rows.
hasStickyHeader
boolean
- false (default)
density
enum
- short
- medium (default)
- tall
valign
enum
- top (default)
- middle
- baseline
th
). See MDN reference on vertical-align for more details.
selectableColumnKey
string
@model
item property is used to sort items by selection state. If this argument is not provided, the option to sort by selection state will not be available.
caption
string
identityKey
'none'|string
- @identity (default)
each
iterator. If identityKey="none"
, this is interpreted as an undefined
value for the @identity
key option.
sortedMessageText
string
- Sorted by (label), (asc/desc)ending (default)
caption
element when a sort is performed.
childrenKey
string
- children (default)
@model
item property is used to render nested rows. If this argument is not provided, the default will be used.
onSort
function
sortBy
and sortOrder
as arguments.
…attributes
...attributes
.
AdvancedTable::Tr
This component can contain Hds::AdvancedTable::Th
or Hds::AdvancedTable::Td
components.
yield
<div role="row">
HTML element.
isSelected
boolean
- false (default)
isSelectable
on the Advanced Table).
selectionKey
string | number
isSelectable
on the Advanced Table and returned in the onSelectionChange
callback arguments). It’s required if isSelectable=true
.
selectionAriaLabelSuffix
string
aria-label
attribute applied to the checkbox used to select the row (used in conjunction with setting isSelectable
on the AdvancedTable
). The component automatically prepends “Select/Deselect” to the string, depending on the selection status. It’s required if isSelectable=true
.
…attributes
...attributes
.
AdvancedTable::Th
If the Th
component is passed as the first cell of a body row, role="rowheader"
is automatically applied for accessibility purposes.
align
enum
- left (default)
- center
- right
scope
string
- col (default)
- row
scope
should be set to row
for accessibility purposes. Note: you only need to manually set this if you’re creating a custom table using the child components; if you use the standard invocation for the table, this scope is already provided for you.
width
string
tooltip
string
Tooltip
. May contain basic HTML tags for formatting text such as strong
and em
tags. Not intended for multi-paragraph text or other more complex content. May not contain interactive content such as links or buttons. The placement
and offset
are automatically set and can’t be overwritten.
isVisuallyHidden
boolean
- false (default)
true
, it visually hides the column’s text content (it will still be available to screen readers for accessibility).
colspan
string
rowspan
string
yield
<div role="columnheader">
or <div role="rowheader">
HTML element.
…attributes
...attributes
.
AdvancedTable::Td
align
enum
- left (default)
- center
- right
colspan
string
rowspan
string
yield
<div role="gridcell">
HTML element.
…attributes
...attributes
.
Anatomy
Advanced Table headers
Element | Usage |
---|---|
Checkbox | Optional, but required when cells yield a checkbox |
Label | Required |
Tooltip button | Optional |
Sort button | Optional, Options: none, ascending, descending |
Container | Required |
Advanced Table cells
Expandable cell
Element | Usage |
---|---|
Expand | Optional |
Cell content | Required |
Icon | Optional |
Container | Required |
Nested cell
Element | Usage |
---|---|
Nested | Required |
Cell content | Required |
Icon | Optional |
Container | Required |
Selection cell
Element | Usage |
---|---|
Checkbox | Optional, but required when the header yields a checkbox |
Cell content | Required |
Icon | Optional |
Container | Required |
States
Sort button
Sortable header columns have interactive state variants, indicating their interactivity. Non-sortable header columns are static and do not include state variants.
Advanced Table cells
Cells have a default and focused state. The focused state provides visual feedback for keyboard navigation.
Conformance rating
When used as recommended, there should not be any WCAG conformance issues with this component.
Best practices
Interactive rows
The table row element cannot receive interactions, meaning actions cannot be attached directly to a table row. If you need an interactive element, place it within a table cell element in that row (i.e., <div role="gridcell"><a href="somelink.html">Some link</a></div>
).
Focus in Advanced Tables
Unlike the Table component, each cell receives focus in the Advanced Table to support users navigating through the table efficiently with a keyboard. For any other interactions, you must use interactive elements (buttons, links, etc.) within the cells.
Row selection
You should clearly communicate to the user how many rows are selected and how many rows there are total outside of the Advanced Table. For additional considerations, read the Multi-select usability and accessibility considerations.
Applicable WCAG Success Criteria
This section is for reference only. This component intends to conform to the following WCAG Success Criteria:
-
1.3.1
Info and Relationships (Level A):
Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. -
1.3.2
Meaningful Sequence (Level A):
When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. -
1.4.1
Use of Color (Level A):
Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. -
1.4.10
Reflow (Level AA):
Content can be presented without loss of information or functionality, and without requiring scrolling in two dimensions. -
1.4.11
Non-text Contrast (Level AA):
The visual presentation of the following have a contrast ratio of at least 3:1 against adjacent color(s): user interface components; graphical objects. -
1.4.12
Text Spacing (Level AA):
No loss of content or functionality occurs by setting all of the following and by changing no other style property: line height set to 1.5; spacing following paragraphs set to at least 2x the font size; letter-spacing set at least 0.12x of the font size, word spacing set to at least 0.16 times the font size. -
1.4.13
Content on Hover or Focus (Level AA):
Where receiving and then removing pointer hover or keyboard focus triggers additional content to become visible and then hidden, the following are true: dismissible, hoverable, persistent (see link). -
1.4.3
Minimum Contrast (Level AA):
The visual presentation of text and images of text has a contrast ratio of at least 4.5:1 -
1.4.4
Resize Text (Level AA):
Except for captions and images of text, text can be resized without assistive technology up to 200 percent without loss of content or functionality. -
2.1.1
Keyboard (Level A):
All functionality of the content is operable through a keyboard interface. -
2.1.2
No Keyboard Trap (Level A):
If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface. -
2.1.4
Character Key Shortcuts (Level A):
If a keyboard shortcut is implemented in content using only letter (including upper- and lower-case letters), punctuation, number, or symbol characters, then it should be able to be turned off, remapped, or active only on focus. -
2.4.3
Focus Order (Level A):
If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability. -
2.4.7
Focus Visible (Level AA):
Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible. -
4.1.2
Name, Role, Value (Level A):
For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.
Support
If any accessibility issues have been found within this component, let us know by submitting an issue.
4.16.0
Added AdvancedTable
component and related sub-components
Related
-
Table
Used to display organized, two-dimensional tabular data.
-
Pagination
Used to let users navigate through content broken down into pages.
-
Filter patterns
Guidelines and best practices for filtering a data set using Helios components.
-
Table Multi-select
Guidelines, compositions, and interaction patterns for selecting and transforming results in a Table.