Skip to content

Side navigation

A vertical list of links between the main areas of an app or documentation site, optionally in labeled groups. For a collapsible app sidebar, put it in a Sidebar.

Code
demo.tsx
import {SideNavigation} from '@earendil-works/elanor-react';

export default function SideNavigationDemo() {
  return (
    <SideNavigation.Root aria-label="Workspace">
      <SideNavigation.Group>
        <SideNavigation.GroupLabel>Workspace</SideNavigation.GroupLabel>
        <SideNavigation.List>
          <SideNavigation.Item>
            <SideNavigation.Link href="#" active>
              Projects
            </SideNavigation.Link>
          </SideNavigation.Item>
          <SideNavigation.Item>
            <SideNavigation.Link href="#">Members</SideNavigation.Link>
          </SideNavigation.Item>
          <SideNavigation.Item>
            <SideNavigation.Link href="#">Settings</SideNavigation.Link>
          </SideNavigation.Item>
        </SideNavigation.List>
      </SideNavigation.Group>
    </SideNavigation.Root>
  );
}

Usage

import {SideNavigation} from '@earendil-works/elanor-react';

<SideNavigation.Root aria-label="Workspace">
  <SideNavigation.Group>
    <SideNavigation.GroupLabel />
    <SideNavigation.List>
      <SideNavigation.Item>
        <SideNavigation.Link href="/projects" active />
      </SideNavigation.Item>
    </SideNavigation.List>
  </SideNavigation.Group>
</SideNavigation.Root>;

Root renders a nav. active marks the current page with aria-current="page": it gets the active fill and semantic.color.text.primary at semantic.fontWeight.ui.selected; other links are semantic.color.text.secondary. Groups are optional. Long labels wrap to two lines, then truncate.

Sections

Collapsible holds a section that opens and closes, with a CollapsibleTrigger as its title and a CollapsiblePanel holding a List of links. It is built on Base UI’s Collapsible, like Progressive disclosure. Pass defaultOpen to the section that contains the current page, or control it with open and onOpenChange; its title is marked like the current page. Nested links indent their text by semantic.dimension.space.padding.xLarge.

The panel grows and shrinks to its measured height with semantic.transition.movement.disclosure, the same in every browser, and changes instantly when the reader prefers reduced motion. Closed panels stay in the document (keepMounted), so their links are part of the page’s HTML.

Code
sections.tsx
import {SideNavigation} from '@earendil-works/elanor-react';

export default function SideNavigationSections() {
  return (
    <SideNavigation.Root aria-label="Help center">
      <SideNavigation.List>
        <SideNavigation.Item>
          <SideNavigation.Link href="#">Introduction</SideNavigation.Link>
        </SideNavigation.Item>
        <SideNavigation.Item>
          <SideNavigation.Collapsible defaultOpen>
            <SideNavigation.CollapsibleTrigger>Guides</SideNavigation.CollapsibleTrigger>
            <SideNavigation.CollapsiblePanel>
              <SideNavigation.List>
                <SideNavigation.Item>
                  <SideNavigation.Link href="#">Getting started</SideNavigation.Link>
                </SideNavigation.Item>
                <SideNavigation.Item>
                  <SideNavigation.Link href="#" active>
                    Deploying a project
                  </SideNavigation.Link>
                </SideNavigation.Item>
              </SideNavigation.List>
            </SideNavigation.CollapsiblePanel>
          </SideNavigation.Collapsible>
        </SideNavigation.Item>
        <SideNavigation.Item>
          <SideNavigation.Collapsible>
            <SideNavigation.CollapsibleTrigger>Reference</SideNavigation.CollapsibleTrigger>
            <SideNavigation.CollapsiblePanel>
              <SideNavigation.List>
                <SideNavigation.Item>
                  <SideNavigation.Link href="#">Command-line tool</SideNavigation.Link>
                </SideNavigation.Item>
                <SideNavigation.Item>
                  <SideNavigation.Link href="#">Configuration file</SideNavigation.Link>
                </SideNavigation.Item>
              </SideNavigation.List>
            </SideNavigation.CollapsiblePanel>
          </SideNavigation.Collapsible>
        </SideNavigation.Item>
      </SideNavigation.List>
    </SideNavigation.Root>
  );
}

Icons

iconStart and iconEnd add decorative icons; the label names the link.

Code
icons.tsx
import {FolderOpenIcon, GroupIcon, SettingsIcon} from '@earendil-works/elanor-icons-react';
import {SideNavigation} from '@earendil-works/elanor-react';

export default function SideNavigationIcons() {
  return (
    <SideNavigation.Root aria-label="Workspace">
      <SideNavigation.List>
        <SideNavigation.Item>
          <SideNavigation.Link href="#" active iconStart={<FolderOpenIcon />}>
            Projects
          </SideNavigation.Link>
        </SideNavigation.Item>
        <SideNavigation.Item>
          <SideNavigation.Link href="#" iconStart={<GroupIcon />}>
            Members
          </SideNavigation.Link>
        </SideNavigation.Item>
        <SideNavigation.Item>
          <SideNavigation.Link href="#" iconStart={<SettingsIcon />}>
            Settings
          </SideNavigation.Link>
        </SideNavigation.Item>
      </SideNavigation.List>
    </SideNavigation.Root>
  );
}

Compact

variant="compact" shows only icons, with each label in a tooltip on hover and focus. Every link needs iconStart, iconEnd, or compactContent, such as initials; otherwise it throws. Labels and group labels stay available to screen readers.

Links with plain text are named from their text; give links with rich content an aria-label. Keep text-only documentation navigation expanded.

Code
compact.tsx
import {FolderOpenIcon, GroupIcon, SettingsIcon} from '@earendil-works/elanor-icons-react';
import {SideNavigation} from '@earendil-works/elanor-react';

export default function SideNavigationCompact() {
  return (
    <SideNavigation.Root aria-label="Workspace" variant="compact">
      <SideNavigation.Group>
        <SideNavigation.GroupLabel>Workspace</SideNavigation.GroupLabel>
        <SideNavigation.List>
          <SideNavigation.Item>
            <SideNavigation.Link href="#" active iconStart={<FolderOpenIcon />}>
              Projects
            </SideNavigation.Link>
          </SideNavigation.Item>
          <SideNavigation.Item>
            <SideNavigation.Link href="#" iconStart={<GroupIcon />}>
              Members
            </SideNavigation.Link>
          </SideNavigation.Item>
          <SideNavigation.Item>
            <SideNavigation.Link href="#" iconStart={<SettingsIcon />}>
              Settings
            </SideNavigation.Link>
          </SideNavigation.Item>
          <SideNavigation.Item>
            <SideNavigation.Link href="#" compactContent="AM">
              Alex Morgan
            </SideNavigation.Link>
          </SideNavigation.Item>
        </SideNavigation.List>
      </SideNavigation.Group>
    </SideNavigation.Root>
  );
}

Compact navigation is one link wide: semantic.dimension.size.medium (32px). Switching variant keeps the same links, so focus and tab order are preserved. Sidebar switches it when the sidebar collapses.

Static rendering

On pages rendered on the server without React in the browser, use the static root and sections; lists, items, and links stay the same. Bind the behaviors right after the navigation’s markup, so restored sections and the scroll position are in place before the first paint:

<SideNavigation.StaticRoot aria-label="Documentation" persist="docs">
  <SideNavigation.List>
    <SideNavigation.Item>
      <SideNavigation.StaticCollapsible defaultOpen persist="guides">
        <SideNavigation.StaticCollapsibleTrigger>Guides</SideNavigation.StaticCollapsibleTrigger>
        <SideNavigation.StaticCollapsiblePanel>
          <SideNavigation.List>…</SideNavigation.List>
        </SideNavigation.StaticCollapsiblePanel>
      </SideNavigation.StaticCollapsible>
    </SideNavigation.Item>
  </SideNavigation.List>
</SideNavigation.StaticRoot>
import {createCollapsible, createSideNavigation, enhance} from '@earendil-works/elanor-react';

enhance({'side-navigation': createSideNavigation, collapsible: createCollapsible});

Sections open and close like Collapsible’s static parts. With persist on the root, the navigation keeps its scroll position and the states of sections with a persist name across pages, in session storage; a section with the current page stays open, and a section that find in page opens is remembered open. With a client-side router that replaces the navigation, call the behaviors’ destroy() before the swap, so the scroll position is saved, and enhance again after it. Where the browser blocks session storage, as with cookies blocked, or it is full, the navigation reports the error and works without keeping its state. The current page’s link scrolls into view when it is hidden.

API

SideNavigation.Root

PropTypeDefaultDescription
variant'default' | 'compact''default'compact shows only each link's icon or compactContent, with the label in a tooltip. Every link then needs one of them.

Renders <nav>. Also accepts its HTML attributes and ref.

SideNavigation.Group

Renders <div>. Accepts its HTML attributes and ref.

SideNavigation.GroupLabel

Renders <div>. Accepts its HTML attributes and ref.

SideNavigation.List

Renders <ul>. Accepts its HTML attributes and ref.

SideNavigation.Item

Renders <li>. Accepts its HTML attributes and ref.

PropTypeDefaultDescription
activebooleanfalseMarks the current page and sets aria-current="page".
iconStartReactNode-Decorative icon before the label.
iconEndReactNode-Decorative icon after the label.
compactContentReactNode-Decorative content shown instead of the icon in compact navigation, such as initials.

Renders <a>. Also accepts its HTML attributes and ref.

SideNavigation.StaticRoot

A side navigation for pages without React in the browser; createSideNavigation restores it and scrolls the current page into view.

PropTypeDefaultDescription
persiststring-Keeps the navigation's scroll position and its sections' persisted states across pages, in session storage under this key.
data-elanorstring-The page's own behaviors for this element, attached after the component's.
variant'default' | 'compact'-compact shows only each link's icon or compactContent, with the label in a tooltip. Every link then needs one of them.

Also accepts HTML attributes and ref.

SideNavigation.StaticCollapsible

PropTypeDefaultDescription
persiststring-Remembers whether the section is open under this name, with the root's persist.
data-elanorstring-The page's own behaviors for this element, attached after the component's.

Also accepts Base UI Collapsible.Root props.

SideNavigation.StaticCollapsibleTrigger

Renders <button>. Accepts its HTML attributes and ref.

SideNavigation.StaticCollapsiblePanel

Accepts Base UI Collapsible.Panel props.

SideNavigation.Collapsible

A section of links that opens and closes. Put a CollapsibleTrigger and a CollapsiblePanel with a List inside, in an Item. Pass defaultOpen to the section that contains the current page, or control it with open and onOpenChange.

Accepts Base UI Collapsible.Root props.

SideNavigation.CollapsibleTrigger

The section's title, a button styled like a link with a chevron at the end.

Accepts Base UI Collapsible props.

SideNavigation.CollapsiblePanel

The section's links. It grows and shrinks to its measured height. Closed panels stay in the document by default (keepMounted), so their links are part of the page's HTML.

PropTypeDefaultDescription
keepMountedbooleantrueWhether to keep the element in the DOM while the panel is hidden.

Also accepts Base UI Collapsible.Panel props.