MobileTabBar
Five destinations at the bottom of the screen below 640 px, in place of the Sidebar. They are links in a navigation landmark, and More opens the rest in a bottom sheet.
Import
import { MobileTabBar, MobileTabBarMore } from '@nizivo/react/mobile-tab-bar';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/drawer.css and @nizivo/react/css/icon-button.css
import '@nizivo/react/css/mobile-tab-bar.css';Static or interactive
Split. MobileTabBar.Root and MobileTabBar.Item render on the server. Two client parts come with
them: a small island in the root, which writes the bar's height to the page (below), and
MobileTabBarMore, which brings the Drawer for its sheet. 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.
Destinations, not tabs
Each destination is a page, so the bar is a <nav> named "Primary" around a list of links, not a
tablist. The page says which one is current (aria-current="page", the filled pill). Four
destinations and More fit a 390 px screen. A count shows on the icon's corner and is part of the
link's text, so write it to read well after the label ("12 open").
More
MobileTabBarMore is the fifth cell: a button that opens a bottom sheet, titled "More"
(labels.more, or heading), holding the destinations without a tab, usually NavItems. The sheet
is modal: focus moves in and stays there, Escape or a press outside closes it, and focus returns to
More. Following a link in it closes it, as a route change does. When the page shown is one of the
sheet's, pass current to More: the pill fills, and the sheet's own link carries aria-current.
The bottom inset
While the bar is laid out, its island writes the bar's height to --nz-layout-bottom-inset on
<html> and sets data-nz-bottom-bar there, in a layout effect, so the first paint already has
them. StickyBar sticks above the bar, toasts rise above it, and AppShell's <main> leaves room for
it. When the bar is hidden (from 640 px in the shell) or unmounted, both are removed. The bar's
bottom padding includes the device's safe area (env(safe-area-inset-bottom), which needs
viewport-fit=cover in the viewport meta).
The bar is not fixed on its own: AppShell pins it to the bottom below 640 px.
Hooks: --nz-mobile-tab-bar-bg, --nz-mobile-tab-bar-border.
Examples
Basic
Figma's Mobile Tab Bar: four destinations and More, links in a <nav> (each is a page, so not a tablist). More opens the other destinations in a bottom sheet; following one closes it. Below 640 px AppShell pins the bar to the bottom of the screen.
import type { JSX, ReactNode } from 'react';
import { NavItem } from '@nizivo/react/nav-item';
import { MobileTabBar, MobileTabBarMore } from '@nizivo/react/mobile-tab-bar';
/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
);
}
const ICONS = {
home: (
<Glyph>
<path d="M3 10.5 12 3l9 7.5V21H3z" />
<path d="M9 21v-7h6v7" />
</Glyph>
),
orders: (
<Glyph>
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
</Glyph>
),
products: (
<Glyph>
<path d="m21 8-9-5-9 5v8l9 5 9-5z" />
<path d="m3 8 9 5 9-5M12 13v8" />
</Glyph>
),
customers: (
<Glyph>
<circle cx="9" cy="8" r="4" />
<path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
</Glyph>
),
analytics: (
<Glyph>
<path d="M3 3v18h18M8 17v-5M13 17V8M18 17v-9" />
</Glyph>
),
settings: (
<Glyph>
<circle cx="12" cy="12" r="3" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
</Glyph>
),
};
export function MobileTabBarExample(): JSX.Element {
return (
<MobileTabBar.Root style={{ inlineSize: 390 }}>
<MobileTabBar.Item href="/" icon={ICONS.home} current>
Home
</MobileTabBar.Item>
<MobileTabBar.Item href="/orders" icon={ICONS.orders} count={12}>
Orders
</MobileTabBar.Item>
<MobileTabBar.Item href="/products" icon={ICONS.products}>
Products
</MobileTabBar.Item>
<MobileTabBar.Item href="/customers" icon={ICONS.customers}>
Customers
</MobileTabBar.Item>
<MobileTabBarMore>
<NavItem href="/analytics" icon={ICONS.analytics}>
Analytics
</NavItem>
<NavItem href="/settings" icon={ICONS.settings}>
Settings
</NavItem>
</MobileTabBarMore>
</MobileTabBar.Root>
);
}
More Current
A page from the sheet is shown: More draws the filled indicator, the sheet's link is current.
import type { JSX, ReactNode } from 'react';
import { NavItem } from '@nizivo/react/nav-item';
import { MobileTabBar, MobileTabBarMore } from '@nizivo/react/mobile-tab-bar';
/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
);
}
const ICONS = {
home: (
<Glyph>
<path d="M3 10.5 12 3l9 7.5V21H3z" />
<path d="M9 21v-7h6v7" />
</Glyph>
),
orders: (
<Glyph>
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
</Glyph>
),
products: (
<Glyph>
<path d="m21 8-9-5-9 5v8l9 5 9-5z" />
<path d="m3 8 9 5 9-5M12 13v8" />
</Glyph>
),
customers: (
<Glyph>
<circle cx="9" cy="8" r="4" />
<path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
</Glyph>
),
analytics: (
<Glyph>
<path d="M3 3v18h18M8 17v-5M13 17V8M18 17v-9" />
</Glyph>
),
settings: (
<Glyph>
<circle cx="12" cy="12" r="3" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
</Glyph>
),
};
export function MobileTabBarMoreCurrentExample(): JSX.Element {
return (
<MobileTabBar.Root style={{ inlineSize: 390 }}>
<MobileTabBar.Item href="/" icon={ICONS.home}>
Home
</MobileTabBar.Item>
<MobileTabBar.Item href="/orders" icon={ICONS.orders}>
Orders
</MobileTabBar.Item>
<MobileTabBar.Item href="/products" icon={ICONS.products}>
Products
</MobileTabBar.Item>
<MobileTabBar.Item href="/customers" icon={ICONS.customers}>
Customers
</MobileTabBar.Item>
<MobileTabBarMore current>
<NavItem href="/analytics" icon={ICONS.analytics} current>
Analytics
</NavItem>
<NavItem href="/settings" icon={ICONS.settings}>
Settings
</NavItem>
</MobileTabBarMore>
</MobileTabBar.Root>
);
}
Props
Client component Renders data-slot="mobile-tab-bar-more" with the class nz-mobile-tab-bar-more. Props: MobileTabBarMoreProps. Source: src/mobile-tab-bar/mobile-tab-bar-more.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The sheet's content: the other destinations, usually |
current | boolean | false | The page being shown is one of the sheet's: draws the bar's filled indicator on More. It is a button, so nothing is announced; the sheet's link carries |
heading | ReactNode | – | The sheet's title, when it differs from the button's label. |
icon | ReactNode | – | The glyph in the bar. Defaults to the internal |
labels | Partial<MobileTabBarMoreLabels> | – |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
Parts
Use the parts through the namespace (MobileTabBar.Root) or by their flat names (MobileTabBarRoot); both work from Server Components.
MobileTabBar.Root
The <nav> and its list, with the bottom-inset island.
Server component Renders data-slot="mobile-tab-bar" with the class nz-mobile-tab-bar. Props: MobileTabBarRootProps. Source: src/mobile-tab-bar/mobile-tab-bar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The destinations: |
labels | Partial<MobileTabBarLabels> | – |
- Also takes every attribute of
<nav>;childrenis redeclared above. (ComponentPropsWithRef<"nav">)
MobileTabBar.Item
One destination: a list item around a link, with its icon in Figma's indicator pill and its label under it. Props other than icon, count and current go to the link.
Server component Renders data-slot="mobile-tab-bar-item" with the class nz-mobile-tab-bar-item. Props: MobileTabBarItemProps. Source: src/mobile-tab-bar/mobile-tab-bar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The label under the icon. |
count | ReactNode | – | A count on the icon's corner, such as open orders. It is part of the link's text, so it is announced with the label: write it so it reads well there ("12 open"). |
current | boolean | false | The page being shown: |
iconrequired | ReactNode | – | A decorative 20 px glyph in the indicator; it is |
render | RenderProp<MobileTabBarItemState> | – | 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 (MobileTabBarItemState):
| Name | Type | Default | Description |
|---|---|---|---|
currentrequired | boolean | – |
Other exports
MobileTabBarItemPropstypeimport type { MobileTabBarItemProps } from '@nizivo/react/mobile-tab-bar'The props of
MobileTabBar.Item, listed above.MobileTabBarItemStatetypeimport type { MobileTabBarItemState } from '@nizivo/react/mobile-tab-bar'The render state of
MobileTabBar.Item, listed above.MobileTabBarLabelstypeimport type { MobileTabBarLabels } from '@nizivo/react/mobile-tab-bar'Strings this part renders (§16.4).
Name Type Default Description navrequiredstring'Primary'The navigation landmark's name.
MobileTabBarMoreLabelstypeimport type { MobileTabBarMoreLabels } from '@nizivo/react/mobile-tab-bar'Strings this part renders (§16.4).
Name Type Default Description closerequiredstring'Close'The sheet's close button.
morerequiredstring'More'The button's label in the bar, and the sheet's title.
MobileTabBarMorePropstypeimport type { MobileTabBarMoreProps } from '@nizivo/react/mobile-tab-bar'The props of
MobileTabBarMore, listed above.MobileTabBarRootPropstypeimport type { MobileTabBarRootProps } from '@nizivo/react/mobile-tab-bar'The props of
MobileTabBar.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 |
|---|---|---|
mobile-tab-bar.nz-mobile-tab-bar | MobileTabBar.Rootroot (no render) | none |
mobile-tab-bar-count.nz-mobile-tab-bar-count | MobileTabBar.Iteminner element (CSS only) | none |
mobile-tab-bar-indicator.nz-mobile-tab-bar-indicator | MobileTabBar.Iteminner element (CSS only) | none |
mobile-tab-bar-item.nz-mobile-tab-bar-item | MobileTabBar.Itemroot (takes render) | none |
mobile-tab-bar-label.nz-mobile-tab-bar-label | MobileTabBar.Iteminner element (CSS only) | none |
mobile-tab-bar-link.nz-mobile-tab-bar-link | MobileTabBar.Iteminner element (CSS only); interactive, focus: self | none |
mobile-tab-bar-list.nz-mobile-tab-bar-list | MobileTabBar.Rootinner element (CSS only) | none |
mobile-tab-bar-more.nz-mobile-tab-bar-more | MobileTabBarMoreroot (no render) | none |
mobile-tab-bar-more-indicator.nz-mobile-tab-bar-more-indicator | MobileTabBarMoreinner element (CSS only) | none |
mobile-tab-bar-more-label.nz-mobile-tab-bar-more-label | MobileTabBarMoreinner element (CSS only) | none |
mobile-tab-bar-more-trigger.nz-mobile-tab-bar-more-trigger | MobileTabBarMoreinner element (CSS only); interactive, focus: self |
|
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-mobile-tab-bar-bg | --nz-bg-surface |
|
--nz-mobile-tab-bar-border | --nz-border-subtle |
|
Layers: nz.components. Classes: .nz-mobile-tab-bar, .nz-mobile-tab-bar-count, .nz-mobile-tab-bar-icon, .nz-mobile-tab-bar-indicator, .nz-mobile-tab-bar-item, .nz-mobile-tab-bar-label, .nz-mobile-tab-bar-link, .nz-mobile-tab-bar-list, .nz-mobile-tab-bar-more, .nz-mobile-tab-bar-more-indicator, .nz-mobile-tab-bar-more-label, .nz-mobile-tab-bar-more-sheet, .nz-mobile-tab-bar-more-trigger.
Tokens this stylesheet reads (29)
--nz-bg-brand, --nz-bg-brand-subtle, --nz-bg-muted, --nz-bg-pressed, --nz-bg-surface, --nz-border-subtle, --nz-border-width-default, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-font-label-m-strong-font-weight, --nz-font-label-s, --nz-icon-brand, --nz-icon-secondary, --nz-radius-full, --nz-radius-md, --nz-size-icon-md, --nz-space-1, --nz-space-1-5, --nz-space-16, --nz-space-2, --nz-space-4, --nz-space-5, --nz-text-brand, --nz-text-on-brand, --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:
mobile-tab-bar-link: draws the focus outline itself on keyboard focus.mobile-tab-bar-more-trigger: draws the focus outline itself on keyboard focus.
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Tab, Shift+Tab | Moves between the destinations and More, as on any link or button. |
| Enter | On a destination, follows its link. On More, opens the sheet and moves focus into it. |
| Space | On More, opens the sheet. |
| Escape | Closes the sheet and returns focus to More. |
Source: native <a> elements for the destinations; More is the Drawer's trigger and its sheet a bottom Drawer (react-aria-components 1.22.1 ModalOverlay and Modal), which contains focus while open. The browser lane (test/browser/app-shell.spec.tsx) checks opening, Escape with focus back on More, and a followed link closing the sheet, in Chromium, Firefox and WebKit.
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: Figma node
7275:18747 - Ghost masters: Figma node
764:3482
Metric
A label, a number and its change, the block Figma draws in both the KPI tile and the Revenue card. It takes the loading, empty and error contract, and keeps its box in all four states.
MobileTopBar
The bar above the page below 640 px, with the store's mark or a back link, the page's name, and icon buttons for search and notifications.