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 {TabsModule} from "@qualcomm-ui/angular/tabs"Examples
Horizontal
The default orientation is horizontal. The left and right arrow keys can be used to navigate between tabs.
<div defaultValue="documents" q-tabs-root>
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
<div q-tabs-panel value="documents">Documents</div>
<div q-tabs-panel value="products">Products</div>
<div q-tabs-panel value="software">Software</div>
<div q-tabs-panel value="hardware">Hardware</div>
</div>
Vertical
In vertical orientation, the up and down arrow keys are used instead.
<div
class="w-full"
defaultValue="documents"
orientation="vertical"
q-tabs-root
>
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
<div q-tabs-panel value="documents">Documents</div>
<div q-tabs-panel value="products">Products</div>
<div q-tabs-panel value="software">Software</div>
<div q-tabs-panel value="hardware">Hardware</div>
</div>
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.
import {Component} from "@angular/core"
import {Code, Cpu, FileText, Smartphone} from "lucide-angular"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
@Component({
imports: [TabsModule],
providers: [provideIcons({Code, Cpu, FileText, Smartphone})],
selector: "tabs-icons-demo",
template: `
<div class="flex flex-col gap-6">
<div q-tabs-root>
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
<div iconVariant="filled" q-tabs-root>
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
<div q-tabs-root variant="contained">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
<div iconVariant="filled" q-tabs-root variant="contained">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
</div>
`,
})
export class TabsIconsDemo {}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 {Component, signal} from "@angular/core"
import {ChevronLeft, ChevronRight} from "lucide-angular"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
@Component({
imports: [TabsModule, ButtonModule],
providers: [provideIcons({ChevronLeft, ChevronRight})],
selector: "tabs-controlled-value-demo",
template: `
<div q-tabs-root [value]="value()" (valueChanged)="value.set($event)">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
<div q-tabs-panel value="software">
<div class="flex flex-col gap-4">
<div>Software Panel</div>
<button
emphasis="primary"
endIcon="ChevronRight"
q-button
size="sm"
(click)="goToHardwareTab()"
>
View Hardware
</button>
</div>
</div>
<div q-tabs-panel value="hardware">
<div class="flex flex-col gap-4">
<div>Hardware Panel</div>
<button
emphasis="primary"
q-button
size="sm"
startIcon="ChevronLeft"
(click)="goToSoftwareTab()"
>
View Software
</button>
</div>
</div>
</div>
`,
})
export class TabsControlledValueDemo {
protected readonly value = signal<string>("software")
protected goToSoftwareTab() {
this.value.set("software")
}
protected goToHardwareTab() {
this.value.set("hardware")
}
}Sizes
Line
The line variant supports four sizes: sm, md, lg, and xl
import {Component} from "@angular/core"
import {Code, Cpu, FileText, Smartphone} from "lucide-angular"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import type {QdsTabsSize} from "@qualcomm-ui/qds-core/tabs"
@Component({
imports: [TabsModule],
providers: [provideIcons({Code, Cpu, FileText, Smartphone})],
selector: "tabs-line-sizes-demo",
template: `
<div class="flex flex-col gap-4">
@for (size of sizes; track size) {
<div class="flex items-center gap-4">
<div class="font-heading-xs text-neutral-primary w-16">
{{ size }}
</div>
<div defaultValue="documents" q-tabs-root [size]="size">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
</div>
</div>
}
</div>
`,
})
export class TabsLineSizesDemo {
protected sizes: QdsTabsSize[] = ["sm", "md", "lg", "xl"]
}Contained
The contained variant supports only two sizes: sm and md
@for (size of sizes; track size) {
<div
defaultValue="documents"
q-tabs-root
variant="contained"
[size]="size"
>
<div q-tabs-list>
<div q-tab-root value="documents">
<button q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
</div>
}
Disabled
Disable specific tabs with the disabled prop on the <Tab.Root> component.
import {Component} from "@angular/core"
import {Code, Cpu, FileText, Smartphone} from "lucide-angular"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
@Component({
imports: [TabsModule],
providers: [provideIcons({Code, Cpu, FileText, Smartphone})],
selector: "tabs-disabled-demo",
template: `
<div class="flex flex-col gap-6">
<div defaultValue="documents" q-tabs-root variant="line">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div disabled q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
<div q-tabs-root variant="contained">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button endIcon="FileText" q-tab-button>Documents</button>
</div>
<div disabled q-tab-root value="products">
<button endIcon="Smartphone" q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button endIcon="Code" q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button endIcon="Cpu" q-tab-button>Hardware</button>
</div>
</div>
</div>
</div>
`,
})
export class TabsDisabledDemo {}URL Search Parameters
The following example demonstrates how to render tabs that sync with the Angular router's query parameters.
import {Component, inject, signal} from "@angular/core"
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"
import {ActivatedRoute, Router} from "@angular/router"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
const tabValues = ["documents", "products", "software", "hardware"]
@Component({
imports: [TabsModule],
selector: "tabs-links-demo",
template: `
<div q-tabs-root [value]="value()" (valueChanged)="handleTabChange($event)">
<div q-tabs-list>
<div q-tabs-indicator></div>
<div q-tab-root value="documents">
<button q-tab-button>Documents</button>
</div>
<div q-tab-root value="products">
<button q-tab-button>Products</button>
</div>
<div q-tab-root value="software">
<button q-tab-button>Software</button>
</div>
<div q-tab-root value="hardware">
<button q-tab-button>Hardware</button>
</div>
</div>
<div q-tabs-panel value="documents">Documents</div>
<div q-tabs-panel value="products">Products</div>
<div q-tabs-panel value="software">Software</div>
<div q-tabs-panel value="hardware">Hardware</div>
</div>
`,
})
export class TabsLinksDemo {
protected readonly value = signal<string>("documents")
private route = inject(ActivatedRoute)
private router = inject(Router)
constructor() {
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => {
const tab = params["tab"] || "documents"
if (tabValues.includes(tab)) {
this.value.set(tab)
}
})
}
handleTabChange(newValue: string) {
this.value.set(newValue)
this.router.navigate([], {
queryParams: {tab: newValue},
queryParamsHandling: "merge",
})
}
}Context
Use the tabsContext structural directive to access the tabs API from the template.
<ng-container *tabsContext="let tabsApi">
<div q-tabs-panel value="tab-1">
<button
endIcon="ChevronRight"
q-button
size="sm"
variant="outline"
(click)="tabsApi.setValue('tab-2')"
>
Go to next tab
</button>
</div>
<div class="flex items-center gap-2" q-tabs-panel value="tab-2">
<button
q-button
size="sm"
startIcon="ChevronLeft"
variant="outline"
(click)="tabsApi.setValue('tab-1')"
>
Go to prev tab
</button>
<button
endIcon="ChevronRight"
q-button
size="sm"
variant="outline"
(click)="tabsApi.setValue('tab-3')"
>
Go to next tab
</button>
</div>
<div q-tabs-panel value="tab-3">
<button
q-button
size="sm"
startIcon="ChevronLeft"
variant="outline"
(click)="tabsApi.setValue('tab-2')"
>
Go to previous tab
</button>
</div>
</ng-container>
Add and Remove Tabs
import {Component, signal} from "@angular/core"
import {Plus} from "lucide-angular"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {TabsModule} from "@qualcomm-ui/angular/tabs"
import {LoremIpsumDirective} from "@qualcomm-ui/angular-core/lorem-ipsum"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
interface Item {
content: string
id: string
title: string
}
@Component({
imports: [TabsModule, LoremIpsumDirective, ButtonModule],
providers: [provideIcons({Plus})],
selector: "tabs-add-remove-demo",
template: `
<div
q-tabs-root
[value]="selectedTab()"
(valueChanged)="selectedTab.set($event)"
>
<div q-tabs-list>
@for (tab of tabs(); track tab.id) {
<div q-tab-root [value]="tab.id">
<button q-tab-button>{{ tab.title }} {{ tab.id }}</button>
<button
q-tab-dismiss-button
[aria-label]="'Dismiss ' + tab.title + ' ' + tab.id"
(click)="removeTab(tab.id)"
></button>
</div>
}
<button
q-button
size="sm"
startIcon="Plus"
variant="ghost"
(click)="addTab()"
>
Add Tab
</button>
</div>
@for (tab of tabs(); track tab.id) {
<div q-tabs-panel [value]="tab.id">
<div class="font-heading-xs text-neutral-primary my-6">
{{ tab.content }} {{ tab.id }}
</div>
<div class="font-body-sm text-neutral-primary">
<div q-lorem-ipsum></div>
</div>
</div>
}
</div>
`,
})
export class TabsAddRemoveDemo {
protected readonly tabs = signal<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"},
])
protected readonly selectedTab = signal<string | null>(this.tabs()[0].id)
addTab() {
const newTabs = [...this.tabs()]
newTabs.push({
content: `Tab Body`,
id: `${parseInt(this.tabs()[newTabs.length - 1]?.id ?? "0") + 1}`,
title: `Tab`,
})
this.tabs.set(newTabs)
this.selectedTab.set(newTabs[newTabs.length - 1].id)
}
removeTab = (id: string) => {
if (this.tabs().length > 1) {
const newTabs = [...this.tabs()].filter((tab) => tab.id !== id)
this.tabs.set(newTabs)
if (this.selectedTab() === id) {
this.selectedTab.set(newTabs[0].id)
}
}
}
}API
Tabs
q-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 the 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 |
The orientation of the tabs. Can be horizontal or vertical | | 'horizontal' | "horizontal" |
| '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' | |
Callback to be called when the focused tab changes | string | |
Callback to be called when the selected/active tab changes | string |
| 'automatic'
| 'manual'
booleanline variant.booleanstringboolean'ltr' | 'rtl'
() =>
| Node
| ShadowRoot
| Document
| 'ghost'
| 'filled'
booleanboolean| 'horizontal'
| 'vertical'
horizontal or vertical| 'sm'
| 'md'
| 'lg'
| 'xl'
lg
and xl are not supported by the contained variant.{
listLabel?: string
}
booleanstring| 'line'
| 'contained'
stringstring| Attribute / Property | Value |
|---|---|
class | 'qui-tabs__root' |
data-focus | |
data-orientation | | 'horizontal' |
data-part | 'root' |
data-scope | 'tabs' |
data-size | | 'sm' |
class'qui-tabs__root'data-focusdata-orientation| 'horizontal'
| 'vertical'
data-part'root'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
q-tabs-list
| Prop | Type |
|---|---|
id attribute. If
omitted, a unique identifier will be generated for accessibility. | string |
string| Attribute / Property | Value |
|---|---|
class | 'qui-tabs__list' |
data-focus | |
data-orientation | | 'horizontal' |
data-part | 'list' |
data-scope | 'tabs' |
data-size | | 'sm' |
data-variant | | 'line' |
class'qui-tabs__list'data-focusdata-orientation| 'horizontal'
| 'vertical'
data-part'list'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-variant| 'line'
| 'contained'
q-tabs-indicator
This directive is automatically rendered by the q-tabs-list directive. It can be customized like so:
<div q-tabs-list>
<!-- customize the element here -->
<div q-tabs-indicator></div>
<!-- ... -->
</div>| Prop | Type |
|---|---|
id attribute. If
omitted, a unique identifier will be generated for accessibility. | string |
string| Attribute / Property | Value |
|---|---|
class | '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 |
class'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.styleq-tabs-panel
| Prop | Type |
|---|---|
The value of the associated tab | string |
id attribute. If
omitted, a unique identifier will be generated for accessibility. | string |
stringstring| Attribute / Property | Value |
|---|---|
class | 'qui-tabs__panel' |
data-orientation | | 'horizontal' |
data-ownedby | string |
data-part | 'panel' |
data-scope | 'tabs' |
data-selected | |
hidden | boolean |
tabIndex | -1 | 0 |
class'qui-tabs__panel'data-orientation| 'horizontal'
| 'vertical'
data-ownedbystringdata-part'panel'data-scope'tabs'data-selectedhiddenbooleantabIndex-1 | 0
Tab
q-tab-root
| Attribute / Property | Value |
|---|---|
class | 'qui-tab__root' |
data-orientation | | 'horizontal' |
data-part | 'tab' |
data-scope | 'tabs' |
data-size | | 'sm' |
data-variant | | 'line' |
class'qui-tab__root'data-orientation| 'horizontal'
| 'vertical'
data-part'tab'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
data-variant| 'line'
| 'contained'
q-tab-button
| Prop | Type |
|---|---|
lucide-angular icon, positioned after the label. | | LucideIconData |
To customize the element, provide it using the directive instead:
<button q-tab-button> | |
id attribute. If
omitted, a unique identifier will be generated for accessibility. | string |
lucide-angular icon, positioned before the label. | | LucideIconData |
To customize the element, provide it using the directive instead:
<button q-tab-button> | |
| LucideIconData
| string
<button q-tab-button>
<svg q-end-icon icon="..."></svg>
</button>
string| LucideIconData
| string
<button q-tab-button>
<svg q-start-icon icon="..."></svg>
</button>
| Attribute / Property | Value |
|---|---|
class | '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 |
class'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
q-tab-dismiss-button
| Prop | Type |
|---|---|
string |
string| Attribute / Property | Value |
|---|---|
class | 'qui-tab__dismiss-button' |
data-part | 'tab-dismiss-button' |
data-scope | 'tabs' |
data-size | | 'sm' |
class'qui-tab__dismiss-button'data-part'tab-dismiss-button'data-scope'tabs'data-size| 'sm'
| 'md'
| 'lg'
| 'xl'
tabsContext
Used to access the tabs API from the template.
<div q-tabs-root>
<!-- type-aware context for the tabs API -->
<ng-container *tabsContext="let tabsApi">
<button (click)="tabsApi.setValue('...')"></button>
</ng-container>
</div>| 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