@xsolla/xui-tabs
v0.110.0
Published
A cross-platform React tabs component for organizing content into multiple panels that users can switch between. Implements WAI-ARIA tablist pattern for accessibility.
Readme
Tabs
A cross-platform React tabs component for organizing content into multiple panels that users can switch between. Implements WAI-ARIA tablist pattern for accessibility.
Variants:
- Line (default): Traditional underlined tabs with bottom border indicator
- Segmented: Button-group style segmented control with sliding active indicator
Installation
npm install @xsolla/xui-tabs
# or
yarn add @xsolla/xui-tabsDemo
Basic Tabs
import * as React from 'react';
import { Tabs, TabPanel } from '@xsolla/xui-tabs';
export default function BasicTabs() {
const [activeTab, setActiveTab] = React.useState('tab1');
const tabs = [
{ id: 'tab1', label: 'Overview' },
{ id: 'tab2', label: 'Features' },
{ id: 'tab3', label: 'Pricing' },
];
return (
<div>
<Tabs
id="basic-tabs"
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
/>
<TabPanel id="tab1" tabsId="basic-tabs" hidden={activeTab !== 'tab1'}>
<p>Overview content goes here.</p>
</TabPanel>
<TabPanel id="tab2" tabsId="basic-tabs" hidden={activeTab !== 'tab2'}>
<p>Features content goes here.</p>
</TabPanel>
<TabPanel id="tab3" tabsId="basic-tabs" hidden={activeTab !== 'tab3'}>
<p>Pricing content goes here.</p>
</TabPanel>
</div>
);
}Tabs with Icons
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
import { Home, User, Settings } from '@xsolla/xui-icons';
export default function TabsWithIcons() {
const [activeTab, setActiveTab] = React.useState('home');
const tabs = [
{ id: 'home', label: 'Home', icon: <Home size={16} /> },
{ id: 'profile', label: 'Profile', icon: <User size={16} /> },
{ id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
];
return (
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
/>
);
}Tabs with Counter
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function TabsWithCounter() {
const [activeTab, setActiveTab] = React.useState('inbox');
const tabs = [
{ id: 'inbox', label: 'Inbox', counter: 12 },
{ id: 'sent', label: 'Sent', counter: 5 },
{ id: 'drafts', label: 'Drafts', counter: 3 },
{ id: 'spam', label: 'Spam', badge: true },
];
return (
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
/>
);
}Tabs Sizes
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function TabsSizes() {
const tabs = [
{ id: 'a', label: 'Tab A' },
{ id: 'b', label: 'Tab B' },
{ id: 'c', label: 'Tab C' },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<Tabs tabs={tabs} activeTabId="a" size="sm" />
<Tabs tabs={tabs} activeTabId="a" size="md" />
<Tabs tabs={tabs} activeTabId="a" size="lg" />
<Tabs tabs={tabs} activeTabId="a" size="xl" />
</div>
);
}Segmented Variant
A button-group style segmented control with a sliding active indicator animation.
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function SegmentedTabs() {
const [activeTab, setActiveTab] = React.useState('daily');
const tabs = [
{ id: 'daily', label: 'Daily' },
{ id: 'weekly', label: 'Weekly' },
{ id: 'monthly', label: 'Monthly' },
];
return (
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
variant="segmented"
/>
);
}Stretched Tabs
Use the stretched prop to make tabs fill the entire container width.
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function StretchedTabs() {
const [activeTab, setActiveTab] = React.useState('tab1');
const tabs = [
{ id: 'tab1', label: 'First' },
{ id: 'tab2', label: 'Second' },
{ id: 'tab3', label: 'Third' },
];
return (
<div style={{ width: '100%' }}>
{/* Line variant stretched */}
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
stretched
/>
{/* Segmented variant stretched */}
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
variant="segmented"
stretched
/>
</div>
);
}Anatomy
Import the components and compose them:
import { Tabs, TabPanel } from '@xsolla/xui-tabs';
// Tab list
<Tabs
id="my-tabs" // Unique ID for ARIA
tabs={tabItems} // Array of tab definitions
activeTabId={activeId} // Currently active tab
onChange={handleChange} // Tab selection handler
size="md" // Size variant
variant="line" // Visual variant: "line" | "segmented"
stretched={false} // Fill container width
alignLeft={true} // Alignment (line variant only)
activateOnFocus={true} // Auto-activate on focus
/>
// Tab panels
<TabPanel
id="tab-id" // Matches tab.id
tabsId="my-tabs" // Parent Tabs id
hidden={!isActive} // Visibility control
>
Panel content
</TabPanel>Examples
Disabled Tab
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function DisabledTab() {
const [activeTab, setActiveTab] = React.useState('active');
const tabs = [
{ id: 'active', label: 'Active Tab' },
{ id: 'disabled', label: 'Disabled Tab', disabled: true },
{ id: 'another', label: 'Another Tab' },
];
return (
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
/>
);
}Manual Activation
import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
export default function ManualActivation() {
const [activeTab, setActiveTab] = React.useState('tab1');
const tabs = [
{ id: 'tab1', label: 'First' },
{ id: 'tab2', label: 'Second' },
{ id: 'tab3', label: 'Third' },
];
return (
<div>
<p>Use Arrow keys to navigate, Enter/Space to activate</p>
<Tabs
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
activateOnFocus={false}
/>
</div>
);
}Full Example with Content
import * as React from 'react';
import { Tabs, TabPanel } from '@xsolla/xui-tabs';
import { Settings } from '@xsolla/xui-icons';
import { LayoutDashboard, BarChart3, FileText } from '@xsolla/xui-icons-base';
export default function FullTabsExample() {
const [activeTab, setActiveTab] = React.useState('dashboard');
const tabs = [
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} /> },
{ id: 'analytics', label: 'Analytics', icon: <BarChart3 size={16} />, counter: 5 },
{ id: 'reports', label: 'Reports', icon: <FileText size={16} /> },
{ id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
];
return (
<div style={{ width: '100%' }}>
<Tabs
id="main-tabs"
tabs={tabs}
activeTabId={activeTab}
onChange={setActiveTab}
size="md"
/>
<div style={{ padding: 24, border: '1px solid #eee', borderTop: 'none' }}>
<TabPanel id="dashboard" tabsId="main-tabs" hidden={activeTab !== 'dashboard'}>
<h3>Dashboard</h3>
<p>Welcome to your dashboard overview.</p>
</TabPanel>
<TabPanel id="analytics" tabsId="main-tabs" hidden={activeTab !== 'analytics'}>
<h3>Analytics</h3>
<p>View your analytics and metrics here.</p>
</TabPanel>
<TabPanel id="reports" tabsId="main-tabs" hidden={activeTab !== 'reports'}>
<h3>Reports</h3>
<p>Generate and view reports.</p>
</TabPanel>
<TabPanel id="settings" tabsId="main-tabs" hidden={activeTab !== 'settings'}>
<h3>Settings</h3>
<p>Configure your preferences.</p>
</TabPanel>
</div>
</div>
);
}API Reference
Tabs
The tab list component.
Tabs Props:
| Prop | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
| tabs | TabItemType[] | - | Required. Array of tab definitions. |
| activeTabId | string | - | Currently active tab ID. |
| onChange | (id: string) => void | - | Callback when tab selection changes. |
| size | "xl" \| "lg" \| "md" \| "sm" | "md" | Size of the tabs. |
| variant | "line" \| "segmented" | "line" | Visual variant. Line shows underlined tabs; segmented shows button-group style. |
| stretched | boolean | false | Whether to stretch tabs to fill container width. |
| alignLeft | boolean | true | Left vs center alignment (line variant only). |
| activateOnFocus | boolean | true | Auto-activate tab on focus. |
| id | string | - | Unique ID for ARIA association. |
| aria-label | string | - | Accessible label for tab list. |
| aria-labelledby | string | - | ID of labeling element. |
| testID | string | - | Test identifier. |
TabItemType:
interface TabItemType {
id: string; // Unique tab identifier
label: string; // Display label
icon?: ReactNode; // Optional icon
counter?: string | number; // Optional counter badge
badge?: boolean | string | number; // Optional badge
disabled?: boolean; // Whether tab is disabled
"aria-label"?: string; // Custom accessible label
}TabPanel
Container for tab content.
TabPanel Props:
| Prop | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
| id | string | - | Required. Matches corresponding tab.id. |
| tabsId | string | - | Required. Parent Tabs component id. |
| hidden | boolean | - | Whether panel is hidden. |
| children | ReactNode | - | Panel content. |
| aria-label | string | - | Accessible label. |
| testID | string | - | Test identifier. |
Keyboard Navigation
| Key | Action | | :-- | :----- | | Arrow Right/Down | Move to next enabled tab | | Arrow Left/Up | Move to previous enabled tab | | Home | Jump to first enabled tab | | End | Jump to last enabled tab | | Enter/Space | Activate tab (when activateOnFocus is false) |
Theming
Tabs uses the design system theme for colors:
Line Variant
// Colors accessed via theme
theme.colors.content.primary // Active tab text
theme.colors.content.tertiary // Inactive tab text
theme.colors.border.primary // Active tab indicator (bottom border)
theme.colors.border.secondary // Container bottom border
theme.colors.overlay.mono // Hover backgroundSegmented Variant
// Colors accessed via theme
theme.colors.control.segmented.bg // Container background
theme.colors.control.segmented.bgHover // Tab hover background
theme.colors.control.segmented.bgActive // Active tab indicator background
theme.colors.control.segmented.textActive // Active tab text color
theme.colors.control.text.primary // Inactive tab text
theme.colors.control.text.disable // Disabled tab textAccessibility
- Implements WAI-ARIA tablist pattern
- Uses
role="tablist",role="tab",role="tabpanel" aria-selectedindicates active tabaria-controlslinks tabs to panelsaria-labelledbylinks panels to tabs- Full keyboard navigation support
- Focus management follows ARIA best practices
- Segmented variant sliding indicator is marked with
aria-hidden
Platform Support
Both variants support web and React Native platforms:
- Web: Segmented variant includes smooth sliding animation for the active indicator
- React Native: Falls back to static background without animation
