NavTree

A sidebar that nests itself.
Hand it the flat list of routes an app already has — with a category on each — and it groups, orders and renders them as a tree. The branches above the current page open on their own, the row for that page is marked and scrolled to, and which branches are open survives a reload.
Rows carrying an href render as real <a> elements on web, so cmd-click, middle-click, "copy link address" and crawlers all work; a plain left-click goes to onNavigate for client-side routing. Omit onNavigate and the rows stay ordinary links the browser follows.
Built on Tree, so keyboard navigation, guide lines, filtering and the ARIA tree/treeitem roles come along with it.

Grouped routes

A flat list of routes becomes a nested sidebar. The group above the active route is already open, because activeHref opened it.

Loading demo…

import

 

React

,

 

{

useState

}

 

from

 

'react'

;

import

 

{

 

NavTree

,

 

type

 

NavTreeItem

 

}

 

from

 

'@platform-blocks/react-ui-library'

;

 

// The whole input: a flat list, with a category on each row. Nothing here

// describes the tree — `NavTree` derives it.

const

 

ROUTES

:

 

NavTreeItem

[

]

 

=

 

[

  

{

label

:

 

'Getting Started'

,

href

:

 

'/getting-started'

 

}

,

  

{

label

:

 

'Button'

,

href

:

 

'/components/Button'

,

group

:

 

[

'Components'

,

 

'Input'

]

 

}

,

  

{

label

:

 

'Select'

,

href

:

 

'/components/Select'

,

group

:

 

[

'Components'

,

 

'Input'

]

 

}

,

  

{

label

:

 

'Checkbox'

,

href

:

 

'/components/Checkbox'

,

group

:

 

[

'Components'

,

 

'Input'

]

 

}

,

  

{

label

:

 

'Card'

,

href

:

 

'/components/Card'

,

group

:

 

[

'Components'

,

 

'Display'

]

 

}

,

  

{

label

:

 

'Badge'

,

href

:

 

'/components/Badge'

,

group

:

 

[

'Components'

,

 

'Display'

]

 

}

,

  

{

label

:

 

'Tabs'

,

href

:

 

'/components/Tabs'

,

group

:

 

[

'Components'

,

 

'Navigation'

]

 

}

,

]

;

 

export

 

function

 

Demo

(

)

 

{

  

const

 

[

route

,

setRoute

]

 

=

 

useState

(

'/components/Select'

)

;

 

  

return

 

(

    

<NavTree

      

items

=

{

ROUTES

}

      

activeHref

=

{

route

}

      

onNavigate

=

{

item

=>

 

setRoute

(

item

.

href

)

}

      showGuides

    

/>

  

)

;

}

Counts and order

groupOrder curates the sections that matter and leaves the rest alphabetical. renderEndSection hangs a count off each branch, and openDepth={0} starts everything closed.

Loading demo…

import

 

React

,

 

{

useState

}

 

from

 

'react'

;

import

 

{

 

Badge

,

 

NavTree

,

 

type

 

NavTreeItem

 

}

 

from

 

'@platform-blocks/react-ui-library'

;

 

const

 

ROUTES

:

 

NavTreeItem

[

]

 

=

 

[

  

{

label

:

 

'Button'

,

href

:

 

'/components/Button'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Select'

,

href

:

 

'/components/Select'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Checkbox'

,

href

:

 

'/components/Checkbox'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Card'

,

href

:

 

'/components/Card'

,

group

:

 

'Display'

 

}

,

  

{

label

:

 

'Badge'

,

href

:

 

'/components/Badge'

,

group

:

 

'Display'

 

}

,

  

{

label

:

 

'Tabs'

,

href

:

 

'/components/Tabs'

,

group

:

 

'Navigation'

 

}

,

]

;

 

export

 

function

 

Demo

(

)

 

{

  

const

 

[

route

,

setRoute

]

 

=

 

useState

(

'/components/Card'

)

;

 

  

return

 

(

    

<NavTree

      

items

=

{

ROUTES

}

      

activeHref

=

{

route

}

      

onNavigate

=

{

item

=>

 

setRoute

(

item

.

href

)

}

      

// Curate the order that matters and let the rest sort themselves.

      

groupOrder

=

{

[

'Input'

,

 

'Display'

]

}

      

openDepth

=

{

0

}

      

renderEndSection

=

{

node

=>

        node

.

children

?

 

<Badge

 

size

=

"xs"

 

variant

=

"light"

>

{

node

.

children

.

length

}

</Badge

>

 

:

 

null

      

}

    

/>

  

)

;

}

Filtering

searchable adds a filter field wired to the tree. Typing hides the rows that do not match, opens the branches above the ones that do, and marks the matched substring — past a certain length, three letters beat any amount of nesting.

Loading demo…

import

 

React

,

 

{

useState

}

 

from

 

'react'

;

import

 

{

 

NavTree

,

 

type

 

NavTreeItem

 

}

 

from

 

'@platform-blocks/react-ui-library'

;

 

const

 

ROUTES

:

 

NavTreeItem

[

]

 

=

 

[

  

{

label

:

 

'Button'

,

href

:

 

'/components/Button'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Checkbox'

,

href

:

 

'/components/Checkbox'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Select'

,

href

:

 

'/components/Select'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'TextArea'

,

href

:

 

'/components/TextArea'

,

group

:

 

'Input'

 

}

,

  

{

label

:

 

'Badge'

,

href

:

 

'/components/Badge'

,

group

:

 

'Display'

 

}

,

  

{

label

:

 

'Card'

,

href

:

 

'/components/Card'

,

group

:

 

'Display'

 

}

,

  

{

label

:

 

'Breadcrumbs'

,

href

:

 

'/components/Breadcrumbs'

,

group

:

 

'Navigation'

 

}

,

  

{

label

:

 

'Tabs'

,

href

:

 

'/components/Tabs'

,

group

:

 

'Navigation'

 

}

,

]

;

 

export

 

function

 

Demo

(

)

 

{

  

const

 

[

route

,

setRoute

]

 

=

 

useState

(

'/components/Card'

)

;

 

  

return

 

(

    

<NavTree

      

items

=

{

ROUTES

}

      

activeHref

=

{

route

}

      

onNavigate

=

{

item

=>

 

setRoute

(

item

.

href

)

}

      searchable

      

searchPlaceholder

=

"Filter components…"

    

/>

  

)

;

}

Collapsed rail

collapsed drops the sidebar to a strip of top-level icons — the group holding the current route stays marked, and pressing one lands on the first page inside it. A sidebar with a hundred routes shows a handful of icons here, not a hundred.

Loading demo…

import

 

React

,

 

{

useState

}

 

from

 

'react'

;

import

 

{

 

Column

,

 

Icon

,

 

NavTree

,

 

Switch

,

 

type

 

NavTreeItem

 

}

 

from

 

'@platform-blocks/react-ui-library'

;

 

const

 

ROUTES

:

 

NavTreeItem

[

]

 

=

 

[

  

{

label

:

 

'Button'

,

href

:

 

'/components/Button'

,

group

:

 

'Components'

 

}

,

  

{

label

:

 

'Card'

,

href

:

 

'/components/Card'

,

group

:

 

'Components'

 

}

,

  

{

label

:

 

'LineChart'

,

href

:

 

'/components/LineChart'

,

group

:

 

'Charts'

 

}

,

  

{

label

:

 

'BarChart'

,

href

:

 

'/components/BarChart'

,

group

:

 

'Charts'

 

}

,

  

{

label

:

 

'useHover'

,

href

:

 

'/hooks/useHover'

,

group

:

 

'Hooks'

 

}

,

]

;

 

const

GROUP_ICONS

=

 

{

  

Components

:

 

<Icon

 

name

=

"grid"

 

size

=

{

18

}

 

/>

,

  

Charts

:

 

<Icon

 

name

=

"chart-bar"

 

size

=

{

18

}

 

/>

,

  

Hooks

:

 

<Icon

 

name

=

"hook"

 

size

=

{

18

}

 

/>

,

}

;

 

export

 

function

 

Demo

(

)

 

{

  

const

 

[

collapsed

,

setCollapsed

]

 

=

 

useState

(

true

)

;

  

const

 

[

route

,

setRoute

]

 

=

 

useState

(

'/components/Card'

)

;

 

  

return

 

(

    

<Column

 

gap

=

"md"

>

      

<Switch

 

checked

=

{

collapsed

}

 

onChange

=

{

setCollapsed

}

 

label

=

"Collapsed"

 

/>

      

<NavTree

        

items

=

{

ROUTES

}

        

activeHref

=

{

route

}

        

onNavigate

=

{

item

=>

 

setRoute

(

item

.

href

)

}

        

groupIcons

=

{

GROUP_ICONS

}

        

collapsed

=

{

collapsed

}

      

/>

    

</Column

>

  

)

;

}

</>

react-ui-library

100+ accessible, themeable components that work seamlessly across iOS, Android, and Web.

A Platform Blocks product.