@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
705 lines (637 loc) • 17.8 kB
Markdown
---
title: 'Tabs'
description: 'Tabs are a set of buttons which allow navigation between content that is related and on the same level of hierarchy.'
version: 11.0.0
generatedAt: 2026-04-21T13:57:52.969Z
checksum: 279ae1c75d5d7f8776ede8b9c75e15fd4873dfff3474e45baf461935db6d2bff
---
# Tabs
## Import
```tsx
import { Tabs } from '@dnb/eufemia'
```
## Description
Tabs are a set of buttons that allow navigation between content that is related and on the same level of hierarchy.
## Relevant links
- [Figma](https://www.figma.com/design/cdtwQD8IJ7pTeE45U148r1/%F0%9F%92%BB-Eufemia---Web?node-id=4243-1498)
- [Source code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/components/tabs)
- [Docs code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/uilib/components/tabs)
## Accessibility
The Tabs component follows the [WAI-ARIA Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). It uses `role="tablist"`, `role="tab"`, and `role="tabpanel"` with proper ARIA attributes. Keyboard navigation includes arrow keys to move between tabs and Tab key to navigate into the active panel content.
## Demos
### Tabs where content is provided from outside
As this may be a more common use case, we still have to ensure our tabs content is linked together with the tabs – because of accessibility.
You have to provide an `id` to both of the components.
**NB:** You do not need to use a function inside `Tabs.Content` – it can contain any element you need, as long as it is a React Node.
```tsx
render(
<Wrapper>
<ComponentBox>
<Tabs
id="unique-linked-id"
data={[
{
title: 'One',
key: 'one',
},
{
title: 'Two',
key: 'two',
},
]}
/>
<Tabs.Content id="unique-linked-id" key="unique-linked-key">
{({ key }) => {
return <H2>{key}</H2>
}}
</Tabs.Content>
</ComponentBox>
</Wrapper>
)
```
### Tabs using 'data' property and content object
```tsx
render(
<Wrapper>
<ComponentBox
scope={{
exampleContent,
}}
data-visual-test="tabs-tablist"
>
<Tabs
data={[
{
title: 'First',
key: 'first',
},
{
title: 'Second',
key: 'second',
},
{
title: 'Third',
key: 'third',
disabled: true,
},
{
title: 'Fourth',
key: 'fourth',
},
]}
>
{exampleContent /* See Example Content below */}
</Tabs>
</ComponentBox>
</Wrapper>
)
```
### Tabs using 'data' property only
```tsx
render(
<Wrapper>
<ComponentBox
data-visual-test="tabs-clickhandler"
scope={{
exampleContent,
}}
>
<Tabs
data={{
first: {
title: 'First',
// See Example Content below
content: exampleContent.first,
},
second: {
title: 'Second',
// See Example Content below
content: exampleContent.second,
},
}}
// Only use "onClick" if you really have to
onClick={({ selectedKey }) => {
console.log('onClick', selectedKey)
}}
// Preferred way to listen on changes
onChange={({ selectedKey }) => {
console.log('onChange', selectedKey)
}}
/>
</ComponentBox>
</Wrapper>
)
```
### Tabs using React Components only
Also, this is an example of how to define a different content background color, by providing `contentStyle`.
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-section-styles">
<Tabs tabsStyle="information" contentStyle="information">
<Tabs.Content title="First" key="first">
<Section
innerSpace={{
block: 'large',
}}
top
bottom
backgroundColor="white"
>
<H2 top={0} bottom>
First
</H2>
</Section>
</Tabs.Content>
<Tabs.Content title="Second" key="second">
<Section
innerSpace={{
block: 'large',
}}
top
bottom
backgroundColor="white"
>
<H2 top={0} bottom>
Second
</H2>
</Section>
</Tabs.Content>
</Tabs>
</ComponentBox>
</Wrapper>
)
```
### Tabs without bottom border
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-no-border">
<Tabs noBorder={true}>
<Tabs.Content title="First" key="first">
<H2 top={0} bottom>
First
</H2>
</Tabs.Content>
<Tabs.Content title="Second" key="second">
<H2 top={0} bottom>
Second
</H2>
</Tabs.Content>
</Tabs>
</ComponentBox>
</Wrapper>
)
```
### Tabs without breakout
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-no-breakout">
<Tabs breakout={false}>
<Tabs.Content title="First" key="first">
<H2 top={0} bottom>
First
</H2>
</Tabs.Content>
<Tabs.Content title="Second" key="second">
<H2 top={0} bottom>
Second
</H2>
</Tabs.Content>
</Tabs>
</ComponentBox>
</Wrapper>
)
```
### Tabs and `keepInDOM`
By using `keepInDOM={true}` the content is kept inside the DOM.
Also, when switching the tabs, the height is animated.
```tsx
render(
<Wrapper>
<ComponentBox>
<>
<Tabs keepInDOM contentStyle="information">
<Tabs.Content title="Tab 1" key="first">
<H2>Content 1</H2>
</Tabs.Content>
<Tabs.Content title="Tab 2" key="second">
<div
style={{
height: '10rem',
display: 'flex',
alignItems: 'flex-end',
}}
>
<H2>Content 2</H2>
</div>
</Tabs.Content>
<Tabs.Content title="Tab 3" key="third">
<div
style={{
height: '20rem',
display: 'flex',
alignItems: 'flex-end',
}}
>
<H2>Content 3</H2>
</div>
</Tabs.Content>
</Tabs>
<P top>Smile at me 📸</P>
</>
</ComponentBox>
</Wrapper>
)
```
### Tabs optimized for narrow screens
Navigation buttons will be shown and the tabs-list will be scrollable.
```tsx
render(
<Wrapper>
<ComponentBox
data-visual-test="tabs-tablist-scrollable"
scope={{
manyTabs,
manyTabsContent,
}}
>
<Tabs selectedKey="second" data={manyTabs}>
{manyTabsContent}
</Tabs>
</ComponentBox>
</Wrapper>
)
```
### Horizontal aligned tabs
```tsx
const FlexWrapper = styled.div`
display: flex;
flex-direction: row;
`
const LeftArea = styled.div`
/* Ensure no-wrap */
flex-shrink: 0;
`
const RightArea = styled.div`
/* Ensure the tab bar is hidden outside this area */
overflow: hidden;
/* Ensure the focus ring is visible! (because of overflow: hidden) */
margin: -2px;
padding: 2px;
`
function TabsHorizontalAligned() {
return (
<FlexWrapper>
<LeftArea>
<ToggleButton.Group value="first">
<ToggleButton text="first" value="first" />
<ToggleButton text="second" value="second" />
</ToggleButton.Group>
</LeftArea>
<RightArea>
<Tabs
left
noBorder
selectedKey="first"
id="unique-tabs-row"
data={manyTabs}
/>
</RightArea>
</FlexWrapper>
)
}
render(<TabsHorizontalAligned />)
```
### max-width usage
```tsx
const MaxWidthWrapper = styled.div`
max-width: 30rem;
background: var(--color-white);
`
function TabsMaxWidth() {
return (
<MaxWidthWrapper>
<Tabs
top
noBorder
selectedKey="fifth"
id="unique-tabs-max-width"
data={manyTabs}
/>
</MaxWidthWrapper>
)
}
render(<TabsMaxWidth />)
```
### Router integration
This demo uses `@gatsbyjs/reach-router` (Gatsby's maintained fork of `@reach/router`).
<TabsExampleReachRouterNavigation />
### Example Content
```jsx
const exampleContent = {
first: () => <H2>First</H2>,
second: () => <Input label="Label:">Focus me with next Tab key</Input>,
third: () => (
<>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</>
),
fourth: 'Fourth as a string only',
}
```
### Tabs with badge notification
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-badge-notification">
<Tabs
data={[
{
title: (
<>
Transaksjoner{' '}
<Badge
content={1}
label="Transaksjoner"
variant="notification"
vertical="top"
/>
</>
),
key: 'one',
},
{
title: 'Second',
key: 'second',
},
{
title: 'Third',
key: 'third',
},
]}
/>
</ComponentBox>
</Wrapper>
)
```
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-single-children-react-element">
<Tabs>
<Tabs.Content title="First" key="first">
<div>hello1</div>
</Tabs.Content>
</Tabs>
</ComponentBox>
</Wrapper>
)
```
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-single-element-data">
<Tabs
data={[
{
title: 'First',
key: 1,
content: <div>hello1</div>,
},
]}
/>
</ComponentBox>
</Wrapper>
)
```
```tsx
render(
<Wrapper>
<ComponentBox data-visual-test="tabs-align-property">
<Tabs
align="left"
data={[
{
title: 'Left',
key: 1,
content: <H2>Content</H2>,
},
]}
/>
<Tabs
align="center"
data={[
{
title: 'Center',
key: 1,
content: <H2>Content</H2>,
},
]}
/>
<Tabs
align="right"
data={[
{
title: 'Right',
key: 1,
content: <H2>Content</H2>,
},
]}
/>
</ComponentBox>
</Wrapper>
)
```
## Properties
```json
{
"props": {
"selectedKey": {
"doc": "In case one of the tabs should be opened by a `key`.",
"type": ["string", "number"],
"status": "optional"
},
"align": {
"doc": "To align the tab list on the right side `align=\"right\"`. Defaults to `left`.",
"type": ["\"left\"", "\"center\"", "\"right\""],
"status": "optional"
},
"contentStyle": {
"doc": "To enable the visual helper `.dnb-section` on to the content wrapper. Use a supported modifier from the [Section component](/uilib/components/section/properties). Defaults to `null`.",
"type": ["\"divider\"", "\"white\"", "\"transparent\""],
"status": "optional"
},
"contentInnerSpace": {
"doc": "To modify the inner space of the content wrapper. Defaults to `{ top: 'large' }`.",
"type": ["boolean", "string", "InnerSpaceType"],
"status": "optional"
},
"tabsStyle": {
"doc": "To enable the visual helper `.dnb-section` inside the tabs list. Use a supported modifier from the [Section component](/uilib/components/section/properties). Defaults to `null`.",
"type": ["\"divider\"", "\"white\"", "\"transparent\""],
"status": "optional"
},
"tabsInnerSpace": {
"doc": "To modify the top padding of the tab list. Only applies `paddingTop`. Defaults to `undefined`.",
"type": ["boolean", "string"],
"status": "optional"
},
"tabElement": {
"doc": "Define what HTML element should be used. You can provide e.g. `tabElement={GatsbyLink}` – you may then provide the `to` property inside every entry (`data={[{ to: '/url', ... }]}`). Defaults to `<button>`.",
"type": "React.ReactNode",
"status": "optional"
},
"[data](/uilib/components/tabs/properties/#data-object)": {
"doc": "Defines the data structure to load as an object.",
"type": "object",
"status": "optional"
},
"children": {
"doc": "The content to render. Can be a function, returning the current tab content `(key) => ('Current tab')`, a React Component or an object with the keys and content `{key1: 'Current tab'}`.",
"type": ["React.ReactNode", "object"],
"status": "optional"
},
"content": {
"doc": "The content to render. Can be a function, returning the current tab content `(key) => ('Current tab')`, a React Component or an object with the keys and content `{key1: 'Current tab'}`.",
"type": ["React.ReactNode", "object"],
"status": "optional"
},
"keepInDOM": {
"doc": "If set to `true`, the Tabs content will pre-render all contents. The visibility will be handled by using the `hidden` and `aria-hidden` HTML attributes. Defaults to `false`.",
"type": "boolean",
"status": "optional"
},
"preventRerender": {
"doc": "If set to `true`, the Tabs content will stay in the DOM. The visibility will be handled by using the `hidden` and `aria-hidden` HTML attributes. Similar to `keepInDOM`, but in contrast, the content will render once the user is activating a tab. Defaults to `false`.",
"type": "boolean",
"status": "optional"
},
"scroll": {
"doc": "If set to `true`, the content will scroll on tab change, until all tabs will be visible on the upper side of the browser window view. Defaults to `false`.",
"type": "boolean",
"status": "optional"
},
"noBorder": {
"doc": "If set to `true`, the default horizontal border line under the tablist will be removed. Defaults to `false`.",
"type": "boolean",
"status": "optional"
},
"navButtonEdge": {
"doc": "If set to `true`, the navigation icons will have a straight border at their outside. This feature is meant to be used when the Tabs component goes all the way to the browser window. Defaults to `false`.",
"type": "boolean",
"status": "optional"
},
"skeleton": {
"doc": "If set to `true`, an overlaying skeleton with animation will be shown.",
"type": "boolean",
"status": "optional"
},
"breakout": {
"doc": "If set to `false`, the default horizontal border line under the tablist remains inside the parent boundaries. Defaults to `true`.",
"type": "boolean",
"status": "optional"
},
"[Space](/uilib/layout/space/properties)": {
"doc": "Spacing properties like `top` or `bottom` are supported.",
"type": ["string", "object"],
"status": "optional"
}
}
}
```
## Data object
```json
{
"props": {
"title": {
"doc": "The title of the tab.",
"type": ["string", "React.ReactNode"],
"status": "required"
},
"key": {
"doc": "The unique key of the tab.",
"type": ["string", "number"],
"status": "required"
},
"content": {
"doc": "The content of the tab.",
"type": "React.ReactNode",
"status": "optional"
},
"selected": {
"doc": "If set to `true`, the tab will be selected.",
"type": "boolean",
"status": "optional"
},
"disabled": {
"doc": "If set to `true`, the tab will be disabled.",
"type": "boolean",
"status": "optional"
}
}
}
```
## Key
The key can be a string or a number.
But if the key is a number (integer), we have to deliver the content directly in the tab item:
```ts
const tabsDataWithContent = [
{ title: 'First', key: 1, content: <H2>First</H2> },
{ title: 'Second', key: 2, content: () => <H2>Second</H2> },
]
```
## Example Data
```ts
const tabsData = [
{ title: 'First', key: 'first' },
{ title: 'Second', key: 'second' },
{ title: 'Third', key: 'third', disabled: true },
{ title: 'Fourth', key: 'fourth' },
]
```
## Current tab
The current Tab content can be a `string`, a function returning content or a `React component`.
## Events
```json
{
"props": {
"onChange": {
"doc": "(preferred) this event gets triggered once the tab changes its selected key. Returns `{ key, selectedKey, focusKey, title, event }`.",
"type": "function",
"status": "optional"
},
"onClick": {
"doc": "This event gets triggered once the tab gets clicked. Returns `{ key, selectedKey, focusKey, title, event }`.",
"type": "function",
"status": "optional"
},
"onFocus": {
"doc": "This event gets triggered once the tab changes its focus key. Returns `{ key, selectedKey, focusKey, title, event }`.",
"type": "function",
"status": "optional"
},
"onMouseEnter": {
"doc": "This event gets triggered once the user's mouse enters a tab (hover). Returns `{ key, selectedKey, focusKey, title, event }`.",
"type": "function",
"status": "optional"
}
}
}
```
### Prevent a change
You can prevent a change from happening by returning false on the `onClick` event handler:
```tsx
<Tabs
onClick={() => {
if (condition === true) {
return false
}
}}
onChange={() => {
// Will not get emitted
}}
/>
```