TabsNavigation
Navigate between sections or filtered views without tab-panel semantics
TabsNavigation shares its visual styles with Tabs, but the two components have different semantic contracts. Use Tabs when each trigger reveals one corresponding content panel. Use TabsNavigation for page sections or filters that update an independent view.
pnpm add @wandercom/design-system-web
Use TabsNavigation when controls move to sections on the current page or update a filtered view without replacing an associated content panel.
import { TabsNavigation } from '@wandercom/design-system-web/ui/tabs-navigation';
import { useState } from 'react';
export function PropertyTabsNavigation() {
const [activeSection, setActiveSection] = useState("photos");
return (
<TabsNavigation
aria-label="Property sections"
items={[
{ label: "Photos", value: "photos" },
{ label: "About", value: "about" },
{ label: "Amenities", value: "amenities" },
]}
value={activeSection}
onValueChange={(value) => {
setActiveSection(value);
document.getElementById(value)?.scrollIntoView({
behavior: "smooth",
});
}}
variant="underline"
bordered
/>
);
}Visual appearance does not determine semantics. Tabs uses the WAI-ARIA Tabs keyboard model, where arrow keys move through a single tab stop. TabsNavigation renders ordinary buttons, so every enabled item remains in the normal sequential tab order.
The default variant uses pill-shaped buttons. The underline variant uses a moving active indicator and supports a full-width bottom border with bordered.
<TabsNavigation
aria-label="Review filters"
items={[
{ label: "All", value: "all" },
{ label: "Published", value: "published" },
{ label: "Drafts", value: "drafts", disabled: true },
]}
value={filter}
onValueChange={setFilter}
variant="default"
size="sm"
/>aria-label
items
value
onValueChange
variant?
size?
bordered?
className?
listRef?
classNames?
- Renders a labeled
<nav>landmark containing native buttons. - Marks only the active item with
aria-current="location". - Does not emit
tablist,tab, ortabpanelroles. - Keeps every enabled item in normal DOM-order keyboard navigation; disabled items use the native
disabledattribute. - Activating the current item calls
onValueChangeagain so scroll and navigation integrations can repeat their action. - Uses the design system focus ring and AA-compliant inactive text color in both light and dark themes.
- The underline transition respects
prefers-reduced-motion. - Item labels accept
ReactNode, so phrases in another language can provide alangattribute on their ownspan.
The high-level panel-less Tabs API emits tab semantics without corresponding panels. Replace that usage with TabsNavigation:
// Before: legacy panel-less tab semantics
<Tabs
classNames={{ trigger: "font-medium" }}
items={items}
onChange={setFilter}
value={filter}
/>
// After: navigation and filtering semantics
<TabsNavigation
aria-label="Reservation filters"
classNames={{ item: "font-medium" }}
items={items}
onValueChange={setFilter}
value={filter}
/>Do not migrate compound Tabs that already pair every TabsTrigger with a corresponding TabsContent. That is the correct Tabs pattern.