Navigation

Let people move between the places of an application, see where they are, and find their way back.

BetaSince 1.0.0
On this page

Import

import {
  Badge,
  Breadcrumbs,
  Button,
  Column,
  CommandPalette,
  DescriptionList,
  IconButton,
  Link,
  LinkProvider,
  List,
  NavigationBar,
  NavigationDrawer,
  NavigationRail,
  Page,
  PageHeader,
  Pager,
  Panes,
  Row,
  Scaffold,
  Surface,
  Tabs,
  Text,
  Tooltip,
  TopBar,
} from '@envsemble/envui';

Usage

Build one array of destinations, each with its label, icon and href, current on the place shown, and optionally a count, a group and an onClick.

  • Give the same array to a NavigationBar, a NavigationRail and a NavigationDrawer, in the Scaffold's navigation as bar, rail and drawer.
  • Render every href through the router's link with LinkProvider.
  • After the router shows the new page, set document.title, such as "Projects – Northwind", and move focus to the body or the page's heading.
PrimaryNavigation.tsx
import { useRef, useState, type CSSProperties, type ReactNode } from 'react';
import {
  NavigationBar,
  NavigationDrawer,
  Page,
  PageHeader,
  NavigationRail,
  Scaffold,
  Text,
  TopBar,
  type Destination,
} from '@envsemble/envui';
import { IconCategory } from '@envsemble/envui/icons/Category';
import { IconDescription } from '@envsemble/envui/icons/Description';
import { IconGroup } from '@envsemble/envui/icons/Group';
import { IconHome } from '@envsemble/envui/icons/Home';
import { IconInbox } from '@envsemble/envui/icons/Inbox';
import { IconManageAccounts } from '@envsemble/envui/icons/ManageAccounts';
import { IconSettings } from '@envsemble/envui/icons/Settings';

/**
 * Stands in for the window. An application renders its Scaffold at its root, as tall as the window;
 * this preview is a box 26 rem tall and adds no landmarks to the page around it.
 */
const preview = {
  blockSize: '26rem',
  borderRadius: '1.75rem',
} as CSSProperties;

export default function PrimaryNavigation() {
  const [current, setCurrent] = useState('projects');
  const main = useRef<HTMLElement>(null);
  const page = [...destinations, ...workspace].find(({ id }) => id === current) ?? destinations[0];

  const go = (id: string) => {
    setCurrent(id);
    // After a change of page, focus moves to the body, so the keyboard and the screen reader go on
    // from the new page. An application also sets document.title to the page's name.
    main.current?.focus();
  };

  // One array of destinations, which every navigation form takes; the router renders the page,
  // and the example keeps it in state.
  const items: Destination[] = [...destinations, ...workspace].map(
    ({ id, label, href, icon, unread }, index) => ({
      label,
      href,
      icon,
      count: unread,
      group: index < destinations.length ? undefined : 'Workspace',
      current: id === current,
      onClick: () => go(id),
    }),
  );

  return (
    <Scaffold
      scroll="body"
      landmarks={false}
      style={preview}
      bodyRef={main}
      topBar={<TopBar title="Northwind" />}
      // The one primary navigation, named Main, its current page marked, in each form.
      navigation={{
        bar: <NavigationBar destinations={items} />,
        rail: <NavigationRail destinations={items} />,
        drawer: <NavigationDrawer destinations={items} headingLevel={3} />,
      }}
    >
      <Page width="md" header={<PageHeader title={page.label} headingLevel={3} />}>
        <Text emphasis="muted">{page.description}</Text>
      </Page>
    </Scaffold>
  );
}

interface Place {
  id: string;
  label: string;
  href: string;
  icon: ReactNode;
  description: string;
  unread?: number;
}

const destinations: Place[] = [
  {
    id: 'home',
    label: 'Home',
    href: '/',
    icon: <IconHome />,
    description: 'What changed in your projects since you last looked.',
  },
  {
    id: 'inbox',
    label: 'Inbox',
    href: '/inbox',
    icon: <IconInbox />,
    description: 'Comments and requests that wait for you.',
    unread: 3,
  },
  {
    id: 'projects',
    label: 'Projects',
    href: '/projects',
    icon: <IconCategory />,
    description: 'Website refresh, Quarterly report and four more projects.',
  },
  {
    id: 'documents',
    label: 'Documents',
    href: '/documents',
    icon: <IconDescription />,
    description: 'Every document in the workspace, newest first.',
  },
];

const workspace: Place[] = [
  {
    id: 'team',
    label: 'Team',
    href: '/team',
    icon: <IconGroup />,
    description: 'Alex Morgan, Sam Lee, Priya Shah and the rest of Northwind.',
  },
  {
    id: 'members',
    label: 'Members',
    href: '/settings/members',
    icon: <IconManageAccounts />,
    description: 'Who can sign in to Northwind, and what each person can do.',
  },
  {
    id: 'settings',
    label: 'Settings',
    href: '/settings',
    icon: <IconSettings />,
    description: 'The workspace’s name, its notifications and its appearance.',
  },
];

Hierarchy

Pass Breadcrumbs with wrap={false} to the PageHeader's breadcrumbs, from two levels down. Each level above the page is an item with label and href; the last item, the page, has only its label.

<PageHeader
  title="Brand guide"
  breadcrumbs={
    <Breadcrumbs
      wrap={false}
      items={[
        { label: 'Projects', href: '/projects' },
        { label: 'Website refresh', href: '/projects/website-refresh' },
        { label: 'Brand guide' },
      ]}
    />
  }
/>

Views and quick open

Tabs go under the PageHeader for the views of the object the page is about; a link to another page, such as its settings, goes in the PageHeader's actions. A CommandPalette, opened by a search button in the TopBar and by ⌘K or Ctrl+K, lists the destinations as items and opens the chosen one through the router.

List and detail

Panes.Root with behavior="list-detail" and a backLabel that names the list, such as "All projects", holds a Panes.List, a Panes.Divider and a Panes.Detail, each with its label.

<Panes.Root
  behavior="list-detail"
  open={detailShown}
  onOpenChange={setDetailShown}
  backLabel="All projects"
>
  <Panes.List label="Projects">{list}</Panes.List>
  <Panes.Divider label="Resize project list" />
  <Panes.Detail label={chosen.name}>{detail}</Panes.Detail>
</Panes.Root>

Composition

The primary navigation is a NavigationRail here, from Scaffold's navigation; the trail is Breadcrumbs in the PageHeader, the views Tabs below it, and quick open an IconButton in the TopBar's actions. A Pager ends a page read in order. The parts are the rail, env-navigation-rail, the quick-open button, env-top-bar-actions > env-icon-button, the trail, env-breadcrumbs, and the tabs, env-tabs-list.

Accessibility

You provide

  • current on the place shown, focus after each change of page, and document.title.
  • LinkProvider with the router's link, and an href on every place.

envUI provides

  • Skip links, the banner and main landmarks, and a body that takes focus without being a tab stop.
  • aria-current="page" on the current destination and the last trail level.
  • Tabs with their roles and roving tab stop; a palette that announces the number of results.
  • Panes' focus moves between the list and the detail while one shows at a time.

Changelog

1.0.0

The page is on the figure template.