# 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](/components/sidebar.md).

```tsx
// 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

```tsx
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](https://base-ui.com/react/components/collapsible), like [Progressive disclosure](/components/progressive-disclosure.md). 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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](/components/sidebar.md) 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:

```tsx
<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>
```

```ts
import {createCollapsible, createSideNavigation, enhance} from '@earendil-works/elanor-react';

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

Sections open and close like [Collapsible](/components/collapsible.md)'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`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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`.

### `SideNavigation.Link`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `active` | `boolean` | `false` | Marks the current page and sets `aria-current="page"`. |
| `iconStart` | `ReactNode` | - | Decorative icon before the label. |
| `iconEnd` | `ReactNode` | - | Decorative icon after the label. |
| `compactContent` | `ReactNode` | - | 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `persist` | `string` | - | Keeps the navigation's scroll position and its sections' `persist`ed states across pages, in session storage under this key. |
| `data-elanor` | `string` | - | 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`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `persist` | `string` | - | Remembers whether the section is open under this name, with the root's `persist`. |
| `data-elanor` | `string` | - | The page's own behaviors for this element, attached after the component's. |

Also accepts Base UI [`Collapsible.Root`](https://base-ui.com/react/components/collapsible.md) props.

### `SideNavigation.StaticCollapsibleTrigger`

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

### `SideNavigation.StaticCollapsiblePanel`

Accepts Base UI [`Collapsible.Panel`](https://base-ui.com/react/components/collapsible.md) 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`](https://base-ui.com/react/components/collapsible.md) props.

### `SideNavigation.CollapsibleTrigger`

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

Accepts Base UI [`Collapsible`](https://base-ui.com/react/components/collapsible.md) 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | `true` | Whether to keep the element in the DOM while the panel is hidden. |

Also accepts Base UI [`Collapsible.Panel`](https://base-ui.com/react/components/collapsible.md) props.
