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
NavItemwithhref,componentorasChild, or one that holds aNavLink. 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.
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:
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":
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.
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.
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.
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.
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.
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.
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.
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
NavOverflow
| Prop | Type | Default |
|---|---|---|
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? | number | 0 |
| The fewest items to leave in the list. They stay even when they don't fit. | ||