The Advanced Table is designed for complex datasets, featuring sticky headers, keyboard navigation, and expandable rows. The Table and Advanced Table features aren't interchangeable.
Usage
When to use
- For large data sets with many properties that benefit from being viewed in a scrollable container.
- When expandable rows are needed for hierarchical data.
- When users would benefit from more efficient keyboard navigation, e.g., when there are many rows or columns.
- When users would benefit from customizing the view of the data set (column width, order, etc).
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 data visualizations 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
- While multiple columns may offer sorting, only one column can be sorted at a time.
- In addition to standard sorting methods (like alphabetical or chronological), domain-specific sorting, such as by status severity, can also be useful.
- Sorting is not relevant for all content and should be applied thoughtfully.

Tooltips
Labels should be concise and straightforward. If more context is 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 within the table structure.

Alignment
The content's alignment can impact readability and scannability. The proper alignment method depends on the content type and its relative position in the table.
Use consistent alignment between the header label and the cell content in a column.

Avoid misaligned header labels and content.

Left alignment
By default, align content to the left. This lends itself to the default left-to-right reading order of most content types.
Use left alignment for:
- Strings (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, 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, currency amounts, or other numbers with decimal values.
- In a column with 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 align content of varied lengths to the right. This can make it difficult to read by forcing an unnatural reading pattern.

Other alignment methods
We don’t recommend centered or justified content alignment. These can be difficult to read, especially when the content varies in length.
Don’t center header labels or cell content within a table.

Reordering columns
If hasReorderableColumns is enabled on the Ember component, users can reorder columns either by clicking and dragging on the column reorder handle with a mouse, or by moving focus to the handle with a keyboard and using the right and left arrow keys.


Actions related to moving columns are displayed in a context menu in the table header. These are not customizable and include:
- Move column: moves focus to the reordering handle
- Move column to start/end: moves the column to the first or last position in the table unless the column is already in this position.

Resizing columns
Columns can be resized by dragging the "resize border" with a mouse or by moving focus to it and using the left and right arrow keys.
The Figma component does not support this resizing feature. Instead, we publish a Resize Border component and Templates to use as a starting point for expressing this interaction. We also provide examples that you can copy and paste into your design files.

When resizable columns are enabled, actions related to each function are rendered in a context menu in the table header. These functions are not customizable.

Minimum and maximum width
To prevent a column from being resized beyond a reasonable amount, we enforce a default minimum width of 150px and a maximum width of 800px. These can be overridden via the component API if necessary.

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 concise and straightforward.
- The label should clearly indicate what type of content is contained within the cell (string, number, status, etc).
- Labels should always use sentence-case.
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.

Expand/Collapse All Button
The Expand/Collapse All button allows users to expand or collapse all rows, including nested rows. It provides quick access to more content but may impact readability when content is long or detailed.
Interactions
Default state
The Advanced Table supports any combination of expanded or collapsed rows on load. The button reflects the initial state.
Collapsed state
Expanded state
Mixed state
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
The rowPlacement property determines the border radius of a cell. It is only available on cells where the colPlacement property is set to start or end.

Cells
For the user to scan, sort, and filter the table easily, each cell should contain a single piece of data. Having more than one piece of data in a cell makes it harder for users to navigate the relationships between headers and cells.
Density
- By default, use the
mediumdensity for balance and readability. - To fit more rows on a page, use the
shortdensity. Use this only for text-heavy tables, as it can make them harder to scan. - For a smaller dataset, e.g., basic user data, consider using the
talldensity to provide the content with more breathing room.
Horizontal scrolling
Use horizontal scrolling when the number of columns expands beyond the viewport or container.

Sticky headers and columns
The header and first column can be pinned, helping users navigate large datasets while persisting key values, such as names or IDs.
There are a few things to consider when implementing a sticky header or column:
- Instances of the Advanced Table with nested rows, expandable rows, and
colSpanorrowSpando not support a sticky column because what classifies as the first column is variable depending on these properties. - Setting the first column as sticky in a table with multi-selection will couple the multi-select column and the first column of data together.
If hasStickyFirstColumn is set to true or false in the Ember component, a control will be exposed in the context menu allowing users to "Pin" and "Unpin" the first column in the Advanced Table.


Multi-Select
Multi-select allows users to select multiple rows to perform bulk actions, such as deleting or exporting data. Selection states are maintained across pagination and filtering.
A multi-select pattern consists of:
A "Select all" checkbox is used in the header row to allow the simultaneous selection or deselection of all child rows.

Individual checkboxes added to each row allow for the selection of that row.

For more details, see the Multi-Select Table Pattern.
Empty state
The Advanced Table supports displaying an empty state using the Application State component to display an informative message and prompt user action. There are a number of reasons that will cause an empty state to occur; the data set is empty, applying filters did not return any results, there is an error fetching the data, etc.

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 };
}
}
Then, in the template code you will need to:
- pass the data model to the
@modelargument of the AdvancedTable component - provide a
@columnsargument to describe the expected columns (see Component API for details) - insert your own content into the
:bodyblock (the component will take care of looping over the@model) - use the
.datakey to access the@modelrecord content (it’s yielded asdata)
<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" isExpandable=true)
(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>
Reordering columns
Set the @hasReorderableColumns argument to true in order to make columns reorderable either by clicking and dragging on the column reorder handle, or by moving focus to the handle and using the right and left arrow keys.
Columns will render in the order they appear in the @columns array. However, this order can be overridden by providing an array of column keys to the @columnOrder argument.
Optionally, the @onColumnReorder attribute accepts a callback function that receives the updated column key order.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@hasReorderableColumns={{true}}
@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>
Resizing Columns
Set the @hasResizableColumns argument to true in order to make columns resizable either by clicking and dragging on the column border with a mouse, or by moving focus to the resize border with a keyboard and using the right and left arrow keys.
Optionally, the @onColumnResize attribute accepts a callback function that receives the resized column's key and new size in CSS pixels (e.g., "12px").
Reset the column to its original width by choosing the "Reset column width" option in the header cell context menu.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@hasResizableColumns={{true}}
@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>
By default, the minimum and maximum width of each column are set to 150px and 800px respectively. This can be overridden if necessary by passing either a minWidth or maxWidth argument to the columns array.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@hasResizableColumns={{true}}
@columns={{array
(hash key="artist" label="Artist" isSortable=true)
(hash
key="album"
label="Album"
isSortable=true
width="300px"
minWidth="200px"
maxWidth="500px"
)
(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>
Content wrapping
By default, content within the cells will wrap according to the browser’s natural reflow. This may result in the layout shifting.
How resizing for the cell content works is determined by the implementation. For example, truncation with an ellipsis can be achieved by applying custom CSS to the relevant element within the table cell, e.g., text-overflow: ellipsis; white-space: nowrap; overflow: hidden;.
<Hds::AdvancedTable
@model={{this.model.myDemoData}}
@hasResizableColumns={{true}}
@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>
<div class="doc-advanced-table-cell-content-div">
<span class="doc-advanced-table-text-truncate">
{{B.data.artist}}
</span>
</div>
</B.Td>
<B.Td>
<div class="doc-advanced-table-cell-content-div">
<span class="doc-advanced-table-text-truncate">
{{B.data.album}}
</span>
</div>
</B.Td>
<B.Td>
<div class="doc-advanced-table-cell-content-div">
<span class="doc-advanced-table-text-truncate">
{{B.data.year}}
</span>
</div>
</B.Td>
</B.Tr>
</:body>
</Hds::AdvancedTable>
<style>
.doc-advanced-table-cell-content-div {
display: flex;
align-items: center;
min-width: 0;
}
.doc-advanced-table-text-truncate {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
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
sortingFunctionin the column hash. - include a custom
onSortaction 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;
}
Filtering
The Advanced Table supports filtering through the actions named block and the FilterBar contextual component. The FilterBar contextual component utilizes the HDS Filter Bar component. You can read more about how the component works, and the features supported within it in the Filter Bar component docs.
The Filter Bar component doesn't handle filtering the data, that must still be handled by the consumer, but it provides a way for users to submit filters and for those filters applied to be shown.