DashboardNav
Tabbed navigation for switching dashboard views or smooth-scrolling to page sections. Supports controlled and uncontrolled modes, URL sync, live badges, keyboard navigation, and nests inside FilterBar via FilterBar.Nav.
import { DashboardNav } from "metricui";Overview
DashboardNav renders a horizontal tab strip with a sliding underline indicator. In tabs mode (default), use value / onChange to control which content panel is visible. In scroll mode, clicking a tab smooth-scrolls to the matching section ID and an IntersectionObserver keeps the active tab in sync as the user scrolls.
<DashboardNav
tabs={[
{ value: "overview", label: "Overview" },
{ value: "revenue", label: "Revenue" },
{ value: "customers", label: "Customers" },
{ value: "settings", label: "Settings" },
]}
value={activeTab}
onChange={setActiveTab}
/>Tab Mode
The default mode. Use controlled value / onChange to conditionally render content below the nav. Click the tabs to swap the content panel.
const [activeTab, setActiveTab] = useState("overview");
<Dashboard>
<DashboardNav
tabs={tabs}
value={activeTab}
onChange={setActiveTab}
/>
{activeTab === "overview" && (
<MetricGrid columns={3}>
<KpiCard title="Total Revenue" value={128400} format={{ style: "currency" }} />
<KpiCard title="Active Users" value={3842} format="compact" />
<KpiCard title="Churn Rate" value={0.024} format={{ style: "percent" }} />
</MetricGrid>
)}
{activeTab === "revenue" && <KpiCard title="Monthly Revenue" value={42800} />}
</Dashboard>Scroll Mode
Set mode="scroll" and give each page section an id that matches the tab value. Clicking a tab smooth-scrolls to the section, and the active tab updates automatically via IntersectionObserver as the user scrolls. Pair with sticky for a fixed nav that stays in view.
Metrics
Charts
Table
<DashboardNav
tabs={[
{ value: "section-metrics", label: "Metrics" },
{ value: "section-charts", label: "Charts" },
{ value: "section-table", label: "Table" },
]}
mode="scroll"
sticky
/>
<div id="section-metrics">...</div>
<div id="section-charts">...</div>
<div id="section-table">...</div>Inside FilterBar
Nest DashboardNav inside the FilterBar.Nav slot to render it as the top row of the filter bar. Filters sit below in FilterBar.Primary. This keeps navigation and filtering in a single, cohesive bar.
<FilterBar>
<FilterBar.Nav>
<DashboardNav
tabs={tabs}
value={activeTab}
onChange={setActiveTab}
/>
</FilterBar.Nav>
<FilterBar.Primary>
<PeriodSelector presets={["7d", "30d", "90d"]} />
<DropdownFilter label="Region" dimension="region" options={regions} />
</FilterBar.Primary>
</FilterBar>Badges
Each tab can display a live badge. Numeric badges are formatted through the format engine (e.g., 1489 becomes "1.5K" with badgeFormat: "compact"). String badges render as-is.
<DashboardNav
tabs={[
{ value: "alerts", label: "Alerts", badge: 12 },
{ value: "users", label: "Users", badge: 1489, badgeFormat: "compact" },
{ value: "tasks", label: "Tasks", badge: "NEW" },
]}
/>URL Sync
Pass a syncUrl param name to persist the active tab in the URL search params. The component reads the initial value from the URL on mount, and updates it via history.replaceState on each tab change, making dashboards deep-linkable and shareable.
// URL will update to ?view=revenue when the tab is clicked
<DashboardNav
tabs={tabs}
syncUrl="view"
value={activeTab}
onChange={setActiveTab}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs* | DashboardNavTab[] | — | Array of tab definitions. Each: { value, label, icon?, badge?, badgeFormat? }. |
value | string | — | Controlled active tab value. |
defaultValue | string | First tab | Default active tab for uncontrolled usage. |
onChange | (value: string) => void | — | Callback fired when the active tab changes. |
mode | "tabs" | "scroll" | "tabs" | Navigation mode. "tabs" swaps content via value/onChange. "scroll" smooth-scrolls to section IDs matching tab values. |
syncUrl | string | — | URL search param name. Persists the active tab in the URL for deep-linking. |
sticky | boolean | false | Stick to viewport top with frosted-glass backdrop blur. |
size | "sm" | "md" | "lg" | "md" | Size variant controlling text, padding, and icon sizing. |
dense | boolean | false | Compact layout. Falls back to MetricProvider config. |
variant | CardVariant | — | Visual variant. Falls back to MetricProvider config. |
className | string | — | Additional CSS classes. |
id | string | — | HTML id attribute. |
data-testid | string | — | Test id. |
Data Shape
interface DashboardNavTab {
value: string;
label: string;
icon?: ReactNode;
badge?: number | string;
badgeFormat?: FormatOption;
}Notes
- Uses forwardRef. Passes through id, data-testid, and className.
- In scroll mode, an IntersectionObserver highlights the section currently in view. A 1-second lock prevents the observer from overriding the active tab immediately after a click-to-scroll.
- The sliding underline indicator animates with a 200ms cubic-bezier transition.
- Sticky mode applies frosted-glass styling (backdrop-blur-xl, 80% card-bg opacity) and sticks to the viewport top with z-index 31.
- When dense is true and size is 'md', the component automatically downsizes to 'sm'.
- Badge formatting uses the same format engine as KpiCard.
- Full ARIA tablist semantics: role='tablist' on the container, role='tab' and aria-selected on each button.
- Works both standalone and inside FilterBar.Nav. When inside FilterBar, omit the sticky prop.
- The
aiContextprop (inherited from BaseComponentProps) adds business context for AI Insights analysis. See the AI Insights guide for details.