- Docs
- /
- Navigation
- /
- Rail
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
| Prop | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | true | Bind 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). |
expandable | boolean | true | Shows 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. |
railTop | number | 0 | Top offset in pixels applied via a CSS custom property. Use when an AppBar sits above the rail, e.g. railTop={64}. |
rounded | boolean | false | Adds rounded-xl corner rounding to the rail panel. |
mobileNav | boolean | true | When true, the Rail automatically renders a bottom navigation bar on screens smaller than the md breakpoint. |
fab | Snippet | — | Optional Floating Action Button rendered above the navigation items. On mobile, when mobileNav is true, it floats above the bottom bar. |
railFooter | Snippet | — | Optional snippet pinned to the bottom of the rail. Typical use: an Avatar or settings link. |
showMobileFab | boolean | true | Whether to show the fab snippet above the mobile navbar. Only applies when mobileNav is true. |
showMobileFooter | boolean | false | Whether to include the railFooter snippet inside the mobile navbar. |
RailItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
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 | string | — | The text label. Shown below the icon when collapsed, or beside it when the rail is expanded. |
iconProps required | IconProps | — | Passed directly to the Icon component. At minimum provide { name: "symbol_name" } using any Material Symbols icon name. |
badge | number | 0 | Numeric badge overlaid on the icon. Pass -1 to show a small dot indicator with no number (useful for "unread" states). |
selected | boolean | false | Manually force the active/selected visual state regardless of the current URL. Useful for non-route-based selection or demo purposes. |
disabled | boolean | false | Prevents navigation and renders the item at 38% opacity with a not-allowed cursor. |
external | boolean | false | Opens 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.
| Breakpoint | Rail | Scrim | Mobile navbar | Ghost width |
|---|---|---|---|---|
| < md (mobile) | hidden | hidden | fixed bottom bar | 0 |
| md – lg (tablet) | visible, 96 px | shown on expand | hidden | 96 px |
| lg+ (desktop) | visible, 96 px | hidden | hidden | 96 px → 240 px |
Accessibility
Keyboard navigation
All RailItem links are focusable via Tab. The hamburger toggle is a native <button> and activates with Enter or Space.
Active state
Active items receive aria-current="page" from the underlying NavigationMenu.Link (Bits UI), which assistive technology announces as the current page.
Disabled items
Disabled RailItems set aria-disabled="true" and tabindex="-1", removing them from the focus order while keeping them visible.
Mobile navbar
The mobile navbar is a <nav> with aria-label="Mobile navigation", ensuring screen readers announce it as a separate landmark.