Navigation Patterns
This guide covers patterns for building navigation components—navbars, sidebars, menus, and other hierarchical link structures.
Automatic Navigation
Foundation components have access to the complete page structure of the site. This means headers and footers can build their menus automatically from your page hierarchy—no content definition required.
Using the Page Hierarchy
Use the useWebsite hook to access pages:
import { useWebsite } from '@uniweb/kit'
function Header() {
const { website } = useWebsite()
const pages = website.getPageHierarchy({ for: 'header' })
return (
<nav>
{pages.map((page) => (
<a key={page.id} href={page.route}>
{page.label || page.title}
</a>
))}
</nav>
)
}The getPageHierarchy() method returns pages with their nested children, respecting visibility settings. Each page object includes:
| Field | Description |
|---|---|
id | Page identifier |
route | URL path |
title | Page title |
label | Short nav label (falls back to title) |
children | Nested child pages |
Page Visibility Control
Pages can opt out of automatic navigation via page.yml — while staying reachable:
# pages/admin/page.yml
title: Admin Dashboard
hideIn: ['*'] # Reachable by URL, but shown in no nav menu| Option | Effect |
|---|---|
hideIn: ['*'] | Hide from every nav menu; page stays routed + reachable by URL |
hideIn: [header] | Hide from named nav areas only (header, footer, sidebar, …) |
hidden: true | Different axis — exclude the page (and its subtree) from the published site (draft) |
hideIn lists the nav areas to suppress the page from; '*' means every area. (The legacy hideInHeader / hideInFooter booleans still work as shorthand for hideIn: [header] / hideIn: [footer].) For the full reachability-vs-nav distinction, see Page Configuration → Page Visibility.
Manual Navigation
Automatic navigation is convenient but limited. Define navigation manually when you need:
- Icons alongside menu items
- Descriptions or secondary text
- External links mixed with internal pages
- Custom grouping different from the page hierarchy
- Mega menus with rich content
Two Approaches for Manual Navigation
| Approach | Best For | Complexity |
|---|---|---|
| Markdown lists | Simple flat or nested links | Low |
| Nav schema (YAML) | Icons, descriptions, metadata | Medium |
Supporting Both Modes
A well-designed header or footer component supports both automatic and manual navigation:
function Header({ content }) {
const { website } = useWebsite()
// Manual nav from content (if provided)
const manualNav = content.data?.nav
// Automatic nav from page structure (fallback)
const autoNav = website.getPageHierarchy({ for: 'header' })
const navItems =
manualNav ||
autoNav.map((p) => ({
label: p.label || p.title,
href: p.route,
children: p.children?.map((c) => ({
label: c.label || c.title,
href: c.route,
})),
}))
return <nav>{/* render navItems */}</nav>
}This gives content authors flexibility—leave the header section empty for automatic nav, or provide a nav block for full control with icons and custom structure.
Markdown Lists
For simple manual navigation, markdown bullet lists work well. Each list item with a link becomes a navigation entry.
Flat Navigation
---
type: Navbar
---
- [Home](/)
- [About](/about)
- [Contact](/contact)Your component receives:
{
lists: [
{
style: 'bullet',
items: [
{ links: [{ href: '/', label: 'Home' }] },
{ links: [{ href: '/about', label: 'About' }] },
{ links: [{ href: '/contact', label: 'Contact' }] },
],
},
]
}Nested Navigation (Dropdowns)
Nested lists create hierarchical navigation—perfect for dropdown menus:
- [Products](/products)
- [Widgets](/products/widgets)
- [Gadgets](/products/gadgets)
- [Company](/company)
- [About](/company/about)
- [Careers](/company/careers)Each nested list becomes a lists array inside the parent item:
{
lists: [
{
style: 'bullet',
items: [
{
links: [{ href: '/products', label: 'Products' }],
lists: [
{
style: 'bullet',
items: [
{ links: [{ href: '/products/widgets', label: 'Widgets' }] },
{ links: [{ href: '/products/gadgets', label: 'Gadgets' }] },
],
},
],
},
// ...
],
},
]
}Adding Descriptions
Text after a link becomes a paragraph—useful for mega menus:
- [Widgets](/products/widgets)
Our award-winning widget collection.
- [Gadgets](/products/gadgets)
The latest in gadget technology.items: [
{
links: [{ href: '/products/widgets', label: 'Widgets' }],
paragraphs: ['Our award-winning widget collection.'],
},
// ...
]Rendering Nested Lists
Here's a recursive component for nested navigation:
function NavList({ list, depth = 0 }) {
return (
<ul className={depth === 0 ? 'nav-root' : 'nav-submenu'}>
{list.items.map((item, i) => (
<li key={i}>
{item.links?.[0] && (
<a href={item.links[0].href}>{item.links[0].label}</a>
)}
{item.paragraphs?.[0] && (
<span className="description">{item.paragraphs[0]}</span>
)}
{item.lists?.map((nested, j) => (
<NavList key={j} list={nested} depth={depth + 1} />
))}
</li>
))}
</ul>
)
}Nav Schema (Tagged YAML)
For richer navigation with icons, targets, and metadata, use the nav schema via tagged YAML blocks:
---
type: Header
---
```yaml:nav
- icon: /icons/home.svg
label: Home
href: /
- icon: /icons/docs.svg
label: Documentation
href: /docs
children:
- label: Getting Started
href: /docs/getting-started
text: Quick introduction
- label: API Reference
href: /docs/api
text: Complete API docs
- icon: /icons/github.svg
label: GitHub
href: https://github.com/example
target: _blank
```Nav Schema Fields
| Field | Type | Description |
|---|---|---|
label | string | Required. Display text |
href | string | Link destination |
icon | string | Path to icon file (e.g., /icons/home.svg) |
text | string | Secondary text (description, subtitle) |
target | string | Link target (_self, _blank) |
children | nav[] | Nested navigation items (recursive) |
order | number | Custom sort order |
hidden | boolean | Hide from display |
current | boolean | Mark as current/active page |
Accessing Nav Data
Tagged YAML appears in content.data:
function Header({ content }) {
const nav = content.data?.nav || []
return (
<nav>
{nav.map((item, i) => (
<NavItem key={i} item={item} />
))}
</nav>
)
}
function NavItem({ item }) {
return (
<div className={item.current ? 'active' : ''}>
{item.icon && <img src={item.icon} alt="" className="w-5 h-5" />}
<a href={item.href} target={item.target}>
{item.label}
</a>
{item.text && <span className="text-sm text-gray-500">{item.text}</span>}
{item.children?.length > 0 && (
<div className="ml-4">
{item.children.map((child, i) => (
<NavItem key={i} item={child} />
))}
</div>
)}
</div>
)
}Multiple Nav Blocks
Use different tags for different navigation areas:
```yaml:main-nav
- label: Home
href: /
- label: Products
href: /products
```
```yaml:footer-nav
- label: Privacy
href: /privacy
- label: Terms
href: /terms
```Access them separately:
const mainNav = content.data?.['main-nav'] || []
const footerNav = content.data?.['footer-nav'] || []Choosing Your Approach
| Scenario | Recommendation |
|---|---|
| Simple site, few pages | Automatic navigation |
| Need icons or descriptions | Nav schema (YAML) |
| Quick prototype | Markdown lists |
| Deep nesting (3+ levels) | Nav schema (YAML) |
| Mix of internal + external links | Nav schema (YAML) |
| Content authors prefer markdown | Markdown lists |
Common Patterns
Header with Dropdown Menu
```yaml:nav
- label: Products
children:
- label: Widgets
href: /products/widgets
text: For small projects
- label: Gadgets
href: /products/gadgets
text: For enterprises
- label: Pricing
href: /pricing
- label: About
href: /about
```Sidebar with Sections
```yaml:nav
- label: Getting Started
children:
- label: Installation
href: /docs/install
- label: Quick Start
href: /docs/quickstart
- label: Guides
children:
- label: Components
href: /docs/components
- label: Theming
href: /docs/theming
```Social Links (Icon-Only)
```yaml:social
- icon: /icons/twitter.svg
label: Twitter
href: https://twitter.com/example
target: _blank
- icon: /icons/github.svg
label: GitHub
href: https://github.com/example
target: _blank
```The label provides accessibility (screen readers) while only the icon is displayed visually.
Table of Contents
For auto-generated tables of contents from page headings, see the Content Structure guide on using the sequence array to extract headings.
Active State Detection
Components often need to highlight the current page in navigation. Use useActiveRoute from @uniweb/kit:
import { useActiveRoute, Link } from '@uniweb/kit'
function NavLink({ href, label }) {
const { isActive, isActiveOrAncestor } = useActiveRoute()
// Exact match for home, ancestor match for everything else
const active = href === '/' ? isActive(href) : isActiveOrAncestor(href)
return (
<Link href={href} className={active ? 'text-blue-600 font-semibold' : 'text-gray-600'}>
{label}
</Link>
)
}Both isActive and isActiveOrAncestor accept page objects or route strings:
isActive(page) // page object from getPageHierarchy()
isActive('/blog') // route string — same logic
isActiveOrAncestor('/research') // matches /research, /research/papers, etc.This matters because headers often mix automatic pages (objects from getPageHierarchy()) with manual nav items (strings from content.data). With useActiveRoute, both work without branching:
const renderNavItem = (item) => {
const href = item.href || item.navigableRoute
const label = item.label || item.title
const active = href === '/'
? isActive(href)
: isActiveOrAncestor(href)
return <Link href={href} className={active ? 'active' : ''}>{label}</Link>
}Why useActiveRoute over manual comparison: Three things you'd otherwise have to handle yourself:
Route normalization and base path —
isActiveOrAncestordelegates toWebsite.isRouteActiveOrAncestor(), which normalizes slashes and handles subdirectory deployments (base:insite.yml). A rawstartsWithcomparison breaks when the site is deployed at/docs/.Reactive during navigation — The hook reads from React Router's
useLocation(), which updates synchronously when the user clicks a link.website.activePageis a property on a vanilla JS singleton — it's correct but doesn't trigger React re-renders on its own. If you readwebsite.activePageduring render, you may see the previous page until the next render cycle.Consistent API — The same
isActive(pageOrString)call works whether the nav item came fromgetPageHierarchy()(page objects) or from ayaml:navblock (plain strings withhref). No branching onitem.route ? ... : ...needed.
See Also
- Content Structure — Full content parsing reference
- Page Configuration — Navigation visibility options (hidden, hideIn)
- Linking — The
page:protocol for stable internal links - Component Metadata — Documenting nav expectations in meta.js