Tabs
Accessible tabs to switch between panels of content.
View SourceInstallation
Follow instructions for individual framework usage below
Styles
Variants
Scripts
The Tabs script switches active panels and keeps tab ARIA state in sync.
Examples
Default
html
<x-tabs id="tabsDefault">
<div class="x-tabs-list accent-main" role="tablist">
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-all" id="all" aria-selected="true" command="--toggle" commandfor="tabsDefault" data-x-tabs-part="tab">
All
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-photos" id="photos" command="--toggle" commandfor="tabsDefault" data-x-tabs-part="tab">
Photos
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-music" id="music" command="--toggle" commandfor="tabsDefault" data-x-tabs-part="tab">
Music
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-documents" id="documents" command="--toggle" commandfor="tabsDefault" data-x-tabs-part="tab">
Documents
</button>
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="false" id="tab-all" aria-labelledby="all" data-x-tabs-part="tabpanel">
All
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-photos" aria-labelledby="photos" data-x-tabs-part="tabpanel">
Photos
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-music" aria-labelledby="music" data-x-tabs-part="tabpanel">
Music
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-documents" aria-labelledby="documents" data-x-tabs-part="tabpanel">
Documents
</div>
</x-tabs>liquid
<div data-controller="x-tabs" id="tabsDefault">
<div class="x-tabs-list accent-main" role="tablist">
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-all" id="all" aria-selected="true" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
All
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-photos" id="photos" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Photos
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-music" id="music" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Music
</button>
<button class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]" role="tab" aria-controls="tab-documents" id="documents" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Documents
</button>
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="false" id="tab-all" aria-labelledby="all" data-x-tabs-target="tabpanel">
All
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-photos" aria-labelledby="photos" data-x-tabs-target="tabpanel">
Photos
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-music" aria-labelledby="music" data-x-tabs-target="tabpanel">
Music
</div>
<div class="aria-hidden:hidden p-2 text-center" role="tabpanel" aria-hidden="true" id="tab-documents" aria-labelledby="documents" data-x-tabs-target="tabpanel">
Documents
</div>
</div>vue
<script setup lang="ts">
import { ref } from 'vue'
import { Tabs, TabsList } from '@/components/tabs'
const tabs = ['All', 'Photos', 'Music', 'Documents']
const tabElements = ref<HTMLElement[]>([])
const tabPanelElements = ref<HTMLElement[]>([])
</script>
<template>
<Tabs v-slot="{ toggleTab }" :refs="{ tabElements, tabPanelElements }">
<TabsList class="accent-main">
<button
v-for="(tab, index) in tabs"
:key="tab"
ref="tabElements"
class="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]"
role="tab"
:aria-controls="`tab-${tab.toLowerCase()}`"
:id="tab.toLowerCase()"
:aria-selected="index === 0 ? 'true' : 'false'"
@click="toggleTab"
>
{{ tab }}
</button>
</TabsList>
<div
v-for="(tab, index) in tabs"
:key="tab"
ref="tabPanelElements"
class="aria-hidden:hidden"
role="tabpanel"
:aria-hidden="index === 0 ? 'false' : 'true'"
:id="`tab-${tab.toLowerCase()}`"
:aria-labelledby="tab.toLowerCase()"
>
{{ tab }}
</div>
</Tabs>
</template>tsx
import { createRef, useRef } from 'react'
import { Tabs, TabsList } from "@/components/tabs"
const tabs = ['All', 'Photos', 'Music', 'Documents']
export function Example() {
const tabElements = useRef(tabs.map(() => createRef<HTMLButtonElement>())).current
const tabPanelElements = useRef(tabs.map(() => createRef<HTMLDivElement>())).current
return (
<Tabs refs={{ tabElements, tabPanelElements }}>
{({ toggleTab }) => (
<>
<TabsList className="accent-main">
{tabs.map((tab, index) => (
<button
key={tab}
ref={tabElements[index]}
className="x-button ghosted aria-selected:[--x-button-background-color-opacity:100%] aria-selected:[--x-button-color:var(--color-accent-foreground)]"
role="tab"
aria-controls={`tab-${tab.toLowerCase()}`}
id={tab.toLowerCase()}
aria-selected={index === 0 ? 'true' : 'false'}
onClick={toggleTab}
>
{tab}
</button>
))}
</TabsList>
{tabs.map((tab, index) => (
<div
key={tab}
ref={tabPanelElements[index]}
className="aria-hidden:hidden"
role="tabpanel"
aria-hidden={index === 0 ? 'false' : 'true'}
id={`tab-${tab.toLowerCase()}`}
aria-labelledby={tab.toLowerCase()}
>
{tab}
</div>
))}
</>
)}
</Tabs>
)
}Custom
html
<x-tabs id="tabsCustom">
<div class="x-tabs-list flex gap-0 p-0 bg-transparent" role="tablist">
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-all" id="all" aria-selected="true" command="--toggle" commandfor="tabsCustom" data-x-tabs-part="tab">
All
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-photos" id="photos" command="--toggle" commandfor="tabsCustom" data-x-tabs-part="tab">
Photos
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-music" id="music" command="--toggle" commandfor="tabsCustom" data-x-tabs-part="tab">
Music
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-documents" id="documents" command="--toggle" commandfor="tabsCustom" data-x-tabs-part="tab">
Documents
</button>
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="false" id="tab-all" aria-labelledby="all" data-x-tabs-part="tabpanel">
All
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-photos" aria-labelledby="photos" data-x-tabs-part="tabpanel">
Photos
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-music" aria-labelledby="music" data-x-tabs-part="tabpanel">
Music
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-documents" aria-labelledby="documents" data-x-tabs-part="tabpanel">
Documents
</div>
</x-tabs>liquid
<div data-controller="x-tabs" id="tabsCustom">
<div class="x-tabs-list flex gap-0 p-0 bg-transparent" role="tablist">
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-all" id="all" aria-selected="true" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
All
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-photos" id="photos" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Photos
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-music" id="music" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Music
</button>
<button class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100" role="tab" aria-controls="tab-documents" id="documents" data-action="click->x-tabs#toggle" data-x-tabs-target="tab">
Documents
</button>
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="false" id="tab-all" aria-labelledby="all" data-x-tabs-target="tabpanel">
All
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-photos" aria-labelledby="photos" data-x-tabs-target="tabpanel">
Photos
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-music" aria-labelledby="music" data-x-tabs-target="tabpanel">
Music
</div>
<div class="aria-hidden:hidden" role="tabpanel" aria-hidden="true" id="tab-documents" aria-labelledby="documents" data-x-tabs-target="tabpanel">
Documents
</div>
</div>vue
<script setup lang="ts">
import { ref } from 'vue'
import { Tabs, TabsList } from '@/components/tabs'
const tabs = ['All', 'Photos', 'Music', 'Documents']
const tabElements = ref<HTMLElement[]>([])
const tabPanelElements = ref<HTMLElement[]>([])
</script>
<template>
<Tabs v-slot="{ toggleTab }" :refs="{ tabElements, tabPanelElements }">
<TabsList class="flex gap-0 p-0 bg-transparent">
<button
v-for="(tab, index) in tabs"
:key="tab"
ref="tabElements"
class="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100"
role="tab"
:aria-controls="`tab-${tab.toLowerCase()}`"
:id="tab.toLowerCase()"
:aria-selected="index === 0 ? 'true' : 'false'"
@click="toggleTab"
>
{{ tab }}
</button>
</TabsList>
<div
v-for="(tab, index) in tabs"
:key="tab"
ref="tabPanelElements"
class="aria-hidden:hidden"
role="tabpanel"
:aria-hidden="index === 0 ? 'false' : 'true'"
:id="`tab-${tab.toLowerCase()}`"
:aria-labelledby="tab.toLowerCase()"
>
{{ tab }}
</div>
</Tabs>
</template>tsx
import { createRef, useRef } from 'react'
import { Tabs, TabsList } from '@/components/tabs'
const tabs = ['All', 'Photos', 'Music', 'Documents']
export function Example() {
const tabElements = useRef(tabs.map(() => createRef<HTMLButtonElement>())).current
const tabPanelElements = useRef(tabs.map(() => createRef<HTMLDivElement>())).current
return (
<Tabs refs={{ tabElements, tabPanelElements }}>
{({ toggleTab }) => (
<>
<TabsList className="flex gap-0 p-0 bg-transparent">
{tabs.map((tab, index) => (
<button
key={tab}
ref={tabElements[index]}
className="p-2 border-b-2 border-b-main/25 transition aria-selected:border-b-main/100"
role="tab"
aria-controls={`tab-${tab.toLowerCase()}`}
id={tab.toLowerCase()}
aria-selected={index === 0 ? 'true' : 'false'}
onClick={toggleTab}
>
{tab}
</button>
))}
</TabsList>
{tabs.map((tab, index) => (
<div
key={tab}
ref={tabPanelElements[index]}
className="aria-hidden:hidden"
role="tabpanel"
aria-hidden={index === 0 ? 'false' : 'true'}
id={`tab-${tab.toLowerCase()}`}
aria-labelledby={tab.toLowerCase()}
>
{tab}
</div>
))}
</>
)}
</Tabs>
)
}Accessibility
Tabs follow the ARIA tabs pattern — triggers use role="tab" with aria-controls and aria-selected inside a role="tablist" wrapper, panels use role="tabpanel" with aria-labelledby. The toggleTab helper keeps aria-selected and aria-hidden in sync when switching tabs.
JavaScript API
toggleTab
- Type:
(element: HTMLElement | Element, options?: ToggleTabOptions) => void - Kind:
sync
Toggles tab and tab panel aria attributes.
ToggleTabOptions
tabElements
- Type:
NodeListOf<Element> | Element[] - Default:
undefined
Elements representing tabs.
tabPanelElements
- Type:
NodeListOf<Element> | Element[] - Default:
undefined
Elements representing tab panels.