Import#
import { Table } from '@dnb/eufemia'// Or with sub-components and hooks:import Table, {Th,Td,Tr,useTableKeyboardNavigation,} from '@dnb/eufemia/components/Table'
Description#
The Table component is an all-inclusive and accessible table based on correct HTML semantics.
Relevant links#
Please use the properties instead of overwriting the styles. If you miss a feature, get in touch with us.
NB: If you have more than three (3) columns, please consider using the border property to enhance accessibility.
Accessibility#
Tables both serve as a way of navigation for screen readers and other assistive technologies, and help to give data an ordered structure.
Use the documentation from MDN – The Table element for more information on making semantic correct tables, including scope, align, colSpan and rowSpan.
Here is a list of things you may follow along in order to ensure your coded tables still are accessible:
- Keep a semantic correct structure.
- Let tables align the column width, when possible.
- Do not use CSS
displayproperty on any table element. - Do not overwrite styles in general, but rather get in touch with DNB UX.
- Never put a table inside a table.
- Text inside tables do not need to be wrapped inside a paragraph as well. They give screen readers no additional useful information.
When openOnFind is enabled on accordion rows, find-in-page support is powered by HeightAnimation; see its accessibility notes for behavior details and browser support.
Table header components#
<Th.SortButton />to be used for additional sorting functionality.<Th.HelpButton />to be used for help related content.
Alignment#
Use e.g. align="right" on a <Th>, <Td> or <Tr> to align a table header or a table data element.
Fixed layout#
You may consider using table-layout: fixed;. You can use the modifier property fixed for doing so and combine it with CSS e.g. width: 40% on specific table headers.
Scrollable#
Depending on your situation, you may want to wrap your Table within Table.ScrollView:
import { Table } from '@dnb/eufemia'render(<Table.ScrollView><Table /></Table.ScrollView>)
Sticky header#
You have two options (both have their downsides):
-
use
sticky={true}. It works even when using aTable.ScrollViewor whenoverflow: hidden;is used on any parent elements. It also works inside a Drawer. The downside is that it uses JavaScript and the browser may drop some frames, which results in potential flickering during scrolling. -
use
sticky="css-position"for using the CSSposition: sticky;method. It is super smooth. But then you cannot use aoverflow: hidden;oroverflow: auto;on any parent elements. This is a known issue happening on every modern browser.
Method no. 2 should be used when a max-height is set to the wrapping Table.ScrollView e.g.:
<Table.ScrollView style={{ maxHeight: '20rem' }}><Table sticky="css-position" /></Table.ScrollView>
Have a look at this example.
Sortable table#
Optionally, make use of the following React Hook to handle the Th.SortButton directions.
It can be used as a "controller" for your own sorting logic of your data.
By default, it will cycle through three stages ['asc', 'desc', 'off'].
Read more: How to use the useHandleSortState React Hook.
import useHandleSortState from '@dnb/eufemia/components/table/useHandleSortState'// You can also provide a default that will be used as the fallback e.g.const defaultOptions = { direction: 'asc', modes: ['asc', 'desc', 'off'] }export const YourComponent = () => {const { sortState, sortHandler, activeSortName } = useHandleSortState({// Define your column names with options (optional)column1: { active: true }, //column2: { direction: 'desc', modes: ['asc', 'desc'] }, // overwrite the defaultOptionscolumn3: { modes: ['asc', 'off'] }, // will only allow one directioncolumn4: {}, // etc.},defaultOptions)// Use these properties for your custom sorting logicconsole.log(sortState.column1.direction) // returns either "asc", "desc" or "off"console.log(activeSortName) // returns the current active one: "column1" (returns null when nothing is active)// Handle your logicuseEffect(() => {switch (sortState.column1.direction) {default:case 'asc':setYourLocalState(mockData.sort(compareFunctionAsc))breakcase 'desc':setYourLocalState(mockData.sort(compareFunctionsDesc))breakcase 'off':setYourLocalState(mockData)break}}, [sortState.column1.direction])return (<Table><thead><Tr><Thsortableactive={sortState.column1.active}reversed={sortState.column1.reversed}><Th.SortButtontext="Column 1"title="Sort this column"onClick={sortHandler.column1}/></Th></Tr></thead></Table>)}
Keyboard navigation#
Use the useTableKeyboardNavigation hook to enable arrow-key navigation between table cells. When a cell contains a focusable element (such as an input, button, or link), that element receives focus. Otherwise, the cell itself is focused.
import Table, {useTableKeyboardNavigation,} from '@dnb/eufemia/components/Table'function MyTable() {const navRef = useTableKeyboardNavigation()return (<Table ref={navRef}><tbody><tr><Table.Td><input /></Table.Td><Table.Td><input /></Table.Td></tr></tbody></Table>)}
The hook accepts an optional options object:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether keyboard navigation is active |
Related components#
Table is part of the Content category. Other components for similar needs:
- Accordion — to let people open and close sections of related content.
- Avatar — to make a person, company, or profile easier to recognize.
- Card — to group related content in a clear, separated area.
- CountryFlag — to show a country by its flag from an ISO country code.
- DateFormat — to show dates in the correct DNB format.
- Heading — to create accessible page headings with the correct level.
Demos#
Basic table#
NB: In this example, the sort buttons do react on your input. But will not change the table data.
Complex table#
You can force a row to overwrite the automated odd/even counting by providing e.g. variant="even" to a <Tr />. You can use this in combination with rowSpan.
NB: The table header in the first column needs to have scope="row"!
Row scope headers only#
This table has only scope="row" and scope="rowgroup" headers – without the default scope="col".
Disable striped rows#
Use striped={false} to disable alternating row background colors.
Fixed table#
Medium and small sized#
A small sized table is only for special circumstances, where a lot of data needs to be shown on the screen at the same time.
Table with accordion#
Expand a single container#
The second table uses both a border and an outline.
Expand additional rows#
It's also possible to use accordion to expand the table with more rows.
Collapse all rows at once#
You can collapse all expanded rows by sending a ref to the collapseAllHandleRef property and calling the .current() function on your ref.
const myTableCollapseAll = React.useRef<(() => void) | undefined>(undefined)return (<button onClick={() => myTableCollapseAll.current?.()}>Close all rows</button><Table mode="accordion" collapseAllHandleRef={myTableCollapseAll}>{/* ... your table code */}</Table>)
Table with clickable rows (navigation mode)#
Use mode="navigation" on the <Table> and onClick on individual <Tr> rows to make them clickable. A chevron icon is rendered in an additional cell at the end of each clickable row for screen reader and keyboard accessibility. Rows respond to click as well as keyboard interaction with Space and Enter. Hover and focus indicators are sufficient to indicate interactivity per WCAG 1.4.1.
Table with clickable cells#
Use onClick on individual <Td> cells to make them clickable. A native <button> is rendered inside the cell for screen reader and keyboard accessibility. The chevron icon is included by default, but is only shown on hover, active, and keyboard focus. Use icon={false} to hide it entirely, or pass a custom icon. Hover and focus indicators are sufficient to indicate interactivity per WCAG 1.4.1.
When the selected prop is provided (either true or false) together with onClick, the cell button is announced as a toggle button by screen readers, conveying its pressed state. The selected prop requires onClick to take effect, since the selected styling targets the cell button.
Table with keyboard navigation#
Use the useTableKeyboardNavigation hook to navigate between cells with arrow keys. Focusable elements inside cells (inputs, buttons) receive focus automatically.
import Table, {useTableKeyboardNavigation,} from '@dnb/eufemia/components/Table'
Table with sticky header#
Table with a max height#
A sticky table header with sticky="css-position" and max-height on the Table.ScrollView.
Several tables in one container#
Read more: How the import and syntax is structured.
With no (empty) head and foot content.
Table with long header text (wrapping)#
Table with pagination#
Table in Card#
Using the subtle variant on the <Th>.
The first <Tr> has the property variant="even" which will ensure a border below it to visually separate it from the body rows.
Responsive table in a Card#
NB: For tables with lots of content, it's best to avoid repeating the header for each row. This can be overwhelming for users who rely on screen readers.
Also, it is important that the <td> without a <th> has a aria-label={header.title} to let users with screen readers know where "these tools" belong to.
This example uses scope="row" with a table header (<th>) in each row.
Cell highlighting#
Use highlight on <Th>, <Tr>, or <Td> to apply a subtle background and border. When set on a <Tr>, all cells in that row are highlighted. You can also set it on individual <Td> cells.
NB: Highlighted cells should include an aria-label that conveys both the highlight state and the reason for it. Screen reader users cannot perceive the visual highlight, so this label is their only way to understand that a cell stands out and why.
To adjust the border colors accordingly, use the useTableHighlight hook and pass the returned ref to <Table>.
import Table, { useTableHighlight } from '@dnb/eufemia/components/Table'const highlightRef = useTableHighlight()render(<Table ref={highlightRef}>...</Table>)
Multiple table body sections#
| Marked | Forsinkelse (min) | Åpningstid |
|---|---|---|
| Norge | ||
| Oslo Børs | Sanntid | 09:00-16:30 (UTC+1) |
| NOTC (NFMF) | 15 minutter | 09:00-16:30 (UTC+1) |
| Norden | ||
| København | 15 minutter | 09:00-16:30 (UTC+1) |
| Helsinki | 15 / Sanntid** | 09:00-16:30 (UTC+1) |
| Stockholm | 15 / Sanntid** | 09:00-16:30 (UTC+1) |
<Table.ScrollView> <Table outline border="horizontal" striped={false}> <caption className="dnb-sr-only"> Market data with delay and opening hours </caption> <thead> <Tr> <Th scope="col">Marked</Th> <Th scope="col">Forsinkelse (min)</Th> <Th scope="col">Åpningstid</Th> </Tr> </thead> <tbody> <Tr> <Th scope="rowgroup" colSpan={3}> Norge </Th> </Tr> <Tr variant="even"> <Td>Oslo Børs</Td> <Td> <Badge content="Sanntid" /> </Td> <Td>09:00-16:30 (UTC+1)</Td> </Tr> <Tr> <Td>NOTC (NFMF)</Td> <Td> <Badge content="15 minutter" /> </Td> <Td>09:00-16:30 (UTC+1)</Td> </Tr> </tbody> <tbody> <Tr> <Th scope="rowgroup" colSpan={3}> Norden </Th> </Tr> <Tr variant="even"> <Td>København</Td> <Td> <Badge content="15 minutter" /> </Td> <Td>09:00-16:30 (UTC+1)</Td> </Tr> <Tr> <Td>Helsinki</Td> <Td>15 / Sanntid**</Td> <Td>09:00-16:30 (UTC+1)</Td> </Tr> <Tr> <Td>Stockholm</Td> <Td>15 / Sanntid**</Td> <Td>09:00-16:30 (UTC+1)</Td> </Tr> </tbody> </Table> </Table.ScrollView>