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.
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 withNavbar 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 { Rail, RailItem } from '@noxlovette/material'; 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.
Storybook is the canonical place to play with Rail's props and Controls — this page sticks to prose and usage guidance rather than reimplementing that preview.
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> 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> 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> 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> | 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. |
withNavbar | boolean | false | When true, the Rail automatically renders a bottom navigation bar on screens smaller than the md breakpoint. Opt in — leave false if you render your own mobile nav. |
fab | Snippet | — | Optional Floating Action Button rendered above the navigation items. On mobile, when withNavbar 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 withNavbar is true. |
showMobileFooter | boolean | false | Whether to include the railFooter snippet inside the mobile navbar. |
| 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. |
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 |
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.