Skip to main contentSkip to docs navigation

Nav Overflow

Collapse the items of a Nav, a TabList or a NavbarNav that don't fit their container into a "More" menu, the Priority+ pattern.

Overview

<NavOverflow> wraps a list of links and keeps it on one line. It measures its own width, and moves the items that don't fit into a menu behind a "More" toggle at the end of the list. It follows the width of its container, not of the viewport, so it works in a sidebar, a card or a resizable panel as it does across a page.

It wraps a Nav, the TabList of a Tabs, or a NavbarNav in a Navbar, and renders chassis-css's nav overflow markup: a .nav-overflow wrapper, the list, and a toggle item with a .menu.

A few rules hold at every width:

  • Items keep their order, in the list and in the menu.
  • The active item stays in the list. So does the item that has keyboard focus, and in a tab list the tab that is its Tab stop.
  • An item moves only if it is a link: a NavItem with href, component or asChild, or one that holds a NavLink. An item with anything else inside, such as a menu, stays in the list.

Basic usage

Wrap the Nav in <NavOverflow>. Each example on this page sits in a box that resizes from its bottom right corner: drag it narrower to move items into the menu.

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => (
  <NavOverflow>
    <Nav variant="segments">
      <NavItem href="#" active>
        Home
      </NavItem>
      <NavItem href="#">Dashboard</NavItem>
      <NavItem href="#">Products</NavItem>
      <NavItem href="#">Services</NavItem>
      <NavItem href="#">Analytics</NavItem>
      <NavItem href="#">Reports</NavItem>
      <NavItem href="#">Settings</NavItem>
      <NavItem href="#">Help</NavItem>
    </Nav>
  </NavOverflow>
)

An item in the menu is a MenuItem rendered with the props of its link: href, onClick, disabled, and the element given with asChild. A router link keeps working from the menu:

TSX
import Link from 'next/link'
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

<NavOverflow component="nav" aria-label="Main">
  <Nav>
    <NavItem asChild active={pathname === '/'}>
      <Link href="/">Home</Link>
    </NavItem>
    <NavItem asChild active={pathname === '/pricing'}>
      <Link href="/pricing">Pricing</Link>
    </NavItem>
  </Nav>
</NavOverflow>

Tabs

The pattern works with every Nav variant. With variant="tabs":

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => (
  <NavOverflow>
    <Nav variant="tabs">
      <NavItem href="#" active>
        Overview
      </NavItem>
      <NavItem href="#">Details</NavItem>
      <NavItem href="#">History</NavItem>
      <NavItem href="#">Activity</NavItem>
      <NavItem href="#">Comments</NavItem>
      <NavItem href="#">Attachments</NavItem>
      <NavItem href="#">Related</NavItem>
    </Nav>
  </NavOverflow>
)

For tabs that switch panels, wrap the TabList of a Tabs, inside the Tabs. A tab in the menu selects itself when chosen, which brings it back into the list: the selected tab is never in the menu.

JSX
import { NavOverflow, Tab, TabList, TabPanel, Tabs } from '@chassis-ui/react'

const sections = ['Overview', 'Details', 'History', 'Activity', 'Comments', 'Attachments']

export const Example = () => (
  <Tabs defaultSelectedKey="Overview">
    <NavOverflow>
      <TabList aria-label="Sections">
        {sections.map((section) => (
          <Tab key={section} id={section}>
            {section}
          </Tab>
        ))}
      </TabList>
    </NavOverflow>
    {sections.map((section) => (
      <TabPanel key={section} id={section} className="p-md">
        The {section.toLowerCase()} panel.
      </TabPanel>
    ))}
  </Tabs>
)

Keeping items visible

Set keepVisible on a NavItem or a Tab that should never move into the menu, such as a contact link. It renders chassis-css's .nav-overflow-keep.

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => (
  <NavOverflow>
    <Nav variant="segments">
      <NavItem href="#" active>
        Home
      </NavItem>
      <NavItem href="#">Products</NavItem>
      <NavItem href="#">Services</NavItem>
      <NavItem href="#">About</NavItem>
      <NavItem href="#">Blog</NavItem>
      <NavItem href="#">Careers</NavItem>
      <NavItem href="#" keepVisible>
        Contact
      </NavItem>
    </Nav>
  </NavOverflow>
)

Disabled items

A disabled link is disabled in the menu too.

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => (
  <NavOverflow>
    <Nav variant="segments">
      <NavItem href="#" active>
        Active
      </NavItem>
      <NavItem href="#">Link</NavItem>
      <NavItem href="#">Another link</NavItem>
      <NavItem href="#" disabled>
        Disabled
      </NavItem>
      <NavItem href="#">More content</NavItem>
      <NavItem href="#">Even more</NavItem>
    </Nav>
  </NavOverflow>
)

In a navbar

Inside a Navbar, wrap the NavbarNav. The wrapper takes the space the brand leaves.

JSX
import {
  Container,
  Navbar,
  NavbarBrand,
  NavbarNav,
  NavItem,
  NavLink,
  NavOverflow
} from '@chassis-ui/react'

export const Example = () => (
  <Navbar expand className="bg-even" aria-label="Main navigation">
    <Container fluid>
      <NavbarBrand href="#">Brand</NavbarBrand>
      <NavOverflow>
        <NavbarNav>
          <NavItem>
            <NavLink href="#" active>
              Home
            </NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="#">Features</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="#">Pricing</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="#">About</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="#">Contact</NavLink>
          </NavItem>
        </NavbarNav>
      </NavOverflow>
    </Container>
  </Navbar>
)

The toggle

moreText sets the toggle's text and iconPlacement the side its icon is on. With moreText={false} the toggle is the icon alone, and moreLabel gives it its accessible name. moreIcon takes an icon name or an element; the default is IconProvider's more icon.

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

const pages = ['Home', 'Dashboard', 'Products', 'Services', 'Analytics', 'Reports', 'Settings']

const items = pages.map((page, index) => (
  <NavItem key={page} href="#" active={index === 0}>
    {page}
  </NavItem>
))

export const Example = () => (
  <div className="vstack gap-md">
    <NavOverflow moreText="See all" iconPlacement="end">
      <Nav variant="segments">{items}</Nav>
    </NavOverflow>
    <NavOverflow moreText={false} moreLabel="More pages" moreIcon="bars-outline">
      <Nav variant="segments">{items}</Nav>
    </NavOverflow>
  </div>
)

The menu opens at menuPlacement, bottom-end by default. It is rendered inside the toggle's item, so an ancestor with overflow set clips it. menuContainer renders it in document.body, or in an element, instead.

Minimum visible items

threshold is the fewest items to leave in the list. They stay even when they don't fit.

JSX
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => (
  <NavOverflow threshold={3}>
    <Nav variant="segments">
      <NavItem href="#" active>
        Home
      </NavItem>
      <NavItem href="#">Dashboard</NavItem>
      <NavItem href="#">Products</NavItem>
      <NavItem href="#">Services</NavItem>
      <NavItem href="#">Analytics</NavItem>
      <NavItem href="#">Reports</NavItem>
    </Nav>
  </NavOverflow>
)

Collapsing every item

Below collapseBelow every item moves into the menu at once, apart from the active one and those marked keepVisible. It takes a width in pixels, or the name of a breakpoint, read from chassis-css's --cx-breakpoint-{name}. It compares the width of the component, not of the viewport.

onOverflow fires when items move into the menu or back, with the number in the menu and the number left in the list.

6 in the list, 0 in the menu.

JSX
import { useState } from 'react'
import { Nav, NavItem, NavOverflow } from '@chassis-ui/react'

export const Example = () => {
  const [counts, setCounts] = useState({ overflowCount: 0, visibleCount: 6 })
  return (
    <>
      <NavOverflow collapseBelow="sm" onOverflow={setCounts}>
        <Nav variant="segments">
          <NavItem href="#" active>
            Home
          </NavItem>
          <NavItem href="#">Dashboard</NavItem>
          <NavItem href="#">Products</NavItem>
          <NavItem href="#">Services</NavItem>
          <NavItem href="#">Analytics</NavItem>
          <NavItem href="#">Reports</NavItem>
        </Nav>
      </NavOverflow>
      <p className="mt-md mb-0">
        {counts.visibleCount} in the list, {counts.overflowCount} in the menu.
      </p>
    </>
  )
}

Server rendering

A server can't know what fits, so it renders every item and no toggle. Until the page has hydrated, the items that don't fit are clipped at the wrapper's edge, and the row scrolls. The first render in the browser measures and collapses before anything is painted. See what the server renders.

Accessibility

The toggle is a menu button: Enter, Space or the down arrow opens the menu, the arrow keys move through it, and Escape closes it and returns focus to the toggle. An item in the menu is hidden in the list, so it is reached and announced once. It carries the link's onClick, and none of its other handlers or its id: those belong to the link in the list.

When a resize takes away what has focus, focus moves rather than falling to the page: to the last item in the list when the toggle goes, and to the toggle when a focused menu item returns to the list.

In a TabList the toggle is one of the tabs, with aria-haspopup. It is not a Tab stop of its own: the arrow keys, Home and End move among the tabs that are shown and the toggle, and skip the tabs in the menu. Choosing a tab in the menu selects it and moves focus to it in the list. The menu of a tab list is always rendered in document.body, since a tablist may hold only tabs.

Scope

One line, left to right or right to left. The wrapper stops the list from wrapping, so it doesn't fit a vertical nav or a vertical Tabs.

A list of items. The toggle is a list item, so the list has to be a <ul> or an <ol> of NavItems. A <Nav component="nav"> of bare NavLinks stays as it is, with a warning in development.

No update method. chassis-css's plugin has one to measure again. The component measures when its width, an item's width, the items or the active item change.

API

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
children?ReactNode—
The list to collapse: a Nav, a NavbarNav or a TabList.
className?string—
A string of all className you want applied to the component.
collapseBelow?number | Breakpoint—
Moves every item into the menu while the component is narrower than this: a width in pixels, or the name of a breakpoint, read from --cx-breakpoint-{name}.
component?ElementType'div'
Component used for the root node. Either a string to use an HTML element or a component.
iconPlacement?"start" | "end"'start'
Which side of the toggle's text its icon is on.
menuContainer?boolean | Element—
Renders the menu in a container element instead of inside the toggle's item, where an ancestor with overflow set would clip it: an element, or true for document.body. The menu of a TabList is always rendered outside it.
menuPlacement?'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end''bottom-end'
Where the menu opens, relative to the toggle. It flips to stay in view.
moreIcon?IconValue—
The toggle's icon: an icon name, or an element of your own icon set. Defaults to IconProvider's more icon.
moreLabel?string'More'
The accessible name of a toggle without text (moreText={false}).
moreText?ReactNode'More'
The toggle's text. false leaves the icon alone, named by moreLabel.
onOverflow?((detail: NavOverflowDetail) => void)—
Callback fired when items move into the menu or back out of it, with the number of items in the menu and in the list.
ref?any—
threshold?number0
The fewest items to leave in the list. They stay even when they don't fit.

keepVisible is a prop of NavItem and of Tab.