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

Used to display organized, two-dimensional tabular data.

The Table component should be used for displaying tabular data; it renders an HTML table element.

Usage

When to use

  • When comparing, sorting, and filtering multi-dimensional data and objects.
  • For data that only requires simple sorting or pagination, and does not require features like nested rows, advanced keyboard navigation, or sticky headers.

When not to use

  • As a layout mechanism. Instead, use Flex or Grid layout helpers.
  • When data requires scrolling, more levels of hierarchy, or when keyboard navigation is needed to achieve usability. Instead, use the Advanced Table.

Columns

Sorting

While multiple columns may offer sorting, only one column can be sorted at a time. 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 common examples where it may be useful to provide 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.

Tooltips in a Header Column

Don’t

We recommend against using a tooltip in all or most column labels in a table as this can add unnecessary visual clutter and increase the cognitive load on the user.

Tooltips in every column in a table

Width

Columns will fit the longest content in the column unless a specific column width has been declared.

Placement

Differences between Figma and code

The column placement property is only relevant in Figma and doesn’t exist as a property in code.

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.

Do

Use consistent alignment between the header label and the cell content in a column.

A table where the cells in the header have the same alignment as the cells in the column.

Don’t

Avoid misaligned header labels and content.

A table where the cells in the header don't match the alignment of the cells in the rest of the column.

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

Don’t align content of varied lengths to the right. This can make it difficult to read by forcing an unnatural reading pattern.

Right alignment with content that is variable in length

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

Don’t center header labels or cell content within a table.

Example of centered content within a table

Rows

Striping

Striping always starts with the second row, distinguishing it from the header.

Table striping examples

Benefits of striping

Accessibility alert

Ensure that content within striped rows maintains adequate color contrast with the striped background.

While striping is not required, we recommend it because it enhances readability by alternating row colors, making it easier to scan the data.

Table striping examples

Placement

Differences between Figma and code

The row placement property is only relevant in Figma and doesn’t exist as a property in code.

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.

Headers

  • Labels in headers should be concise and straightforward.
  • The label should clearly indicate what type of content is contained within the cell, e.g., Created date, Email, Project name.
  • Labels should always use sentence-case.

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 medium density for balance and readability.
  • To fit more rows on a page, use the short density. 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 tall density to provide the content with more breathing room.

Horizontal scrolling

Use horizontal scrolling when the number of columns exceeds the viewport or container. Use the Advanced Table when keyboard navigation and sticky columns ease the reading experience for large datasets.

For more information on the types of approaches used to implement horizontal scrolling, refer to the code tab

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 consists of:

A "Select all" checkbox is used in the header row to allow the simultaneous selection or deselection of all child rows.

Example of multi-select in a table header

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

Example of multi-select within table cells

For more details around using a multi-select Table, recommended patterns, and intended interactions visit the Multi-select patterns documentation.

Intended interaction

  • When individual rows are selected, the "Select all" checkbox in the header displays an indeterminate state.
  • When no or some rows (but not all) are selected, clicking "Select all" in the header will check the checkbox, and all rows on that page will be selected.
  • When all rows are selected, clicking "Select all" in the header will uncheck the checkbox, and all rows on that page will be deselected.
  • An additional action outside of the Table is needed in order to select all rows across a paginated Table.

How to use this component

Table with no model defined

If you want to use the component but have no model defined (e.g., there are only a few pieces of data but it’s still tabular data), you can manually add each row, or use an each to loop over the data (e.g., an array of objects defined in the route) to render the rows.

Manual row implementation

your custom, meaningful caption goes here
Column Header One Column Header Two Column Header Three
Cell one A Cell two A Cell three A
Cell one B Cell two B Cell three B
<Hds::Table @caption="your custom, meaningful caption goes here">
  <:head as |H|>
    <H.Tr>
      <H.Th>Column Header One</H.Th>
      <H.Th>Column Header Two</H.Th>
      <H.Th>Column Header Three</H.Th>
    </H.Tr>
  </:head>
  <:body as |B|>
    <B.Tr>
      <B.Td>Cell one A</B.Td>
      <B.Td>Cell two A</B.Td>
      <B.Td>Cell three A</B.Td>
    </B.Tr>
    <B.Tr>
      <B.Td>Cell one B</B.Td>
      <B.Td>Cell two B</B.Td>
      <B.Td>Cell three B</B.Td>
    </B.Tr>
  </:body>
</Hds::Table>

Using each to loop over records to create rows

Products that use Helios
Product Brand Color Uses Helios
Terraform purple true
Nomad green true
Vault yellow true
<Hds::Table @caption="Products that use Helios">
  <:head as |H|>
    <H.Tr>
      <H.Th>Product</H.Th>
      <H.Th>Brand Color</H.Th>
      <H.Th>Uses Helios</H.Th>
    </H.Tr>
  </:head>
  <:body as |B|>
    {{#each this.myDataItems as |item|}}
      <B.Tr>
        <B.Td>{{item.product}}</B.Td>
        <B.Td>{{item.brandColor}}</B.Td>
        <B.Td>{{item.usesHelios}}</B.Td>
      </B.Tr>
    {{/each}}
  </:body>
</Hds::Table>

Non-sortable Table with model defined

To use a Table with a model, first define the data model in your route or model:

import Route from '@ember/routing/route';

export default class ComponentsTableRoute 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 };
  }
}

Consumer responsibility

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.

Then, in the template code you will need to:

  • pass the data model to the @model argument of the Table component
  • provide a @columns argument to describe the expected columns (see Component API for details)
  • insert your own content into the :body block (the component will take care of looping over the @model)
  • use the .data key to access the @model record content (it’s yielded as data)
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::Table
  @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::Table>

Sortable table

Code tip

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

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

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::Table
  @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::Table>

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::Table
  @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::Table>
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::Table
  @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::Table>

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::Table
  @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::Table>

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;
}

Custom sorting using the yielded sorting arguments/functions

This is a pretty advanced example, intended to cover some edge cases that we encountered. We strongly suggest using one of the sorting methods described above, or contact the Design Systems Team before using this approach to make sure there are no better alternatives.

The Hds::Table exposes (via yielding) some of its internal properties and methods, to allow extremely customized sorting functionalities:

  • setSortBy is the internal function used to set the sortBy and sortOrder tracked values
  • sortBy is the "key" of the column used for sorting (when the table is sorted)
  • sortOrder is the sorting direction (ascending or descending)

For more details about these properties refer to the Component API section below.

Below you can see an example of a Table that renders a list of clusters, in which the sorting is based on a custom function that depends on the sorting column (sortBy) and direction (sortOrder):

The code has been simplified for clarity.

<Hds::Table>
  <:head as |H|>
    <H.Tr>
      <H.ThSort @onClickSort={{fn H.setSortBy "peer-name"}} @sortOrder={{if (eq "peer-name" H.sortBy) H.sortOrder}}>Peer Name</H.ThSort>
      <H.ThSort @onClickSort={{fn H.setSortBy "status"}} @sortOrder={{if (eq "status" H.sortBy) H.sortOrder}}>Status</H.ThSort>
      <H.ThSort @onClickSort={{fn H.setSortBy "partition"}} @sortOrder={{if (eq "partition" H.sortBy) H.sortOrder}}>Partition</H.ThSort>
      <H.Th>Description</H.Th>
    </H.Tr>
  </:head>
  <:body as |B|>
    {{#each (call (fn this.myDemoCustomSortingFunction B.sortBy B.sortOrder)) as |cluster|}}
      <B.Tr>
        <B.Td>{{cluster.peer-name}}</B.Td>
        <B.Td><ClusterStatusBadge @status={{cluster.status}} /></B.Td>
        <B.Td>{{cluster.cluster-partition}}</B.Td>
        <B.Td>{{cluster.description}}</B.Td>
      </B.Tr>
    {{/each}}
  </:body>
</Hds::Table>

In the <:head> the setSortBy function is invoked when the <ThSort> element is clicked to set the values of sortBy and sortOrder in the table; in turn these values are then used by the <ThSort> element to assign the sorting icon via the @sortOrder argument.

In the <:body> the values of sortBy and sortOrder are provided instead as arguments to a consumer-side function that takes care of custom sorting the model/data.

Notice: in this case for the example we're using the call helper from @nullvoxpopuli/ember-composable-helpers.

The sorting function in the backing class code will look something like this (the actual implementation will depend on the consumer-side/business-logic context):

The code has been simplified for clarity.

myDemoCustomSortingFunction = (sortBy, sortOrder) => {
  // here goes the logic for the custom sorting of the `model` or `data` array
  // based on the `sortBy/sortOrder` arguments
  if (sortBy === 'peer-name') {
    myDemoDataArray.sort((s1, s2) => {
      // logic for sorting by `peer-name` goes here
    });
  } else if (sortBy === 'status') {
    myDemoDataArray.sort((s1, s2) => {
      // logic for sorting by `status` goes here
    });
  //
  // same for all the other conditions/columns
  // ...
  }
  return myDemoDataArray;
};

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::Table
  @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::Table>

Alignment

Vertical alignment

To indicate that the table’s content should have a middle vertical-align, use @valign in the table’s invocation.

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::Table
  @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")
  }}
  @valign="middle"
>
  <: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::Table>

Vertical alignment with additional cell content

Code tip

Note that vertical-align only applies to inline, inline-block and table-cell elements: you can’t use it to vertically align block-level elements (see MDN reference).

If you have more than just text content in the table cell, you'll want to wrap that content in a flex box and style accordingly.

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::Table
  @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")
  }}
  @valign="middle"
>
  <:body as |B|>
    <B.Tr>
      <B.Td>
        <div class="doc-table-valign-demo">
          <Hds::Icon @name="headphones" /> {{B.data.artist}}
        </div>
      </B.Td>
      <B.Td>{{B.data.album}}</B.Td>
      <B.Td>{{B.data.year}}</B.Td>
    </B.Tr>
  </:body>
</Hds::Table>

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::Table
  @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::Table>

Tooltip

Table headers 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::Table
  @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::Table>

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.

That said, there may be cases when it’s necessary to show a table with a large number of columns and allow the user to scroll horizontally. In this case the consumer can use different approaches, depending on their context, needs and design specs.

Below we show a couple of examples of how a scrollable table could be implemented: use them as starting point (your mileage may vary).

Using a container with overflow: auto

In most cases, wrapping the table with a container that has overflow: auto does the trick.

The default table layout is auto which means the browser will try to optimize the width of the columns to fit their different content. In some cases, this will mean the content may wrap (see the Phone column as an example) in which case you may want to apply a width to suggest to the browser to apply a specific width to a column (see the Biography column).

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-table-scrollable-wrapper">
  <Hds::Table
    @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::Table>
</div>

Using a container with overflow: auto and a sub-container with width: max-content

If you have specified the width of some of the columns, leaving the others to adapt to their content automatically, and you want to avoid the wrapping of content within the cells, you need to introduce a secondary wrapping element around the table with its width set to max-content.

In this case the table layout is still set to auto (default). If instead you want to set it to fixed (using the @isFixedLayout argument) you will have to specify the width for every column or the table will explode horizontally.

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-table-scrollable-wrapper">
  <!-- this is an element with "width: max-content" -->
  <div class="doc-table-max-content-width">
    <Hds::Table
      @model={{this.demoDataWithLargeNumberOfColumns}}
      @columns={{array
        (hash key="first_name" label="First Name" isSortable=true width="200px")
        (hash key="last_name" label="Last Name" isSortable=true width="200px")
        (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::Table>
  </div>
</div>

Multi-select table

A multi-select table includes checkboxes enabling users to select multiple rows in a table for purposes of performing bulk operations. Checking or unchecking the checkbox in the table header either selects or deselects the checkboxes on each row in the table body. Individual checkboxes in the rows can also be selected or deselected.

Add isSelectable=true to create a multi-select table. The onSelectionChange argument can be used to pass a callback function to receive selection keys when the selected table 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 table to an item in your data model.

Multi-select table using a model

Code consideration

If you want the state of the checkboxes to persist after the model updates, you will need to provide an identityKey value.

This is a simple example of a 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::Table
  @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::Table>

Accessibility alert

To make the table correctly 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 table’s tbody.

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.

Code consideration

While it’s technically possible to use the multi-select feature in a table implemented without using a model, we strongly suggest converting the code to provide a @model to the table using a local dataset (created using the information/data you need to display).

Multi-select table using a model with sorting by selection state

To enable sorting by selected rows in a table, you need to set @selectableColumnKey to the key in each row that tracks its selection state. This allows you to sort the table based on whether rows are selected or not.

In the demo below, we set up a multi-select table that can be sorted based on the selection state of its rows.

Sorted by isSelected descending
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::Table
  @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::Table>

Multi-select table without a model with sorting by selection state

To enable sorting by selected rows in a table without using a model, you need to manage the data, selection state, and sorting logic. Set @selectableColumnKeyto the key in each row that tracks its selection state. Implement the @onSelectionChange and @onSort actions to handle selection changes and sorting events, updating your data and sorting parameters accordingly.

In the demo below, we set up a multi-select table without a model, where the selection and sorting are controlled externally. This approach allows the table to be sorted based on the selection state of its rows.

Artist Album Year
The Beatles Abbey Road 1969
Bob Dylan Bringing It All Back Home 1965
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
<Hds::Table
  @isSelectable={{true}}
  @selectableColumnKey="isSelected"
  @onSelectionChange={{this.demoSortBySelectedControlledOnSelectionChange}}
  @sortBy={{this.demoSortBySelectedControlledSortBy}}
  @sortOrder={{this.demoSortBySelectedControlledSortOrder}}
  @onSort={{this.demoSortBySelectedControlledOnSort}}
>
  <:head as |H|>
    <H.Tr>
      <H.Th>Artist</H.Th>
      <H.Th>Album</H.Th>
      <H.Th>Year</H.Th>
    </H.Tr>
  </:head>
  <:body as |B|>
    {{#each this.demoSortBySelectedControlledSortedData as |data|}}
      <B.Tr
        @selectionKey={{data.id}}
        @isSelected={{data.isSelected}}
        @selectionAriaLabelSuffix="row {{data.artist}} / {{data.album}}"
      >
        <B.Td>{{data.artist}}</B.Td>
        <B.Td>{{data.album}}</B.Td>
        <B.Td>{{data.year}}</B.Td>
      </B.Tr>
    {{/each}}
  </:body>
</Hds::Table>

Multi-select table with pagination and persisted selection status

This is a more complex example, where a 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 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-table-multiselect-with-pagination-demo">
  <Hds::Table
    @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::Table>
  <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 by the checkbox, there are some important considerations to keep in mind when implementing a multi-select 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 table header. While the expected behavior might seem straightforward when all rows are displayed in the table, it may not be obvious what the expected behavior is when the table 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 table headers

Labels within the table header 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::Table
  @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::Table>

Notice: only non-sortable headers can be visually hidden.

Internationalized column headers, overflow menu dropdown

Here’s a Table implementation that uses an array hash with strings for the column headers, indicates which columns should be sortable, and adds an overflow menu.

<Hds::Table
  @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::Table>

Component API

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

Table

<:head> named block
A named block where the content for the table head (<thead>) is rendered.
Note: most consumers are unlikely to need to use this named block directly.
[H].setSortBy function
The function used internally by the table to set the sortBy and sortOrder tracked values.
[H].sortBy string
Hook into this property to access the state of the internal sortBy tracked variable.
[H].sortOrder string
Hook into this property to access the state of the internal sortOrder tracked variable.
<:body> named block
This is a named block where the content for the table body (<tbody>) is rendered.
[B].rowIndex number
The value of the index associated with the @each loop. Available only when the @model/@columns arguments are provided.
[B].sortBy string
The value of the internal sortBy tracked variable.
[B].sortOrder string
The value of the internal sortOrder tracked variable.
model array
The data model to be used by the table.
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.
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.
onSelectionChange function
Use in conjunction with isSelectable to pass a callback function to know the table 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)
Define on the table invocation. If set to true, even-numbered rows will have a different background color from odd-numbered rows.
isFixedLayout boolean
  • false (default)
If set to true, the table-display(CSS) property will be set to fixed. See MDN reference on table-layout for more details.
density enum
  • short
  • medium (default)
  • tall
If set, determines the density (height) of the table 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 table caption is paired with the automatically generated sorted message text.
identityKey '@identity'|'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.
…attributes
This component supports use of ...attributes.
onSort function
Callback function that is invoked when one of the sortable table headers is clicked (or has a keyboard interaction performed). The function receives the values of sortBy and sortOrder as arguments.

Table::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 Th, Td elements.

This component can contain Hds::Table::Th, Hds::Table::ThSort, or Hds::Table::Td components.

yield
Elements passed as children are yielded as inner content of a <tr> HTML element.
isSelected boolean
  • false (default)
Sets the initial selection state for the row (used in conjunction with setting isSelectable on the Table).
selectionKey string
Required value to associate an unique identifier to each table row (used in conjunction with setting isSelectable on the Table 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 Table). 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.

Table::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 Th element.

If the Th component is passed as the first cell of a table body row, scope="row" 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).
yield
Elements passed as children are yielded as inner content of a <th> HTML element.
…attributes
This component supports use of ...attributes.

Table::ThSort

This is the component that supports column sorting; use instead of Hds::Table::Th if creating a custom table implementation.

sortOrder string
  • asc
  • desc
If defined, indicates which direction the column should be sorted. Controls the sort icon indicator and the aria-sort value.
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.
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.
onClickSort function
Callback function invoked when the sort button is clicked. By default, the sort is set by the column’s key.
yield
Elements passed as children are yielded as inner content of a <button> nested in a <th> HTML element. For this reason, you should avoid providing interactive elements as children (interactive controls should never be nested for accessibility reasons).
…attributes
This component supports use of ...attributes.

Table::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 Td element.

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).
yield
Elements passed as children are yielded as inner content of a <td> HTML element.
…attributes
This component supports use of ...attributes.

Anatomy

Table headers

Table header anatomy

Element Usage
Checkbox Optional, but required when cells yield a checkbox
Label Required
Tooltip button Optional
Sort button Options: none, ascending, descending
Container Required

Table cells

Table cells anatomy

Element Usage
Checkbox Optional, but required when the header yields a checkbox
Cell content Required
Icon Optional
Container Required

States

Sort button

Header column icon buttons, e.g., tooltip and sorting, have interactive states.

Table cells anatomy

For general content recommendations, refer to our Tables content guidelines documentation.

Conformance rating

Conformant

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

Focus in Tables

  • Table headers and labels are not eligible to receive focus, rather, focus will move through interactive elements (sort and tooltip buttons) contained within the header sequentially.
  • Interactive elements within cells will receive focus, but entire cells and entire rows will not.
Do

Example of focus order being properly applied to a table

Don’t

Example of focus order being incorrectly applied to a table

Best practices

Interactive rows

The table row element (tr) is not eligible to receive interactions. That is, actions cannot be attached to a table row. If an interactive element is desired, place it within a table cell element (td) within that row (i.e., <td><a href="somelink.html">Some link</a></td>).

For engineers

When providing additional or alternative styles to the table element, do not change the display property in the CSS. This alters how the table is presented to the user with assistive technology; they will no longer be presented with a table.

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

Update the model and returned B.data to use generic types, so the type of the data is retained.

4.22.0

Updated @isSelected argument type from false to boolean

Translated template strings

4.17.1

Removed unused updateAriaLabel function and event listener

4.16.0

Updated the visual design of Table cells by adding borders, making them more distinguishable when spanning rows or columns.

Fixed the aria-labels for select row and select all checkboxes so they do not change based on the state of the checkbox.

4.15.0

Exposed the index of the @each loop over the @model as rowIndex

4.11.0

Updated

Hds::Table

  • Added @selectableColumnKey argument which enables sorting by row selection state and specifies the corresponding selection state key.

Hds::Table::Tr

  • Added @selectableColumnKey argument which enables sorting by row selection state and specifies the corresponding selection state key.
  • Added @sortBySelectedOrder argument which determines the state of the sort button in the selected item column.
  • Added @onClickSortBySelected argument which is the callback for the sort button in the selected item column.

Hds::Table::ThSelectable

  • Added @onClickSortBySelected argument which is the callback for the sort button in the selected item column.
  • Added @sortBySelectedOrder argument which determines the state of the sort button in the selected item column.

4.10.0

Converted component and sub-components to TypeScript.


Related