Navigation rail
A navigation rail is the top level navigation for medium sized screens. It is a narrow vertical strip of destinations that stays visible next to the content. Use it for three to seven destinations. For fewer, use a tab bar. The expanded rail replaces the navigation drawer, which Material 3 no longer recommends.
Each item needs an icon and a label. Mark the current destination with selected. This fills the icon, gives the label the stronger colour and draws the pill
behind the icon.
Usage
<script lang="ts">
import { NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
let selection = $state(1)
</script>
<NavigationRail>
<NavigationRailItem
selected={selection === 1}
onclick={() => {
selection = 1
}}
label="Videos"
>
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationRailItem>
<NavigationRailItem
selected={selection === 2}
onclick={() => {
selection = 2
}}
label="Styles"
>
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationRailItem>
<NavigationRailItem
selected={selection === 3}
onclick={() => {
selection = 3
}}
label="Settings"
>
{#snippet icon()}<Icon>settings</Icon>{/snippet}
</NavigationRailItem>
</NavigationRail>
Expanded
The rail has two widths. Collapsed, it is 96dp wide with the label under each icon. Expanded, it
is 220dp to 360dp wide, shows the label beside the icon inside the indicator and pushes the
content aside. menu adds a button at the top that toggles expanded. You
can also bind expanded.
Pass a FAB through the fab snippet. It sits above the destinations. The snippet
receives expanded, so an ExtendedFab can show its label only while the rail
is expanded.
When the rail changes width, the destinations move into place, the indicator grows around the
label and the labels cross-fade. --np-motion-standard-fast-spatial sets the timing.
The modal rail's sheet uses --np-motion-expressive-default-spatial, so it settles a
moment after its items. With reduced motion the rail switches at once.
alignment="center" groups the destinations in the vertical centre of the rail. This is
easier to reach on a tablet. The menu button and the FAB stay at the top.
<script lang="ts">
import { ExtendedFab, NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'videos', label: 'Videos', icon: 'videocam' },
{ value: 'styles', label: 'Styles', icon: 'palette' },
{ value: 'favorites', label: 'Favorites', icon: 'favorite', badgeLabel: 3 },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let expanded = $state(false)
let selected = $state('videos')
</script>
<div style="display:flex;height:30rem;width:36rem;max-width:100%">
<NavigationRail menu bind:expanded aria-label="Main">
{#snippet fab({ expanded })}
<ExtendedFab collapsed={!expanded} label="New video">
{#snippet icon()}<Icon>add</Icon>{/snippet}
</ExtendedFab>
{/snippet}
{#each destinations as destination (destination.value)}
<NavigationRailItem
label={destination.label}
selected={selected === destination.value}
badge={destination.badgeLabel !== undefined}
badgeLabel={destination.badgeLabel}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationRailItem>
{/each}
</NavigationRail>
<div style="flex:1;padding:1.5rem">Content moves aside as the rail expands.</div>
</div>
Sections
NavigationRailSection holds secondary destinations under a header. They show only in the
expanded rail and fade in as it opens. In the collapsed rail, the section is hidden from the keyboard
and screen readers too. In the expanded rail, screen readers announce it as a group named by its header.
<script lang="ts">
import { NavigationRail, NavigationRailItem, NavigationRailSection } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
type Destination = { value: string; label: string; icon: string }
const main: Destination[] = [
{ value: 'home', label: 'Home', icon: 'home' },
{ value: 'search', label: 'Search', icon: 'search' },
{ value: 'mail', label: 'Mail', icon: 'mail' },
]
const library: Destination[] = [
{ value: 'saved', label: 'Saved', icon: 'bookmark' },
{ value: 'favorites', label: 'Favorites', icon: 'favorite' },
{ value: 'downloads', label: 'Downloads', icon: 'download' },
]
let expanded = $state(true)
let selected = $state('home')
</script>
{#snippet item(destination: Destination)}
<NavigationRailItem
label={destination.label}
selected={selected === destination.value}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationRailItem>
{/snippet}
<div style="display:flex;height:36rem;width:36rem;max-width:100%">
<NavigationRail menu bind:expanded aria-label="Main, with sections">
{#each main as destination (destination.value)}
{@render item(destination)}
{/each}
<NavigationRailSection label="Library">
{#each library as destination (destination.value)}
{@render item(destination)}
{/each}
</NavigationRailSection>
</NavigationRail>
<div style="flex:1;padding:1.5rem">Collapse the rail and the library goes with it.</div>
</div>
Modal
With modal, the expanded rail opens over the content behind a scrim instead of
pushing it aside. Use it in smaller windows. Focus moves into the rail. Esc or a click on
the scrim closes it.
<script lang="ts">
import { NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'videos', label: 'Videos', icon: 'videocam' },
{ value: 'styles', label: 'Styles', icon: 'palette' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let expanded = $state(false)
let selected = $state('videos')
</script>
<div style="display:flex;height:22rem;width:100%">
<NavigationRail modal menu bind:expanded aria-label="Main, modal">
{#each destinations as destination (destination.value)}
<NavigationRailItem
label={destination.label}
selected={selected === destination.value}
onclick={() => {
selected = destination.value
expanded = false
}}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationRailItem>
{/each}
</NavigationRail>
<div style="flex:1;padding:1.5rem">The content stays put. The expanded rail opens over it.</div>
</div>
Hide when collapsed
hideWhenCollapsed removes the collapsed rail, so the content gets the full width.
Open the rail with your own menu button that sets expanded. The menu button inside the open rail closes it.
Collapsed, the rail is gone and the content has the full width.
<script lang="ts">
import { IconButton, NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'videos', label: 'Videos', icon: 'videocam' },
{ value: 'styles', label: 'Styles', icon: 'palette' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let expanded = $state(false)
let selected = $state('videos')
</script>
<div style="display:flex;align-items:flex-start;gap:0.5rem;width:100%">
<IconButton
aria-label="Open navigation"
aria-expanded={expanded}
onclick={() => (expanded = true)}
>
<Icon>menu</Icon>
</IconButton>
<NavigationRail modal hideWhenCollapsed menu bind:expanded aria-label="Main, hidden">
{#each destinations as destination (destination.value)}
<NavigationRailItem
label={destination.label}
selected={selected === destination.value}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationRailItem>
{/each}
</NavigationRail>
<p style="margin:0.5rem 0">Collapsed, the rail is gone and the content has the full width.</p>
</div>
Links
Pass an href and the item renders as an <a> instead of a <button>. Use links for real navigation, so people can open a destination in a
new tab. In a routed app, set selected from the current URL, for example page.url.pathname === href with SvelteKit's page from $app/state.
<script lang="ts">
import { NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<NavigationRail>
<NavigationRailItem href="#videos" selected label="Videos">
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationRailItem>
<NavigationRailItem href="#styles" label="Styles">
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationRailItem>
</NavigationRail>
Accessibility
The rail renders a <nav>, and the selected item gets aria-current="page", so assistive technology announces the current destination.
The items share one tab stop. Tab moves into and out of the rail. ↑ and ↓ move between the destinations and wrap around at the ends. Home and End jump to the first and the last one. The menu button and the FAB
come before them, each with its own tab stop. The menu button reports aria-expanded. When a page has several navigations, give each <nav> an aria-label so they can be told apart.
Theming
| Token | Default value |
|---|---|
--np-navigation-rail-container-color | --np-color-surface |
--np-navigation-rail-modal-container-color | --np-color-surface-container |
--np-navigation-rail-modal-container-shape | --np-shape-corner-large |
--np-navigation-rail-padding-top | max(0.5rem, env(safe-area-inset-top)) |
--np-navigation-rail-item-active-indicator-color | --np-color-secondary-container |
--np-navigation-rail-item-font-weight | 500 |
--np-navigation-rail-item-selected-font-weight | 500 |
The colours come from the theme. The rail uses surface, or surface-container where it should stand apart from the content. Idle icons and labels
use on-surface-variant. The pill behind the selected icon uses secondary-container with on-secondary-container. Raise --np-navigation-rail-item-selected-font-weight to make the current destination stand out
more.
Example
<script lang="ts">
import { NavigationRail, NavigationRailItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<NavigationRail --np-navigation-rail-item-selected-font-weight="700">
<NavigationRailItem selected label="Videos">
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationRailItem>
<NavigationRailItem label="Styles">
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationRailItem>
</NavigationRail>
API
NavigationRail attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
expanded | boolean | false | Shows the expanded rail, with the label beside each icon. Bindable. |
modal | boolean | false | Opens the expanded rail over the content, behind a scrim. |
hideWhenCollapsed | boolean | false | Hides the collapsed rail. Set expanded from a button of your own. |
menu | boolean | false | Adds a menu button at the top that toggles expanded. |
menuLabel | string | 'Menu' | The name of the menu button. It shows no tooltip. |
alignment | 'top' | 'center' | 'top' | Groups the destinations at the top or in the vertical centre. |
fab | Snippet<[{ expanded: boolean }]> | undefined | A FAB above the destinations, resting at elevation 0. |
Other attributes, such as aria-label, class and style, go
to the <nav> element.
NavigationRailItem attributes
Other attributes, including onclick and href, go to the <button> or <a>.
| Attribute | Type | Default | Description |
|---|---|---|---|
icon | Snippet | Required. The destination's icon. | |
label | string | Required. Text below the icon. | |
selected | boolean | undefined | undefined | Marks the current destination and sets aria-current="page". |
href | string | undefined | undefined | Renders the item as a link instead of a button. |
type | 'submit' | 'reset' | 'button' | null | undefined | Button type, for an item inside a form. |
NavigationRailSection attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | The section header, shown in the expanded rail and naming the group. |