Vize

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

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.

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.

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.