Skip to content

Tabs

Accessible tabs to switch between panels of content.

View Source 

Installation

Follow instructions for individual framework usage below

  • winduum 
  • winduum-elements 
  • winduum-stimulus 
  • winduum-vue 
  • winduum-react 

Styles

Variants

  • list 

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.

Released under the MIT License.