Skip to main content

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 install @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 widthApp-owned navigation surface
<600px compactrc-navigation-bar in the bottom app chrome
>=600px mediumcollapsed rc-navigation-rail in the leading app chrome
>=840px expandedrc-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

PropertyMarkupTypeDefaultDescription
expandedexpandedbooleanNot specifiedWhether the rail is expanded. Host writes are silent.
defaultExpandeddefault-expandedbooleanNot specifiedInitial expanded state for uncontrolled usage.
activeSelectoractive-selectorstring'a[aria-current]:not([aria-current="false"])'Selector used to find the active link. Defaults to `aria-current`.
indicatorTargetindicator-targetstring'[data-rc-navigation-indicator], [data-rc-navigation-icon]'Selector inside the active link used for indicator geometry.

Methods

MethodDescription
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

EventDetail typeDescription
rc-navigation-rail-toggleCustomEventFired when user interaction or a method toggles expanded state.

Slots

NameDescription
defaultNavigation links. Direct `<a>` children are recommended.
headerContent above the navigation links.
footerContent pinned after the navigation links.
toggleNative `<button>` or `rc-button` expand/collapse control.

CSS Custom Properties

PropertyDefaultDescription
--rc-navigation-rail-bgCanvasRail surface background.
--rc-navigation-rail-colorCanvasTextRail text color.
--rc-navigation-rail-inline-size5remCollapsed rail inline size.
--rc-navigation-rail-expanded-inline-size16remExpanded rail inline size.
--rc-navigation-rail-padding-block0.75remRail block-axis padding.
--rc-navigation-rail-padding-inline0.5remRail inline-axis padding.
--rc-navigation-rail-gap0.75remGap between rail regions.
--rc-navigation-rail-item-gap0.75remGap between navigation items.
--rc-navigation-rail-link-gap0.25remGap between item icon and label.
--rc-navigation-rail-item-min-block-size3.5remCollapsed item minimum block size.
--rc-navigation-rail-expanded-item-min-block-size3.5remExpanded item minimum block size.
--rc-navigation-rail-item-padding-block0.25remItem block-axis padding.
--rc-navigation-rail-item-padding-inline0.5remCollapsed item inline-axis padding.
--rc-navigation-rail-expanded-item-padding-inline1remExpanded item inline-axis padding.
--rc-navigation-rail-collapsed-label-inline-size4remCollapsed label maximum inline size.
--rc-navigation-rail-item-colorinheritResting item text color.
--rc-navigation-rail-item-text-decorationNot specifiedSlotted link text decoration (defers to native anchor text-decoration when unset).
--rc-navigation-rail-active-colorNot specifiedActive item text color. Falls back to `--rc-navigation-rail-item-color`, then `inherit`.
--rc-navigation-rail-indicator-bgtransparentActive indicator background.
--rc-navigation-rail-indicator-border1px solid HighlightActive indicator border.
--rc-navigation-rail-indicator-radius0Active indicator corner radius.
--rc-navigation-rail-toggle-size3remToggle region minimum block size.
--rc-navigation-rail-toggle-inline-offset0.5remToggle control inline-start offset.
--rc-navigation-rail-duration200msRail expand/collapse transition duration.
--rc-navigation-rail-easingeaseRail expand/collapse transition easing.
--rc-navigation-rail-indicator-duration0msActive indicator transition duration.
--rc-navigation-rail-indicator-easingeaseActive indicator transition easing.
--rc-navigation-rail-focus-ringNot specifiedSlotted link focus outline (defers to native outline when unset).
--rc-navigation-rail-focus-ring-offset2pxSlotted link outline offset.

CSS Parts

PartDescription
rootThe rail layout container.
navThe navigation layout container.
indicatorThe active item indicator.
toggleToggle slot container.
headerHeader slot container.
footerFooter slot container.