NavList

NavList is a vertical navigation list for sidebars: grouped link entries with optional headings, trailing badges and a highlighted current entry. The sidebar of this documentation is a NavList.

import { NavList } from 'jarvis-react-ui';

Groups and badges

groups is an array of blocks. Each group has an optional label (omit it for an ungrouped block at the top) and its items. An item needs a unique id, a label and an href; badge adds trailing content such as a Pill. The entry whose id equals activeId is highlighted.

import { NavList, Pill } from 'jarvis-react-ui';
import type { NavListGroup } from 'jarvis-react-ui';

const GROUPS: NavListGroup[] = [
    { items: [{ id: 'overview', label: 'Overview', href: '#overview' }] },
    {
        label: 'Systems',
        items: [
            { id: 'power', label: 'Power', href: '#power' },
            { id: 'comms', label: 'Comms', href: '#comms', badge: <Pill variant="warn">2</Pill> },
            { id: 'sensors', label: 'Sensors', href: '#sensors' },
        ],
    },
    {
        label: 'Labs',
        items: [
            {
                id: 'orb',
                label: 'Orb studio',
                href: '#orb',
                badge: <Pill variant="info">new</Pill>,
            },
        ],
    },
];

export default function Groups() {
    return <NavList className="w-[220px]" aria-label="Systems" groups={GROUPS} activeId="power" />;
}

Client-side routing

Entries are real <a href> links, so the list works without JavaScript and supports opening in a new tab. onItemClick runs before the browser follows the link; call event.preventDefault() to handle navigation yourself and update activeId.

import { useState } from 'react';
import { NavList } from 'jarvis-react-ui';
import type { NavListGroup } from 'jarvis-react-ui';

const GROUPS: NavListGroup[] = [
    {
        label: 'Mission',
        items: [
            { id: 'brief', label: 'Briefing', href: '#brief' },
            { id: 'route', label: 'Route', href: '#route' },
            { id: 'debrief', label: 'Debrief', href: '#debrief' },
        ],
    },
];

export default function ClientSide() {
    const [active, setActive] = useState('brief');

    return (
        <div className="flex items-start gap-6">
            <NavList
                className="w-[200px]"
                aria-label="Mission"
                groups={GROUPS}
                activeId={active}
                onItemClick={(item, e) => {
                    e.preventDefault();
                    setActive(item.id);
                }}
            />
            <p className="m-0 font-mono text-[11px] text-text-secondary">
                Active: <span className="text-accent">{active}</span>
            </p>
        </div>
    );
}

With a router, derive activeId from the current location and navigate in onItemClick. Let modified clicks (Ctrl, Cmd, Shift, middle button) through so "open in new tab" keeps working. This is how the sidebar of this site is wired up with React Router:

import { useLocation, useNavigate } from 'react-router';
import { NavList } from 'jarvis-react-ui';
import type { NavListGroup } from 'jarvis-react-ui';

const GROUPS: NavListGroup[] = [
    {
        label: 'Components',
        items: [
            { id: 'button', label: 'Button', href: '/components/button' },
            { id: 'tabs', label: 'Tabs', href: '/components/tabs' },
        ],
    },
];

export function Sidebar() {
    const navigate = useNavigate();
    const { pathname } = useLocation();

    return (
        <NavList
            aria-label="Documentation"
            groups={GROUPS}
            activeId={pathname.split('/').pop()}
            onItemClick={(item, e) => {
                if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
                e.preventDefault();
                void navigate(item.href);
            }}
        />
    );
}

Sound

Hovering an entry plays hover_button and clicking plays click through the nearest SfxProvider. Without a provider the list stays silent. See Sound effects.

Accessibility

  • Renders a <nav> landmark named by aria-label (default "Navigation"). Give each NavList on a page a distinct name.
  • Every group is a <ul> list of <li> items, so screen readers announce the number of entries.
  • The active entry gets aria-current="page" in addition to the visual highlight.
  • Entries are native links: Tab moves between them, Enter follows them, and the focus outline is visible on keyboard focus.
  • Group labels are plain text, not headings, so they do not appear in the heading outline.

API

Prop Type Default Description
groups (required) NavListGroup[] Navigation groups in display order.
activeId string Id of the current entry; it is highlighted and gets aria-current="page".
aria-label string Navigation Accessible name of the <nav> landmark.
className string Additional class names for the root <nav> element.
onItemClick ((item: NavListItem, event: MouseEvent<HTMLAnchorElement, MouseEvent>) => void) Called when an entry is clicked, before the browser follows the link. Call event.preventDefault() to handle navigation yourself (e.g. with a client-side router).

Type NavListGroup

A titled block of NavList entries.

Field Type Default Description
items (required) NavListItem[] Entries in this group.
label string Group heading. Omit for an ungrouped block at the top.

Type NavListItem

A single link entry of a NavList.

Field Type Default Description
href (required) string Link target. Entries are rendered as anchors so they work without JavaScript.
id (required) string Unique id of the entry; compared with activeId.
label (required) ReactNode Visible text of the entry.
badge ReactNode Optional trailing badge, e.g. a Pill saying "new".