Skip to main content

Custom Sidebar Navigation

Add your own navigation links, grouped sections, and nested menus to the sidebar alongside (or instead of) the auto-generated spec navigation.

Quick Start​

import { OmniSpecRenderer } from '@apiboost/omnispec'
import type { SidebarNavConfig } from '@apiboost/omnispec'

const sidebarNav: SidebarNavConfig = {
heading: 'Quick Links',
items: [
{ id: 'home', label: 'Home', href: '/' },
{ id: 'guides', label: 'Getting Started', href: '/docs/guides' },
{ id: 'status', label: 'API Status', href: 'https://status.example.com' },
],
}

<OmniSpecRenderer spec={specUrl} sidebarNav={sidebarNav} />

Custom links appear above the spec navigation with a divider between them.

Placement​

Control where custom navigation renders relative to the spec-derived nav:

// Above spec nav (default)
sidebarNav={{ placement: 'before', items: [...] }}

// Below spec nav
sidebarNav={{ placement: 'after', items: [...] }}

// Replace spec nav entirely
sidebarNav={{ placement: 'replace', items: [...] }}
PlacementSpec nav visibleDivider shown
before (default)YesYes (configurable)
afterYesYes (configurable)
replaceNoNo

Hide the divider with showDivider: false.

Grouped Sections​

Organize links into collapsible groups with labeled headings:

const sidebarNav: SidebarNavConfig = {
placement: 'before',
items: [
{
id: 'docs-group',
label: 'Documentation',
items: [
{ id: 'overview', label: 'Overview', href: '/docs' },
{ id: 'auth-guide', label: 'Authentication', href: '/docs/auth' },
{ id: 'rate-limits', label: 'Rate Limits', href: '/docs/rate-limits' },
],
},
{
id: 'resources-group',
label: 'Resources',
items: [
{ id: 'sdks', label: 'SDKs & Libraries', href: '/sdks' },
{ id: 'support', label: 'Support', href: 'https://support.example.com' },
],
},
],
}

Groups are collapsible — click the heading to expand/collapse. Groups start expanded by default; set defaultExpanded: false to start collapsed.

Nested Items​

Items can have children for multi-level navigation (arbitrary depth):

const sidebarNav: SidebarNavConfig = {
items: [
{
id: 'guides',
label: 'Guides',
children: [
{ id: 'quickstart', label: 'Quickstart', href: '/docs/quickstart' },
{
id: 'auth-section',
label: 'Authentication',
children: [
{ id: 'oauth', label: 'OAuth 2.0', href: '/docs/auth/oauth' },
{ id: 'api-keys', label: 'API Keys', href: '/docs/auth/keys' },
{ id: 'jwt', label: 'JWT Tokens', href: '/docs/auth/jwt' },
],
},
],
},
],
}

URLs starting with http:// or https:// are automatically detected as external links. They open in a new tab and display an external-link icon:

{ id: 'github', label: 'GitHub', href: 'https://github.com/acme/api' }

Override the auto-detection with the target property:

// Force external URL to open in same tab
{ id: 'portal', label: 'Portal', href: 'https://portal.example.com', target: '_self' }

Badges​

Add color-coded badges to highlight new or special items:

{
id: 'webhooks',
label: 'Webhooks',
href: '/docs/webhooks',
badge: 'New',
badgeColor: '#22c55e',
}

The badge prop accepts a string for simple text badges, or a ReactNode for custom rendering.

Icons​

Pass any ReactNode as an icon — it renders before the label:

import { BookOpen, Code, Headphones } from 'lucide-react'

const sidebarNav: SidebarNavConfig = {
items: [
{ id: 'docs', label: 'Documentation', href: '/docs', icon: <BookOpen size={14} /> },
{ id: 'sdks', label: 'SDKs', href: '/sdks', icon: <Code size={14} /> },
{ id: 'support', label: 'Support', href: '/support', icon: <Headphones size={14} /> },
],
}

Active State​

Uncontrolled (Default)​

When no activeId is provided, the sidebar tracks active state automatically by matching item href values against the current URL hash or pathname.

  • href="#section-id" matches window.location.hash
  • href="/docs/guide" matches window.location.pathname

Controlled​

For apps with their own routing (React Router, Next.js, etc.), pass activeId to control which item is highlighted:

import { useLocation } from 'react-router-dom'

function ApiPage() {
const location = useLocation()
const activeId = getNavIdFromPath(location.pathname)

return (
<OmniSpecRenderer
spec={specUrl}
sidebarNav={{
activeId,
onItemClick: (item) => {
if (item.href && !item.href.startsWith('http')) {
navigate(item.href)
return false // Prevent default navigation
}
},
items: navItems,
}}
/>
)
}

When activeId is provided, the component becomes "controlled" — you manage active state entirely.

Collapsing the Sidebar​

On desktop, the sidebar can be collapsed to give the specification content the full width. A chevron control at the top of the sidebar collapses it to a thin rail; clicking the rail expands it again. This is built in and requires no configuration — it applies whether the sidebar shows spec-derived nav, custom sidebarNav, or host-injected slots. On mobile the sidebar is a drawer instead, opened and closed with the floating navigation button.

Click Handler​

The onItemClick callback fires when any custom nav item is clicked. Return false to prevent the default navigation behavior:

sidebarNav={{
onItemClick: (item) => {
analytics.track('sidebar_click', { id: item.id, href: item.href })
// Return undefined to allow default navigation
// Return false to prevent it
},
items: [...],
}}

Combining with Slots​

sidebarNav and slots work together. The full sidebar rendering order:

+-----------------------------------+
| slots.sidebarHeader |
+-----------------------------------+
| sidebarNav heading | (placement: "before")
| Custom nav items / groups |
+-----------------------------------+
| ──── divider ──── |
+-----------------------------------+
| Search bar | (spec-derived)
| Operations / Channels / Types |
| Schemas |
+-----------------------------------+
| sidebarNav heading | (placement: "after")
| Custom nav items / groups |
+-----------------------------------+
| slots.sidebarFooter |
+-----------------------------------+

Example combining both:

<OmniSpecRenderer
spec={specUrl}
slots={{
sidebarHeader: <CompanyLogo />,
sidebarFooter: <VersionInfo version="2.1.0" />,
}}
sidebarNav={{
placement: 'before',
heading: 'Docs',
items: [
{ id: 'overview', label: 'Overview', href: '/docs' },
{ id: 'changelog', label: 'Changelog', href: '/changelog' },
],
}}
/>

See Slots for details on all available slot positions.

TypeScript Reference​

import type {
SidebarNavConfig,
SidebarNavItem,
SidebarNavGroup,
SidebarNavPlacement,
} from '@apiboost/omnispec'

SidebarNavItem​

interface SidebarNavItem {
id: string // Unique identifier
label: string // Display text
href?: string // Navigation target (hash, path, or URL)
target?: '_self' | '_blank'
icon?: ReactNode // Icon before label
badge?: string | ReactNode
badgeColor?: string // Badge background color (string badges only)
children?: SidebarNavItem[]
defaultExpanded?: boolean
separator?: boolean // Render as non-interactive heading
className?: string // Custom CSS class
}

SidebarNavGroup​

interface SidebarNavGroup {
id: string // Unique identifier
label: string // Section heading text
items: SidebarNavItem[] // Items in this group
defaultExpanded?: boolean
icon?: ReactNode // Icon for group heading
}

SidebarNavConfig​

interface SidebarNavConfig {
items: Array<SidebarNavItem | SidebarNavGroup>
placement?: 'before' | 'after' | 'replace' // Default: 'before'
activeId?: string // Controlled active state
onItemClick?: (item: SidebarNavItem) => void | false
showDivider?: boolean // Default: true
heading?: string // Section heading above custom nav
}

Spec Type Support​

Custom sidebar navigation works identically across all spec types — OpenAPI, AsyncAPI, GraphQL, SOAP, and gRPC. The buildSidebar helper that composes custom and spec navigation is shared across all renderers.

Full Example​

A complete portal-style sidebar with guides, resources, external links, badges, and nested items:

import { OmniSpecRenderer } from '@apiboost/omnispec'
import type { SidebarNavConfig } from '@apiboost/omnispec'

const sidebarNav: SidebarNavConfig = {
placement: 'before',
showDivider: true,
items: [
{
id: 'guides',
label: 'Guides',
items: [
{ id: 'getting-started', label: 'Getting Started', href: '/' },
{ id: 'auth', label: 'Authentication', href: '/docs/auth' },
{ id: 'rate-limits', label: 'Rate Limits', href: '/docs/rate-limits' },
{ id: 'pagination', label: 'Pagination', href: '/docs/pagination' },
],
},
{
id: 'resources',
label: 'Resources',
items: [
{ id: 'changelog', label: 'Changelog', href: '/docs/changelog' },
{
id: 'sdks',
label: 'SDKs & Libraries',
defaultExpanded: false,
children: [
{ id: 'sdk-js', label: 'JavaScript', href: '/docs/sdks/javascript' },
{ id: 'sdk-python', label: 'Python', href: '/docs/sdks/python' },
{ id: 'sdk-go', label: 'Go', href: '/docs/sdks/go' },
],
},
{
id: 'webhooks',
label: 'Webhooks',
href: '/docs/webhooks',
badge: 'Beta',
badgeColor: '#f59e0b',
},
{ id: 'github', label: 'GitHub', href: 'https://github.com/acme/api' },
{ id: 'status', label: 'API Status', href: 'https://status.example.com' },
],
},
],
}

function OmniSpecPage() {
return (
<OmniSpecRenderer
spec="/specs/openapi.json"
theme={{ base: 'light' }}
sidebarNav={sidebarNav}
slots={{
sidebarHeader: <img src="/logo.svg" alt="Acme" style={{ padding: '0.75rem' }} />,
}}
/>
)
}