Navigation Rail
Side navigation rail: a labelled navigation landmark of top-level destinations whose current one carries aria-current="page", with an optional expanded state that shows labels beside the icons.
| Package | @vizejs/ui/navigation-rail |
| Maturity | stable |
| Own the source | vize lib pull navigation-rail |
| Requires | context, controllable-state, id |
| Aliases | nav rail, side rail, side navigation, vertical tab bar |
| Covers | HTML nav landmark, aria-current=page, Material navigation rail |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { NavigationRail, NavigationRailItem, NavigationRailToggle } from "@vizejs/ui/navigation-rail";
Or copy the source into your project with vize lib pull navigation-rail (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Collapsible side rail with three destinations and an unread badge. -->
<script setup lang="ts">
import { ref } from "vue";
import { NavigationRail, NavigationRailItem, NavigationRailToggle } from "@vizejs/ui/navigation-rail";
const destinations = ["home", "search", "inbox"] as const;
const current = ref<(typeof destinations)[number]>("home");
const expanded = ref(false);
</script>
<template>
<NavigationRail v-model="current" v-model:expanded="expanded" :destinations>
<NavigationRailToggle aria-label="Toggle navigation labels">
{{ expanded ? "«" : "»" }}
</NavigationRailToggle>
<NavigationRailItem value="home">Home</NavigationRailItem>
<NavigationRailItem value="search">Search</NavigationRailItem>
<NavigationRailItem value="inbox" :badge="3">Inbox</NavigationRailItem>
</NavigationRail>
</template>
API
NavigationRail<Value extends string>
Source: src/families/navigation/navigation-rail/navigation-rail.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
destinations (required) |
readonly Value[] |
required |
Destination values in order; their literal union types v-model and select. |
id |
string | null |
undefined |
Consumer-owned id. null and undefined select a deterministic fallback. |
modelValue |
Value |
undefined |
Controlled active destination. undefined selects uncontrolled behavior. |
defaultValue |
Value |
undefined |
Initial uncontrolled destination. |
expanded |
boolean |
undefined |
Controlled expanded state. undefined selects uncontrolled behavior. |
defaultExpanded |
boolean |
false |
Initial expanded state for uncontrolled use. |
ariaLabel |
string |
"Primary" |
Accessible name of the navigation landmark. |
ariaLabelledby |
string |
undefined |
Ids that label the navigation landmark (overrides ariaLabel). |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: Value] |
Fired when an item requests to become the active destination. |
select |
[value: Value, nativeEvent: MouseEvent] |
Fired after an item is activated, with its value and the native click. |
update:expanded |
[value: boolean] |
Fired with the requested expanded value (supports v-model:expanded). |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
NavigationRailSlotState<Value> |
Toggle and NavigationRailItem destinations with the active value and expanded state. |
Exposed
| Member | Type | Description |
|---|---|---|
id |
string |
Id of the navigation landmark. |
expanded |
boolean |
Whether destinations show their labels beside the icons. |
setExpanded |
(value: boolean) => void |
Request an expanded value. |
toggle |
() => void |
Flip the expanded value. |
NavigationRailItem
Source: src/families/navigation/navigation-rail/navigation-rail-item.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value (required) |
string |
required |
Destination value; must be one of the root destinations. |
href |
string |
undefined |
Link target. With href the item renders an <a>, otherwise a <button>. |
disabled |
boolean |
false |
Disable the destination. |
badge |
string | number |
undefined |
Badge text rendered with the label (for example an unread count). |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly active: boolean; readonly expanded: boolean } |
Icon and label contents with the active and expanded state. |
badge |
{ readonly badge: string | number } |
Visual badge contents; defaults to the badge text. |
NavigationRailToggle
Source: src/families/navigation/navigation-rail/navigation-rail-toggle.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled |
boolean |
false |
Disable expanding and collapsing. |
ariaLabel |
string |
undefined |
Accessible name when the toggle renders only an icon. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly expanded: boolean } |
Toggle contents with the expanded state. |
Behavior
Normative state x input -> outcome table for navigation-rail.vue,
navigation-rail-item.vue, and navigation-rail-toggle.vue
(@vizejs/ui/navigation-rail). A navigation rail is the side counterpart of a bottom tab
bar: it is navigation, not an APG tablist, so it renders a labelled <nav> landmark and marks
the current destination with aria-current="page". Expanding the rail only publishes state;
labels stay in the DOM in both states so collapsed destinations keep their accessible names.
Every row is proven by the named test in navigation-rail.test.ts or
navigation-rail-ssr.test.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| NR1 | seeded | render | <nav aria-label="Primary">; items with href render links, others type="button"; badges render; current item has aria-current="page" |
renders a labelled navigation landmark with links, buttons, and badges |
| NR2 | uncontrolled | click | the item becomes current (aria-current, data-state="active") and emits update:modelValue then select |
activating an item moves aria-current and emits typed selections |
| NR3 | collapsed / expanded | toggle click | aria-expanded and data-expanded flip on the toggle, the rail, and every item; the toggle's aria-controls names the rail |
the toggle expands and collapses the rail and names what it controls |
| NR4 | controlled expanded / expose |
toggle, toggle() |
controlled values win and still emit update:expanded; toggle() and setExpanded() request changes |
controlled expansion wins and the exposed surface requests changes |
| NR5 | disabled toggle | click | native disabled; the expanded state is unchanged; a deterministic id is generated without an id prop |
a disabled toggle keeps the expanded state |
| NR6 | controlled / disabled / unknown | click | controlled values win; disabled buttons are native-disabled, disabled links drop href with aria-disabled; unknown values never emit |
controlled values win, and disabled or unknown items do not activate |
| NR7 | no provider | setup | items and the toggle throw VIZE_UI_CONTEXT_MISSING: NavigationRail |
items and the toggle require a NavigationRail provider |
| NR8 | SSR / hydration | isolated requests | byte-identical markup, toggle wired to the server-rendered rail id, and hydration without diagnostics | renders byte-identical rail markup and hydrates without mismatches |
Pair with SafeArea (edges: ["left"] or ["right"]) to clear display cutouts. The subpath
ships no CSS.