Tabs
Tabs provide top-level navigation for switching between related views or sections within the same context. They use a minimal, low-chrome visual treatment defined primarily by a selection indicator beneath the active tab.
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"Examples
Horizontal
The default orientation is horizontal. The left and right arrow keys can be used to navigate between tabs.
import type {ReactElement} from "react"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
export function TabsHorizontalDemo(): ReactElement {
return (
<Tabs.Root defaultValue="documents">
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="documents">
<Tab.Button>Documents</Tab.Button>
</Tab.Root>
<Tab.Root value="products">
<Tab.Button>Products</Tab.Button>
</Tab.Root>
<Tab.Root value="software">
<Tab.Button>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
<Tabs.Panel value="documents">Documents</Tabs.Panel>
<Tabs.Panel value="products">Products</Tabs.Panel>
<Tabs.Panel value="software">Software</Tabs.Panel>
<Tabs.Panel value="hardware">Hardware</Tabs.Panel>
</Tabs.Root>
)
}Vertical
In vertical orientation, the up and down arrow keys are used instead.
import type {ReactElement} from "react"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
export function TabsVerticalDemo(): ReactElement {
return (
<Tabs.Root
className="w-full"
defaultValue="documents"
orientation="vertical"
>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="documents">
<Tab.Button>Documents</Tab.Button>
</Tab.Root>
<Tab.Root value="products">
<Tab.Button>Products</Tab.Button>
</Tab.Root>
<Tab.Root value="software">
<Tab.Button>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
<Tabs.Panel value="documents">Documents</Tabs.Panel>
<Tabs.Panel value="products">Products</Tabs.Panel>
<Tabs.Panel value="software">Software</Tabs.Panel>
<Tabs.Panel value="hardware">Hardware</Tabs.Panel>
</Tabs.Root>
)
}Icons and variants
Tabs support start and end icons. Customize the variant with the iconVariant prop. Use the variant prop to change the appearance of the tab.
<Tabs.Root>
<TabContent />
</Tabs.Root>
<Tabs.Root iconVariant="filled">
<TabContent />
</Tabs.Root>
<Tabs.Root variant="contained">
<TabContent />
</Tabs.Root>
<Tabs.Root iconVariant="filled" variant="contained">
<TabContent />
</Tabs.Root>
Controlled Value
The tab's active value can be controlled via the value, onValueChange, and defaultValue properties. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {ChevronLeft, ChevronRight} from "lucide-react"
import {Button} from "@qualcomm-ui/react/button"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
export function TabsControlledValueDemo(): ReactElement {
const [value, setValue] = useState<string>("software")
const goToSoftwareTab = () => setValue("software")
const goToHardwareTab = () => setValue("hardware")
return (
<Tabs.Root onValueChange={setValue} value={value}>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="software">
<Tab.Button>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
<Tabs.Panel value="software">
<div className="flex flex-col gap-4">
<div>Software Panel</div>
<Button
emphasis="primary"
endIcon={ChevronRight}
onClick={goToHardwareTab}
size="sm"
>
View Hardware
</Button>
</div>
</Tabs.Panel>
<Tabs.Panel value="hardware">
<div className="flex flex-col gap-4">
<div>Hardware Panel</div>
<Button
emphasis="primary"
onClick={goToSoftwareTab}
size="sm"
startIcon={ChevronLeft}
>
View Software
</Button>
</div>
</Tabs.Panel>
</Tabs.Root>
)
}Sizes
Line
The line variant supports four sizes: sm, md, lg, and xl
import type {ReactElement} from "react"
import {Code, Cpu, FileText, Smartphone} from "lucide-react"
import type {QdsTabsSize} from "@qualcomm-ui/qds-core/tabs"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
const sizes: QdsTabsSize[] = ["sm", "md", "lg", "xl"]
export function TabsLineSizesDemo(): ReactElement {
return (
<div className="flex flex-col gap-4">
{sizes.map((size) => (
<div key={size} className="flex items-center gap-4">
<div className="font-heading-xs text-neutral-primary w-16">
{size}
</div>
<Tabs.Root defaultValue="documents" size={size}>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="documents">
<Tab.Button endIcon={FileText}>Documents</Tab.Button>
</Tab.Root>
<Tab.Root value="products">
<Tab.Button endIcon={Smartphone}>Products</Tab.Button>
</Tab.Root>
<Tab.Root value="software">
<Tab.Button endIcon={Code}>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button endIcon={Cpu}>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
</Tabs.Root>
</div>
))}
</div>
)
}Contained
The contained variant supports only two sizes: sm and md
import type {ReactElement} from "react"
import {Code, Cpu, FileText, Smartphone} from "lucide-react"
import type {QdsTabsSize} from "@qualcomm-ui/qds-core/tabs"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
const sizes: QdsTabsSize[] = ["sm", "md"]
export function TabsContainedSizesDemo(): ReactElement {
return (
<div className="flex flex-col gap-6">
{sizes.map((size) => (
<Tabs.Root
key={size}
defaultValue="documents"
size={size}
variant="contained"
>
<Tabs.List>
<Tab.Root value="documents">
<Tab.Button endIcon={FileText}>Documents</Tab.Button>
</Tab.Root>
<Tab.Root value="products">
<Tab.Button endIcon={Smartphone}>Products</Tab.Button>
</Tab.Root>
<Tab.Root value="software">
<Tab.Button endIcon={Code}>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button endIcon={Cpu}>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
</Tabs.Root>
))}
</div>
)
}Lazy Mounted
Use the lazyMount and unmountOnExit props to control render behavior for inactive tabs.
import {type ReactElement, useEffect, useState} from "react"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
export function TabsLazyMountedDemo(): ReactElement {
return (
<Tabs.Root className="w-80" defaultValue="tab-1" lazyMount unmountOnExit>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="tab-1">
<Tab.Button>Tab 1</Tab.Button>
</Tab.Root>
<Tab.Root value="tab-2">
<Tab.Button>Tab 2</Tab.Button>
</Tab.Root>
<Tab.Root value="tab-3">
<Tab.Button>Tab 3</Tab.Button>
</Tab.Root>
</Tabs.List>
<Tabs.Panel value="tab-1">
<TabContent />
</Tabs.Panel>
<Tabs.Panel value="tab-2">
<TabContent />
</Tabs.Panel>
<Tabs.Panel value="tab-3">
<TabContent />
</Tabs.Panel>
</Tabs.Root>
)
}
function TabContent() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((prevCount) => prevCount + 1)
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
return (
<div className="font-body-sm text-neutral-primary">
<span>This component has been alive for</span>
<span className="font-body-sm-bold text-brand-primary">{` ${count} `}</span>
<span>seconds</span>
</div>
)
}Disabled
Disable specific tabs with the disabled prop on the <Tab.Root> component.
<Tab.Root disabled value="products">
<Tab.Button endIcon={Smartphone}>Products</Tab.Button>
</Tab.Root>
URL Search Parameters
The following example demonstrates how to render tabs that sync with the URL search parameters using react-router.
import {type ReactElement, useEffect, useState} from "react"
import {useSearchParams} from "react-router"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
const tabValues = ["documents", "products", "software", "hardware"]
export function TabsLinksDemo(): ReactElement {
const [searchParams, setSearchParams] = useSearchParams()
const [value, setValue] = useState<string>(
searchParams.get("tab") || "documents",
)
useEffect(() => {
// handle back/forward navigation
const tab = searchParams.get("tab")
if (tab && tabValues.includes(tab)) {
setValue(tab)
}
}, [searchParams])
const handleTabChange = (newValue: string) => {
setValue(newValue)
setSearchParams({tab: newValue}, {preventScrollReset: true})
}
return (
<Tabs.Root onValueChange={handleTabChange} value={value}>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="documents">
<Tab.Button>Documents</Tab.Button>
</Tab.Root>
<Tab.Root value="products">
<Tab.Button>Products</Tab.Button>
</Tab.Root>
<Tab.Root value="software">
<Tab.Button>Software</Tab.Button>
</Tab.Root>
<Tab.Root value="hardware">
<Tab.Button>Hardware</Tab.Button>
</Tab.Root>
</Tabs.List>
<Tabs.Panel value="documents">Documents</Tabs.Panel>
<Tabs.Panel value="products">Products</Tabs.Panel>
<Tabs.Panel value="software">Software</Tabs.Panel>
<Tabs.Panel value="hardware">Hardware</Tabs.Panel>
</Tabs.Root>
)
}Context
Use the <Tabs.Context> component to access the tabs API from JSX.
<Tabs.Context>
{(context) => (
<>
<Tabs.Panel value="tab-1">
<Button
endIcon={ChevronRight}
onClick={() => context.setValue("tab-2")}
size="sm"
variant="outline"
>
Go to next tab
</Button>
</Tabs.Panel>
<Tabs.Panel className="flex items-center gap-2" value="tab-2">
<Button
onClick={() => context.setValue("tab-1")}
size="sm"
startIcon={ChevronLeft}
variant="outline"
>
Go to prev tab
</Button>
<Button
endIcon={ChevronRight}
onClick={() => context.setValue("tab-3")}
size="sm"
variant="outline"
>
Go to next tab
</Button>
</Tabs.Panel>
<Tabs.Panel value="tab-3">
<Button
onClick={() => context.setValue("tab-2")}
size="sm"
startIcon={ChevronLeft}
variant="outline"
>
Go to previous tab
</Button>
</Tabs.Panel>
</>
)}
</Tabs.Context>
Add and Remove Tabs
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas.
import {type ReactElement, type ReactNode, useState} from "react"
import {Plus} from "lucide-react"
import {Button} from "@qualcomm-ui/react/button"
import {Tab, Tabs} from "@qualcomm-ui/react/tabs"
import {LoremIpsum} from "@qualcomm-ui/react-core/lorem-ipsum"
interface Item {
content: ReactNode
id: string
title: string
}
const items: Item[] = [
{content: "Tab Content", id: "1", title: "Tab"},
{content: "Tab Content", id: "2", title: "Tab"},
{content: "Tab Content", id: "3", title: "Tab"},
{content: "Tab Content", id: "4", title: "Tab"},
]
export function TabsAddRemoveDemo(): ReactElement {
const [tabs, setTabs] = useState<Item[]>(items)
const [selectedTab, setSelectedTab] = useState<string | null>(items[0].id)
const addTab = () => {
const newTabs = [...tabs]
newTabs.push({
content: `Tab Body`,
id: `${parseInt(tabs[newTabs.length - 1]?.id ?? "0") + 1}`,
title: `Tab`,
})
setTabs(newTabs)
setSelectedTab(newTabs[newTabs.length - 1].id)
}
const removeTab = (id: string) => {
if (tabs.length > 1) {
const newTabs = [...tabs].filter((tab) => tab.id !== id)
setTabs(newTabs)
if (selectedTab === id) {
setSelectedTab(newTabs[0].id)
}
}
}
return (
<Tabs.Root onValueChange={setSelectedTab} size="sm" value={selectedTab}>
<Tabs.List>
<Tabs.Indicator />
{tabs.map((item) => (
<Tab.Root key={item.id} value={item.id}>
<Tab.Button>
{item.title} {item.id}
</Tab.Button>
<Tab.DismissButton onClick={() => removeTab(item.id)} />
</Tab.Root>
))}
<Button onClick={addTab} size="sm" startIcon={Plus} variant="ghost">
Add Tab
</Button>
</Tabs.List>
{tabs.map((item) => (
<Tabs.Panel key={item.id} value={item.id}>
<div className="font-heading-xs text-neutral-primary my-6">
{item.content} {item.id}
</div>
<div className="font-body-sm text-neutral-primary">
<LoremIpsum />
</div>
</Tabs.Panel>
))}
</Tabs.Root>
)
}API
Tabs
<Tabs.Root>
| Prop | Type | Default |
|---|---|---|
The activation mode of the tabs. | | 'automatic' | "automatic" |
If true, the indicator's position change will animate when the active tab
changes. Only applies to the line variant. | boolean | true |
Determines whether tabs act as a standalone composite widget (true) or as a
non-focusable component within another widget (false). | boolean | true |
The initial selected tab value when rendered.
Use when you don't need to control the selected tab value. | string | |
Whether the active tab can be deselected when clicking on it. | boolean | |
The document's text/writing direction. | 'ltr' | 'rtl' | 'ltr' |
A root node to correctly resolve document in custom environments. i.e.,
Iframes, Electron. | () => | |
The visual style of tab icons. | | 'ghost' | 'ghost' |
When true, the component will not be rendered in the DOM until it becomes
visible or active. | boolean | false |
Whether the keyboard navigation will loop from last tab to first, and vice versa. | boolean | true |
Callback to be called when the focused tab changes | ( | |
Callback to be called when the selected/active tab changes | ( | |
The orientation of the tabs. Can be horizontal or vertical | | 'horizontal' | "horizontal" |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement | |
| 'sm' | 'md' | |
Specifies the localized strings that identifies the accessibility elements and
their states | { | |
When true, the component will be completely removed from the DOM when it
becomes inactive or hidden, rather than just being hidden with CSS. | boolean | false |
The controlled selected tab value | string | |
Governs the appearance of the tab. | | 'line' |
| 'automatic'
| 'manual'
booleanline variant.booleanstringboolean'ltr' | 'rtl'
() =>
| Node
| ShadowRoot
| Document
| 'ghost'
| 'filled'
booleanboolean(
value: string,
) => void
(
value: string,
) => void
| 'horizontal'
| 'vertical'
horizontal or vertical| ReactElement
| ((
props: object,
) => ReactElement)
| 'sm'
| 'md'
| 'lg'
| 'xl'
lg
and xl are not supported by the contained variant.{
listLabel?: string
}
booleanstring| 'line'
| 'contained'
| Attribute / Property | Value |
|---|---|
className | 'qui-tabs__root' |
data-focus | |
data-orientation | | 'horizontal' |
data-part | 'root' |
data-scope | 'tabs' |
data-size | | 'sm' |
className'qui-tabs__root'data-focusdata-orientation| 'horizontal'
| 'vertical'
data-part'root'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
<Tabs.List>
<div> element by default.| Prop | Type |
|---|---|
id attribute. If
omitted, a unique identifier will be automatically generated for accessibility. | string |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
string| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tabs__list' |
data-focus | |
data-orientation | | 'horizontal' |
data-part | 'list' |
data-scope | 'tabs' |
data-size | | 'sm' |
data-variant | | 'line' |
className'qui-tabs__list'data-focusdata-orientation| 'horizontal'
| 'vertical'
data-part'list'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-variant| 'line'
| 'contained'
<Tabs.Indicator>
<div> element by
default. You only need to render a single <Tabs.Indicator> component per tab
list.<Tabs.Root>
<Tabs.List>
<Tabs.Indicator />
<Tab.Root value="tab-1">
<Tab.Button>Tab 1</Tab.Button>
</Tab.Root>
</Tabs.List>
</Tabs.Root>
| Prop | Type |
|---|---|
id attribute. If
omitted, a unique identifier will be automatically generated for accessibility. | string |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
string| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tabs__indicator' |
data-animate | |
data-focusWhether any tab has the simulated focus state. | |
data-focus-visibleWhether any tab has the simulated focus-visible state. | |
data-orientation | | 'horizontal' |
data-part | 'indicator' |
data-scope | 'tabs' |
data-size | | 'sm' |
data-variant | | 'line' |
hiddenHidden for contained variant. | boolean |
style |
className'qui-tabs__indicator'data-animatedata-focusdata-focus-visibledata-orientation| 'horizontal'
| 'vertical'
data-part'indicator'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-variant| 'line'
| 'contained'
hiddenbooleancontained variant.style<Tabs.Panel>
<div> element by default.| Prop | Type |
|---|---|
The value of the associated tab | string |
id attribute. If
omitted, a unique identifier will be automatically generated for accessibility. | string |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
stringstring| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tabs__panel' |
data-orientation | | 'horizontal' |
data-ownedby | string |
data-part | 'panel' |
data-scope | 'tabs' |
data-selected | |
hidden | boolean |
tabIndex | -1 | 0 |
className'qui-tabs__panel'data-orientation| 'horizontal'
| 'vertical'
data-ownedbystringdata-part'panel'data-scope'tabs'data-selectedhiddenbooleantabIndex-1 | 0
Tab
<Tab.Root>
<div> element by default.stringboolean| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tab__root' |
data-orientation | | 'horizontal' |
data-part | 'tab' |
data-scope | 'tabs' |
data-size | | 'sm' |
data-variant | | 'line' |
className'qui-tab__root'data-orientation| 'horizontal'
| 'vertical'
data-part'tab'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-variant| 'line'
| 'contained'
<Tab.Button>
<button> element by
default.| Prop | Type |
|---|---|
Use the disabled prop on the parent <Tab.Root> component instead. | never |
| LucideIcon | |
id attribute. If
omitted, a unique identifier will be automatically generated for accessibility. | string |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon |
neverdisabled prop on the parent <Tab.Root> component instead.| LucideIcon
| ReactNode
LucideIcon, the size will automatically match the size prop.
Supply as a ReactElement for additional customization.string| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tab__button' |
data-disabled | |
data-focus | |
data-focus-visible | |
data-indicator-rendered | |
data-orientation | | 'horizontal' |
data-ownedby | string |
data-part | 'tab-button' |
data-scope | 'tabs' |
data-selected | |
data-size | | 'sm' |
data-value | string |
data-variant | | 'line' |
tabIndex | -1 | 0 |
className'qui-tab__button'data-disableddata-focusdata-focus-visibledata-indicator-rendereddata-orientation| 'horizontal'
| 'vertical'
data-ownedbystringdata-part'tab-button'data-scope'tabs'data-selecteddata-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-valuestringdata-variant| 'line'
| 'contained'
tabIndex-1 | 0
<Tab.DismissButton>
<button> element by default.| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-tab__dismiss-button' |
data-part | 'tab-dismiss-button' |
data-scope | 'tabs' |
data-size | | 'sm' |
className'qui-tab__dismiss-button'data-part'tab-dismiss-button'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
<Tabs.Context>
| Prop | Type |
|---|---|
Render Prop
that provides the current TabsApi context. |
TabsApi
| Prop | Type |
|---|---|
Clears the value of the tabs. | () => void |
Set focus on the selected tab button | () => void |
The value of the tab that is currently focused. | string |
Returns the state of the tab with the given props | (props: { |
Sets the indicator rect to the tab with the given value | ( |
Sets the value of the tabs. | ( |
Synchronizes the tab index of the content element.
Useful when rendering tabs within a select or combobox | () => void |
The current value of the tabs. | string |
() => void
() => void
string(props: {
disabled?: boolean
value: string
}) => {
disabled: boolean
focused: boolean
selected: boolean
}
(
value: string,
) => void
(
value: string,
) => void
() => void
string
<div>element by default.