Nested Lists and Multi Level Menus

A sublist goes inside the li it belongs to, not beside it. That one rule fixes almost every broken nested list, and it is what makes a multi level menu work.

Concept

Lists nest. A list item can contain a whole list of its own, which is how you express a hierarchy: chapters and sections, categories and subcategories, a menu with submenus.

There is exactly one rule, and it is the rule everyone gets wrong the first time:

The nested list goes inside the li it belongs to, before that item is closed. Not between two li elements.

Syntax

<!-- correct -->
<ul>
  <li>Fruit
    <ul>
      <li>Mango</li>
      <li>Guava</li>
    </ul>
  </li>
  <li>Vegetables</li>
</ul>

<!-- invalid: the ul is a direct child of the outer ul -->
<ul>
  <li>Fruit</li>
  <ul>
    <li>Mango</li>
  </ul>
</ul>

The invalid version usually still looks right, because the browser indents it anyway. But the sublist is no longer attached to Fruit, so a screen reader announces it as a separate list rather than as part of that item, and the relationship is lost.

Example: a category tree

<ul>
  <li>Kitchen
    <ul>
      <li>Cookware
        <ul>
          <li>Pans</li>
          <li>Pressure cookers</li>
        </ul>
      </li>
      <li>Storage</li>
    </ul>
  </li>
  <li>Lighting</li>
</ul>

Three levels deep is about the practical limit. Beyond that, readers lose track and screen reader announcements - list, three items, item one, list, two items - become exhausting.

Mixing list types

The inner list does not have to match the outer one. Choose each level on its own merits.

<ol>
  <li>Prepare the surface
    <ul>
      <li>Wire brush</li>
      <li>Detergent</li>
      <li>Clean cloth</li>
    </ul>
  </li>
  <li>Apply the primer</li>
  <li>Apply two coats of paint</li>
</ol>

The steps are ordered; the tools for a step are not.

A multi level menu

<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>

    <li>
      <button type="button" aria-expanded="false" aria-controls="menu-courses">
        Courses
      </button>
      <ul id="menu-courses" hidden>
        <li><a href="/courses/design">Design</a></li>
        <li>
          <button type="button" aria-expanded="false" aria-controls="menu-data">
            Data
          </button>
          <ul id="menu-data" hidden>
            <li><a href="/courses/data/analytics">Analytics</a></li>
            <li><a href="/courses/data/engineering">Engineering</a></li>
          </ul>
        </li>
      </ul>
    </li>

    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Every submenu sits inside the item that owns it. Every trigger is a button, because opening a menu is an action rather than navigation. Every trigger declares aria-expanded, which the script must keep in step with reality - that attribute is the only signal a screen reader gets that the menu opened.

hidden on the closed submenu matters too: it removes those links from the tab order. A menu hidden only with opacity: 0 is invisible but still focusable, so a keyboard user tabs through links they cannot see.

Styling the tree

/* remove default look, keep the structure */
nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* indent each level */
nav li ul {
  padding-left: 1.25rem;
}

/* different marker per level, when markers are wanted */
ul            { list-style-type: disc; }
ul ul         { list-style-type: circle; }
ul ul ul      { list-style-type: square; }

ol            { list-style-type: decimal; }
ol ol         { list-style-type: lower-alpha; }
ol ol ol      { list-style-type: lower-roman; }

Important rules

  • A nested list is always a child of an li.
  • Close the inner list before closing the li that contains it.
  • Any depth is technically legal; three levels is the practical limit.
  • Indentation in the source is for humans. The nesting is what the browser reads.
  • Hidden submenus should use hidden or display: none so their links leave the tab order.

Common mistakes

  • Putting the sublist between two li elements. It looks right and is structurally wrong.
  • Forgetting to close the inner list, so the parser nests everything that follows.
  • Building a menu five levels deep.
  • Hover only submenus, which no keyboard user can open.
  • Leaving aria-expanded stuck at false while a class does the real toggling.
  • Using nested lists purely to indent a paragraph.

Best practices

  • Indent your source one level per nesting level so mistakes are visible.
  • Keep menus to two levels; put the rest on a landing page.
  • Use a button for every submenu trigger and keep aria-expanded accurate.
  • Close open menus on Escape and return focus to the trigger.
  • Validate the markup - an unclosed nested list is a classic silent bug.

Practice

  1. Build a three level category tree and validate it. Then move one sublist outside its li and see which errors appear.
  2. Write an ordered list of steps where one step contains an unordered list of tools.
  3. Build a two level menu with button triggers and hidden submenus, then tab through it and confirm hidden links are skipped.
  4. Style a nested list with a different marker at each of three levels.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →
HTML

Project Exams

Five larger assessments where the deliverable is a working site. Requirements, constraints and a rubric for each.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.