# Side Nav

A grouped list of links that turns into a scrolling row on small screens.

Source: [side-nav.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/side-nav.tsx)

```tsx
import {
  BellIcon,
  BlocksIcon,
  CreditCardIcon,
  SettingsIcon,
  ShieldIcon,
  UserIcon,
  UserPlusIcon,
  UsersIcon,
} from "lucide-react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import {
  PageHeader,
  PageHeaderActions,
  PageHeaderContent,
  PageHeaderDescription,
  PageHeaderTitle,
} from "@/components/ui/page-header";
import { SideNav, SideNavGroup, SideNavItem } from "@/components/ui/side-nav";

const groups = [
  {
    label: "Account",
    items: [
      { id: "profile", label: "Profile", icon: UserIcon },
      { id: "security", label: "Security", icon: ShieldIcon },
      { id: "notifications", label: "Notifications", icon: BellIcon },
    ],
  },
  {
    label: "Organization",
    items: [
      { id: "general", label: "General", icon: SettingsIcon },
      { id: "members", label: "Members", icon: UsersIcon },
      { id: "billing", label: "Billing", icon: CreditCardIcon },
      { id: "integrations", label: "Integrations", icon: BlocksIcon },
    ],
  },
];

const pages: Record<string, { description: string; action?: React.ReactNode }> = {
  profile: { description: "How you appear to your teammates.", action: "Save" },
  security: { description: "Keep your account safe." },
  notifications: { description: "Choose what we email you about.", action: "Save" },
  general: { description: "Your workspace's name, address and language.", action: "Save" },
  members: {
    description: "6 people have access to Acme.",
    action: (
      <>
        <UserPlusIcon />
        Invite
      </>
    ),
  },
  billing: { description: "You're on the Team plan, billed monthly." },
  integrations: { description: "Connect Acme to the tools your team uses." },
};

export default function SideNavSettingsPage() {
  const [active, setActive] = React.useState("general");
  const title = groups.flatMap((group) => group.items).find((item) => item.id === active)?.label;
  const { description, action } = pages[active];

  return (
    <div className="grid w-full grid-cols-1 gap-6 lg:grid-cols-[11rem_minmax(0,1fr)]">
      <SideNav aria-label="Settings">
        {groups.map((group) => (
          <SideNavGroup key={group.label} label={group.label}>
            {group.items.map(({ id, label, icon: Icon }) => (
              <SideNavItem
                key={id}
                href="#"
                active={active === id}
                icon={<Icon />}
                onClick={(event) => {
                  event.preventDefault();
                  setActive(id);
                }}
              >
                {label}
              </SideNavItem>
            ))}
          </SideNavGroup>
        ))}
      </SideNav>
      <PageHeader size="sm" className="self-start">
        <PageHeaderContent>
          <PageHeaderTitle render={<h3 />}>{title}</PageHeaderTitle>
          <PageHeaderDescription>{description}</PageHeaderDescription>
        </PageHeaderContent>
        {action != null && (
          <PageHeaderActions>
            <Button variant="secondary" size="sm">
              {action}
            </Button>
          </PageHeaderActions>
        )}
      </PageHeader>
    </div>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/side-nav
```

## Usage

```tsx
import { SideNav, SideNavGroup, SideNavItem } from "@/components/ui/side-nav";

export function SettingsNav() {
  return (
    <SideNav aria-label="Settings">
      <SideNavGroup label="Account">
        <SideNavItem href="/settings/profile" active>
          Profile
        </SideNavItem>
        <SideNavItem href="/settings/security">Security</SideNavItem>
      </SideNavGroup>
      <SideNavGroup label="Organization">
        <SideNavItem href="/settings/members">Members</SideNavItem>
        <SideNavItem href="/settings/billing">Billing</SideNavItem>
      </SideNavGroup>
    </SideNav>
  );
}
```

Give `SideNav` an `aria-label` and set `active` on the current page's item. Below the `lg`
breakpoint the list becomes a scrolling row, so place it above the content on small screens. The
active item scrolls into view, and the edges fade wherever there's more to scroll. To keep one
layout at every width, set `orientation`.

The active item announces itself as the current page. For links to sections of one page, pass
`aria-current="location"` along with `active`.

To use your router's link, pass it to `render` on `SideNavItem`, as in
`render={<Link to="/settings/profile" />}`.

## Composition

- `SideNav`
  - `SideNavGroup`
    - `SideNavItem`

## Examples

### Without groups

Put `SideNavItem`s directly in `SideNav` for a short list.

```tsx
import * as React from "react";

import { SideNav, SideNavItem } from "@/components/ui/side-nav";

const pages = ["Overview", "Activity", "Deployments", "Analytics", "Logs", "Settings"];

export default function SideNavWithoutGroups() {
  const [active, setActive] = React.useState("Overview");

  return (
    <SideNav aria-label="Project" className="w-full lg:w-44">
      {pages.map((page) => (
        <SideNavItem
          key={page}
          href="#"
          active={active === page}
          onClick={(event) => {
            event.preventDefault();
            setActive(page);
          }}
        >
          {page}
        </SideNavItem>
      ))}
    </SideNav>
  );
}
```

### Orientation

`orientation` is `responsive` by default, a row below `lg` and a column from it. Set it to
`horizontal` to keep the scrolling row at every width, as in a bar of a page's sections, or to
`vertical` to keep the column, as in a menu drawer on a phone.

```tsx
import * as React from "react";

import { SideNav, SideNavItem } from "@/components/ui/side-nav";
import { preventNavigation } from "@/examples/data";

const pages = ["Overview", "Activity", "Deployments", "Analytics", "Logs", "Settings"];

export default function SideNavOrientation() {
  const [active, setActive] = React.useState("Overview");

  const items = pages.map((page) => (
    <SideNavItem
      key={page}
      href="#"
      active={active === page}
      onClick={(event) => {
        preventNavigation(event);
        setActive(page);
      }}
    >
      {page}
    </SideNavItem>
  ));

  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <SideNav aria-label="Project, as a row" orientation="horizontal">
        {items}
      </SideNav>
      <SideNav aria-label="Project, as a column" orientation="vertical" className="w-44">
        {items}
      </SideNav>
    </div>
  );
}
```

### Scrolling

Give `SideNav` a height to scroll a long list in the sidebar. It's a
[Scroll Area](https://ui.jarv.is/components/scroll-area.md), so it fades at the edges and shows a scrollbar on hover.

```tsx
import * as React from "react";

import { SideNav, SideNavGroup, SideNavItem } from "@/components/ui/side-nav";

const groups = [
  { label: "Account", pages: ["Profile", "Security", "Notifications", "Connected apps"] },
  { label: "Organization", pages: ["General", "Members", "Teams", "Roles", "Billing"] },
  { label: "Developers", pages: ["API keys", "Webhooks", "Audit log"] },
];

export default function SideNavScrolling() {
  const [active, setActive] = React.useState("Webhooks");

  return (
    <SideNav aria-label="Settings" className="w-full lg:h-64 lg:w-44">
      {groups.map((group) => (
        <SideNavGroup key={group.label} label={group.label}>
          {group.pages.map((page) => (
            <SideNavItem
              key={page}
              href="#"
              active={active === page}
              onClick={(event) => {
                event.preventDefault();
                setActive(page);
              }}
            >
              {page}
            </SideNavItem>
          ))}
        </SideNavGroup>
      ))}
    </SideNav>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `SideNav` | [`ScrollArea`](https://ui.jarv.is/components/scroll-area.md) |
| `SideNavGroup` | `<div>` |
| `SideNavItem` | `<a>` |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### SideNav

| Prop                     | Type                                                                                 | Description                                                                                                                                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `overflowEdgeThreshold?` | `number \| Partial<{ xStart: number; xEnd: number; yStart: number; yEnd: number; }>` | The threshold in pixels that must be passed before the overflow edge attributes are applied. Accepts a single number for all edges or an object to configure them individually. Default: `0`                                    |
| `scrollFade?`            | `boolean`                                                                            | Fades the content out at any edge it can scroll past, and keeps focused content clear of the fade. Set `--scroll-fade-size` on the root to change the fade (1.5rem by default), or pass `false` to turn it off. Default: `true` |
| `orientation?`           | `"horizontal" \| "responsive" \| "vertical"`                                         | Lays the list out in a column, in a scrolling row, or in a row below the `lg` breakpoint and a column from it. Default: `"responsive"`                                                                                          |

### SideNavGroup

| Prop     | Type              | Description |
| -------- | ----------------- | ----------- |
| `label?` | `React.ReactNode` |             |

### SideNavItem

| Prop      | Type              | Description                                                                                                                                                                                |
| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `active?` | `boolean`         | Highlights the item, scrolls it into view and sets `aria-current="page"`. Pass `aria-current` to announce something else, such as `"location"` for a section of the page. Default: `false` |
| `icon?`   | `React.ReactNode` |                                                                                                                                                                                            |
