Import
import { Table } from '@dnb/eufemia'
Tables
The Table component is an all inclusive and accessible table based on correct HTML semantics.
Please use the properties instead of overwriting the styles. And if you miss a feature, get in touch with us.
NB: If you have more than three (3) columns, please consider to use border
property in order to enhance accessibility.
Accessibility
Tables do both serve as a way of navigation for screen readers and other assertive technologies. But they also 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
display
property 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.
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.ScrollView
or aoverflow: hidden;
is used on any parent elements. And it works inside a Drawer as well. The downside is, that it uses JavaScript and the browser may drop some frames, which results in a potential flickering during scrolling. -
use
sticky="css-position"
for using the CSSposition: sticky;
method. It is super smooth. But then you can not use aoverflow: hidden;
oroverflow: auto;
on any parent elements. This is a know 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 trough three stages ['asc', 'desc', 'off']
.
Show 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"on_click={sortHandler.column1}/></Th></Tr></thead></Table>)}
Demos
Basic table
NB: In this example, the sort buttons do react on your input. But will not change the table data.
Column | Help Button | ||
---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | Row 2 |
Row 3 with paragraph | Row 3 with code | Row 3 with medium paragraph | Row 3 with medium text |
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"
!
Column 2 newline | Column 3 that spans | ||
---|---|---|---|
Row 1+2 Header | Row 1 that spans | Row 1 | Row 1 |
Row 2 | Row 2 | ||
Row 3 Header newline | Row 3 | noSpacing + align="right" | |
Row 4 Header | Row 4 | Row 4 |
Row scope headers only
This table has only scope="row"
and scope="rowgroup"
headers – without the default scope="col"
.
Header A | Row 1 | Row 1 |
---|---|---|
Header B | Row 2 | Row 2 |
Fixed table
Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 | Column 7 | Column 8 |
---|---|---|---|---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 | Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | Row 2 | Row 2 | Row 2 | Row 2 | Row 2 |
Row 3 | Row 3 | Row 3 | Row 3 | Row 3 | Row 3 | Row 3 | Row 3 |
Row 4 | Row 4 | Row 4 | Row 4 | Row 4 | Row 4 | Row 4 | Row 4 |
Medium and small sized
Column | Column | |
---|---|---|
Row 1 | Row 1 | Row 1 |
Row 2 with paragraph | Row 2 with medium paragraph | Row 2 with medium text |
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.
Column | Column | |
---|---|---|
Row 1 | Row 1 | Row 1 |
Row 2 with paragraph | Row 2 with medium paragraph | Row 2 with medium text |
Table with accordion
Expand a single container
The second table uses both a border
and an outline
.
Column A | Column B | Column C | Column D | |
---|---|---|---|---|
Row 1 | Row 1 | |||
Row 2 | Row 2 | |||
Row 3 | Row 3 |
Column A | Column B | Column C | Column D | |
---|---|---|---|---|
Row 1 | Row 1 | Row 1 | ||
Row 2 | Row 2 | Row 2 | ||
Row 3 | Row 3 | Row 3 |
Expand additional rows
It's also possible to use accordion to expand the table with more rows.
Column A | Column B | Column C | Column D | |
---|---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 | |
Row 2 | Row 2 | Row 2 | Row 2 |
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>()return (<button onClick={() => myTableCollapseAll.current()}>Close all rows</button><Table mode="accordion" collapseAllHandleRef={myTableCollapseAll}>{/* ... your table code */}</Table>)
Table with navigation
Column A | Column B | Column C | Column D | |
---|---|---|---|---|
Row 1 | Row 1 | |||
Row 2 | Row 2 | |||
Row 3 | Row 3 |
Column A | Column B | Column C | Column D | |
---|---|---|---|---|
Row 1 | Row 1 | Row 1 | ||
Row 2 | Row 2 | Row 2 | ||
Row 3 | Row 3 | Row 3 |
Table with sticky header
Header | |||
---|---|---|---|
Row 1 with p | Row 1 with code | Row 1 with span | Row 1 |
Column which spans over two columns | Row 2 | Row 2 | |
Row 3 | Row 3 | Row 3 | Row 3 |
Footer | Sum |
Table with a max height
A sticky table header with sticky="css-position"
and max-height
on the Table.ScrollView
.
Column 1 | Column 2 | Column 3 | Column 4 |
---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | Row 2 |
Row 3 | Row 3 | Row 3 | Row 3 |
Row 4 | Row 4 | Row 4 | Row 4 |
Row 5 | Row 5 | Row 5 | Row 5 |
Row 6 | Row 6 | Row 6 | Row 6 |
Row 7 | Row 7 | Row 7 | Row 7 |
Row 8 | Row 8 | Row 8 | Row 8 |
Row 9 | Row 9 | Row 9 | Row 9 |
Row 10 | Row 10 | Row 10 | Row 10 |
Row 11 | Row 11 | Row 11 | Row 11 |
Row 12 | Row 12 | Row 12 | Row 12 |
Several tables in one container
Show how the import and syntax is structured.
<TableContainer> <TableContainer.Head> <H2>Heading</H2> </TableContainer.Head> <TableContainer.Body> <Table>Content</Table> <Table>Content</Table> </TableContainer.Body> <TableContainer.Foot> <P>Footer</P> </TableContainer.Foot> </TableContainer>
Header
Text
I have a superscript 1 | Column 2 | Column 3 | Column 4 |
---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | Row 2 |
Column 1 | Column 2 | Column 3 | Column 4 |
---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | |
Row 3 | Row 3 | Row 3 |
Row Header Group | Row 1 | Row 1 |
---|---|---|
Row 2 | Row 2 |
Footer
With no (empty) head
and foot
content.
Column 1 | Column 2 | Column 3 | Column 4 |
---|---|---|---|
Row 1 | Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 | Row 2 |
Table with long header text (wrapping)
Static long header senectus ornare convallis ut at erat imperdiet commodo | |||
---|---|---|---|
col span of 4 |
Table with pagination
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.
Tittel | Lorem ipsum |
---|---|
Beskrivelse | Lorem ipsum |
Status | Ikke påbegynt |
Frist | 17.04.2025 |
Tittel | Lorem ipsum |
Beskrivelse | Lorem ipsum |
Status | Ikke påbegynt |
Frist | 17.04.2025 |
Example usage without and with classes
Header | ||
---|---|---|
Row 1 | Row 1 | Row 1 |
Row 2 | Row 2 | Row 2 |
Row 3 | Row 3 | Row 3 |
.dnb-table__th | ||
---|---|---|
.dnb-table__tr--even > .dnb-table__td | ||
.dnb-table__tr--odd > .dnb-table__td |