Route Tabs
Tabs whose panels are pages — a named nav of links drawn as pill tabs, one URL per tab, current at once on click with a pending bar while the page loads.
- Status
- Since
0.25.89- Accessibility pattern
- nav landmark; aria-current; aria-busy while loading
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Route Tabs before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify route-tabsInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/route-tabsRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/tabs.
Usage
"use client"; // app/connections/connections-tabs.tsx — it reads the path
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
RouteTabs,
RouteTabsPanel,
RouteTabsProvider,
} from "@/components/ui/route-tabs";
export function ConnectionsFrame({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<RouteTabsProvider>
<RouteTabs
label="Connections"
currentHref={pathname}
linkRender={<Link href="" scroll={false} />}
tabs={[
{ href: "/connections", label: "Your connections" },
{ href: "/connections/everyone", label: "Everyone", count: 12 },
{ href: "/connections/activity", label: "Activity" },
]}
/>
<RouteTabsPanel>{children}</RouteTabsPanel>
</RouteTabsProvider>
);
}When each tab is its own URL, the tabs are navigation, not a Tabs widget: a nav of links that
look like the pill tabs. Put them in the section's layout so a switch swaps only the page below.
A clicked tab is current at once and carries a thin pending bar until the router's path moves, and
the page below dims meanwhile — so a slow page never leaves the previous tab looking chosen with
its old content (or its empty state) on screen.
Scope
- Owns: the strip's look (Tabs' own recipes), the current-tab match (
hrefor analsopath), the optimistic current tab and its pending bar, counts, the sideways scroll that keeps the current tab in view, and the dimmed panel. - Does not own: routing — links render through
linkRender, the host passescurrentHref(and, if it knows better,pendingHref). - Compose with:
PageHeader's tabs slot, or a section layout;RouteTabsProviderjoins the tabs and the panel when they sit apart.
Anatomy
Examples
Page tabs
Choose a tab: it is current at once, the pending bar runs under it, and the page below dims until
the new page arrives (the demo waits 1.2s). A count follows the label in the muted ink; an also
path — a record's own page under the tab — keeps the tab current.
A long strip
overflow="scroll" lets the strip scroll sideways when it outgrows its column, with edge fades,
and keeps the current tab in view — counts arriving after the first paint included.
API Reference
RouteTabs
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | The nav landmark's accessible name ("Connections"). |
tabs* | readonly RouteTab[] | — | The tabs, in order. |
currentHref | string | null | — | The page being shown — the router's pathname. The tab whose href (or one of whose also
paths) equals it is current. |
formatCount | ((count: number) => string) | (n) => n.toLocaleString("en") | Formats a tab's count. |
linkRender | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | <a /> | The element each tab renders as — your router's link (<Link scroll={false} />). The tab's
href, classes, aria-current, data attributes and children are merged into it; its own
onClick still runs. |
onPendingHrefChange | ((href: string | null) => void) | — | Fired when a tab starts loading (its href) and when loading ends or is cancelled (null).
With pendingHref controlled it carries the click's request — pass it back as pendingHref,
and clear it yourself once the router lands. |
overflow | "scroll" | "visible" | "visible" | scroll lets a long strip scroll sideways, keeping the current tab in view; visible never
clips. |
pendingHref | string | null | — | The tab whose page is loading. Omitted, the tabs track it themselves: a click marks its tab
until currentHref changes. Pass it (with onPendingHrefChange) to drive it from the router. |
Data attributes and CSS variables on RouteTabs
| Attribute | Values |
|---|---|
data-orientation | "horizontal" |
data-overflow | mirrors a prop or state value |
data-pending | "" |
data-slot | "route-tabs" | "route-tabs-pending" | "tabs-list" |
data-variant | "default" |
Each tab:
| Prop | Type | Default | Description |
|---|---|---|---|
href* | string | — | The page this tab opens. |
label* | React.ReactNode | — | The tab's text. |
also | readonly string[] | — | Other paths that show this tab as current (a record's dialog route, a nested page). |
aria-label | string | — | The link's accessible name when the visible text is not enough ("Tasks, 3 open"). |
count | number | null | — | A muted tabular-nums count after the label (null or omitted: none). |
RouteTabsPanel
| Prop | Type | Default | Description |
|---|---|---|---|
pending | boolean | — | The page is being replaced: it dims and is marked busy. Omitted, it follows the nearest
RouteTabsProvider. |
Data attributes and CSS variables on RouteTabsPanel
| Attribute | Values |
|---|---|
data-pending | "" |
data-slot | "route-tabs-panel" |
RouteTabsProvider
Holds the loading tab for the RouteTabs and RouteTabsPanel below it; it takes only children.
Accessibility
- The strip is a
navlandmark named bylabel; the current tab's link carriesaria-current="page", and a tab's ownaria-labelreplaces its visible name when that is not enough ("Tasks, 3 open"). - While a page loads, the nav and the panel are
aria-busy; the pending bar is decorative. - Modified clicks (⌘, Ctrl, Shift, Alt) open the link as the browser would and start nothing.
| Key | Action |
|---|---|
| Tab | Moves between the tab links |
| Enter | Opens the focused tab's page |
| Contract | States tested |
|---|---|
| Behaviour | default, current-page, also-match, count, pending, controlled-pending, panel-pending, overflow-scroll |
| Accessibility | landmark, labeled, current-page, busy, browser-accessibility-test |
| Visual | default, hover, current, pending, dimmed-panel, scroll |