TabNav
Links styled as tabs, for views that are URLs (status filters, sub-pages). A navigation landmark of plain links with aria-current="page" on the current one, server-rendered with no JavaScript; a router's link goes in through render. In-page views with panels are Tabs.
Import
import { TabNav } from '@nizivo/react/tab-nav';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/tab-nav.css';Static or interactive
Server. It ships no client JavaScript when a Server Component renders it, in every Next.js build
mode; its code is bundled only when a client component (a 'use client' file) imports it. Its links
are plain <a> elements. A router link passed through render is the router's own code: on the
Next.js fixture's /tab-nav page, three links through next/link load no Nizivo module and 3,302 B
(gzip, Turbopack) of Next's. It is the static alternative to Tabs when every view is a URL. How to
choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
Examples
Basic
Status filters that are URLs: each one is a link, the current one is aria-current="page". It is server-rendered and needs no JavaScript; Tab moves between the links.
import type { JSX } from 'react';
import { TabNav } from '@nizivo/react/tab-nav';
const STATUSES = [
['all', 'All orders'],
['unfulfilled', 'Unfulfilled'],
['unpaid', 'Unpaid'],
['open', 'Open'],
['archived', 'Archived'],
] as const;
export function TabNavExample(): JSX.Element {
const status: string = 'unfulfilled';
return (
<TabNav.Root aria-label="Order status">
{STATUSES.map(([value, label]) => (
<TabNav.Link key={value} href={`?status=${value}`} current={value === status}>
{label}
</TabNav.Link>
))}
</TabNav.Root>
);
}
Router
A router's link goes in through render; a count follows the label.
import type { ComponentPropsWithRef, JSX } from 'react';
import { TabNav } from '@nizivo/react/tab-nav';
/** Stands in for a router link (Next `Link`, React Router `Link`). */
function Link({ children, ...props }: ComponentPropsWithRef<'a'>): JSX.Element {
return <a {...props}>{children}</a>;
}
export function TabNavRouterExample(): JSX.Element {
return (
<TabNav.Root aria-label="Customers">
<TabNav.Link render={<Link href="?segment=vip" />} count={24} current>
VIP
</TabNav.Link>
<TabNav.Link render={<Link href="?segment=all" />} count="1,280">
All
</TabNav.Link>
<TabNav.Link render={<Link href="?segment=subscribers" />} count={311}>
Subscribers
</TabNav.Link>
<TabNav.Link render={<Link href="?segment=at-risk" />} count={9}>
At risk
</TabNav.Link>
</TabNav.Root>
);
}
Parts
Use the parts through the namespace (TabNav.Root) or by their flat names (TabNavRoot); both work from Server Components.
TabNav.Root
Server component Renders data-slot="tab-nav" with the class nz-tab-nav. Props: TabNavRootProps. Source: src/tab-nav/tab-nav.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
aria-label | string | – | Names the landmark ("Order status"). A page with more than one navigation landmark needs a name on each. |
render | RenderProp<Record<string, never>> | – | Must render a navigation landmark (a |
- Also takes every attribute of
<nav>;aria-labelis redeclared above. (ComponentPropsWithRef<"nav">)
render takes an element, or, in client code, a function of the props and no state.
TabNav.Link
Server component Renders data-slot="tab-nav-link" with the class nz-tab-nav-link. Props: TabNavLinkProps. Source: src/tab-nav/tab-nav.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | |
count | ReactNode | – | A count after the label, such as the number of orders in a status. |
current | boolean | false | The link to the page being shown: sets |
icon | ReactNode | – | A decorative glyph before the label (16 px). |
render | RenderProp<TabNavLinkState> | – | The router's link, for example |
- Also takes every attribute of
<a>exceptchildren. (Omit<ComponentPropsWithRef<"a">, "children">)
render takes an element, or, in client code, a function of the props and this state (TabNavLinkState):
| Name | Type | Default | Description |
|---|---|---|---|
currentrequired | boolean | – |
Other exports
TabNavLinkPropstypeimport type { TabNavLinkProps } from '@nizivo/react/tab-nav'The props of
TabNav.Link, listed above.TabNavLinkStatetypeimport type { TabNavLinkState } from '@nizivo/react/tab-nav'The render state of
TabNav.Link, listed above.TabNavRootPropstypeimport type { TabNavRootProps } from '@nizivo/react/tab-nav'The props of
TabNav.Root, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
tab-nav.nz-tab-nav | TabNav.Rootroot (takes render) | none |
tab-nav-count.nz-tab-nav-count | TabNav.Linkinner element (CSS only) | none |
tab-nav-icon.nz-tab-nav-icon | TabNav.Linkinner element (CSS only) | none |
tab-nav-label.nz-tab-nav-label | TabNav.Linkinner element (CSS only) | none |
tab-nav-link.nz-tab-nav-link | TabNav.Linkroot (takes render); interactive, focus: self; forced colours: fill on [aria-current='page']::before |
|
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-tab-nav-border | --nz-border-subtle |
|
--nz-tab-nav-count-bg | --nz-bg-subtle |
|
--nz-tab-nav-fg | --nz-text-secondary |
|
--nz-tab-nav-fg-current | --nz-text-primary |
|
--nz-tab-nav-indicator | --nz-bg-brand |
|
Layers: nz.components. Classes: .nz-tab-nav, .nz-tab-nav-count, .nz-tab-nav-icon, .nz-tab-nav-label, .nz-tab-nav-link, .nz-tab-nav-sizer, .nz-tab-nav-text.
Tokens this stylesheet reads (23)
--nz-bg-brand, --nz-bg-subtle, --nz-border-strong, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-font-label-m-strong-font-weight, --nz-font-label-s, --nz-radius-full, --nz-size-control-md, --nz-size-icon-sm, --nz-space-0-5, --nz-space-1, --nz-space-1-5, --nz-space-3, --nz-text-primary, --nz-text-secondary, --nz-tracking-label-m, --nz-tracking-label-m-strong, --nz-tracking-label-s
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
tab-nav-link: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: tab-nav-link[aria-current='page']::before (fill).
Keyboard
| Keys | Action |
|---|---|
| Tab, Shift+Tab | Moves between the links, as on any link. |
| Enter | Follows the focused link. |
Source: native <a> elements in a <nav> (tab-nav.tsx, no client code). The browser lane (test/browser/tabs.spec.tsx) checks Tab between links and the focus outline.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: none
- Ghost masters: none
Switch
An on or off setting that applies at once, drawn as a track and thumb. A native checkbox with role switch, so it submits with the form before hydration.
Table
A native table in server-rendered parts, for orders, products and customers. Rows can link to a detail page as a whole, columns sort through the URL, and number columns line up. No client JS.