Nested Menu

Sidebar navigation component with collapsible cascading levels using native details/summary elements

Nested Menu

Nested menus provide sidebar navigation with collapsible cascading levels. Built on native <details>/<summary> elements, they offer accessible expand/collapse behavior with zero JavaScript.

Unlike the flat Menu component (designed for dropdowns), nested menus are purpose-built for persistent sidebar navigation with section headers, active states, and unlimited nesting depth.

Basic Usage

A simple nested menu with links and a section title:

Basic Nested Menu

Collapsible Sections

Use <details> and <summary> for collapsible groups. Add the open attribute to expand a section by default:

Collapsible Sections

Deep Nesting

Nested menus support unlimited depth. Each level adds indentation automatically:

Deep Nesting

Active State

Mark the current page with the active class or aria-current="page":

Active State

aria-current Support

For better accessibility, use aria-current="page" instead of or alongside active:

aria-current Active

Disabled Items

Use the disabled class on <li> elements to disable navigation items:

Disabled Items

Size Variants

Extra Small

Extra Small

Small

Small

Large

Large

Bordered Variant

Add a panel-like appearance with border, surface background, and shadow:

Bordered Variant

Compact Variant

Tighter padding for dense interfaces:

Compact Variant

Combining Variants

Variants can be combined for tailored results:

Bordered + Compact + Small

Full Sidebar Example

A realistic documentation sidebar combining all features:

Documentation Sidebar

Button Items

Menu items can also be <button> elements for non-navigation actions:

Button Items

  • Actions

Best Practices

Accessibility

  • Use aria-current="page" to indicate the current page for screen readers
  • The <details>/<summary> pattern is natively keyboard accessible (Enter/Space to toggle)
  • Add aria-label="Navigation" to the wrapping <nav> element
  • Disabled items still appear in the DOM — use aria-disabled="true" alongside the .disabled class

Accessible Navigation

Content Guidelines

  • Keep section titles short (1-2 words)
  • Use descriptive link text — avoid “Click here”
  • Group related items under collapsible sections
  • Don’t nest deeper than 3 levels
  • Place the most important items at the top

Framework Examples

React

interface NavItem {
  label: string;
  href?: string;
  active?: boolean;
  disabled?: boolean;
  children?: NavItem[];
}

interface NestedMenuProps {
  title?: string;
  items: NavItem[];
  variant?: 'default' | 'bordered' | 'compact';
}

function NavItemComponent({ item }: { item: NavItem }) {
  if (item.children) {
    return (
      <li>
        <details open>
          <summary>{item.label}</summary>
          <ul>
            {item.children.map((child, i) => (
              <NavItemComponent key={i} item={child} />
            ))}
          </ul>
        </details>
      </li>
    );
  }

  return (
    <li className={item.disabled ? 'disabled' : ''}>
      <a
        href={item.href}
        className={item.active ? 'active' : ''}
        aria-current={item.active ? 'page' : undefined}
      >
        {item.label}
      </a>
    </li>
  );
}

export function NestedMenu({ title, items, variant = 'default' }: NestedMenuProps) {
  const classes = [
    'nested-menu',
    variant === 'bordered' && 'nested-menu-bordered',
    variant === 'compact' && 'nested-menu-compact',
  ].filter(Boolean).join(' ');

  return (
    <ul className={classes}>
      {title && <li className="nested-menu-title">{title}</li>}
      {items.map((item, i) => (
        <NavItemComponent key={i} item={item} />
      ))}
    </ul>
  );
}

Vue

<template>
  <ul :class="menuClasses">
    <li v-if="title" class="nested-menu-title">{{ title }}</li>
    <NavItem v-for="(item, i) in items" :key="i" :item="item" />
  </ul>
</template>

<script setup>
import { computed } from 'vue';
import NavItem from './NavItem.vue';

const props = defineProps({
  title: String,
  items: { type: Array, required: true },
  bordered: Boolean,
  compact: Boolean,
});

const menuClasses = computed(() => [
  'nested-menu',
  props.bordered && 'nested-menu-bordered',
  props.compact && 'nested-menu-compact',
]);
</script>

API Reference

Class Names

ClassElementDescription
.nested-menu<ul>Root container (required)
.nested-menu-title<li>Section header (uppercase, muted)
.active<a> / <button>Active item highlight
.disabled<li>Disabled item (opacity, no pointer)
.nested-menu-xsrootExtra small size variant
.nested-menu-smrootSmall size variant
.nested-menu-lgrootLarge size variant
.nested-menu-borderedrootPanel look with border, bg, shadow
.nested-menu-compactrootTighter padding throughout

HTML Structure

No classes are needed on <details>, <summary>, or nested <ul> elements — they are all styled via descendant selectors from .nested-menu.

<ul class="nested-menu">
  <li class="nested-menu-title">Section</li>
  <li><a href="/page">Leaf item</a></li>
  <li>
    <details open>
      <summary>Collapsible group</summary>
      <ul>
        <li><a href="/child">Nested item</a></li>
      </ul>
    </details>
  </li>
</ul>
  • Menu - Flat dropdown menu for temporary surfaces
  • Drawer - Slide-out panel for sidebar navigation
  • Accordion - Expandable content panels
  • Tabs - Tabbed navigation