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:

FieldDescription
idPage identifier
routeURL path
titlePage title
labelShort nav label (falls back to title)
childrenNested 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
OptionEffect
hideIn: ['*']Hide from every nav menu; page stays routed + reachable by URL
hideIn: [header]Hide from named nav areas only (header, footer, sidebar, …)
hidden: trueDifferent 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

ApproachBest ForComplexity
Markdown listsSimple flat or nested linksLow
Nav schema (YAML)Icons, descriptions, metadataMedium

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>
  )
}

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
```
FieldTypeDescription
labelstringRequired. Display text
hrefstringLink destination
iconstringPath to icon file (e.g., /icons/home.svg)
textstringSecondary text (description, subtitle)
targetstringLink target (_self, _blank)
childrennav[]Nested navigation items (recursive)
ordernumberCustom sort order
hiddenbooleanHide from display
currentbooleanMark 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

ScenarioRecommendation
Simple site, few pagesAutomatic navigation
Need icons or descriptionsNav schema (YAML)
Quick prototypeMarkdown lists
Deep nesting (3+ levels)Nav schema (YAML)
Mix of internal + external linksNav schema (YAML)
Content authors prefer markdownMarkdown 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
```
```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
```
```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:

  1. Route normalization and base path — isActiveOrAncestor delegates to Website.isRouteActiveOrAncestor(), which normalizes slashes and handles subdirectory deployments (base: in site.yml). A raw startsWith comparison breaks when the site is deployed at /docs/.

  2. Reactive during navigation — The hook reads from React Router's useLocation(), which updates synchronously when the user clicks a link. website.activePage is a property on a vanilla JS singleton — it's correct but doesn't trigger React re-renders on its own. If you read website.activePage during render, you may see the previous page until the next render cycle.

  3. Consistent API — The same isActive(pageOrString) call works whether the nav item came from getPageHierarchy() (page objects) or from a yaml:nav block (plain strings with href). No branching on item.route ? ... : ... needed.

See Also