Usage
Tabs organizes navigation across views. It is one component family split across a few pieces that only make sense together: Tab is a single tab, TabGroup shares selection between a tab list and its content, and TabPanels/TabPanel render the content for the selected tab. Only Tab children are rendered; other children are ignored.
import { Tab, Tabs } from "@udixio/ui-react"import { Tab, Tabs } from '@udixio/ui-react';
import { iHome } from '@udixio/icons-rounded-400/home';
import { iSettings } from '@udixio/icons-rounded-400/settings';
import { iPerson } from '@udixio/icons-rounded-400/person';
export default function TabsBasicReact() {
return (
<Tabs defaultSelectedTab={0}>
<Tab label="Home" icon={iHome} />
<Tab label="Settings" icon={iSettings} />
<Tab label="Profile" icon={iPerson} />
</Tabs>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Tab, Tabs } from '@udixio/ui-angular';
import { iHome } from '@udixio/icons-rounded-400/home';
import { iSettings } from '@udixio/icons-rounded-400/settings';
import { iPerson } from '@udixio/icons-rounded-400/person';
@Component({
selector: 'docs-tabs-basic-angular',
standalone: true,
imports: [Tabs, Tab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-tabs [defaultSelectedTab]="0">
<udx-tab label="Home" [icon]="iHome" />
<udx-tab label="Settings" [icon]="iSettings" />
<udx-tab label="Profile" [icon]="iPerson" />
</udx-tabs>
`,
})
export class TabsBasicAngular {
readonly iHome = iHome;
readonly iSettings = iSettings;
readonly iPerson = iPerson;
}<script lang="ts">
import { iHome } from '@udixio/icons-rounded-400/home';
import { iPerson } from '@udixio/icons-rounded-400/person';
import { iSettings } from '@udixio/icons-rounded-400/settings';
import { Tab, Tabs } from '@udixio/ui-svelte';
</script>
<Tabs defaultSelectedTab={0}>
<Tab label="Home" icon={iHome} />
<Tab label="Settings" icon={iSettings} />
<Tab label="Profile" icon={iPerson} />
</Tabs>disabled removes a tab from the roving tab order and blocks selection.
Variants
variant is primary (default, icon above the label, with a thicker 3px indicator) or secondary (icon beside the label, with a thinner 2px indicator).
import { Tab, Tabs } from '@udixio/ui-react';
export default function TabsVariantsReact() {
return (
<div className="flex flex-col gap-2 w-full">
<Tabs variant="primary" defaultSelectedTab={0}>
<Tab label="One" />
<Tab label="Two" />
</Tabs>
<Tabs variant="secondary" defaultSelectedTab={0}>
<Tab label="One" />
<Tab label="Two" />
</Tabs>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Tab, Tabs } from '@udixio/ui-angular';
@Component({
selector: 'docs-tabs-variants-angular',
standalone: true,
imports: [Tabs, Tab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-2 w-full">
<udx-tabs variant="primary" [defaultSelectedTab]="0">
<udx-tab label="One" />
<udx-tab label="Two" />
</udx-tabs>
<udx-tabs variant="secondary" [defaultSelectedTab]="0">
<udx-tab label="One" />
<udx-tab label="Two" />
</udx-tabs>
</div>
`,
})
export class TabsVariantsAngular {}<script lang="ts">
import { Tab, Tabs } from '@udixio/ui-svelte';
</script>
<div class="flex w-full flex-col gap-2">
<Tabs variant="primary" defaultSelectedTab={0}><Tab label="One" /><Tab label="Two" /></Tabs>
<Tabs variant="secondary" defaultSelectedTab={0}><Tab label="One" /><Tab label="Two" /></Tabs>
</div>Connecting tabs to panels
Wrap Tabs and TabPanels in a TabGroup to connect them; this lets you place the tab list and its panels anywhere in the layout, and drives the panel’s slide animation. Tabs alone (no TabGroup) is enough for a navigation-only tab list with no panels, as in the example above.
import { TabGroup, Tabs, Tab, TabPanels, TabPanel } from "@udixio/ui-react"Profile
This is the profile panel content.
import { Tab, TabGroup, TabPanel, TabPanels, Tabs } from '@udixio/ui-react';
export default function TabsPanelsReact() {
return (
<TabGroup defaultSelectedTab={0}>
<Tabs>
<Tab label="Profile" />
<Tab label="Settings" />
<Tab label="Notifications" />
</Tabs>
<TabPanels>
<TabPanel>
<div className="p-4">
<h3 className="text-title-medium">Profile</h3>
<p className="text-body-medium text-on-surface-variant">
This is the profile panel content.
</p>
</div>
</TabPanel>
<TabPanel>
<div className="p-4">
<h3 className="text-title-medium">Settings</h3>
<p className="text-body-medium text-on-surface-variant">
Configure your preferences here.
</p>
</div>
</TabPanel>
<TabPanel>
<div className="p-4">
<h3 className="text-title-medium">Notifications</h3>
<p className="text-body-medium text-on-surface-variant">
Manage your notification settings.
</p>
</div>
</TabPanel>
</TabPanels>
</TabGroup>
);
}Profile
This is the profile panel content.
Settings
Configure your preferences here.
Notifications
Manage your notification settings.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Tab, TabGroup, TabPanel, TabPanels, Tabs } from '@udixio/ui-angular';
@Component({
selector: 'docs-tabs-panels-angular',
standalone: true,
imports: [Tabs, Tab, TabGroup, TabPanels, TabPanel],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-tab-group [defaultSelectedTab]="0">
<udx-tabs>
<udx-tab label="Profile" />
<udx-tab label="Settings" />
<udx-tab label="Notifications" />
</udx-tabs>
<udx-tab-panels>
<udx-tab-panel>
<div class="p-4">
<h3 class="text-title-medium">Profile</h3>
<p class="text-body-medium text-on-surface-variant">
This is the profile panel content.
</p>
</div>
</udx-tab-panel>
<udx-tab-panel>
<div class="p-4">
<h3 class="text-title-medium">Settings</h3>
<p class="text-body-medium text-on-surface-variant">
Configure your preferences here.
</p>
</div>
</udx-tab-panel>
<udx-tab-panel>
<div class="p-4">
<h3 class="text-title-medium">Notifications</h3>
<p class="text-body-medium text-on-surface-variant">
Manage your notification settings.
</p>
</div>
</udx-tab-panel>
</udx-tab-panels>
</udx-tab-group>
`,
})
export class TabsPanelsAngular {}<script lang="ts">
import { Tab, TabGroup, TabPanel, TabPanels, Tabs } from '@udixio/ui-svelte';
</script>
<TabGroup defaultSelectedTab={0}>
<Tabs>
<Tab label="Profile" />
<Tab label="Settings" />
<Tab label="Notifications" />
</Tabs>
<TabPanels>
<TabPanel>
<div class="p-4"><h3 class="text-title-medium">Profile</h3><p class="text-body-medium text-on-surface-variant">This is the profile panel content.</p></div>
</TabPanel>
<TabPanel>
<div class="p-4"><h3 class="text-title-medium">Settings</h3><p class="text-body-medium text-on-surface-variant">Configure your preferences here.</p></div>
</TabPanel>
<TabPanel>
<div class="p-4"><h3 class="text-title-medium">Notifications</h3><p class="text-body-medium text-on-surface-variant">Manage your notification settings.</p></div>
</TabPanel>
</TabPanels>
</TabGroup>Only the active TabPanel is mounted by the React and Svelte adapters; on Angular, every projected udx-tab-panel keeps its component instance and state alive and the inactive ones are hidden with the native hidden attribute instead — Angular content projection has no cheap equivalent to conditionally mounting projected children. All three remove inactive panels from the accessibility tree and the tab order identically.
Controlled selection and scrollable
Use selectedTab/onSelectedTabChange (React), selectedTab/ selectedTabChange (Angular), or bind:selectedTab (Svelte) for controlled selection on Tabs or TabGroup; defaultSelectedTab (defaults to 0) seeds the initial selection when uncontrolled. Set scrollable to true to enable horizontal scroll with automatic centering of the selected tab.
Do not bind selectedTab directly on Tabs when it is also wrapped in a TabGroup — the TabGroup is the single source of truth in that case, and the two owners would fight over the selection.
import { useState } from 'react';
import { Tab, Tabs } from '@udixio/ui-react';
export default function TabsControlledReact() {
const [tab, setTab] = useState<number | null>(0);
return (
<Tabs selectedTab={tab} onSelectedTabChange={setTab} scrollable>
<Tab label="Overview" />
<Tab label="Activity" />
<Tab label="Settings" />
<Tab label="Billing" />
<Tab label="Advanced" />
</Tabs>
);
}import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Tab, Tabs } from '@udixio/ui-angular';
@Component({
selector: 'docs-tabs-controlled-angular',
standalone: true,
imports: [Tabs, Tab],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-tabs
[selectedTab]="tab()"
[scrollable]="true"
(selectedTabChange)="tab.set($event)"
>
<udx-tab label="Overview" />
<udx-tab label="Activity" />
<udx-tab label="Settings" />
<udx-tab label="Billing" />
<udx-tab label="Advanced" />
</udx-tabs>
`,
})
export class TabsControlledAngular {
readonly tab = signal(0);
}<script lang="ts">
import { Tab, Tabs } from '@udixio/ui-svelte';
let selectedTab = $state<number | null>(0);
</script>
<Tabs bind:selectedTab scrollable>
<Tab label="Overview" />
<Tab label="Activity" />
<Tab label="Settings" />
<Tab label="Billing" />
<Tab label="Advanced" />
</Tabs>Link or action
If the href prop is provided on a Tab, it renders an <a> link; otherwise it renders a <button>. label can come from a string child as an alternative to the label prop.
<Tabs>
<Tab label="Dashboard" />
<Tab label="Documentation" href="/docs" />
<Tab label="External" href="https://udixio.dev" />
</Tabs>Motion
The sliding tab indicator and the panel’s enter transition are implemented once with Motion JavaScript in @udixio/core/dom (createTabsIndicatorController, animateTabPanelEnter), so React, Angular, and Svelte share the same timing and reduced-motion behavior — neither adapter implements its own version of either effect.
Accessibility
Tabs renders role="tablist"; each Tab renders role="tab" with a roving tabIndex (0 on the selected tab, or the first enabled tab when nothing is selected, -1 on the rest). ArrowLeft/ArrowRight move focus and selection between enabled tabs and wrap at the ends; Home/End jump to the first/last enabled tab. When a TabGroup connects a TabPanels, each Tab exposes id/aria-controls matched to its TabPanel’s id/ aria-labelledby, and the panel is in the tab order (tabIndex={0}) so keyboard users can move focus into its content.
Migrating to 3.0
| Before | 3.0 |
|---|---|
Tab selected (standalone, per-tab) | Tabs/TabGroup defaultSelectedTab (single source of truth) |
Tabs/TabGroup setSelectedTab (raw dispatcher) | onSelectedTabChange (React), selectedTabChange (Angular) |
TabGroup defaultTab | defaultSelectedTab |
| No keyboard navigation | Roving tabIndex, ArrowLeft/ArrowRight/Home/End |
No disabled on Tab | disabled input/prop |
| No Angular adapter | Full Angular parity (udx-tabs, udx-tab, udx-tab-group, udx-tab-panels, udx-tab-panel) |