Tabs
Tabs
Roving-focus tabs with a sliding indicator, icon+label rows, and panels switched in place.
Your photo library.
Your home feed.
import { Icon, Surface, Tabs, Text } from '@language-lit/material3-expressive'
export function TabsExample() {
return (
<Surface
as="section"
aria-labelledby="tabs-example-title"
color="surface-container-low"
shape="extra-large"
className="tabs-example"
>
<Text as="h2" id="tabs-example-title" variant="titleLarge" emphasis="emphasized">
Tabs
</Text>
<Text as="p" variant="bodyMedium">
Roving-focus tabs with a sliding indicator, icon+label rows, and
panels switched in place.
</Text>
<Tabs
aria-label="Library sections"
items={[
{
value: 'photos',
label: 'Photos',
icon: <Icon source="photo" />,
panel: <Text as="p" variant="bodyMedium">Your photo library.</Text>,
},
{
value: 'shared',
label: 'Shared',
icon: <Icon source="folder_shared" />,
panel: <Text as="p" variant="bodyMedium">Albums shared with you.</Text>,
},
{
value: 'trash',
label: 'Trash',
icon: <Icon source="delete" />,
disabled: true,
panel: <Text as="p" variant="bodyMedium">Recently deleted items.</Text>,
},
]}
/>
<Tabs
aria-label="Secondary section"
variant="secondary"
defaultValue="overview"
items={[
{ value: 'overview', label: 'Overview' },
{ value: 'activity', label: 'Activity' },
{ value: 'settings', label: 'Settings' },
{ value: 'archived', label: 'Archived', disabled: true },
]}
/>
<Tabs
aria-label="Scrollable section"
scrollable
items={[
{
value: 'home',
label: 'Home',
icon: <Icon source="home" />,
panel: <Text as="p" variant="bodyMedium">Your home feed.</Text>,
},
{
value: 'explore',
label: 'Explore',
icon: <Icon source="explore" />,
panel: <Text as="p" variant="bodyMedium">Discover new content.</Text>,
},
{
value: 'notifications',
icon: <Icon source="notifications" />,
panel: <Text as="p" variant="bodyMedium">Icon-only tab: no label, just an icon.</Text>,
},
{
value: 'messages',
label: 'Messages',
icon: <Icon source="mail" />,
panel: <Text as="p" variant="bodyMedium">Your conversations.</Text>,
},
{
value: 'favorites',
label: 'Favorites',
icon: <Icon source="star" />,
panel: <Text as="p" variant="bodyMedium">Items you starred.</Text>,
},
{
value: 'downloads',
label: 'Downloads',
icon: <Icon source="download" />,
href: '#downloads',
},
{
value: 'archive',
label: 'Archive',
icon: <Icon source="archive" />,
panel: <Text as="p" variant="bodyMedium">Archived items.</Text>,
},
{
value: 'settings-2',
label: 'Settings',
icon: <Icon source="settings" />,
panel: <Text as="p" variant="bodyMedium">App preferences.</Text>,
},
]}
/>
</Surface>
)
}Tabs renders a roving-focus role="tablist" with a sliding selection
indicator. It is fully data-driven: content, panels, and even link-based
navigation tabs are all declared through items.
import { Icon, Tabs } from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
<Tabs
aria-label="Library sections"
items={[
{ value: 'photos', label: 'Photos', icon: <Icon source="photo" />, panel: <PhotoGrid /> },
{ value: 'shared', label: 'Shared', icon: <Icon source="folder_shared" />, panel: <SharedList /> },
]}
/>Contract
items: readonly TabItem[]describes the tab list's content:{ value, label?, icon?, disabled?, href?, panel? }.aria-labeloraria-labelledbyis required to name therole="tablist"region, the same required-naming contract every other group-role component already has.value/defaultValue/onValueChangefollow the same controlled/ uncontrolled shape as every other stateful component.defaultValuefalls back to the first item's value when omitted.variant(default'primary'): a short, rounded indicator that hugs the selected tab's own content width, tintedprimary.'secondary': a full-width underline, tinted plainonSurface— deliberately more subdued, not brand-colored.scrollable(defaultfalse) switches from an evenly distributed fixed row to a horizontally scrolling one that keeps the selected tab in view.
Link-safe navigation tabs
An item with href renders a real <a role="tab" href> instead of
<button role="tab">. Arrow-key movement still updates the local
selected/indicator state, but actual navigation is left entirely to the
browser's native anchor behavior — Tabs never synthesizes a navigation
from a keypress. This lets you drive Tabs from a router's current route:
<Tabs
aria-label="Settings"
value={router.pathname}
items={[
{ value: '/settings/profile', label: 'Profile', href: '/settings/profile' },
{ value: '/settings/billing', label: 'Billing', href: '/settings/billing' },
]}
/>Panels
An item with panel gets one role="tabpanel" region for the selected
item only, correctly associated via id/aria-controls/
aria-labelledby. If no item defines panel — a pure link-tabs usage —
Tabs renders no tabpanel region at all.
Keyboard
| Key | Behavior |
|---|---|
| ArrowLeft / ArrowRight | Move focus and select, wrapping, skipping disabled tabs |
| Home / End | Jump to and select the first/last enabled tab |
Tokens and boundaries
All color, geometry, and motion values live in one --m3e-comp-tabs-*
registration. Theme overrides remain scoped to Material3Provider; Tabs
injects no runtime styles. It imports no Next.js, Vite,
router, animation library, or private application code.