Skip to content

Nav List

A nav list is the vertical list of links a sidebar is made of: sections with small uppercase labels, rows that light up where you are, groups that fold away, and nested levels stepped in behind a guide line. It is the navigation counterpart to Navigation Menu — that one is the horizontal bar with popup panels; this one is the column that stays put.

nav-list/basic.tsx

Import

import { NavList } from "@forte-ui/react";

NavList is a namespace of ten parts. The full anatomy, with every optional layer present:

<NavList.Root aria-label="Documentation">
  <NavList.Section>
    <NavList.SectionLabel>Components</NavList.SectionLabel>
    <NavList.List>
      <NavList.Item>
        <NavList.Link href="/button" active>Button</NavList.Link>
      </NavList.Item>
      <NavList.Group defaultOpen>
        <NavList.GroupTrigger>Forms</NavList.GroupTrigger>
        <NavList.GroupPanel>
          <NavList.List>{/* nested Items */}</NavList.List>
        </NavList.GroupPanel>
      </NavList.Group>
    </NavList.List>
  </NavList.Section>
</NavList.Root>

Most lists need less. Section and SectionLabel exist for a list with titled blocks; a flat list is Root → List → Item → Link and nothing else.

Give Root an aria-label. A page routinely carries several navigation landmarks — the header, the sidebar, a table of contents — and an unnamed one cannot be told from the others in a screen reader's landmark list.

Examples

Sections

Section wraps a label and a list into one role="group", named by its SectionLabel — a screen reader announces "Getting started, group" instead of a run of links with a stray line of text above them. The label is deliberately not a heading: a sidebar sits beside a document with its own outline, and a run of <h2>s in the chrome would interleave with the page's real ones.

The demo at the top of the page is this shape. Sections do not collapse — they are structure, not state. For a block the reader can fold away, use a group.

Collapsible groups

Group is a collapsible subtree: a GroupTrigger styled like a row, and a GroupPanel holding a nested List. It renders an <li>, so it sits in a list as a sibling of ordinary items, and it nests — a group inside a group's panel steps in one more level, each level behind its own guide line.

nav-list/groups.tsx

State is Base UI's Collapsible contract: defaultOpen uncontrolled, open plus onOpenChange controlled. The trigger takes its own active prop for the "the current page is in here" cue — styling only, no aria-current, because the current page is a link and the trigger is the button that reveals it. The demo lights it only while the group is closed over the active row.

The panel keeps its links in the DOM while closed — keepMounted defaults to on, the opposite of Base UI's default, because these are navigation links: unmounted, they are invisible to crawlers and to find-in-page. The hidden attribute keeps the closed copy out of the accessibility tree and the tab order either way.

The edge marker

marker="edge" on the root paints an accent border along the active row's inline-start edge, on top of the fill rather than instead of it — a second cue that is not colour-on-colour. Because it is the row's own border, it follows the row's corner radius: a straight bar at data-forte-radius="none", a crescent hugging the cap at pill.

nav-list/edge.tsx

Sizes

Three sizes, set once on the root. Row padding follows data-forte-density at md, so a compact app gets a compact md for free.

nav-list/sizes.tsx

Icons, badges, disabled rows

Rows are flex, so an icon goes in as a plain child in front of the text and rides currentColor through rest, hover and active. NavList.Badge is a small pill that walks itself to the row's far edge with margin-inline-start: auto — it reads in the row's flow, so a screen reader announces "Insights, New" with no extra wiring.

nav-list/icons-and-badges.tsx

A disabled link drops its href — an anchor without one is unfocusable and inert — sets aria-disabled, and swallows clicks.

Routing

Framework links go in through render, which replaces the rendered <a> without losing the row's styling and states. With Next.js:

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { NavList } from "@forte-ui/react";

export function DocsNav() {
  const pathname = usePathname();

  return (
    <NavList.Root aria-label="Documentation">
      <NavList.List>
        {PAGES.map((page) => (
          <NavList.Item key={page.href}>
            <NavList.Link
              active={pathname === page.href}
              render={<Link href={page.href} />}
            >
              {page.title}
            </NavList.Link>
          </NavList.Item>
        ))}
      </NavList.List>
    </NavList.Root>
  );
}

The same shape works for any router — React Router's <NavLink>, TanStack Router's <Link> — or for hash links in a hand-rolled table of contents, where you would pass aria-current="location" alongside active so the sidebar's own aria-current="page" keeps its meaning.

If the list sits inside a <nav> you already own, swap the root's element the same way: render={<div />} on NavList.Root keeps the page to one landmark.

Accessibility

The keyboard story is deliberately the platform's: rows are real links and real buttons, so Tab walks them in order, Enter follows a link, and Enter or Space toggles a group — there is no roving focus to learn. A GroupTrigger carries aria-expanded and aria-controls; keep its text as the accessible name, since "expanded"/"collapsed" is already announced and needs no "show/hide" wording of its own.

The active row is never colour alone: active also sets aria-current="page" and a weight change, marker="edge" adds geometry, and under forced colors the fill is repainted with the system Highlight pair. Focus rings are inset — the list's usual home is a scroll container hard against the viewport edge, where an outward ring would be clipped.

Theming

Every property below is declared on NavList.Root, so override them there — through its className or an inline style — not on an ancestor, where the root's own declaration would beat the inherited value. The size presets work the same way: data-size="sm" and "lg" re-point a couple of these knobs and nothing else, which is why a knob you set yourself wins over the preset only when set on the root element itself (an unlayered consumer rule beats the library's layer regardless of specificity).

Theming tokens for NavList
PropertyControlsDefault
--forte-nav-list-gapSpace between sectionsvar(--forte-space-5)
--forte-nav-list-item-pxInline padding of a rowvar(--forte-space-2)
--forte-nav-list-item-pyBlock padding of a row. Follows data-forte-densityvar(--forte-list-item-py)
--forte-nav-list-item-gapSpace between an icon and the row's textvar(--forte-control-gap)
--forte-nav-list-item-radiusCorner radius of a row's fillvar(--forte-radius-control)
--forte-nav-list-row-gapSpace between rowsvar(--forte-space-1)
--forte-nav-list-item-font-sizeRow text sizevar(--forte-font-size-2)
--forte-nav-list-item-font-weightRow text weight at restvar(--forte-font-weight-normal)
--forte-nav-list-item-font-weight-activeRow text weight on the active rowvar(--forte-font-weight-medium)
--forte-nav-list-item-colorRow text colour at restvar(--forte-color-foreground-muted)
--forte-nav-list-item-color-hoverRow text colour on hovervar(--forte-color-foreground)
--forte-nav-list-item-color-activeRow text colour on the active rowvar(--forte-color-primary-text)
--forte-nav-list-item-bgRow fill at resttransparent
--forte-nav-list-item-bg-hoverRow fill on hovervar(--forte-color-panel-hover)
--forte-nav-list-item-bg-activeRow fill on the active rowvar(--forte-color-primary-soft)
--forte-nav-list-reveal-marginHow far inside the scroll container's edge a revealed row landsvar(--forte-space-6)
--forte-nav-list-label-font-sizeLabel text sizevar(--forte-font-size-1)
--forte-nav-list-label-colorLabel colourvar(--forte-color-foreground-subtle)
--forte-nav-list-label-trackingLabel letter-spacing. Tracking has to come back at this size or a short uppercase phrase closes up and reads as one word0.06em
--forte-nav-list-label-mbSpace under the labelvar(--forte-space-2)
--forte-nav-list-indentHow far a nested list steps in before its guide linevar(--forte-space-3)
--forte-nav-list-indent-gapSpace between the guide line and the nested rowsvar(--forte-space-2)
--forte-nav-list-guide-widthWidth of the guide line along a nested list. 0px removes it1px
--forte-nav-list-guide-colorColour of the guide linevar(--forte-color-border-muted)
--forte-nav-list-edge-widthWidth of the active-row edge border2px
--forte-nav-list-edge-colorColour of the active-row edge bordervar(--forte-color-primary)
--forte-nav-list-icon-colorChevron colour while the group is closedvar(--forte-color-foreground-subtle)
--forte-nav-list-icon-color-openChevron colour while the group is openvar(--forte-color-foreground)
--forte-nav-list-icon-rotateHow far the chevron turns as the panel opens180deg
--forte-nav-list-badge-bgBadge fillvar(--forte-color-secondary-soft)
--forte-nav-list-badge-colorBadge text colourvar(--forte-color-secondary-text)
--forte-nav-list-durationLength of a group's open/closevar(--forte-duration-normal)
--forte-nav-list-easeCurve of a group's open/closevar(--forte-ease-standard)

The parts also expose their state as data attributes — data-size and data-marker on the root; data-active, data-disabled on rows; Base UI's data-open, data-closed, data-panel-open on the group parts — so a Tailwind arbitrary variant such as data-[active]:... can target them without a wrapper.

API reference

Parts built on Base UI's Collapsible forward every prop it takes — Group accepts defaultOpen, open, onOpenChange and disabled; GroupPanel accepts hiddenUntilFound. The plain parts forward their element's own props.

Props for NavListRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
markerNavListMarkerfillHow the current item is marked. `"fill"` is a tinted row. `"edge"` adds an accent border along the row's inline-start edge on top of the fill — it follows the row's corner radius, and is a second, non-colour-only cue.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<nav>` with another element or component — pass `render={<div />}` when the list sits inside a `<nav>` landmark you already own, so the page does not grow a second one.
revealActivebooleanfalseScroll the active row into view when it mounts or becomes active — for a list in its own scroll column, where a page load would otherwise start the reader back at the top with their place fifty rows below the fold. Scrolls each scrollable ancestor the minimum it needs and a row already in view not at all, so it never fights the browser's own restoration of the window scroll, and activating a visible row moves nothing. How far inside the container's edge the row lands is `--forte-nav-list-reveal-margin`. Off by default: the component does not move a scroll container it does not own unless asked.
sizeNavListSizemdRow height and text size for every link and group trigger inside.
Props for NavListSection
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for NavListSectionLabel
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for NavListList
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for NavListItem
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for NavListLink
PropTypeDefaultDescription
activebooleanfalseWhether this row is where the reader currently is. Publishes `data-active` for styling and `aria-current="page"`, so the cue is never colour alone. For a same-page target — a table of contents — pass your own `aria-current="location"` alongside it; an explicit value wins over the derived one.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseDisables the row. The default `<a>` drops its `href` (an anchor without one is unfocusable and inert), `aria-disabled` is set, and clicks are swallowed — which is also what covers an element supplied through `render`, whose own `href` this component cannot remove.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<a>` with another element or component — `render={<Link href="/pricing" />}` is how a framework's router link goes in without losing the row's styling and states.
Props for NavListGroup
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for NavListGroupTrigger
PropTypeDefaultDescription
activebooleanfalseWhether the reader is currently somewhere INSIDE this group — the cue that keeps the current location visible while the group is closed over it. Styling only (`data-active`): no `aria-current`, because the current page is a link, and this is the button that reveals it.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
iconReactNode<ChevronDownIcon />The marker at the inline-end edge. Defaults to a chevron that rotates as the panel opens. Pass your own node — it goes in the same rotating, `aria-hidden` box — or `null` to drop it.
Props for NavListGroupPanel
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
keepMountedbooleantrueKeep the panel in the DOM while closed. On by default — the opposite of Base UI's — because these are NAVIGATION links: unmounted, they are invisible to crawlers and to find-in-page, and a closed group is exactly where the page a crawler wants usually is. `hidden` keeps the closed copy out of the accessibility tree and the tab order either way.
Props for NavListBadge
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.