Material Design 3 Navigation Rail component for Angular. Extends Angular Material mat-sidenav to provide a collapsible rail navigation pattern.
- Material Design 3 - Implements the Navigation Rail pattern from MD3
- Extends Material - Built on top of
MatSidenavfor reliability - Navigation Items -
rail-nav-itemcomponent with MD3 pill animation, badges, and router support - Contextual Drawer -
[for]on an item opens a side drawer from an<ng-template>, with hover-intent, content cross-fade, outside-tap dismiss and mobile support - Separator & Spacer -
rail-nav-separatorto group items,rail-nav-spacerto anchor items at the bottom - Expand/Collapse - Smooth transition between rail and drawer with adaptive width
- Backdrop - Optional backdrop overlay when expanded
- Position - Support for left (start) or right (end) positioning
- Dark Mode - Automatically adapts to light/dark color scheme
- SCSS Theming - Customizable via SCSS mixin following Angular Material pattern
npm install @softwarity/rail-nav| Package | Version |
|---|---|
| @angular/common | >= 21.0.0 |
| @angular/core | >= 21.0.0 |
| @angular/cdk | >= 21.0.0 |
| @angular/material | >= 21.0.0 |
Import the components in your Angular component:
import {
RailnavComponent,
RailnavContainerComponent,
RailnavContentComponent,
RailnavItemComponent,
RailnavSeparatorComponent, // Optional: separator between item groups
RailnavSpacerComponent, // Optional: push following items to the bottom
RailnavBrandingDirective // Optional: for custom branding
} from '@softwarity/rail-nav';
@Component({
imports: [
RailnavComponent,
RailnavContainerComponent,
RailnavContentComponent,
RailnavItemComponent,
RailnavSeparatorComponent,
RailnavSpacerComponent,
RailnavBrandingDirective
],
template: `
<rail-nav-container>
<rail-nav title="My App" subtitle="v1.0">
<rail-nav-item label="Home" routerLink="/home">
<mat-icon>home</mat-icon>
</rail-nav-item>
<rail-nav-item label="Settings" routerLink="/settings">
<mat-icon>settings</mat-icon>
</rail-nav-item>
</rail-nav>
<rail-nav-content>
<!-- Page content -->
</rail-nav-content>
</rail-nav-container>
`
})
export class AppComponent {}The main navigation rail component. Extends MatSidenav.
| Input | Type | Default | Description |
|---|---|---|---|
position |
'start' | 'end' |
'start' |
Position of the rail (left or right) |
title |
string |
- | Title displayed next to burger when expanded |
subtitle |
string |
- | Subtitle displayed below title when expanded |
hideDefaultHeader |
boolean |
false |
Hide the default header to provide custom content |
autoCollapse |
boolean |
true |
Auto-collapse the rail when an item is clicked |
| Property/Method | Type | Description |
|---|---|---|
expanded |
Signal<boolean> |
Read the current expanded state |
toggleExpanded() |
void |
Toggle between collapsed and expanded |
expand() |
void |
Expand the rail |
collapse() |
void |
Collapse the rail |
Container component. Extends MatSidenavContainer.
| Input | Type | Default | Description |
|---|---|---|---|
showBackdrop |
boolean |
true |
Show backdrop overlay when expanded |
Content area component. Extends MatSidenavContent.
| Input | Type | Default | Description |
|---|---|---|---|
position |
'start' | 'end' |
from the sibling rail | Rail side, when it cannot be read from a sibling rail-nav |
scrollOffset |
number |
0 |
Distance (px) from the top at which a section counts as reached, and the room kept above a section scrolled to, by the rail or by the browser for a URL fragment: the height of a sticky header, if any. See Scroll-spy |
anchorFragment |
boolean |
false |
Mirror the section in view in the URL fragment (#id, replacing the history entry; none above the first section) and scroll to the fragment's section on load |
| Property/Method | Type | Description |
|---|---|---|
activeAnchor |
Signal<string | null> |
Id of the section in view, among the anchors of the rail's items |
scrollToAnchor(id, behavior?) |
void |
Scroll to the section #id — 'smooth' (default) or 'instant' — and make its item active |
Note: When placed inside a rail-nav-container alongside a rail-nav, the component automatically detects the rail position and applies the correct margin using the --rail-nav-collapsed-width CSS variable.
Navigation item with MD3 pill animation.
| Input | Type | Default | Description |
|---|---|---|---|
routerLink |
string | any[] |
- | Router link for navigation |
label |
string |
- | Label text (below icon when collapsed, beside when expanded) |
badge |
string | number | boolean |
- | Badge value. Use true for a small dot badge |
active |
boolean |
- | Whether this item is active. On a routerLink item, left unset the router decides (the link matches the URL, as a prefix); set, it decides alone: false keeps off an item whose link is a prefix of the URL, such as the root. An anchor item is also active while its section is in view |
anchor |
string |
- | Id of an element inside rail-nav-content: a click scrolls to it, and the item is active while that section is in view. See Scroll-spy. Ignored with routerLink |
for |
TemplateRef | null |
null |
Template projected as a contextual drawer when the item is hovered or clicked. Aliased to for for mat-datepicker-toggle-style ergonomics. See Contextual drawer. |
| Output | Type | Description |
|---|---|---|
itemClick |
void |
Emitted when clicked (automatically collapses rail) |
Visual separator between groups of <rail-nav-item>. Hairline 1px rule, vertically centered in a fixed-height host so the spacing stays consistent across collapsed / expanded modes.
<rail-nav>
<rail-nav-item label="Home">...</rail-nav-item>
<rail-nav-separator />
<rail-nav-item label="Trash">...</rail-nav-item>
</rail-nav>Color overridable via --rail-nav-separator-color.
Flexible spacer (flex: 1 1 auto). Place between two groups of items to push everything after it to the bottom of the rail — the classic pattern for primary nav on top and Settings / Profile anchored at the bottom.
<rail-nav>
<rail-nav-item label="Home">...</rail-nav-item>
<rail-nav-item label="Inbox">...</rail-nav-item>
<rail-nav-spacer />
<rail-nav-item label="Settings">...</rail-nav-item>
</rail-nav>For a single page split into sections, point each item at the id of its section instead of a
route. A click scrolls smoothly to the section, and the item of the section in view turns active as
the page scrolls — no active bookkeeping in the host.
<rail-nav-container>
<rail-nav>
<rail-nav-item label="Palette" anchor="palette"><mat-icon>palette</mat-icon></rail-nav-item>
<rail-nav-item label="Forms" anchor="forms"><mat-icon>edit_note</mat-icon></rail-nav-item>
</rail-nav>
<rail-nav-content scrollOffset="64" anchorFragment>
<mat-toolbar class="sticky-header">…</mat-toolbar>
<section id="palette">…</section>
<section id="forms">…</section>
</rail-nav-content>
</rail-nav-container>- The section in view is the last one scrolled up to
scrollOffsetpx below the top of the content. Once the content is scrolled to the very bottom, the last section in view wins, so a short final section still gets its turn. - The browser's own scroll to a URL fragment — a shared link while the page loads, an in-page
<a href="#…">— leaves the same room above the section. Avoidscroll-padding-toponrail-nav-contentfor that: the focus entering a sticky header would scroll the content back up. - Only the sections some item points at are watched. They must sit inside
rail-nav-content, the element that scrolls. - The scroll is animated by the component —
MatSidenavContentignoresscrollTo({ behavior: 'smooth' })— skipped underprefers-reduced-motion, and it stops as soon as the user scrolls. anchorFragmentkeeps#sectionin the address bar without adding history entries, removes it above the first section, and opens a shared link on its section. Leave it off in an app routed withwithHashLocation(): the route lives in the fragment, whichanchorFragmentwould overwrite.- The active item carries
aria-current="location".
Any rail item can declare a contextual side drawer with [for]="someTemplate". The library renders the template in a CDK overlay positioned right next to the rail, handles hover-intent (200ms), close debounce (300ms for cursor transit), outside-tap dismiss, auto-close when the rail is expanded, and re-targets the same overlay when the cursor moves between trigger items.
import {
RailnavComponent,
RailnavContainerComponent,
RailnavContentComponent,
RailnavItemComponent,
} from '@softwarity/rail-nav';
@Component({
imports: [
RailnavComponent,
RailnavContainerComponent,
RailnavContentComponent,
RailnavItemComponent,
],
template: `
<rail-nav-container>
<rail-nav>
<rail-nav-item label="Workspaces" [for]="wsTpl">
<mat-icon>workspaces</mat-icon>
</rail-nav-item>
</rail-nav>
<rail-nav-content>...</rail-nav-content>
</rail-nav-container>
<ng-template #wsTpl>
<a routerLink="/ws/1">Workspace 1</a>
<a routerLink="/ws/2">Workspace 2</a>
</ng-template>
`,
})
export class AppComponent {}The drawer panel applies its own chrome (background, shadow, rounded right corner) and a default nav-list layout (flex column, padding, gap, fixed width) — your template only needs the children. Both desktop hover and mobile tap open the drawer; the lib swallows the synthetic mouse events touch devices fire after a tap so the drawer doesn't close instantly.
Override per rail (or globally) — all read with sane Material defaults as fallback.
| Variable | Default | Description |
|---|---|---|
--rail-nav-drawer-width |
240px |
Fixed drawer width |
--rail-nav-drawer-padding |
12px |
Inner padding |
--rail-nav-drawer-gap |
4px |
Vertical gap between children |
--rail-nav-drawer-surface |
var(--mat-sys-surface) |
Background color |
--rail-nav-drawer-on-surface |
var(--mat-sys-on-surface) |
Text color |
--rail-nav-drawer-shadow |
var(--mat-sys-level2) |
Elevation |
--rail-nav-drawer-radius |
0 12px 12px 0 |
Outer border-radius |
Use the SCSS mixin to customize colors following the Angular Material pattern:
@use '@softwarity/rail-nav/rail-nav-theme' as rail-nav;
:root {
@include rail-nav.overrides((
backdrop-color: rgba(0, 0, 0, 0.5),
primary: #6200ea,
surface-color: #f5f5f5,
));
}| Token | Default | Description |
|---|---|---|
collapsed-width |
72px |
Width of the rail when collapsed |
expanded-width |
fit-content |
Width of the rail when expanded |
header-height |
56px |
Height of the header area |
backdrop-color |
- | Background color for the backdrop overlay |
surface-color |
- | Background color for the rail |
surface-container-high |
- | Background color for hover states |
on-surface |
- | Text color for primary content |
on-surface-variant |
- | Text color for secondary content |
secondary-container |
- | Background color for active items |
on-secondary-container |
- | Text color for active items |
primary |
- | Focus ring color |
error |
- | Badge background color |
on-error |
- | Badge text color |
You can also use CSS custom properties directly.
:root {
--rail-nav-collapsed-width: 72px;
--rail-nav-expanded-width: fit-content;
--rail-nav-header-height: 56px;
}Color properties support the CSS light-dark() function for automatic light/dark theme adaptation:
:root {
/* Static colors (single theme) */
--rail-nav-surface-color: #ffffff;
--rail-nav-primary: #6750a4;
/* Or use light-dark() for automatic theme adaptation */
--rail-nav-surface-color: light-dark(#ffffff, #1e1e1e);
--rail-nav-primary: light-dark(#6750a4, #d0bcff);
}The component uses Material Design 3 system tokens (--mat-sys-*) as fallbacks.
Use the CSS light-dark() function to define colors that automatically adapt to the user's color scheme. The first value is for light mode, the second for dark mode.
@use '@softwarity/rail-nav/rail-nav-theme' as rail-nav;
:root {
@include rail-nav.overrides((
backdrop-color: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6)),
surface-color: light-dark(#ffffff, #1e1e1e),
on-surface: light-dark(#1c1b1f, #e6e1e5),
primary: light-dark(#6750a4, #d0bcff),
));
}:root {
--rail-nav-backdrop-color: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6));
--rail-nav-surface-color: light-dark(#ffffff, #1e1e1e);
--rail-nav-on-surface: light-dark(#1c1b1f, #e6e1e5);
--rail-nav-on-surface-variant: light-dark(#49454f, #cac4d0);
--rail-nav-secondary-container: light-dark(#e8def8, #4a4458);
--rail-nav-on-secondary-container: light-dark(#1d192b, #e8def8);
--rail-nav-primary: light-dark(#6750a4, #d0bcff);
--rail-nav-error: light-dark(#b3261e, #f2b8b5);
--rail-nav-on-error: light-dark(#ffffff, #601410);
}For light-dark() to work, set the color-scheme property on your document:
html {
color-scheme: light;
&.dark-mode { color-scheme: dark; }
}
/* Or use system preference */
html {
color-scheme: light dark;
}@Component({
imports: [
RailnavComponent,
RailnavContainerComponent,
RailnavContentComponent,
RailnavItemComponent,
MatIconModule
],
template: `
<rail-nav-container [showBackdrop]="true">
<rail-nav title="My App" subtitle="v1.0">
<rail-nav-item label="Home" [badge]="3" routerLink="/home">
<mat-icon>home</mat-icon>
</rail-nav-item>
<rail-nav-item label="Favorites" [badge]="true" routerLink="/favorites">
<mat-icon>favorite</mat-icon>
</rail-nav-item>
<rail-nav-item label="Settings" routerLink="/settings">
<mat-icon>settings</mat-icon>
</rail-nav-item>
</rail-nav>
<rail-nav-content>
<main>Your content here</main>
</rail-nav-content>
</rail-nav-container>
`
})
export class MyComponent {}Apache-2.0