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.
Item one content
Item two content
Item three content
Features
- Support for mouse, touch, and keyboard interactions on tabs.
- Support for LTR and RTL keyboard navigation.
- Support for disabled tabs.
- Follows the tabs ARIA pattern, semantically linking tabs and their associated tab panels.
- Focus management for tab panels without any focusable children
Installation
To use the tabs machine in your project, run the following command in your command line:
npm install @zag-js/tabs @zag-js/react # or yarn add @zag-js/tabs @zag-js/react
npm install @zag-js/tabs @zag-js/solid # or yarn add @zag-js/tabs @zag-js/solid
npm install @zag-js/tabs @zag-js/vue # or yarn add @zag-js/tabs @zag-js/vue
npm install @zag-js/tabs @zag-js/vue # or yarn add @zag-js/tabs @zag-js/vue
This command will install the framework agnostic tabs logic and the reactive utilities for your framework of choice.
Anatomy
To set up the tabs correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the tabs package into your project
import * as tabs from "@zag-js/tabs"
The tabs package exports two key functions:
machine
— The state machine logic for the tabs widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the tabs machine in your project 🔥
import * as tabs from "@zag-js/tabs" import { useMachine, normalizeProps } from "@zag-js/react" const data = [ { value: "item-1", label: "Item one", content: "Item one content" }, { value: "item-2", label: "Item two", content: "Item two content" }, { value: "item-3", label: "Item three", content: "Item three content" }, ] export function Tabs() { const [state, send] = useMachine(tabs.machine({ id: "1", value: "item-1" })) const api = tabs.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <div {...api.listProps}> {data.map((item) => ( <button {...api.getTriggerProps({ value: item.value })} key={item.value} > {item.label} </button> ))} </div> {data.map((item) => ( <div {...api.getContentProps({ value: item.value })} key={item.value}> <p>{item.content}</p> </div> ))} </div> ) }
import * as tabs from "@zag-js/tabs" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" const data = [ { value: "item-1", label: "Item one", content: "Item one content" }, { value: "item-2", label: "Item two", content: "Item two content" }, { value: "item-3", label: "Item three", content: "Item three content" }, ] export function Tabs() { const [state, send] = useMachine( tabs.machine({ id: createUniqueId(), value: "item-1" }), ) const api = createMemo(() => tabs.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <div {...api().listProps}> <For each={data}> {(item) => ( <button {...api().getTriggerProps({ value: item.value })}> {item.label} </button> )} </For> </div> <For each={data}> {(item) => ( <div {...api().getContentProps({ value: item.value })}> <p>{item.content}</p> </div> )} </For> </div> ) }
import * as tabs from "@zag-js/tabs" import { normalizeProps, useMachine } from "@zag-js/vue" import { defineComponent, computed, h, Fragment } from "vue" const data = [ { value: "item-1", label: "Item one", content: "Item one content" }, { value: "item-2", label: "Item two", content: "Item two content" }, { value: "item-3", label: "Item three", content: "Item three content" }, ] export default defineComponent({ name: "Tabs", setup() { const [state, send] = useMachine( tabs.machine({ id: "tabs", value: "item-1" }), ) const apiRef = computed(() => tabs.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <div {...api.listProps}> {data.map((item) => ( <button {...api.getTriggerProps({ value: item.value })} key={item.value} > {item.label} </button> ))} </div> {data.map((item) => ( <div {...api.getContentProps({ value: item.value })} key={item.value} > <p>{item.content}</p> </div> ))} </div> ) } }, })
<script setup> import * as tabs from "@zag-js/tabs"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const data = [ { value: "item-1", label: "Item one", content: "Item one content" }, { value: "item-2", label: "Item two", content: "Item two content" }, { value: "item-3", label: "Item three", content: "Item three content" }, ]; const [state, send] = useMachine(tabs.machine({ id: "1", value: "item-1" })); const api = computed(() => tabs.connect(state.value, send, normalizeProps)); </script> <template> <div ref="ref" v-bind="api.rootProps"> <div v-bind="api.listProps"> <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" > <p>{{ item.content }}</p> </div> </div> </template>
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.
// ... return ( <div {...api.rootProps}> <div {...api.listProps}> {data.map((item) => ( <button {...api.getTriggerProps({ value: item.value })} key={item.value} > {item.label} </button> ))} <div {...api.indicatorProps} /> </div> {data.map((item) => ( <div {...api.getContentProps({ value: item.value })} key={item.value}> <p>{item.content}</p> </div> ))} </div> )
// ... return ( <div {...api().rootProps}> <div {...api().listProps}> {data.map((item) => ( <button {...api().getTriggerProps({ value: item.value })} key={item.value} > {item.label} </button> ))} <div {...api().indicatorProps} /> </div> {data.map((item) => ( <div {...api().getContentProps({ value: item.value })} key={item.value}> <p>{item.content}</p> </div> ))} </div> )
// ... return ( <div {...api.rootProps}> <div {...api.listProps}> {data.map((item) => ( <button {...api.getTriggerProps({ value: item.value })} key={item.value} > {item.label} </button> ))} <div {...api.indicatorProps} /> </div> {data.map((item) => ( <div {...api.getContentProps({ value: item.value })} key={item.value}> <p>{item.content}</p> </div> ))} </div> )
// ... <div ref="ref" v-bind="api.rootProps"> <div v-bind="api.listProps"> <button v-for="item in data" v-bind="api.getTriggerProps({ value: item.value })" :key="item.value" > {{ item.label }} </button> <div v-bind="api.indicatorProps" /> </div> <div v-for="item in data" v-bind="api.getContentProps({ value: item.value })" :key="item.value" > <p>{{ item.content }}</p> </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.
//... <button {...api.getTriggerProps({ disabled: true })}></button> //...
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", }), )
RTL Support
The tabs machine provides support right to left writing directions. In this mode, the layout and keyboard interaction is flipped.
To enable RTL support, set the dir
property in the machine's context to rtl
.
const [state, send] = useMachine( tabs.machine({ dir: "rtl", }), )
Styling guide
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|vertical)"] { /* Styles for horizontal/vertical tabs */ } [data-part="root"][data-orientation="(horizontal|vertical)"] { /* Styles for horizontal/vertical root */ } [data-part="indicator"][data-orientation="(horizontal|vertical)"] { /* Styles for horizontal/vertical tab-indicator */ } [data-part="list"][data-orientation="(horizontal|vertical)"] { /* Styles for horizontal/vertical list */ }
The tab indicator
The tab indicator styles have CSS variables for the transitionDuration
and
transitionTimingFunction
defined in it.
The transition definition is applied when the selected tab changes to allow the indicator move smoothly to the new selected tab.
[data-part="indicator"] { --transition-duration: 0.2s; --transition-timing-function: ease-in-out; }
You'll also need to set the styles for the indicator to match your design.
[data-part="indicator"] { --transition-duration: 0.2s; --transition-timing-function: ease-in-out; }
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 statesloopFocus
boolean
Whether the keyboard navigation will loop from last tab to first, and vice versa.value
string
The selected tab idorientation
"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
"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 focusonValueChange
(details: ValueChangeDetails) => void
Callback to be called when the selected/active tab changesonFocusChange
(details: FocusChangeDetails) => void
Callback to be called when the focused tab changesdir
"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 valuegetTriggerState
(props: TriggerProps) => TriggerState
Returns the state of the trigger with the given props
Accessibility
Keyboard Interactions
- TabWhen focus moves onto the tabs, focuses the active trigger. When a trigger is focused, moves focus to the active content.
- ArrowDownMoves focus to the next trigger in vertical orientation and activates its associated content.
- ArrowRightMoves focus to the next trigger in horizontal orientation and activates its associated content.
- ArrowUpMoves focus to the previous trigger in vertical orientation and activates its associated content.
- ArrowLeftMoves focus to the previous trigger in horizontal orientation and activates its associated content.
- HomeMoves focus to the first trigger and activates its associated content.
- EndMoves focus to the last trigger and activates its associated content.
- EnterSpaceIn manual mode, when a trigger is focused, moves focus to its associated content.
Edit this page on GitHub