---
title: Nav
description: A nav tree with roving focus, typeahead, collapsible groups that animate, and a guide ladder down the left edge.
source: nav
---

```tsx title="primitives/nav/demos/basic.tsx"
"use client";

import { Nav } from "@intentface/chat/nav";
import { type ComponentProps, useState } from "react";

/*
 * Modelled on the hard case: a section group with no rail, a group two levels
 * in that has branches, plain indented lists nested inside it, an outer rail
 * carrying on past an expanded inner group, and a collapsed group as the last
 * child.
 *
 * All of it is one `Nav.Group` nesting inside itself. There is no second set of
 * parts for the nesting and no depth-aware CSS — the rail recipe below is a
 * single rule that works at any depth. The Root's `guide` is the default every
 * list inherits — "indent", a lane with nothing drawn in it — and lists opt
 * out with "none" or up to "branches" where the tree forks.
 *
 * Leaves are real links. `render` in its function form hands over everything
 * the part would have put on its own div — attributes, handlers, ref, children
 * — and you decide the element. (That form is also why this is a client
 * component: a function cannot cross the server boundary. The element form,
 * `render={<a href="…" />}`, can.)
 *
 * Written out in full rather than folded into a local Row component, so the
 * anatomy here is the anatomy of the primitive.
 */
export const Basic = () => {
  const [current, setCurrent] = useState("chat-views");

  return (
    <div className="nav-demo w-64 rounded-xl border border-[#f0f0f0] bg-white py-2 [--rail:#e4e4e4] dark:border-[#262626] dark:bg-[#111111] dark:[--rail:#2d2d2d]">
      <RailRecipe />
      <Nav.Root
        aria-label="Main"
        guide="indent"
        defaultExpanded={["teams", "intentface", "chat"]}
        render={<nav />}
        className="flex flex-col gap-0.5 px-2"
      >
        <Nav.List guide="none" className={listClass}>
          <Nav.Item
            value="overview"
            active={current === "overview"}
            className={rowClass}
            render={link("/overview", () => setCurrent("overview"))}
          >
            <Nav.Icon>
              <HomeIcon />
            </Nav.Icon>
            <Nav.Label className="min-w-0 truncate">Overview</Nav.Label>
          </Nav.Item>

          <Nav.Item
            value="inbox"
            active={current === "inbox"}
            className={rowClass}
            render={link("/inbox", () => setCurrent("inbox"))}
          >
            <Nav.Icon>
              <InboxIcon />
            </Nav.Icon>
            <Nav.Label className="min-w-0 truncate">Inbox</Nav.Label>
          </Nav.Item>

          {/* A section heading whose children are a plain indented list. `guide`
            is a prop rather than something derived from depth precisely so a
            railless group stays possible. */}
          <Nav.Group value="teams" className="mt-3">
            <Nav.Trigger className={rowClass}>
              <Nav.Label className="min-w-0 truncate">Your teams</Nav.Label>
              <Chevron />
            </Nav.Trigger>

            <Nav.List guide="none" className={listClass}>
              <Nav.Group value="intentface">
                <Nav.Trigger className={rowClass}>
                  <Nav.Icon>
                    <BoxIcon />
                  </Nav.Icon>
                  <Nav.Label className="min-w-0 truncate">Intentface</Nav.Label>
                  <Chevron />
                </Nav.Trigger>

                {/* Elbows, and only on groups — one at every leaf turns the rail
                  into a comb and buries where the tree actually forks. */}
                <Nav.List guide="branches" className={listClass}>
                  <Nav.Item
                    value="team-home"
                    active={current === "team-home"}
                    className={rowClass}
                    render={link("/intentface/home", () => setCurrent("team-home"))}
                  >
                    <Nav.Icon>
                      <HomeIcon />
                    </Nav.Icon>
                    <Nav.Label className="min-w-0 truncate">Home</Nav.Label>
                  </Nav.Item>

                  <Nav.Item
                    value="team-issues"
                    active={current === "team-issues"}
                    className={rowClass}
                    render={link("/intentface/issues", () => setCurrent("team-issues"))}
                  >
                    <Nav.Icon>
                      <InboxIcon />
                    </Nav.Icon>
                    <Nav.Label className="min-w-0 truncate">Issues</Nav.Label>
                  </Nav.Item>

                  {/* The rail above carries on past this whole group to its next
                    sibling — that is what the ::after on a group child is for.
                    The group's own list inherits "indent" and just indents. */}
                  <Nav.Group value="chat">
                    <Nav.Trigger className={rowClass}>
                      <Nav.Icon>
                        <ChatIcon />
                      </Nav.Icon>
                      <Nav.Label className="min-w-0 truncate">Chat</Nav.Label>
                      <Chevron />
                    </Nav.Trigger>

                    <Nav.List className={listClass}>
                      <Nav.Item
                        value="chat-home"
                        active={current === "chat-home"}
                        className={rowClass}
                        render={link("/intentface/chat/home", () => setCurrent("chat-home"))}
                      >
                        <Nav.Label className="min-w-0 truncate">Home</Nav.Label>
                      </Nav.Item>
                      <Nav.Item
                        value="chat-views"
                        active={current === "chat-views"}
                        className={rowClass}
                        render={link("/intentface/chat/views", () => setCurrent("chat-views"))}
                      >
                        <Nav.Label className="min-w-0 truncate">Views</Nav.Label>
                      </Nav.Item>
                    </Nav.List>
                  </Nav.Group>

                  {/* Collapsed, and the last child — so the rail stops at its
                    elbow rather than running on into empty space. */}
                  <Nav.Group value="website">
                    <Nav.Trigger className={rowClass}>
                      <Nav.Icon>
                        <BoxIcon />
                      </Nav.Icon>
                      <Nav.Label className="min-w-0 truncate">Website</Nav.Label>
                      <Chevron />
                    </Nav.Trigger>

                    <Nav.List className={listClass}>
                      <Nav.Item
                        value="site-home"
                        active={current === "site-home"}
                        className={rowClass}
                        render={link("/website/home", () => setCurrent("site-home"))}
                      >
                        <Nav.Label className="min-w-0 truncate">Home</Nav.Label>
                      </Nav.Item>
                    </Nav.List>
                  </Nav.Group>
                </Nav.List>
              </Nav.Group>
            </Nav.List>
          </Nav.Group>
        </Nav.List>
      </Nav.Root>
    </div>
  );
};

/**
 * Every leaf is a real anchor — that is the point of the function form. A real
 * app hands it a route and lets the browser navigate; this one is a demo on a
 * docs page, so it keeps the href for the semantics and stops the jump.
 */
const link = (href: string, onSelect: () => void) => (props: ComponentProps<"a">) => (
  <a
    {...props}
    href={href}
    onClick={(event) => {
      event.preventDefault();
      onSelect();
      props.onClick?.(event);
    }}
  />
);

const listClass = "flex flex-col gap-0.5";

/*
 * One row style for leaves and group headings alike — in a sidebar they are the
 * same thing you click, and the only visible difference is the chevron.
 *
 * The explicit height is load-bearing: 14px text has a fractional line-height,
 * so padded rows land on a fraction of a pixel and nothing lines up. And no
 * `truncate` here — `overflow: hidden` on a row would clip the ::before and
 * ::after that draw the rail, which sit outside its box. The label truncates
 * instead, which is what a separate part is for.
 */
const rowClass = [
  "group/row flex h-8 shrink-0 cursor-pointer select-none items-center gap-2 rounded-md px-2 text-sm",
  // No `outline-none` here: it sets --tw-outline-style: none, and the
  // focus-visible ring below resolves its style from that very variable — so
  // the ring would be 2px of nothing.
  "text-[#686868] no-underline transition-colors dark:text-[#9b9b9b]",
  "hover:bg-[#f4f4f4] hover:text-[#1a1a1a] dark:hover:bg-[#232323] dark:hover:text-[#fcfcfc]",
  "focus-visible:-outline-offset-2 focus-visible:outline-2 focus-visible:outline-[#1a1a1a] dark:focus-visible:outline-[#fcfcfc]",
  "data-[active]:bg-[#ececec] data-[active]:text-[#1a1a1a] dark:data-[active]:bg-[#2d2d2d] dark:data-[active]:text-[#fcfcfc]",
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-40",
  "[&_svg]:size-4 [&_svg]:shrink-0",
].join(" ");

/**
 * The disclosure arrow. Not a part of the primitive — it is this sidebar's
 * convention, not the widget's. It rotates with the group it belongs to by
 * reading `data-closed` off the enclosing trigger, so nothing is threaded down.
 */
const Chevron = () => (
  <ChevronIcon className="ml-auto !size-3 text-[#949494] transition-transform group-data-[closed]/row:-rotate-90 dark:text-[#6f6f6f]" />
);

/*
 * The rail, its branches and the collapse. The package publishes the signal
 * (`data-rail`, `data-branches`, `data-open`); the geometry is yours. Copy it
 * and change the numbers:
 *
 *   15px  hangs the rail under the centre of a size-4 icon at px-2, so it drops
 *         out of the parent's icon rather than beside it.
 *   7px   the rail's lane. It is the list's padding rather than the row's,
 *         because an active row paints a background and would cover a line
 *         drawn inside its own box.
 *   6px   the elbow's corner radius. The curve pulls the vertical away 6px
 *         early, so the continuation starts 6px above the row's centre.
 *   2px   the gap between rows, bridged so the line reads as unbroken.
 */
const RailRecipe = () => (
  <style>{`
.nav-demo [data-nav-group] > [data-nav-list] {
  height: var(--nav-list-height);
  overflow: hidden;
  opacity: 1;
  transition: height 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms ease-out;
}
.nav-demo [data-nav-list][data-starting-style],
.nav-demo [data-nav-list][data-ending-style] { height: 0; opacity: 0; }
.nav-demo [data-nav-list] > * { flex-shrink: 0; }
.nav-demo [data-nav-list][data-indent] {
  --nav-row: 2rem;
  margin-top: 2px;
  margin-left: 15px;
  padding-left: 7px;
}
.nav-demo [data-nav-list][data-rail] > * { position: relative; overflow: visible; }
.nav-demo [data-nav-list][data-rail] > *::before,
.nav-demo [data-nav-list][data-rail] > *::after {
  content: "";
  position: absolute;
  left: -7px;
  border-color: var(--rail);
  border-left-width: 1px;
}
.nav-demo [data-nav-list][data-rail] > *::before {
  top: -2px;
  width: 6px;
  height: calc(var(--nav-row) / 2 + 2px);
}
.nav-demo [data-nav-list][data-rail] > *::after {
  top: calc(var(--nav-row) / 2 - 6px);
  bottom: -2px;
}
.nav-demo [data-nav-list][data-rail] > *:last-child::after { display: none; }
.nav-demo [data-nav-list][data-branches] > [data-nav-group]::before {
  border-bottom-width: 1px;
  border-bottom-left-radius: 6px;
}
@media (prefers-reduced-motion: reduce) {
  .nav-demo [data-nav-group] > [data-nav-list] { transition: none; }
}
`}</style>
);

const HomeIcon = (props: ComponentProps<"svg">) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.3"
    aria-hidden="true"
    {...props}
  >
    <path d="M2.5 6.5 8 2.5l5.5 4v6a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-6Z" strokeLinejoin="round" />
  </svg>
);

const InboxIcon = (props: ComponentProps<"svg">) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.3"
    aria-hidden="true"
    {...props}
  >
    <path d="M2.5 8.5h3l1 2h3l1-2h3v3a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-3Z" strokeLinejoin="round" />
    <path
      d="M2.5 8.5l1.6-4.2a1 1 0 0 1 .94-.65h5.92a1 1 0 0 1 .94.65l1.6 4.2"
      strokeLinejoin="round"
    />
  </svg>
);

const BoxIcon = (props: ComponentProps<"svg">) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.3"
    aria-hidden="true"
    {...props}
  >
    <rect x="2.5" y="2.5" width="11" height="11" rx="2.5" strokeLinejoin="round" />
  </svg>
);

const ChatIcon = (props: ComponentProps<"svg">) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.3"
    aria-hidden="true"
    {...props}
  >
    <path
      d="M13.5 8.5a4.5 4.5 0 0 1-4.5 4.5H6l-3 2v-2.6A4.5 4.5 0 0 1 6 4h3a4.5 4.5 0 0 1 4.5 4.5Z"
      strokeLinejoin="round"
    />
  </svg>
);

const ChevronIcon = (props: ComponentProps<"svg">) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="m4 6.5 4 4 4-4" />
  </svg>
);
```

## Usage guidelines

- **Recursive by construction** — a group's list may hold further groups, to any depth, with no second set of parts for the nesting.
- **One tab stop** — the whole tree is a single tab stop; arrow keys move focus between rows, and typing seeks a row by its label.
- **Rows hold actions** — `Nav.Item` is a `div` with `role="button"` rather than an anchor, so a menu affordance can sit inside it. Swap in a real link with `render`.
- **Depth is a variable** — each list publishes its own `--nav-depth`, so one indent rule covers every level.
- **Persists nothing itself** — the open set goes out through `onExpandedChange` and comes back as `defaultExpanded`, so where it is kept is yours.
- **Get started** — see [Quick start](/quick-start) to add the package.

## Anatomy

```tsx
<Nav.Root>
  <Nav.List>
    <Nav.Item>
      <Nav.Icon />
      <Nav.Label />
      <Nav.Action />
    </Nav.Item>
    <Nav.Group>
      <Nav.Trigger>
        <Nav.Label />
      </Nav.Trigger>
      <Nav.List />
    </Nav.Group>
  </Nav.List>
</Nav.Root>
```

A sidebar tree with a rail, one branch open on load, and rows that route:

```tsx
<Nav.Root guide="rail" defaultExpanded={stored ?? ["docs"]} onExpandedChange={save}>
  <Nav.List>
    <Nav.Item value="overview" active={pathname === "/"} render={<Link href="/" />}>
      <Nav.Label>Overview</Nav.Label>
    </Nav.Item>
    <Nav.Group value="docs">
      <Nav.Trigger>
        <Nav.Label>Documentation</Nav.Label>
      </Nav.Trigger>
      <Nav.List>
        <Nav.Item value="quick-start" render={<Link href="/quick-start" />}>
          <Nav.Label>Quick start</Nav.Label>
        </Nav.Item>
      </Nav.List>
    </Nav.Group>
  </Nav.List>
</Nav.Root>
```

## The guide ladder

What a list draws down its left edge is a ladder rather than three independent
flags, because each rung implies the one before it — a rail lives in the indent
lane, and an elbow needs a rail to turn off. Expressing them as booleans would
mean guarding against combinations that mean nothing.

export const guides = [
  { value: '"none"', default: true, description: "No indent lane and no attributes." },
  { value: '"indent"', description: "Emits data-indent — the lane exists, nothing is drawn in it." },
  { value: '"rail"', description: "Emits data-indent and data-rail — a line down the lane." },
  { value: '"branches"', description: "Emits data-indent, data-rail and data-branches — elbows off the rail." },
];

<ValuesTable rows={guides} />

The attributes are cumulative on purpose: a stylesheet asking for
`[data-rail]` gets branches too, which is what keeps the ladder readable in CSS
without wrapping every selector in `:is()`. Set the default on `Nav.Root` and
override it per list.

## Collapsing

`height: auto` is not interpolable, so a collapse can only animate between
lengths — but a list pinned to a length permanently could not hold a group that
expands inside it, and would clip whatever just opened. So the pin is
temporary: `--nav-list-height` holds a measured pixel value while a transition
runs and is released the moment the opening one finishes. An open, settled list
is `auto` and grows freely.

```css
[data-nav-group] > [data-nav-list] {
  height: var(--nav-list-height);
  overflow: hidden;
  opacity: 1;
  /* Not ease-out: the last row is the bottom sliver of the height, and
     ease-out spends its whole tail crawling through exactly that stretch —
     which reads as the row popping in at the end. */
  transition:
    height 200ms cubic-bezier(0.4, 0, 0.2, 1),
    opacity 200ms ease-out;
}

[data-nav-list][data-starting-style],
[data-nav-list][data-ending-style] {
  height: 0;
  opacity: 0;
}

/* The collapsing list squeezes to nothing, and a flex item shrinks below its
   own height when the column runs short — so without this the rows compress
   instead of sliding up behind the clip. */
[data-nav-list] > * {
  flex-shrink: 0;
}
```

With the variable released, `height: var(--nav-list-height)` is invalid at
computed-value time and `height` lands back on `auto` — which is exactly what a
settled list wants, without inheriting anything from an ancestor.

## The rail recipe

The package publishes the signal; the geometry is yours. This is the recipe
rather than a stylesheet you import — copy it and change the numbers:

```css
/* The lane. `indent` is the first rung and every rung above it emits this too,
   so one rule sizes the lane for all of them. */
[data-nav-list][data-indent] {
  /* Where the elbow aims. Not 50% of the child: a child may itself be a group
     several rows tall. */
  --nav-row: 2rem;

  margin-top: 2px;
  margin-left: 15px;
  padding-left: 7px;
}

/* Both halves are laid out unconditionally and only the borders switch on: a
   box with no edges drawn takes no space and paints nothing. */
[data-nav-list][data-rail] > *::before,
[data-nav-list][data-rail] > *::after {
  content: "";
  position: absolute;
  left: -7px;
  border-color: var(--rail-color);
  border-left-width: 1px;
}

[data-nav-list][data-rail] > * {
  position: relative;
  /* Never `hidden`: these pseudo-elements sit outside the row's own box, so
     clipping them erases the rail. Truncate the label instead. */
  overflow: visible;
}

/* The top half: down to the row's centre, 6px wide so an elbow fits. */
[data-nav-list][data-rail] > *::before {
  top: -2px;
  width: 6px;
  height: calc(var(--nav-row) / 2 + 2px);
}

/* …and the rail carries on to the next row. Never past the last one: a line
   running into empty space reads as a list that got cut off. */
[data-nav-list][data-rail] > *::after {
  top: calc(var(--nav-row) / 2 - 6px);
  bottom: -2px;
}

[data-nav-list][data-rail] > *:last-child::after {
  display: none;
}

/* The elbow — only on groups. One at every leaf turns the rail into a comb and
   buries the thing worth spotting, which is where the tree forks. Group and
   Item carry different identity attributes precisely so CSS can tell them
   apart without the package taking a view. */
[data-nav-list][data-branches] > [data-nav-group]::before {
  border-bottom-width: 1px;
  border-bottom-left-radius: 6px;
}
```

The four numbers are all load-bearing: **15px** hangs the rail under the centre
of a `size-4` icon at `px-2`, so it drops out of the parent's icon rather than
beside it; **7px** is the lane, and it is the *list's* padding rather than the
row's, because an active row paints a background and would cover a line drawn
inside its own box; **6px** is the elbow's radius, and the curve pulls the
vertical away that early, so the continuation has to start 6px above the row's
centre or every branch leaves a radius of rail missing; **2px** is the row gap,
bridged so the line reads as unbroken.

One more thing the recipe depends on: give rows an **explicit height**. Text at
a fractional line-height makes a padded row land on a fraction of a pixel, and
then nothing in the tree lines up — `--nav-row` has to match whatever height
you set.

## Keyboard

These are the widget's own keys, not global ones: the handler sits on
`Nav.Root`, so nothing fires unless focus is already inside the tree. Roving
focus is the reason to reach for a headless nav in the first place.

export const keys = [
  { keys: "Arrow up / down", description: "Move focus to the previous or next visible row. Collapsed lists are unmounted, so their rows are simply not there to land on." },
  { keys: "Arrow right", description: "On a closed group, open it; on an open one, step into it. On a leaf, nothing." },
  { keys: "Arrow left", description: "On an open group, close it; otherwise step out to the parent group's trigger — which is where the row came from." },
  { keys: "Enter / Space", description: "Activate the focused row." },
  { keys: "Any letter", description: "Seek to the row whose label starts with what you typed. The query resets after half a second of no typing; turn the whole thing off with the typeahead prop." },
];

<KeysTable rows={keys} />

A field inside the nav keeps its own arrow keys — an input, a textarea, a
select, or anything `contenteditable` — or the caret could never move.

## useNav

Read which groups are open from anywhere inside `<Nav.Root>`:

```tsx
const isOpen = useNav((nav) => nav.expanded.has("docs"));
```

export const hookMembers = [
  { name: "expanded", type: "ReadonlySet<string>", description: "Which groups are open, by value." },
  { name: "toggle", type: "(value: string) => void", description: "Flip one group." },
  { name: "setOpen", type: "(value: string, open: boolean) => void", description: "Open or close one group explicitly." },
  { name: "setExpanded", type: "(expanded: Iterable<string>) => void", description: "Replace the whole open set." },
];

<PropsTable rows={hookMembers} />

`useNavStore(store, selector)` is the outside-the-tree twin, taking an explicit
`Nav.createStore()` handle. There is no global fallback.

## Persistence

Nothing is persisted here either. `onExpandedChange` reports the open set out
and `defaultExpanded` takes it back in, so where it is kept is yours:

```tsx
// A server component reads it before the first paint …
const stored = readNavState((await cookies()).toString());

// … and the tree reports every change back.
<Nav.Root
  defaultExpanded={stored ?? ["docs"]}
  onExpandedChange={(expanded) => writeNavState(expanded)}
>
```

Validate what comes back out of storage — `Array.isArray(x) && x.every((v) =>
typeof v === "string")` is the whole check for Nav — so a stale or hand-edited
value falls back to the default rather than reaching the tree. See
[Shell's persistence](/primitives/shell#persistence) for why the value has to
arrive as a prop rather than be read at init.

## Accessibility

The tree is a roving-tabindex widget: exactly one row is tabbable, and focus
follows the arrow keys. Before the roving state is seeded — a server render, or
the frame before hydration — the `active` row carries the tab stop, which keeps
a row rendered as a real anchor reachable without JavaScript.

`Nav.Trigger` carries `aria-expanded` and `aria-controls` pointing at the list
it governs. Disabled rows get `aria-disabled` rather than being removed from
the ring, so they stay discoverable. `Nav.Icon` is `aria-hidden`, keeping
decoration out of the row's accessible name. `Nav.Action` sits outside the
roving order and stops every event it handles — otherwise activating it would
also activate the row wrapped around it.

Enter and Space are wired by hand, which is the price of a row that can hold an
action rather than being a real `<button>`. A row that already activates itself
— a `<button>`, or an `<a href>` swapped in via `render` — is left alone
instead of being activated twice.

## API reference

Every part accepts `className`, `style`, and `render` (see
[Styling](/handbook/styling)) and emits a bespoke part attribute
(`data-<part>`) unless noted. Every part also carries `data-depth` and, when
nested inside a group, `data-nested`.

### Nav.Root

The provider and container. Renders `data-nav`, and owns the keyboard handling
for the whole tree.

export const rootProps = [
  { name: "defaultExpanded", type: "string[]", description: "Groups open when nothing controls them. Read it from the request so the first paint already has the right branches open." },
  { name: "expanded", type: "string[]", description: "Controlled open set." },
  { name: "onExpandedChange", type: "(expanded: string[]) => void", description: "Fires whenever a group opens or closes." },
  { name: "store", type: "NavStore", description: "An explicit Nav.createStore() handle. Must be stable for the Root's lifetime." },
  { name: "guide", type: "NavGuide", default: '"none"', description: "What every list draws down its left edge, unless it says otherwise." },
  { name: "loop", type: "boolean", default: "false", description: "Wrap at the ends when arrowing past the first or last row." },
  { name: "disabled", type: "boolean", default: "false", description: "Disable every row at once — a read-only view of the tree." },
  { name: "typeahead", type: "boolean", default: "true", description: "Seek a row by typing its label." },
];

<PropsTable rows={rootProps} />

export const rootAttrs = [
  { attribute: "data-nav", description: "The container." },
  { attribute: "data-depth", values: "0", description: "The Root is the top level, so its depth is always zero — spelled out rather than omitted, because `0` is falsy and the default derivation would drop it." },
];

<AttributesTable rows={rootAttrs} />

### Nav.List

A level of the tree. Renders `data-nav-list`. A list inside a group is that
group's collapsible panel; a top-level list is not collapsible, because there
is no trigger above it.

export const listProps = [
  { name: "guide", type: "NavGuide", description: "Overrides the Root's default for this list." },
  { name: "keepMounted", type: "boolean", default: "false", description: "Keep a closed list in the DOM behind `hidden`. Its rows are not navigable while hidden." },
];

<PropsTable rows={listProps} />

export const listAttrs = [
  { attribute: "data-nav-list", description: "The list." },
  { attribute: "data-depth", values: "number", description: "Nesting level; 0 is the top." },
  { attribute: "data-open", description: "Present while open." },
  { attribute: "data-closed", description: "Present while closed." },
  { attribute: "data-indent", description: "Present for every guide but none." },
  { attribute: "data-rail", description: "Present for rail and branches." },
  { attribute: "data-branches", description: "Present for branches only." },
  { attribute: "data-starting-style", description: "Present on the first open frame." },
  { attribute: "data-ending-style", description: "Present while the close animation runs." },
  { attribute: "--nav-depth", values: "number", description: "This list's depth, for one indent rule that covers every level." },
  { attribute: "--nav-list-height", values: "measured px", description: "The content's height, published only while a transition runs so the collapse has a number to animate between. Released once settled open, so the list tracks content that grows." },
  { attribute: "--nav-list-width", values: "measured px", description: "The same, for a horizontal collapse." },
];

<AttributesTable rows={listAttrs} />

### Nav.Group

A collapsible branch. Renders `data-nav-group` set to its `value`, and provides
the group context its trigger and list read.

export const groupProps = [
  { name: "value", type: "string", default: "(required)", description: "Identifies the group in the expanded set, and in persistence." },
  { name: "disabled", type: "boolean", description: "Disable this branch's rows." },
];

<PropsTable rows={groupProps} />

export const groupAttrs = [
  { attribute: "data-nav-group", values: "the group's value", description: "The branch." },
  { attribute: "data-open", description: "Present while open." },
  { attribute: "data-closed", description: "Present while closed." },
];

<AttributesTable rows={groupAttrs} />

### Nav.Trigger

The row that opens a group — its heading and its disclosure in one, because in
a sidebar they are the same thing you click. Renders `data-nav-trigger` set to
the group's value. Takes no `value`: it belongs to the group it is written
inside.

export const triggerProps = [
  { name: "active", type: "boolean", default: "false", description: "This branch is the one being shown." },
  { name: "disabled", type: "boolean", description: "Defaults to the group's own disabled state." },
];

<PropsTable rows={triggerProps} />

export const triggerAttrs = [
  { attribute: "data-nav-trigger", values: "the group's value", description: "The disclosure row, and its identity — the same channel `data-nav-item` uses, which is how one selector walks leaves and headings alike." },
  { attribute: "data-open", description: "Present while the group is open." },
  { attribute: "data-closed", description: "Present while it is closed." },
  { attribute: "data-active", description: "Present while active." },
  { attribute: "data-disabled", description: "Present while disabled." },
];

<AttributesTable rows={triggerAttrs} />

### Nav.Item

A leaf row. Renders `data-nav-item` set to its `value`.

export const itemProps = [
  { name: "value", type: "string", default: "(required)", description: "Identifies the row for roving focus and typeahead." },
  { name: "active", type: "boolean", default: "false", description: "The route this row points at is the one being shown." },
  { name: "disabled", type: "boolean", description: "Defaults to the Root's disabled state." },
];

<PropsTable rows={itemProps} />

export const itemAttrs = [
  { attribute: "data-nav-item", values: "the row's value", description: "The row, and its identity. Root's keyboard handling finds rows by this attribute and reads the value straight back off it — so the DOM is the row order, and no row has to register itself. Select it without the value for styling." },
  { attribute: "data-active", description: "Present while active." },
  { attribute: "data-disabled", description: "Present while disabled." },
];

<AttributesTable rows={itemAttrs} />

### Nav.Label

The row's text. Renders a `<span>` with `data-nav-label` — its own element so
it can truncate while the row does not. A row must never be `overflow: hidden`
itself, because the rail's pseudo-elements sit outside its box.

export const labelAttrs = [
  { attribute: "data-nav-label", description: "The text." },
  { attribute: "data-depth", values: "number", description: "The enclosing list's depth." },
  { attribute: "data-nested", description: "Present inside a group." },
];

<AttributesTable rows={labelAttrs} />

### Nav.Icon

Decoration. Renders a `<span>` with `data-nav-icon` and `aria-hidden`, so it
stays out of the row's accessible name.

export const iconAttrs = [
  { attribute: "data-nav-icon", description: "The icon slot." },
  { attribute: "data-depth", values: "number", description: "The enclosing list's depth." },
  { attribute: "data-nested", description: "Present inside a group." },
];

<AttributesTable rows={iconAttrs} />

### Nav.Action

A control inside a row — the affordance that opens a menu, say. Renders
`data-nav-action` as a `role="button"` with `tabIndex="-1"`: out of the roving
order, so arrowing walks rows rather than stopping at every affordance, and it
stops every event it handles — anything that escaped would activate the row on
its way out of opening the menu.

export const actionAttrs = [
  { attribute: "data-nav-action", description: "The control." },
  { attribute: "data-depth", values: "number", description: "The enclosing list's depth." },
  { attribute: "data-nested", description: "Present inside a group." },
];

<AttributesTable rows={actionAttrs} />
