The page navigation is complete. You may now navigate the page content as you wish.
Skip to main content

Advanced Table

Added in v4.16.0

Used to display organized, two-dimensional tabular data.

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:

Artist
Album
Year
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
Melanie
Candles in the Rain
1971
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Simon and Garfunkel
Bridge Over Troubled Waters
1970
<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>

Important

For clarity, there are a couple of important points to note here:

  • provide a @columns argument (see Component API for details about its shape)
  • use the .data key to access the @model record content (it’s yielded as data)

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.

It is not currently supported to have @isStriped, multi-select, or sortable columns with nested rows. If your use case requires any of these features, please contact the Design Systems Team.

Name
Status
Description
Policy set 1
Pass
Policy set 2
Fail
<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

This component takes advantage of the sort-by helper provided by ember-composable-helpers.

Add isSortable=true to the hash for each column that should be sortable.

At this time, the Advanced Table does not support sortable nested rows. If this is a use case you require, please contact the Design Systems Team.

Artist
Album
Release Year
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
Melanie
Candles in the Rain
1971
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Simon and Garfunkel
Bridge Over Troubled Waters
1970
<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.

Sorted by artist ascending
Artist
Album
Release Year
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Melanie
Candles in the Rain
1971
Nick Drake
Pink Moon
1972
Simon and Garfunkel
Bridge Over Troubled Waters
1970
The Beatles
Abbey Road
1969
<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".

Sorted by artist descending
Artist
Album
Release Year
The Beatles
Abbey Road
1969
Simon and Garfunkel
Bridge Over Troubled Waters
1970
Nick Drake
Pink Moon
1972
Melanie
Candles in the Rain
1971
James Taylor
Sweet Baby James
1970
Bob Dylan
Bringing It All Back Home
1965
<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:

  1. add a custom function as the value for sortingFunction in the column hash,
  2. 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 Table, add @density to the Table’s invocation. Note that it only affects the Table body, not the Table header.

Artist
Album
Release Year
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
Melanie
Candles in the Rain
1971
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Simon and Garfunkel
Bridge Over Troubled Waters
1970
<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).

Artist
Album
Actions
Nick Drake
Pink Moon
The Beatles
Abbey Road
Melanie
Candles in the Rain
Bob Dylan
Bringing It All Back Home
James Taylor
Sweet Baby James
Simon and Garfunkel
Bridge Over Troubled Waters
<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:

Artist
Album
Vinyl Cost (USD)
Nick Drake
Pink Moon
29.27
The Beatles
Abbey Road
25.99
Melanie
Candles in the Rain
46.49
Bob Dylan
Bringing It All Back Home
29.00
James Taylor
Sweet Baby James
16.00
Simon and Garfunkel
Bridge Over Troubled Waters
20.49
<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.

ID
Name
Email
Role
1
Burnaby Kuscha
1_bkuscha0@tiny.cc
Owner
2
Barton Penley
2_bpenley1@miibeian.gov.cn
Admin
3
Norina Emanulsson
3_nemanulsson2@walmart.com
Contributor
4
Orbadiah Smales
4_osmales3@amazon.co.jp
Contributor
5
Dido Titchener
5_dtitchener4@blogs.com
Contributor
6
Trish Horsburgh
6_thorsburgh5@samsung.com
Contributor
7
Orion Laverack
7_olaverack6@techcrunch.com
Contributor
8
Delly Moulsdale
8_dmoulsdale7@sciencedirect.com
Contributor
9
Gil Carlyle
9_gcarlyle8@canalblog.com
Contributor
10
Marinna Corbin
10_mcorbin9@google.ca
Contributor
11
Yardley Entwhistle
11_yentwhistlea@tumblr.com
Contributor
12
Brinn Clack
12_bclackb@blogger.com
Contributor
13
Charleen Millen
13_cmillenc@mtv.com
Contributor
14
Kalie Piers
14_kpiersd@businessweek.com
Contributor
15
Laure Boxer
15_lboxere@elegantthemes.com
Contributor
16
Libby Bonallack
16_lbonallackf@disqus.com
Contributor
17
Zebedee Gofton
17_zgoftong@bbc.co.uk
Contributor
18
Sari Eckford
18_seckfordh@cloudflare.com
Contributor
19
Carlos Byrth
19_cbyrthi@prlog.org
Contributor
20
Avery Allmark
20_aallmarkj@webnode.com
Contributor
21
Ninnette McSpirron
21_nmcspirronk@amazon.com
Contributor
22
Sharlene Ewestace
22_sewestacel@twitpic.com
Contributor
23
Jessamine Kembry
23_jkembrym@hatena.ne.jp
Contributor
24
Homerus Dixcee
24_hdixceen@deviantart.com
Contributor
25
Clevie Clear
25_cclearo@tmall.com
Contributor
26
Mohammed Hubatsch
26_mhubatschp@salon.com
Contributor
27
Gigi Hovard
27_ghovardq@cbslocal.com
Contributor
28
Dorey Tinker
28_dtinkerr@google.co.uk
Contributor
29
Arel Mullarkey
29_amullarkeys@blogs.com
Contributor
30
Veronike Ventura
30_vventurat@google.fr
Contributor
31
Gerti Dranfield
31_gdranfieldu@vistaprint.com
Contributor
<!-- 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.

First Name
Last Name
Age
Email
Phone
Biography
Education Degree
Occupation
Judith
Maxene
43
j.maxene@randatmail.com
697-0732-81
Analyst. Gamer. Friendly explorer. Incurable TV lover. Social media scholar. Amateur web geek. Proud zombie guru.
Upper secondary school
Astronomer
Elmira
Aishah
28
e.aishah@randatmail.com
155-6076-27
Total coffee guru. Food enthusiast. Social media expert. TV aficionada. Extreme music advocate. Zombie fan.
Master in Physics
Actress
Chinwendu
Henderson
62
c.henderson@randatmail.com
155-0155-09
Creator. Internet maven. Coffee practitioner. Troublemaker. Alcohol specialist.
Bachelor in Modern History
Historian
<!-- 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.

At this time, the Advanced Table does not support multi-select nested rows. If this is a use case you require, please contact the Design Systems Team.

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:

Artist
Album
Year
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
Melanie
Candles in the Rain
1971
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Simon and Garfunkel
Bridge Over Troubled Waters
1970
<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>

Important

To make the Advanced Table accessible, each checkbox used for the selection needs to have a distinct aria-label. For this reason, you need to provide a @selectionAriaLabelSuffix value (possibly unique) to the rows in the Advanced Table's body.

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.

Sorted by isSelected descending
selection state
Artist
Album
Year
Selected
Nick Drake
Pink Moon
1972
Yes
Melanie
Candles in the Rain
1971
Yes
James Taylor
Sweet Baby James
1970
Yes
The Beatles
Abbey Road
1969
No
Bob Dylan
Bringing It All Back Home
1965
No
Simon and Garfunkel
Bridge Over Troubled Waters
1970
No
<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.

Artist
Album
Year
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
1–2 of 6
<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 visually via the checkbox selection and for screen-reader users via the aria-label applied to the checkbox, 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:

Artist
Album
Year
Select an action from the menu
Nick Drake
Pink Moon
1972
The Beatles
Abbey Road
1969
Melanie
Candles in the Rain
1971
Bob Dylan
Bringing It All Back Home
1965
James Taylor
Sweet Baby James
1970
Simon and Garfunkel
Bridge Over Troubled Waters
1970
<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, 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 AdvancedTable 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
This is a named block where the content for the AdvancedTable body is rendered.
[B].rowIndex number | string
The value of the index asssociated with the @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
The value of the internal sortBy tracked variable.
[B].sortOrder string
The value of the internal sortOrder tracked variable.
[B].isExpanded boolean
The value of the internal isExpanded tracked variable from the row if it has nested rows. Otherwise returns undefined.
model array
The data model to be used by the AdvancedTable. The model can have any shape, but for nested rows there are two expected keys.
children array
If there are nested rows, the AdvancedTable will use the 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
If there are nested rows, the default state of the toggle can be set by adding isExpanded to the row in the model.
columns array
Array hash that defines each column with key-value properties that describe each column. Options:
label string
Required
The column’s label.
key string
The column’s key (one of the keys in the model’s records); required if the column is sortable.
isSortable boolean
  • false (default)
If set to true, indicates that a column should be sortable.

Important: AdvancedTable does not support having isSelectable true when there are nested rows.
align enum
  • left (default)
  • center
  • right
Determines the horizontal content alignment (sometimes referred to as text alignment) for the column header.
width string
Any valid CSS
If set, determines the column’s width.
isVisuallyHidden boolean
  • false (default)
If set to 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
Callback function to provide support for custom sorting logic. It should implement a typical bubble-sorting algorithm using two elements and comparing them. For more details, see the example of custom sorting in the How To Use section.
tooltip string
Text string which will appear in the tooltip (see Tooltip for details). 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
If defined, the value should be set to the key of the column that should be pre-sorted.
sortOrder string
  • asc (default)
  • desc
Use in conjunction with 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)
If set to 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: AdvancedTable does not support having isSelectable true when there are nested rows.
onSelectionChange function
Use in conjunction with 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 or all if the “select all” checkbox has been toggled
  • selectionCheckboxElement: the checkbox (DOM element) that has been toggled by the user
  • selectedRowsKeys: an array containing all the @selectionKeys 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 its isSelected 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)
Determines if even-numbered rows will have a different background color from odd-numbered rows.

Important: AdvancedTable does not support having isStriped true when there are nested rows.
hasStickyHeader boolean
  • false (default)
Determines if the AdvancedTable has a sticky header.
density enum
  • short
  • medium (default)
  • tall
If set, determines the density (height) of the body's rows.
valign enum
  • top (default)
  • middle
  • baseline
Determines the vertical alignment for content in a table. Does not apply to table headers (th). See MDN reference on vertical-align for more details.
selectableColumnKey string
If set, this key determines which @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
Adds a (non-visible) caption for users with assistive technology. If set on a sortable table, the provided caption is paired with the automatically generated sorted message text.
identityKey 'none'|string
  • @identity (default)
Option to specify a custom key to the 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)
Customizable text added to caption element when a sort is performed.
childrenKey string
  • children (default)
If set, this key determines which @model item property is used to render nested rows. If this argument is not provided, the default will be used.
onSort function
Callback function that is invoked when one of the sortable headers is clicked (or has a keyboard interaction performed). The function receives the values of sortBy and sortOrder as arguments.
…attributes
This component supports use of ...attributes.

AdvancedTable::Tr

Note: This component is not eligible to receive interactions (e.g., it cannot have an onClick event handler attached directly to it). Instead, an interactive element should be placed inside of the AdvancedTable::Th, AdvancedTable::Td components.

This component can contain Hds::AdvancedTable::Th or Hds::AdvancedTable::Td components.

yield
Elements passed as children are yielded as inner content of a <div role="row"> HTML element.
isSelected boolean
  • false (default)
Sets the initial selection state for the row (used in conjunction with setting isSelectable on the AdvancedTable).
selectionKey string | number
Required value to associate an unique identifier to each table row (used in conjunction with setting isSelectable on the AdvancedTable and returned in the onSelectionChange callback arguments). It’s required if isSelectable=true.
selectionAriaLabelSuffix string
Descriptive 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
This component supports use of ...attributes.

AdvancedTable::Th

Note: This component is not eligible to receive interactions (e.g., it cannot have an onClick event handler attached directly to it). Instead, an interactive element should be placed inside of the AdvancedTable::Th component.

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
Determines the horizontal content alignment (sometimes referred to as text alignment) for the column header.
scope string
  • col (default)
  • row
If used as the first item in a table body’s 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
Any valid CSS
If set, determines the column’s width.
tooltip string
Text string which will appear in the tooltip (see Tooltip for details). 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)
If set to true, it visually hides the column’s text content (it will still be available to screen readers for accessibility).
colspan string
The number of columns the cell spans. Used to apply the correct grid styles and the aria-rowspan attribute for accessibility.
rowspan string
The number of rows the cell spans. Used to apply the correct grid styles and the aria-rowspan attribute for accessibility.
yield
Elements passed as children are yielded as inner content of a <div role="columnheader"> or <div role="rowheader"> HTML element.
…attributes
This component supports use of ...attributes.

AdvancedTable::Td

Note: This component is not eligible to receive interactions (e.g., it cannot have an onClick event handler attached directly to it). Instead, an interactive element should be placed inside of the AdvancedTable::Td component.

align enum
  • left (default)
  • center
  • right
Determines the horizontal content alignment (sometimes referred to as text alignment) for the cell (make sure it is also set for the column header).
colspan string
The number of columns the cell spans. Used to apply the correct grid styles and the aria-rowspan attribute for accessibility.
rowspan string
The number of rows the cell spans. Used to apply the correct grid styles and the aria-rowspan attribute for accessibility.
yield
Elements passed as children are yielded as inner content of a <div role="gridcell"> HTML element.
…attributes
This component supports use of ...attributes.

Conformance rating

Conformant

When used as recommended, there should not be any WCAG conformance issues with this component.

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

Added the Hds::AdvancedTable component.


Related