2.0 alpha
These docs describe 2.0. Install it with @once-ui-system/core@alpha.
On 1.8.x? Read the 1.x docs
TrademarkTrademarkDocs
Ctrl k
Search docs...
Components

NavItem and NavGroup

Updated: September 14, 2026
View on GitHub
Every product built on Once UI grew its own sidebar, and they all converged on the same two rows: a link that knows whether it is the current page, and a collapsible group of those links indented behind a rail. These are those two. They are rows, not a sidebar. Headers, footers, org switchers and storage meters differ per product and stay yours to compose. badge prints a count and caps it at 9+; indicator is the dot for unread with no number worth showing.
Prop
Type
Default
labelrequired
The text.
ReactNode
icon
Leading icon. Leave it off inside a NavGroup, where the rail carries the hierarchy instead.
IconName
badge
A count in a pill at the end of the row. Above 9 it prints 9+, with the real number on the label for assistive tech.
number
indicator
A dot at the end of the row, when there is no number worth showing.
boolean
The rail is a vertical Line, not padding — indentation alone stops reading as hierarchy the moment a label wraps. A group manages its own open state unless you pass open and onToggle, which is what you do to open the group holding the current route.
Prop
Type
Default
labelrequired
The group's own text.
ReactNode
icon
Leading icon, matching the NavItems beside it.
IconName
childrenrequired
The rows inside.
React.ReactNode
open
Open state, when the host owns it — to keep one group open at a time, say.
boolean
onToggle
Called on the header. Only needed alongside open.
() => void
defaultOpen
Starting state when the group manages its own.
booleanfalse
selected
Mark the header itself as current, for a group that is also a link.
booleanfalse
…and every FlexProps propinherited
This is the part worth taking from here rather than writing again. pathname === href misses a nested route: on /settings/billing/invoices/42 nothing is highlighted. pathname.startsWith(href) overshoots the other way: /settings is a prefix of every child, so the parent lights up alongside them. The answer is the longest href that matches. It returns null when nothing matches, and will not match a sibling that merely shares a prefix — /settings-v2 is not inside /settings.
Edit this page on GitHub
TrademarkTrademark
Built with curiosity by Lorant One and the Design Engineers Club