Tabs
Switch between content views. Underline (default), pills, vertical, and justified variants.
Overview
The Tabs component is a core building block of the Byteion design system, providing a robust, accessibility-compliant interface for its specific UI capabilities.
Why this exists
Tabs organize related content within a page, reducing scroll and cognitive load. They are the standard pattern for switching between related views.
Mental model
Tabs are mutually exclusive content panels within the same context. Selecting a tab replaces the content area without navigating away.
When to use
When not to use
Basic example
The following example demonstrates standard usage, fully compliant with Byteion's token-first approach.
Underline
Pills
With badge
Vertical
Account
Update your account information.
Security
Manage your password and 2FA.
Notifications
Configure how you get notified.
import { Tabs, TabList, Tab, TabPanel } from '@byteion/react/client';
export default function TabsDemo() {
return (
<div className="space-y-10 p-6">
{/* Underline (Default) */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Underline</h3>
<Tabs defaultValue="home" variant="underline">
<TabList>
<Tab value="home">Home</Tab>
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
<TabPanel value="home" className="text-sm text-muted-foreground dark:text-muted-foreground">Home content</TabPanel>
<TabPanel value="profile" className="text-sm text-muted-foreground dark:text-muted-foreground">Profile content</TabPanel>
<TabPanel value="settings" className="text-sm text-muted-foreground dark:text-muted-foreground">Settings content</TabPanel>
</Tabs>
</div>
{/* Pills */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Pills</h3>
<Tabs defaultValue="overview" variant="pills">
<TabList>
<Tab value="overview">Overview</Tab>
<Tab value="analytics">Analytics</Tab>
<Tab value="reports">Reports</Tab>
</TabList>
<TabPanel value="overview" className="text-sm text-muted-foreground dark:text-muted-foreground">Overview content</TabPanel>
<TabPanel value="analytics" className="text-sm text-muted-foreground dark:text-muted-foreground">Analytics content</TabPanel>
<TabPanel value="reports" className="text-sm text-muted-foreground dark:text-muted-foreground">Reports content</TabPanel>
</Tabs>
</div>
{/* With Badge */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">With badge</h3>
<Tabs defaultValue="inbox" variant="underline">
<TabList>
<Tab value="inbox">
<i className="ti ti-inbox text-base mr-2" />
Inbox
<span className="ml-2 rounded-md bg-primary-100 px-1.5 text-xs font-semibold text-primary-700 dark:bg-primary-500/20 dark:text-primary-300">12</span>
</Tab>
<Tab value="archived">
<i className="ti ti-archive text-base mr-2" />
Archived
</Tab>
<Tab value="trash">
<i className="ti ti-trash text-base mr-2" />
Trash
</Tab>
</TabList>
<TabPanel value="inbox" className="text-sm text-muted-foreground dark:text-muted-foreground">Inbox messages</TabPanel>
<TabPanel value="archived" className="text-sm text-muted-foreground dark:text-muted-foreground">Archived</TabPanel>
<TabPanel value="trash" className="text-sm text-muted-foreground dark:text-muted-foreground">Trash</TabPanel>
</Tabs>
</div>
{/* Vertical */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Vertical</h3>
<Tabs defaultValue="account" variant="vertical">
<TabList>
<Tab value="account">
<i className="ti ti-user text-base mr-2.5" />
Account
</Tab>
<Tab value="security">
<i className="ti ti-lock text-base mr-2.5" />
Security
</Tab>
<Tab value="notifications">
<i className="ti ti-bell text-base mr-2.5" />
Notifications
</Tab>
</TabList>
<div className="min-w-0 flex-1 rounded-xl border border-border bg-card p-5 shadow-card">
<TabPanel value="account">
<h3 className="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Account</h3>
<p className="text-sm text-muted-foreground dark:text-muted-foreground">Update your account information.</p>
</TabPanel>
<TabPanel value="security">
<h3 className="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Security</h3>
<p className="text-sm text-muted-foreground dark:text-muted-foreground">Manage your password and 2FA.</p>
</TabPanel>
<TabPanel value="notifications">
<h3 className="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Notifications</h3>
<p className="text-sm text-muted-foreground dark:text-muted-foreground">Configure how you get notified.</p>
</TabPanel>
</div>
</Tabs>
</div>
</div>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { Tabs, TabList, Tab, TabPanel } from '@byteion/vue';
const active1 = ref('home');
const active2 = ref('overview');
const active3 = ref('inbox');
const active4 = ref('account');
</script>
<template>
<div class="space-y-10 p-6">
<!-- Underline -->
<div>
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Underline</h3>
<Tabs v-model="active1" variant="underline">
<TabList>
<Tab value="home">Home</Tab>
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
<TabPanel value="home" class="text-sm text-muted-foreground dark:text-muted-foreground">Home content</TabPanel>
<TabPanel value="profile" class="text-sm text-muted-foreground dark:text-muted-foreground">Profile content</TabPanel>
<TabPanel value="settings" class="text-sm text-muted-foreground dark:text-muted-foreground">Settings content</TabPanel>
</Tabs>
</div>
<!-- Pills -->
<div>
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Pills</h3>
<Tabs v-model="active2" variant="pills">
<TabList>
<Tab value="overview">Overview</Tab>
<Tab value="analytics">Analytics</Tab>
<Tab value="reports">Reports</Tab>
</TabList>
<TabPanel value="overview" class="text-sm text-muted-foreground dark:text-muted-foreground">Overview content</TabPanel>
<TabPanel value="analytics" class="text-sm text-muted-foreground dark:text-muted-foreground">Analytics content</TabPanel>
<TabPanel value="reports" class="text-sm text-muted-foreground dark:text-muted-foreground">Reports content</TabPanel>
</Tabs>
</div>
<!-- With Badge -->
<div>
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">With badge</h3>
<Tabs v-model="active3" variant="underline">
<TabList>
<Tab value="inbox">
<i class="ti ti-inbox text-base mr-2" />
Inbox
<span class="ml-2 rounded-md bg-primary-100 px-1.5 text-xs font-semibold text-primary-700 dark:bg-primary-500/20 dark:text-primary-300">12</span>
</Tab>
<Tab value="archived">
<i class="ti ti-archive text-base mr-2" />
Archived
</Tab>
<Tab value="trash">
<i class="ti ti-trash text-base mr-2" />
Trash
</Tab>
</TabList>
<TabPanel value="inbox" class="text-sm text-muted-foreground dark:text-muted-foreground">Inbox messages</TabPanel>
<TabPanel value="archived" class="text-sm text-muted-foreground dark:text-muted-foreground">Archived</TabPanel>
<TabPanel value="trash" class="text-sm text-muted-foreground dark:text-muted-foreground">Trash</TabPanel>
</Tabs>
</div>
<!-- Vertical -->
<div>
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-background0">Vertical</h3>
<Tabs v-model="active4" variant="vertical">
<TabList>
<Tab value="account">
<i class="ti ti-user text-base mr-2.5" />
Account
</Tab>
<Tab value="security">
<i class="ti ti-lock text-base mr-2.5" />
Security
</Tab>
<Tab value="notifications">
<i class="ti ti-bell text-base mr-2.5" />
Notifications
</Tab>
</TabList>
<div class="min-w-0 flex-1 rounded-xl border border-border bg-card p-5 shadow-card">
<TabPanel value="account">
<h3 class="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Account</h3>
<p class="text-sm text-muted-foreground dark:text-muted-foreground">Update your account information.</p>
</TabPanel>
<TabPanel value="security">
<h3 class="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Security</h3>
<p class="text-sm text-muted-foreground dark:text-muted-foreground">Manage your password and 2FA.</p>
</TabPanel>
<TabPanel value="notifications">
<h3 class="mb-2 text-sm font-semibold text-foreground dark:text-foreground">Notifications</h3>
<p class="text-sm text-muted-foreground dark:text-muted-foreground">Configure how you get notified.</p>
</TabPanel>
</div>
</Tabs>
</div>
</div>
</template>
Variants
Each variant communicates a different meaning. Choose based on priority, not aesthetics.
- underline — underline style variant.
- pills — pills style variant.
- vertical — vertical style variant.
- justified — justified style variant.
Sizes
Size should match the importance and context of the component.
- sm — sm size.
- md — md size.
- lg — lg size.
States
This component reacts seamlessly to the following interaction and data states:
- default — Resting appearance before interaction.
- hover — Pointer is over the element — visual feedback via color or shadow.
- focus-visible — focus-visible state.
- active — Currently selected or in use.
- disabled — Non-interactive — grayed out, cursor not-allowed.
- inactive — Not selected.
Anatomy
Usage guidelines
- Use underline for page-level tabs, pill for settings.
- Always have one tab active.
- Keep tab labels under 2 words.
Do and Don't
Do
- Always have one tab active.
- Keep labels short.
- Use underline or pill style consistently.
Don't
- Do not use for page navigation.
- Do not leave all tabs unselected.
- Do not exceed 7 tabs.
Accessibility
Tab list has role="tablist". Tabs have role="tab" with aria-selected. Panels have role="tabpanel". Arrow keys navigate between tabs.
Role:tablist
ARIA Attributes
aria-selectedaria-controlsaria-labelledbyaria-orientation
Keyboard Navigation
- Tab: Moves focus into the tablist
- Arrow Left / Right: Switch tabs (horizontal)
- Arrow Up / Down: Switch tabs (vertical variant)
- Home / End: Jump to first / last tab
- Enter / Space: Activate the focused tab
Customization
Active indicator color follows --primary.
Compose with React cva()
Use the exportedcva() +cn() utilities to compose your own variant on top of the Byteion tokens.
import { cva, cn, type VariantProps } from '@byteion/react';
const myTabs = cva('base classes…', {
variants: {
accent: { soft:'border-primary-200', loud:'border-primary-500' },
},
defaultVariants: { accent:'soft' },
});
type Props = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof myTabs>;
Compose with Vue cva()
Use the exportedcva() +cn() utilities inside standard Vue setup blocks.
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import { cva, type VariantProps } from '@byteion/vue';
interface Props { accent?:'soft' |'loud' }
const props = withDefaults(defineProps<Props>(), { accent:'soft' });
const attrs = useAttrs();
const myTabs = cva('base classes…', {
variants: { accent: { soft:'border-primary-200', loud:'border-primary-500' } },
});
const cls = computed(() => myTabs({ accent: props.accent, class: attrs.class as string | undefined }));
</script>
Runtime events
Byteion keeps framework APIs idiomatic: HTML uses DOM custom events, React uses callback props, and Vue uses emitted events / v-model updates.
HTML / JS events
bi:tabs:hidebi:tabs:show
React callbacks
onValueChange(value: string) => void
Vue emits
update:modelValue[v: string]
Remote / AJAX
This component does not implement Byteion JS remote loading. Use normal HTML form submission, framework state, or your application data layer around it.
API Reference
Requires data-bi-component="tabs" for JS behavior.
HTML Data Attributes
data-bi-tabsdata-bi-tabs-toggledata-bi-tabs-panel
React Props · TabsProps
Extracted directly fromreact/src/components/Tabs.tsx.
export interface TabsProps {
/** Currently active tab value. Pass `value` + `onValueChange` for controlled. */
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
variant?: 'underline' | 'pills' | 'vertical' | 'justified';
tone?: 'neutral' | 'primary';
size?: 'sm' | 'md' | 'lg';
className?: string;
children?: ReactNode;
}
Vue Props · TabsProps
Extracted directly fromvue/src/components/Tabs.vue.
export interface TabsProps {
modelValue?: string; // controlled (v-model)
defaultValue?: string; // uncontrolled initial
variant?: 'underline' | 'pills' | 'vertical' | 'justified';
tone?: 'neutral' | 'primary';
size?: 'sm' | 'md' | 'lg';
}
Developer tips
Common mistakes
- Using tabs for global page navigation — use Navbar or Sidebar instead.
- Too many tabs (7+) — users stop scanning after 5–7 items.
- Unrelated tab content that belongs on separate pages.
- No active tab — always ensure one tab is selected.
- Losing unsaved form state when switching tabs — store state externally.
- Disabled tab without explanation — disable with
disabledand add a tooltip or note. - Missing
aria-controls/aria-labelledbyrelationships in custom HTML. - Tab labels wrapping awkwardly — keep labels short and ensure adequate width.
- Using raw colors (
border-primary-600,bg-primary-50) instead of semantic tokens. - Documenting a "justified" variant that is not supported by React or Vue.
Production usage tips
- Test keyboard navigation — ArrowLeft/Right (horizontal), ArrowUp/Down (vertical), Home/End.
- Test active tab state in both controlled (
value/v-model) and uncontrolled (defaultValue) modes. - Test form state preservation when switching tabs — values should not reset unless intended.
- Test mobile overflow — horizontal tabs should scroll or collapse to an accordion on small screens.
- Test URL sync if tabs represent shareable views — update query params and restore on page load.
- Test dark mode and branded themes — token-based colors should adapt automatically.
- Verify variant support — only underline, pills, and vertical are available in React and Vue. Justified is not implemented.
Premium examples
Advanced patterns available in Byteion Pro:
Related components
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced data-tables built with this component.
Explore Pro Components