Skip to content
Destyler UI Destyler UI Destyler UI

Tabs

An accessible tabs component that provides keyboard interactions and ARIA attributes described in the WAI-ARIA Tabs Design Pattern. Tabs consist of a tab list with one or more visually separated tabs. Each tab has associated content, and only the selected tab’s content is shown.

Account

Make changes to your account here. Click save when you're done.

Account

Make changes to your account here. Click save when you're done.

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/tabs @destyler/vue
Terminal window
      
        
npm install @destyler/tabs @destyler/react
Terminal window
      
        
npm install @destyler/tabs @destyler/svelte
Terminal window
      
        
npm install @destyler/tabs @destyler/solid

Anatomy

Import all parts and piece them together.

<script setup lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1'
}))
const api = computed(() => tabs.connect(state.value, send, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<div v-bind="api.getListProps()">
<button
v-for="item in data"
v-bind="api.getTriggerProps({ value: item.value })"
:key="item.value"
>
{{ item.label }}
</button>
</div>
<div
v-for="item in data"
v-bind="api.getContentProps({ value: item.value })"
:key="item.value"
>
{{ item.content }}
</div>
</div>
</template>
import { normalizeProps, useMachine } from '@destyler/react'
import * as tabs from '@destyler/tabs'
import { useId } from 'react'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1',
}))
const api = tabs.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{data.map(item => (
<button
key={item.value}
{...api.getTriggerProps({ value: item.value })}
>
{item.label}
</button>
))}
</div>
{data.map(item => (
<div
key={item.value}
{...api.getContentProps({ value: item.value })}
>
{item.content}
</div>
))}
</div>
)
}
<script lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/svelte'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
const id = $props.id()
const [state, send] = useMachine(tabs.machine({ id, value: 'item-1' }))
const api = $derived(tabs.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{#each data as item}
<button {...api.getTriggerProps({ value: item.value })}>
{item.label}
</button>
{/each}
</div>
{#each data as item}
<div {...api.getContentProps({ value: item.value })}>
{ data.content }
</div>
{/each}
</div>
import { normalizeProps, useMachine } from '@destyler/solid'
import * as tabs from '@destyler/tabs'
import { createMemo, createUniqueId } from 'solid-js'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: createUniqueId(),
value: 'item-1',
}))
const api = createMemo(() => tabs.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<div {...api().getListProps()}>
{data.map(item => (
<button {...api().getTriggerProps({ value: item.value })}>
{item.label}
</button>
))}
</div>
{data.map(item => (
<div {...api().getContentProps({ value: item.value })}>
{item.content}
</div>
))}
</div>
)
}

Setting the selected tab

To set the initially selected tab, pass the value property to the machine’s context.

const [state, send] = useMachine(
tabs.machine({
value: "tab-1",
}),
)

Subsequently, you can use the api.setValue function to set the selected tab.

Changing the orientation

The default orientation of the tabs is horizontal. To change the orientation, set the orientation property in the machine’s context to "vertical".

const [state, send] = useMachine(
tabs.machine({
orientation: "vertical",
}),
)

Showing an indicator

To show an active indicator when a tab is selected, you add the tabIndicatorProps object provided by the connect function.

<script setup lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1'
}))
const api = computed(() => tabs.connect(state.value, send, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<div v-bind="api.getListProps()">
<button
v-for="item in data"
v-bind="api.getTriggerProps({ value: item.value })"
:key="item.value"
>
{{ item.label }}
</button>
<div v-bind="api.getIndicatorProps()" />
</div>
<div
v-for="item in data"
v-bind="api.getContentProps({ value: item.value })"
:key="item.value"
>
{{ item.content }}
</div>
</div>
</template>
import { normalizeProps, useMachine } from '@destyler/react'
import * as tabs from '@destyler/tabs'
import { useId } from 'react'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1',
}))
const api = tabs.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{data.map(item => (
<button
key={item.value}
{...api.getTriggerProps({ value: item.value })}
>
{item.label}
</button>
))}
<div {...api.getIndicatorProps()} />
</div>
{data.map(item => (
<div
key={item.value}
{...api.getContentProps({ value: item.value })}
>
{item.content}
</div>
))}
</div>
)
}
<script lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/svelte'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
const id = $props.id()
const [state, send] = useMachine(tabs.machine({ id, value: 'item-1' }))
const api = $derived(tabs.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{#each data as item}
<button {...api.getTriggerProps({ value: item.value })}>
{item.label}
</button>
{/each}
<div {...api.getIndicatorProps()}></div>
</div>
{#each data as item}
<div {...api.getContentProps({ value: item.value })}>
{ data.content }
</div>
{/each}
</div>
import { normalizeProps, useMachine } from '@destyler/solid'
import * as tabs from '@destyler/tabs'
import { createMemo, createUniqueId } from 'solid-js'
const data = [
{ value: 'item-1', label: 'item-1'},
{ value: 'item-2', label: 'item-2'},
{ value: 'item-3', label: 'item-3'},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: createUniqueId(),
value: 'item-1',
}))
const api = createMemo(() => tabs.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<div {...api().getListProps()}>
{data.map(item => (
<button {...api().getTriggerProps({ value: item.value })}>
{item.label}
</button>
))}
<div {...api().getIndicatorProps()} />
</div>
{data.map(item => (
<div {...api().getContentProps({ value: item.value })}>
{item.content}
</div>
))}
</div>
)
}

Disabling a tab

To disable a tab, set the disabled property in the getTriggerProps to true.

When a Tab is disabled, it is skipped during keyboard navigation and it is not clickable.

<script setup lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const data = [
{ value: 'item-1', label: 'item-1', disabled: false },
{ value: 'item-2', label: 'item-2', disabled: true},
{ value: 'item-3', label: 'item-3', disabled: false},
]
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1'
}))
const api = computed(() => tabs.connect(state.value, send, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<div v-bind="api.getListProps()">
<button
v-for="item in data"
v-bind="api.getTriggerProps({ value: item.value, disabled: item.disabled })"
:key="item.value"
>
{{ item.label }}
</button>
</div>
<div
v-for="item in data"
v-bind="api.getContentProps({ value: item.value })"
:key="item.value"
>
{{ item.content }}
</div>
</div>
</template>
import { normalizeProps, useMachine } from '@destyler/react'
import * as tabs from '@destyler/tabs'
import { useId } from 'react'
const data = [
{ value: 'item-1', label: 'item-1', disabled: false },
{ value: 'item-2', label: 'item-2', disabled: true},
{ value: 'item-3', label: 'item-3', disabled: false},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: useId(),
value: 'item-1',
}))
const api = tabs.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{data.map(item => (
<button
key={item.value}
{...api.getTriggerProps({ value: item.value, disabled: item.disabled })}
>
{item.label}
</button>
))}
</div>
{data.map(item => (
<div
key={item.value}
{...api.getContentProps({ value: item.value })}
>
{item.content}
</div>
))}
</div>
)
}
<script lang="ts">
import * as tabs from '@destyler/tabs'
import { normalizeProps, useMachine } from '@destyler/svelte'
const data = [
{ value: 'item-1', label: 'item-1', disabled: false },
{ value: 'item-2', label: 'item-2', disabled: true},
{ value: 'item-3', label: 'item-3', disabled: false},
]
const id = $props.id()
const [state, send] = useMachine(tabs.machine({ id, value: 'item-1' }))
const api = $derived(tabs.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<div {...api.getListProps()}>
{#each data as item}
<button {...api.getTriggerProps({ value: item.value, disabled: item.disabled })}>
{item.label}
</button>
{/each}
</div>
{#each data as item}
<div {...api.getContentProps({ value: item.value })}>
{ data.content }
</div>
{/each}
</div>
import { normalizeProps, useMachine } from '@destyler/solid'
import * as tabs from '@destyler/tabs'
import { createMemo, createUniqueId } from 'solid-js'
const data = [
{ value: 'item-1', label: 'item-1', disabled: false },
{ value: 'item-2', label: 'item-2', disabled: true},
{ value: 'item-3', label: 'item-3', disabled: false},
]
export default function Tabs() {
const [state, send] = useMachine(tabs.machine({
id: createUniqueId(),
value: 'item-1',
}))
const api = createMemo(() => tabs.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<div {...api().getListProps()}>
{data.map(item => (
<button {...api().getTriggerProps({ value: item.value, disabled: item.disabled })}>
{item.label}
</button>
))}
</div>
{data.map(item => (
<div {...api().getContentProps({ value: item.value })}>
{item.content}
</div>
))}
</div>
)
}

Listening for events

  • onValueChange — Callback invoked when the selected tab changes.

  • onFocusChange — Callback invoked when the focused tab changes.

const [state, send] = useMachine(
tabs.machine({
onFocusChange(details) {
// details => { value: string | null }
console.log("focused tab:", details.value)
},
onValueChange(details) {
// details => { value: string }
console.log("selected tab:", details.value)
},
}),
)

Manual tab activation

By default, the tab can be selected when the receive focus from either the keyboard or pointer interaction. This is called “automatic tab activation”.

The other approach is “manual tab activation” which means the tab is selected with the Enter key or by clicking on the tab.

const [state, send] = useMachine(
tabs.machine({
activationMode: "manual",
}),
)

Styling guide

Earlier, we mentioned that each Tabs part has a data-part attribute added to them to select and style them in the DOM.

Selected state

When a tab is selected, a data-selected attribute is added to the trigger and content elements.

[data-part="trigger"][data-state="active"] {
/* Styles for selected tab */
}
[data-part="content"][data-state="active"] {
/* Styles for selected tab */
}

Disabled state

When a tab is disabled, a data-disabled attribute is added to the trigger element.

[data-part="trigger"][data-disabled] {
/* Styles for disabled tab */
}

Focused state

When a tab is focused, you the :focus or :focus-visible pseudo-class to style it.

[data-part="trigger"]:focus {
/* Styles for focused tab */
}

When any tab is focused, the list is given a data-focus attribute.

[data-part="list"][data-focus] {
/* Styles for when any tab is focused */
}

Orientation styles

All parts of the tabs component have the data-orientation attribute. You can use this to set the style for the horizontal or vertical tabs.

[data-part="trigger"][data-orientation="horizontal"] {
/* Styles for horizontal/vertical tabs */
}
[data-part="root"][data-orientation="horizontal"] {
/* Styles for horizontal/vertical root */
}
[data-part="indicator"][data-orientation="horizontal"] {
/* Styles for horizontal/vertical tab-indicator */
}
[data-part="list"][data-orientation="horizontal"] {
/* Styles for horizontal/vertical list */
}

Methods and Properties

Machine Context

The tabs machine exposes the following context properties:

ids
Partial<{ root: string; trigger: string; list: string; content: string; indicator: string; }>
The ids of the elements in the tabs. Useful for composition.
translations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their states
loopFocus(default: true)
boolean
Whether the keyboard navigation will loop from last tab to first, and vice versa.
value
string
The selected tab id
orientation(default: "horizontal")
"horizontal" | "vertical"
The orientation of the tabs. Can be `horizontal` or `vertical` - `horizontal`: only left and right arrow key navigation will work. - `vertical`: only up and down arrow key navigation will work.
activationMode(default: "automatic")
"manual" | "automatic"
The activation mode of the tabs. Can be `manual` or `automatic` - `manual`: Tabs are activated when clicked or press `enter` key. - `automatic`: Tabs are activated when receiving focus
onValueChange
(details: ValueChangeDetails) => void
Callback to be called when the selected/active tab changes
onFocusChange
(details: FocusChangeDetails) => void
Callback to be called when the focused tab changes
composite
boolean
Whether the tab is composite
deselectable
boolean
Whether the active tab can be deselected when clicking on it.
navigate
(details: NavigateDetails) => void
Function to navigate to the selected tab when clicking on it. Useful if tab triggers are anchor elements.
dir(default: "ltr")
"ltr" | "rtl"
The document's text/writing direction.
id
string
The unique identifier of the machine.
getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

Machine API

The tabs api exposes the following methods:

value
string
The current value of the tabs.
focusedValue
string
The value of the tab that is currently focused.
setValue
(value: string) => void
Sets the value of the tabs.
clearValue
() => void
Clears the value of the tabs.
setIndicatorRect
(value: string) => void
Sets the indicator rect to the tab with the given value
syncTabIndex
() => void
Synchronizes the tab index of the content element. Useful when rendering tabs within a select or combobox
focus
() => void
Set focus on the selected tab trigger
selectNext
(fromValue?: string) => void
Selects the next tab
selectPrev
(fromValue?: string) => void
Selects the previous tab
getTriggerState
(props: TriggerProps) => TriggerState
Returns the state of the trigger with the given props

Data Attributes

Root

attribute
description
data-scope
tabs
data-part
root
data-orientation
The orientation of the tabs
data-focus
Present when focused

List

attribute
description
data-scope
tabs
data-part
list
data-focus
Present when focused
data-orientation
The orientation of the list

Trigger

attribute
description
data-scope
tabs
data-part
trigger
data-orientation
The orientation of the trigger
data-disabled
Present when disabled
data-value
The value of the item
data-selected
Present when selected
data-focus
Present when focused
data-ssr

Content

attribute
description
data-scope
tabs
data-part
content
data-selected
Present when selected
data-orientation
The orientation of the content

Indicator

attribute
description
data-scope
tabs
data-part
indicator
data-orientation
The orientation of the indicator

Accessibility

Keyboard Interaction

name
desc
Tab
When focus moves onto the tabs, focuses the active trigger. When a trigger is focused, moves focus to the active content.
ArrowDown
Moves focus to the next trigger in vertical orientation and activates its associated content.
ArrowRight
Moves focus to the next trigger in horizontal orientation and activates its associated content.
ArrowUp
Moves focus to the previous trigger in vertical orientation and activates its associated content.
ArrowLeft
Moves focus to the previous trigger in horizontal orientation and activates its associated content.
Home
Moves focus to the first trigger and activates its associated content.
End
Moves focus to the last trigger and activates its associated content.
Enter + Space
In manual mode, when a trigger is focused, moves focus to its associated content.