Tabs

Easily create accessible, fully customizable tab interfaces, with robust focus management and keyboard navigation support.

To get started, install Headless UI via npm:

npm install @headlessui/react

Tabs are built using the TabGroup, TabList, Tab, TabPanels, and TabPanel components. By default the first tab is selected, and clicking on any tab or selecting it with the keyboard will activate the corresponding panel.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
    <TabGroup>
      <TabList>
        <Tab>Tab 1</Tab>
        <Tab>Tab 2</Tab>
        <Tab>Tab 3</Tab>
      </TabList>
      <TabPanels>
        <TabPanel>Content 1</TabPanel>
        <TabPanel>Content 2</TabPanel>
        <TabPanel>Content 3</TabPanel>
      </TabPanels>
    </TabGroup>
  )
}

Headless UI keeps track of a lot of state about each component, like which tab is currently selected, whether a popover is open or closed, or which item in a menu is currently focused via the keyboard.

But because the components are headless and completely unstyled out of the box, you can't see this information in your UI until you provide the styles you want for each state yourself.

The easiest way to style the different states of a Headless UI component is using the data-* attributes that each component exposes.

For example, the Tab component exposes a data-selected attribute, which tells you if the tab is currently selected, and a data-hover attribute, which tells you if the tab is currently being hovered by the mouse.

<!-- Rendered `TabGroup` -->
<div>
  <div>
    <button>Tab 1</button>
    <button data-selected>Tab 2</button>
    <button data-hover>Tab 3</button>
  </div>
  <div>
    <div>Content 1</div>
    <div data-selected>Content 2</div>
    <div>Content 3</div>
  </div>
</div>

Use the CSS attribute selector to conditionally apply styles based on the presence of these data attributes. If you're using Tailwind CSS, the data attribute modifier makes this easy:

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
    <TabGroup>
      <TabList>
<Tab className="data-[selected]:bg-blue-500 data-[selected]:text-white data-[hover]:underline">Tab 1</Tab>
<Tab className="data-[selected]:bg-blue-500 data-[selected]:text-white data-[hover]:underline">Tab 2</Tab>
<Tab className="data-[selected]:bg-blue-500 data-[selected]:text-white data-[hover]:underline">Tab 3</Tab>
</TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

See the component API for a list of all the available data attributes.

Each component also exposes information about its current state via render props that you can use to conditionally apply different styles or render different content.

For example, the Tab component exposes a selected state, which tells you if the tab is currently selected, and a hover state, which tells you if the tab is currently being hovered by the mouse.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import clsx from 'clsx'
import { Fragment } from 'react'

function Example() {
  return (
    <TabGroup>
      <TabList>
        <Tab as={Fragment}>
{({ hover, selected }) => (
<button className={clsx(hover && 'underline', selected && 'bg-blue-500 text-white')}>Tab 1</button>
)}
</Tab> <Tab as={Fragment}>
{({ hover, selected }) => (
<button className={clsx(hover && 'underline', selected && 'bg-blue-500 text-white')}>Tab 2</button>
)}
</Tab> <Tab as={Fragment}>
{({ hover, selected }) => (
<button className={clsx(hover && 'underline', selected && 'bg-blue-500 text-white')}>Tab 3</button>
)}
</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

See the component API for a list of all the available render props.

Use the disabled prop to disable a Tab and prevent it from being selected:

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
    <TabGroup>
      <TabList>
        <Tab>Tab 1</Tab>
<Tab disabled className="disabled:opacity-50">
Tab 2 </Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

If you've styled your TabList to appear vertically, use the vertical prop to enable navigating with the up and down arrow keys instead of left and right, and to update the aria-orientation attribute for assistive technologies.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
<TabGroup vertical>
<TabList className="flex flex-col"> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

By default, tabs are automatically selected as the user navigates through them using the arrow keys.

If you'd rather not change the current tab until the user presses Enter or Space, use the manual prop on the TabGroup component. This can be helpful if selecting a tab performs an expensive operation and you don't want to run it unnecessarily.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
<TabGroup manual>
<TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

The manual prop has no impact on mouse interactions — tabs will still be selected as soon as they are clicked.

To change which tab is selected by default, use the defaultIndex={number} prop on the TabGroup component.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
<TabGroup defaultIndex={1}>
<TabList> <Tab>Tab 1</Tab>
{/* Selects this tab by default */}
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel>
{/* Displays this panel by default */}
<TabPanel>Content 2</TabPanel>
<TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

If you happen to provide an index that is out of bounds, then the last non-disabled tab will be selected on initial render. (For example, <TabGroup defaultIndex={5}> in the example above would render the third panel as selected.)

To run a function whenever the selected tab changes, use the onChange prop on the TabGroup component.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function Example() {
  return (
    <TabGroup
onChange={(index) => {
console.log('Changed selected tab to:', index)
}}
>
<TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

By default, the tabs component manages the selected tab internally. However, you can control the selected tab yourself using the selectedIndex prop and onChange callback:

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import { useState } from 'react'

function Example() {
const [selectedIndex, setSelectedIndex] = useState(0)
return (
<TabGroup selectedIndex={selectedIndex} onChange={setSelectedIndex}>
<TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

By default, the TabGroup and its subcomponents each render a default element that is sensible for that component.

For example, TabGroup renders a div, TabList renders a div, and Tab renders a button.

Use the as prop to render the component as a different element or as your own custom component, making sure your custom components forward refs so that Headless UI can wire things up correctly.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import { forwardRef } from 'react'

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { return ( <TabGroup>
<TabList as="aside">
<Tab as={MyCustomButton}>Tab 1</Tab> <Tab as={MyCustomButton}>Tab 2</Tab> <Tab as={MyCustomButton}>Tab 3</Tab> </TabList>
<TabPanels as="section">
<TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

To tell an element to render its children directly with no wrapper element, use a Fragment.

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import { Fragment } from 'react'

function Example() {
  return (
<TabGroup as={Fragment}>
<TabList> <Tab>Tab 1</Tab> <Tab>Tab 2</Tab> <Tab>Tab 3</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel>Content 3</TabPanel> </TabPanels> </TabGroup> ) }

All interactions apply when a Tab component is focused.

CommandDescription

ArrowLeft and ArrowRight

Selects the previous/next non-disabled tab.

ArrowUp and ArrowDownwhen vertical is set

Selects the previous/next non-disabled tab.

Home or PageUp

Selects the first non-disabled tab.

End or PageDown

Selects the last non-disabled tab.

Enter or Spacewhen manual is set

Activates the selected tab.

The main TabGroup component.

PropDefaultDescription
asdiv
String | Component

The element or component the tab group should render as.

defaultIndex0
Number

The default selected index

selectedIndex
number

The selected index if you want to use the tabs component as a controlled component.

onChange
(index: number) => void

A function called whenever the selected tab changes.

verticalfalse
Boolean

When true, the orientation of the TabList will be vertical, otherwise it will be horizontal.

manualfalse
Boolean

When true, the user can only display a panel via the keyboard by first navigating to it using the arrow keys, and then by pressing Enter or Space. By default, panels are automatically displayed when navigated to via the arrow keys. Note that this prop has no affect on mouse behavior.

Data AttributeRender PropDescription
selectedIndex

Number

The currently selected index.

PropDefaultDescription
asdiv
String | Component

The element or component the tab list should render as.

Data AttributeRender PropDescription
selectedIndex

Number

The currently selected index.

PropDefaultDescription
asFragment
String | Component

The element or component the tab should render as.

disabledfalse
Boolean

Whether or not the tab is disabled.

autoFocusfalse
Boolean

Whether or not the tab should receive focus when first rendered.

Data AttributeRender PropDescription
data-selectedselected

Boolean

Whether or not the tab is selected.

data-focusfocus

Boolean

Whether or not the tab is focused.

data-hoverhover

Boolean

Whether or not the tab is hovered.

data-activeactive

Boolean

Whether or not the tab is in an active or pressed state.

data-autofocusautofocus

Boolean

Whether or not the autoFocus prop was set to true.

PropDefaultDescription
asdiv
String | Component

The element or component the tab panels should render as.

Data AttributeRender PropDescription
selectedIndex

Number

The currently selected index.

PropDefaultDescription
asdiv
String | Component

The element or component the tab panel should render as.

staticfalse
Boolean

Whether the element should ignore the internally managed open/closed state.

unmounttrue
Boolean

Whether the element should be unmounted or hidden based on the open/closed state.

Data AttributeRender PropDescription
data-selectedselected

Boolean

Whether or not the tab panel is selected.

If you're interested in predesigned component examples using Headless UI and Tailwind CSS, check out Tailwind UI — a collection of beautifully designed and expertly crafted components built by us.

It's a great way to support our work on open-source projects like this and makes it possible for us to improve them and keep them well-maintained.