Skip to content
lab components / Navigation

Side navigation

Allow users to explore content and features through a primary navigation structure with a clear visual hierarchy

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { SideNavigation } from "@siteimprove/fancylab";

#Examples

Ideal for applications with multiple sections and subsections, where a clear visual hierarchy is essential.

The side navigation consists of:

  • Links: Clickable text or icons leading to different content areas.
  • Icons: Visual representations of navigation items (especially important in the collapsed view).
  • Chevrons: Expandable/collapsible indicators for nested navigation levels.
  • Levels: Primary (top-level), secondary, and tertiary levels to organize content.
  • Collapsed view: Can be used to collapse the navigation in mobile or tablet views.

#Basic usage

This is a basic example, where we have selected an item under "data privacy" and the rest of the settings are at the default values.

Best practices:

  • The selected item should be visually distinct.
  • Navigation structure should mirror the content's logical hierarchy.
  • Ensure the collapsed view uses clear, recognizable icons for each top-level item.
  • Dashboards
  • Core Wins
  • Dynamic Content
  • Brand
Digital Certainty Index
Social Media Performance
Marketing Analytics
Explore more modules
Theme: agentic-ai-2025
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>( "dataprivacy-personal-data-types-phone-number" ); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); return ( <> <div style={{ height: "100vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={data.sideNavigation} bottomItems={data.sideBottomNavigation} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} searchHotkey="M" /> </div> </> );

#Falling back to default expansion

This example shows how the component falls back to showing expanded nodes that match with the selection if the expansion sent into the props are not properly showing the selected item. In this case, we select an item under "data privacy", but the expansion is set to showing something under QA. In this case, it will fall back to expanding the the nodes to show the selected item.

  • Use Case: This behavior is important when the provided expansion state doesn't align with the user's selection. It ensures the user can easily see the context of their current selection.
  • Best Practice: Clearly indicate the selected item, even when the navigation falls back to a default state. This includes a subtle animation to smoothly transition between expansion states.
  • Dashboards
  • Core Wins
  • Dynamic Content
  • Brand
Digital Certainty Index
Social Media Performance
Marketing Analytics
Explore more modules
Theme: agentic-ai-2025
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>( "dataprivacy-personal-data-types-phone-number" ); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState(["qa", "qa-spelling-v2"]); return ( <> <div style={{ height: "100vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={data.sideNavigation} bottomItems={data.sideBottomNavigation} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

#Using labels

Menu-items can have labels. They are rendered using the Badge component. The label can have an optional icon, text, and tooltip.

  • Use Case: To highlight new features, internal use or other contextual information.
  • Best Practice:
    • Keep labels concise and visually distinct (use the Badge component).
    • If using icons, choose universally recognizable ones.
    .
Theme: agentic-ai-2025
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); // Copy and wrangle the basic example to show off how to set labels const nav: NavigationItem[] = data.sideNavigation.slice(0, 6); nav[0].labels = [{ type: "highlight1", icon: <IconFeatureFlag />, tooltip: "Feature Flag Xyz" }]; nav[1].labels = [{ type: "highlight3", text: "Internal", tooltip: "Internal use only" }]; nav[3].children![0].labels = [{ type: "highlight2", text: "Demo", tooltip: "Demo data" }]; nav[4].labels = [ { type: "warning", text: "Beta", tooltip: "Feature is still work in progress" }, { type: "highlight1", icon: <IconFeatureFlag />, tooltip: "Feature Flag Xyz" }, ]; nav[5].labels = [{ type: "highlight2", text: "Demo", tooltip: "Demo data" }]; return ( <> <div style={{ height: "35rem" }}> <SideNavigation data-observe-key="side-nav" mainItems={nav} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

This example has items with both href's but also onclick handlers. The idea is that simple clicks will be handled with the "onClick" handler and "preventDefault" so that the browser doesn't navigate. But if you "open in new tab" click, then the navigation happens.

  • Use Case: This pattern is useful when you need to trigger specific actions in addition to, or instead of, navigating to a new page.
  • Best Practice:
    • Clearly communicate to the user whether a click will result in navigation or a different action.
    • If the custom action fails, provide a fallback to ensure the user can still navigate to the destination.
Digital Certainty Index
Theme: agentic-ai-2025
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); const items: NavigationItem[] = [ { children: [], defaultChildId: null, id: "dashboard", title: "Dashboards", href: "#", openNew: false, icon: '\r\n\t\t\t\t\t<svg width="24" height="24" viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">\r\n\t\t\t\t\t\t<path d="M528-624v-192h288v192H528ZM144-432v-384h288v384H144Zm384 288v-384h288v384H528Zm-384 0v-192h288v192H144Zm72-360h144v-240H216v240Zm384 288h144v-240H600v240Zm0-479h144v-49H600v49ZM216-216h144v-48H216v48Zm144-288Zm240-191Zm0 239ZM360-264Z"></path>\r\n\t\t\t\t\t</svg>\r\n\t\t\t\t', observeKey: "side-nav-dashboard", labels: [], visible: true, }, { children: [ { children: [], defaultChildId: null, id: "dci-overview", title: "DCI overview", href: "#", openNew: false, icon: null, observeKey: "side-nav-dci-overview", labels: [], visible: true, }, { children: [], defaultChildId: null, id: "dci-account-overview", title: "My sites", href: "#", openNew: false, icon: null, observeKey: "side-nav-dci-account-overview", labels: [], visible: true, }, { children: [], defaultChildId: null, id: "dci-sites-progress", title: "Sites progress", href: "#", openNew: false, icon: null, observeKey: "side-nav-dci-sites-progress", labels: [ { type: "highlight1", icon: <IconFeatureFlag />, tooltip: "The feature is hidden behind the Feature Flag: RMDCISiteProgress", }, ], visible: true, }, ], defaultChildId: "dci-overview", id: "dci", title: "Digital Certainty Index", href: "#", openNew: false, icon: '\r\n\t\t\t\t\t<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">\r\n\t\t\t\t\t\t<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0127 3.99743L12.0114 3.99885C12.1595 3.99906 12.3075 4.00337 12.4554 4.0118C13.1287 4.05017 13.799 4.17377 14.4499 4.38308C16.198 4.94522 17.6995 6.09142 18.7027 7.62939C19.7059 9.16735 20.1497 11.0035 19.9596 12.8299C19.7695 14.6562 18.9571 16.3617 17.6587 17.6601C16.3603 18.9585 14.6548 19.7709 12.8285 19.961C11.0021 20.1511 9.16593 19.7073 7.62796 18.7041C6.09 17.7009 4.9438 16.1994 4.38166 14.4513C3.93884 13.0743 3.87966 11.6103 4.19954 10.2142C4.28572 9.83802 4.39943 9.46677 4.54058 9.10346L4.90224 8.17253L3.04038 7.4492L2.67872 8.38013C2.50236 8.83408 2.36027 9.29799 2.25256 9.76809C1.85285 11.5127 1.92677 13.3421 2.48013 15.0628C3.18259 17.2472 4.61487 19.1235 6.53671 20.3771C8.45856 21.6307 10.753 22.1852 13.0353 21.9477C15.3175 21.7101 17.4486 20.695 19.0711 19.0725C20.6935 17.45 21.7087 15.3189 21.9463 13.0367C22.1838 10.7545 21.6293 8.45998 20.3757 6.53814C19.1221 4.61629 17.2458 3.18401 15.0614 2.48156C14.2481 2.22002 13.4105 2.06556 12.569 2.01761C12.3842 2.00708 12.1992 2.00168 12.0143 2.00142L11.0156 2L11.0127 3.99743Z"/>\r\n\t\t\t\t\t</svg>\r\n\t\t\t\t', observeKey: "side-nav-dci", labels: [], visible: true, }, ]; return ( <> <div style={{ height: "35rem" }}> <SideNavigation data-observe-key="side-nav" mainItems={items} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

#One main nav item and fixed view

If there is only one main nav item in mainItems the sub nav will be shown on load and the "Back to main navigation" button will be replaced by a back link that uses the props backLink and backLinkLabel. This can be combined with viewState="fixed" which will hide the view state toggle button to collapse and expand the navigation.

  • Use Case: This is perfect for applications with a single primary section and a fixed secondary navigation structure (E.g Page Inspector)
  • Best Practice: In this scenario, ensure the back link is clearly visible and labeled. Since the navigation is always visible, make sure it doesn't obscure or overlap with the main content area.
Theme: agentic-ai-2025
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>("fixed"); const [expandedItems, setExpandedItems] = useState<string[]>(); return ( <> <div style={{ height: "75vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={dataOneMainNavItem} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} backButtonLabel="Back to main navigation" backButtonUrl="http://www.siteimprove.com" /> </div> </> );

#Properties

#Guidelines

#Best practices

#General

Use SideNavigation for deep hierarchies or when users frequently switch sections.

#Placement

Place SideNavigation on the left (desktop). For smaller screen (e.g mobile), use collapsed view or Horizontal navigation.

#Style

  • Siteimprove Design System: Adhere to Siteimprove's guidelines for color, typography, and spacing. If you are not using a component from Fancy, match the styling of your SideNavigatio to existing components for visual consistency.
  • Limit navigation depth to three levels maximum.
  • Prioritize the most important navigation items at the top.
  • Match page titles to navigation link labels.

#Interaction

  • Use chevrons for expanding/collapsing nested levels.
  • Keep quaternary levels (4th level) as simple page links.
  • If more than five secondary items, consider using sub-menus.

#Responsive layout

  • Default to the expanded view.
  • Provide a collapse option with clear icons.
  • Auto-collapse for smaller screens (e.g. breakpoints of 768 pixels for tablet and mobile).

#Do not use when

  • Navigation is very simple (few top-level items).
  • Horizontal navigation is more suitable (e.g., system-level or shortcuts).

#Accessibility

#For designers

  • Ensure icon meanings are clear and labels are intuitive.

#For developers

  • Navigation link labels should be readable by assistive technologies, even when navigation is collapsed

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • Use clear, concise labels under 20 characters to prevent text wrapping. Labels should be scannable yet descriptive.
  • Keep labels consistent in length, style (nouns or verbs), and sentence case.