Stable

Navigation Rail

Navigation Rails provide ergonomic access to 3–7 primary destinations on medium and large screens. On mobile they collapse into a bottom navigation bar automatically.

Overview

The Rail component implements the Material Design 3 Navigation Rail pattern. It renders as a fixed vertical sidebar on md+ screens, and can expand to show text labels alongside icons. Below the md breakpoint it transparently becomes a bottom navigation bar (via its mobileNav prop).

The Rail emits a ghost element — a hidden flex-item sibling that holds the same width as the rail. This means your page content is always pushed aside by the correct amount without any manual margin-left calculations.

Import

import { Rail, RailItem } from '@noxlovette/material';

Anatomy

The Rail is composed of several cooperating pieces:

Rail

The container. Renders the sidebar, ghost spacer, tablet scrim, and (optionally) the mobile bottom bar.

RailItem

A single navigation destination. Handles active-state detection, badge overlays, disabled state, and adapts its layout for collapsed/expanded/mobile modes.

RailNavContext

Internal context provider that tells RailItem whether it is rendering inside the sidebar or the mobile navbar. You do not use this directly.

railStore

A lightweight reactive store that exposes the current collapsed state for components that need to read it outside the Rail tree.

Live Demo

The demo uses anchor="parent" to position the rail within the sandbox rather than the viewport.

Page content area

Content shifts as the rail expands.

Basic Usage

Mount Rail directly inside your root +layout.svelte, alongside a flex wrapper around your page content. The ghost div handles spacing automatically.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { App, Rail, RailItem } from '@noxlovette/material';

  let collapsed = $state(true);
  const { children } = $props();
</script>

<App>
  <div class="flex min-h-dvh">
    <Rail bind:collapsed>
      <RailItem label="Home"     href="/"        iconProps={{ name: 'house' }} />
      <RailItem label="Messages" href="/messages" iconProps={{ name: 'mail' }} badge={5} />
      <RailItem label="Settings" href="/settings" iconProps={{ name: 'settings' }} />
    </Rail>

    <!-- Ghost pushes content; no margin-left needed -->
    <div class="min-w-0 flex-1">
      {@render children()}
    </div>
  </div>
</App>

With App Bar

When a top AppBar is present, pass its height (in px) to railTop. This shifts the rail down so it doesn't overlap the bar. The ghost element also respects this offset automatically.

<AppBar class="fixed top-0 left-0 right-0 z-50 h-16">
  My App
</AppBar>

<div class="flex">
  <!-- rail starts at 64 px from the top (h-16 = 4rem = 64 px) -->
  <Rail railTop={64} bind:collapsed>
    <RailItem label="Home"     href="/" iconProps={{ name: 'house' }} />
    <RailItem label="Settings" href="/settings" iconProps={{ name: 'settings' }} />
  </Rail>

  <main class="min-w-0 flex-1 pt-16">
    {@render children()}
  </main>
</div>

With FAB and Footer

Use the fab snippet for a primary action button and railFooter for an account avatar or settings entry.

<Rail bind:collapsed>
  {#snippet fab()}
    <FAB iconProps={{ name: 'edit' }} label="Compose" />
  {/snippet}

  <RailItem label="Inbox"   href="/inbox"   iconProps={{ name: 'mail' }} badge={12} />
  <RailItem label="Sent"    href="/sent"    iconProps={{ name: 'send' }} />
  <RailItem label="Drafts"  href="/drafts"  iconProps={{ name: 'draft' }} badge={-1} />
  <RailItem label="Trash"   href="/trash"   iconProps={{ name: 'delete' }} />

  {#snippet railFooter()}
    <Avatar seed="AB" href="/profile" />
  {/snippet}
</Rail>

Anchor Modes

viewport (default)

The rail is fixed to the left edge of the browser viewport. The ghost element in the flex row pushes content to the right. Use this for full-page app layouts.

<Rail bind:collapsed>
  <!-- items -->
</Rail>
parent

The rail is absolutely positioned within the nearest relative ancestor. Use this for embedded layouts, dashboard panels, or contained demos where you do not want the rail to escape its container.

<div class="relative h-[500px] overflow-hidden">
  <Rail anchor="parent" bind:collapsed>
    <!-- items -->
  </Rail>
  <div class="ml-24">
    <!-- content -->
  </div>
</div>

Rail Props

PropTypeDefaultDescription
collapsedbooleantrueBind to this to drive the expanded/collapsed state externally. When true, the rail shows only icons (96 px wide); when false, it expands to show labels (240 px).
expandablebooleantrueShows the hamburger toggle button at the top of the rail. Set to false to lock the rail permanently collapsed.
anchor"viewport" | "parent""viewport"Controls positioning strategy. "viewport" fixes the rail to the left edge of the screen. "parent" positions it absolutely within the nearest relative ancestor — useful for contained demos or modular layouts.
railTopnumber0Top offset in pixels applied via a CSS custom property. Use when an AppBar sits above the rail, e.g. railTop={64}.
roundedbooleanfalseAdds rounded-xl corner rounding to the rail panel.
mobileNavbooleantrueWhen true, the Rail automatically renders a bottom navigation bar on screens smaller than the md breakpoint.
fabSnippetOptional Floating Action Button rendered above the navigation items. On mobile, when mobileNav is true, it floats above the bottom bar.
railFooterSnippetOptional snippet pinned to the bottom of the rail. Typical use: an Avatar or settings link.
showMobileFabbooleantrueWhether to show the fab snippet above the mobile navbar. Only applies when mobileNav is true.
showMobileFooterbooleanfalseWhether to include the railFooter snippet inside the mobile navbar.

RailItem Props

PropTypeDefaultDescription
href required
string"/"The destination URL. Active state is automatically derived by comparing href to the current page URL — no manual tracking needed.
label required
stringThe text label. Shown below the icon when collapsed, or beside it when the rail is expanded.
iconProps required
IconPropsPassed directly to the Icon component. At minimum provide { name: "symbol_name" } using any Material Symbols icon name.
badge
number0Numeric badge overlaid on the icon. Pass -1 to show a small dot indicator with no number (useful for "unread" states).
selected
booleanfalseManually force the active/selected visual state regardless of the current URL. Useful for non-route-based selection or demo purposes.
disabled
booleanfalsePrevents navigation and renders the item at 38% opacity with a not-allowed cursor.
external
booleanfalseOpens the link in a new tab with rel="noopener noreferrer" automatically applied.

Responsive Behaviour

The Rail and its mobile fallback are designed to work together without any manual breakpoint checks in your code.

BreakpointRailScrimMobile navbarGhost width
< md (mobile)hiddenhiddenfixed bottom bar0
md – lg (tablet)visible, 96 pxshown on expandhidden96 px
lg+ (desktop)visible, 96 pxhiddenhidden96 px → 240 px

Accessibility

keyboard

Keyboard navigation

All RailItem links are focusable via Tab. The hamburger toggle is a native <button> and activates with Enter or Space.

visibility

Active state

Active items receive aria-current="page" from the underlying NavigationMenu.Link (Bits UI), which assistive technology announces as the current page.

block

Disabled items

Disabled RailItems set aria-disabled="true" and tabindex="-1", removing them from the focus order while keeping them visible.

smartphone

Mobile navbar

The mobile navbar is a <nav> with aria-label="Mobile navigation", ensuring screen readers announce it as a separate landmark.