rc-navigation-rail
Navigation rail layout that styles consumer-authored links.
Use this for medium and expanded PWA layouts where native anchors should remain
router friendly and work before custom element upgrade. Wrap it in a native
<nav> to provide navigation landmark semantics; the component deliberately
provides no landmark role of its own, matching rc-navigation-bar. The rail
supports a controlled or uncontrolled expanded state, but the app still owns
breakpoint policy.
- Package
@rcarls/rc-navigation-rail- Element
<rc-navigation-rail>- Native dependency
- Consumer-authored <a> links
- State model
- Controlled or uncontrolled expanded state
- Main events
rc-navigation-rail-toggle
Installation
- npm
- Yarn
npm install @rcarls/rc-navigation-rail
yarn add @rcarls/rc-navigation-rail
import '@rcarls/rc-navigation-rail/define';
Usage
<nav aria-label="Main navigation">
<rc-navigation-rail>
<rc-button slot="toggle" icon-only>
<button type="button" aria-label="Toggle navigation">
<span
data-rc-button-icon
data-rc-navigation-expand-icon
class="material-symbols-outlined"
aria-hidden="true"
>menu</span
>
<span
data-rc-button-selected-icon
data-rc-navigation-collapse-icon
class="material-symbols-outlined"
aria-hidden="true"
>menu_open</span
>
</button>
</rc-button>
<a href="/library" aria-current="page">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">local_library</span>
<span>Library</span>
</span>
</a>
<a href="/saved">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">bookmark</span>
<span>Saved</span>
</span>
</a>
<button slot="footer" type="button" aria-label="Settings">settings</button>
</rc-navigation-rail>
</nav>
The native <nav> is the navigation landmark and owns its accessible label,
just as <search> owns landmark semantics around rc-search-bar.
rc-navigation-rail only provides layout, active-state styling, and
indicator positioning.
Live demo
Expanded state
Use default-expanded for uncontrolled initial state or bind expanded from
your app. Programmatic property writes are silent. User interaction with the
slotted toggle dispatches rc-navigation-rail-toggle.
The Material theme keeps each icon at 24px while its indicator target grows
from 56×32px in the collapsed rail to the expanded row height. Both states use
the same inline-start track so icons do not shift horizontally. Wrap each icon
and label in data-rc-navigation-indicator so the expanded Material indicator
contains both.
<nav aria-label="Main navigation">
<rc-navigation-rail default-expanded>
<button slot="toggle" type="button" aria-label="Toggle navigation">
<span data-rc-navigation-expand-icon class="material-symbols-outlined" aria-hidden="true"
>menu</span
>
<span data-rc-navigation-collapse-icon class="material-symbols-outlined" aria-hidden="true"
>menu_open</span
>
</button>
<a href="/library" aria-current="page">Library</a>
<a href="/settings">Settings</a>
</rc-navigation-rail>
</nav>
Collapse control
Provide the collapse control in light DOM as either a native button or an
rc-button wrapping a direct native button. The control remains a semantic,
keyboard-operable button before custom element upgrade; the rail enhances it by
synchronizing aria-expanded, switching the marked icons, and dispatching its
toggle event. With rc-button, mark menu as data-rc-button-icon and
menu_open as data-rc-button-selected-icon; the rail coordinates the
wrapper's selected state.
<nav aria-label="Main navigation">
<rc-navigation-rail>
<button slot="toggle" type="button" aria-label="Toggle navigation">
<span data-rc-navigation-expand-icon class="material-symbols-outlined" aria-hidden="true"
>menu</span
>
<span data-rc-navigation-collapse-icon class="material-symbols-outlined" aria-hidden="true"
>menu_open</span
>
</button>
<a href="/library" aria-current="page">Library</a>
</rc-navigation-rail>
</nav>
Adaptive navigation
Keep compact-to-rail switching in app/layout code. The component does not own media queries, route matching, or persistence; it only styles the native links you render into it.
Use the Material window classes from your app shell:
| Window width | App-owned navigation surface |
|---|---|
<600px compact | rc-navigation-bar in the bottom app chrome |
>=600px medium | collapsed rc-navigation-rail in the leading app chrome |
>=840px expanded | rc-navigation-rail, optionally expandable/persisted |
Share destination data and active-link logic between the bar and rail. Mark the
current native link with aria-current="page", or set active-selector to your
router's active class.
<!-- medium: >=600px -->
<nav aria-label="Main navigation">
<rc-navigation-rail>
<a href="/library" aria-current="page">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">local_library</span>
<span>Library</span>
</span>
</a>
<a href="/settings">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">settings</span>
<span>Settings</span>
</span>
</a>
</rc-navigation-rail>
</nav>
<!-- expanded: >=840px, app decides whether expansion is offered -->
<nav aria-label="Main navigation">
<rc-navigation-rail expanded>
<rc-fab slot="header" position="top-start" style="--rc-fab-position: static">
<button type="button" aria-label="New note">
<span data-rc-button-icon aria-hidden="true">add</span>
</button>
</rc-fab>
<a href="/library" aria-current="page">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">local_library</span>
<span>Library</span>
</span>
</a>
<a href="/settings">
<span data-rc-navigation-indicator>
<span data-rc-navigation-icon aria-hidden="true">settings</span>
<span>Settings</span>
</span>
</a>
</rc-navigation-rail>
</nav>
Persist expanded state in the app, and only apply it while the expanded window class affords the wider rail:
const key = 'app.navigation-rail-expanded';
const isExpanded = matchMedia('(min-width: 840px)').matches;
const rail = document.querySelector('rc-navigation-rail');
rail.expanded = isExpanded && localStorage.getItem(key) === 'true';
rail.addEventListener('rc-navigation-rail-toggle', (event) => {
localStorage.setItem(key, String(event.detail.expanded));
});
The components expose stable surface and indicator parts so apps can opt into View Transitions:
rc-navigation-bar,
rc-navigation-rail {
view-transition-name: app-navigation;
}
rc-navigation-bar::part(indicator),
rc-navigation-rail::part(indicator) {
view-transition-name: app-navigation-indicator;
}
The rail's own collapsed/expanded state uses component-owned CSS transitions and snaps to the final state under reduced motion. The adaptive example uses an app-owned View Transition to switch navigation surfaces and move the FAB from the page action position into the rail's generic header slot.
Material theme
rc-theme-material/components.css applies Material 3 navigation rail metrics:
80dp collapsed rail width, expanded row treatment, 56×32 collapsed indicators,
state-layer hover treatment, label-medium/label-large typography, and reduced
motion fallbacks.
The Substrate theme highlights the active link itself and disables the moving indicator. The Material theme retains its separate animated indicator behind the icon or expanded row target.
API
Properties
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
expanded | expanded | boolean | Not specified | Whether the rail is expanded. Host writes are silent. |
defaultExpanded | default-expanded | boolean | Not specified | Initial expanded state for uncontrolled usage. |
activeSelector | active-selector | string | 'a[aria-current]:not([aria-current="false"])' | Selector used to find the active link. Defaults to `aria-current`. |
indicatorTarget | indicator-target | string | '[data-rc-navigation-indicator], [data-rc-navigation-icon]' | Selector inside the active link used for indicator geometry. |
Methods
| Method | Description |
|---|---|
expand() | Expands the rail and dispatches `rc-navigation-rail-toggle` when it changes. |
collapse() | Collapses the rail and dispatches `rc-navigation-rail-toggle` when it changes. |
toggleExpanded() | Toggles expanded state and dispatches `rc-navigation-rail-toggle` when it changes. |
Events
| Event | Detail type | Description |
|---|---|---|
rc-navigation-rail-toggle | CustomEvent | Fired when user interaction or a method toggles expanded state. |
Slots
| Name | Description |
|---|---|
default | Navigation links. Direct `<a>` children are recommended. |
header | Content above the navigation links. |
footer | Content pinned after the navigation links. |
toggle | Native `<button>` or `rc-button` expand/collapse control. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-navigation-rail-bg | Canvas | Rail surface background. |
--rc-navigation-rail-color | CanvasText | Rail text color. |
--rc-navigation-rail-inline-size | 5rem | Collapsed rail inline size. |
--rc-navigation-rail-expanded-inline-size | 16rem | Expanded rail inline size. |
--rc-navigation-rail-padding-block | 0.75rem | Rail block-axis padding. |
--rc-navigation-rail-padding-inline | 0.5rem | Rail inline-axis padding. |
--rc-navigation-rail-gap | 0.75rem | Gap between rail regions. |
--rc-navigation-rail-item-gap | 0.75rem | Gap between navigation items. |
--rc-navigation-rail-link-gap | 0.25rem | Gap between item icon and label. |
--rc-navigation-rail-item-min-block-size | 3.5rem | Collapsed item minimum block size. |
--rc-navigation-rail-expanded-item-min-block-size | 3.5rem | Expanded item minimum block size. |
--rc-navigation-rail-item-padding-block | 0.25rem | Item block-axis padding. |
--rc-navigation-rail-item-padding-inline | 0.5rem | Collapsed item inline-axis padding. |
--rc-navigation-rail-expanded-item-padding-inline | 1rem | Expanded item inline-axis padding. |
--rc-navigation-rail-collapsed-label-inline-size | 4rem | Collapsed label maximum inline size. |
--rc-navigation-rail-item-color | inherit | Resting item text color. |
--rc-navigation-rail-item-text-decoration | Not specified | Slotted link text decoration (defers to native anchor text-decoration when unset). |
--rc-navigation-rail-active-color | Not specified | Active item text color. Falls back to `--rc-navigation-rail-item-color`, then `inherit`. |
--rc-navigation-rail-indicator-bg | transparent | Active indicator background. |
--rc-navigation-rail-indicator-border | 1px solid Highlight | Active indicator border. |
--rc-navigation-rail-indicator-radius | 0 | Active indicator corner radius. |
--rc-navigation-rail-toggle-size | 3rem | Toggle region minimum block size. |
--rc-navigation-rail-toggle-inline-offset | 0.5rem | Toggle control inline-start offset. |
--rc-navigation-rail-duration | 200ms | Rail expand/collapse transition duration. |
--rc-navigation-rail-easing | ease | Rail expand/collapse transition easing. |
--rc-navigation-rail-indicator-duration | 0ms | Active indicator transition duration. |
--rc-navigation-rail-indicator-easing | ease | Active indicator transition easing. |
--rc-navigation-rail-focus-ring | Not specified | Slotted link focus outline (defers to native outline when unset). |
--rc-navigation-rail-focus-ring-offset | 2px | Slotted link outline offset. |
CSS Parts
| Part | Description |
|---|---|
root | The rail layout container. |
nav | The navigation layout container. |
indicator | The active item indicator. |
toggle | Toggle slot container. |
header | Header slot container. |
footer | Footer slot container. |