Skip to content
Material 3 Expressivev1.2.2
navigation · conformant

Tabs

TabsTabItemTabsPropsTabsVariant

Tabs

Roving-focus tabs with a sliding indicator, icon+label rows, and panels switched in place.

Your photo library.

Downloads

Your home feed.

playground/examples/Tabs.example.tsx

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-label or aria-labelledby is required to name the role="tablist" region, the same required-naming contract every other group-role component already has.
  • value/defaultValue/onValueChange follow the same controlled/ uncontrolled shape as every other stateful component. defaultValue falls 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, tinted primary. 'secondary': a full-width underline, tinted plain onSurface — deliberately more subdued, not brand-colored.
  • scrollable (default false) switches from an evenly distributed fixed row to a horizontally scrolling one that keeps the selected tab centered where the scroll range allows, the same behavior the source's ScrollableTabRow drives through its own scroll state. Only the row itself ever scrolls — selecting a tab never moves an ancestor scroll container.

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

KeyBehavior
ArrowLeft / ArrowRightMove focus and select, wrapping, skipping disabled tabs
Home / EndJump 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.