Lists

Article · 6:00

A list is one of the few elements that tells a screen reader how much is coming before it starts reading. Three kinds cover everything, and the mistake is using the markup for indentation rather than for a set of things.

This lesson has 8 runnable examples. Press Try yourself! under any of them to open it in the editor.

Lists

More of a web page is a list than people realise. Navigation is a list of links. A set of product cards is a list of products. Search results are a list. Steps in a recipe are an ordered list that happens to be styled without numbers.

Marking them up as lists is worth doing because a list announces itself. A screen reader says "list, six items" before it begins, and the reader knows what they are in and how long it is. A stack of div elements styled identically says nothing.

Ordered and unordered

The choice is about whether the order is part of the meaning.

HTML Example 1 14 lines
<h2>Steps</h2>
<ol>
  <li>Open a text editor.</li>
  <li>Write the document skeleton.</li>
  <li>Save the file with a .html extension.</li>
  <li>Open it in a browser.</li>
</ol>

<h2>What you will need</h2>
<ul>
  <li>A text editor</li>
  <li>A browser</li>
  <li>Thirty minutes</li>
</ul>

Open this example in the editor and change it. Nothing you type is saved.

ol when the sequence matters - steps, rankings, instructions. ul when it does not - features, ingredients, navigation. Both contain li elements and nothing else: a ul whose direct child is a div is invalid, and the parser will move the div out of the list.

ol has two attributes worth knowing. start begins the numbering somewhere other than 1, which matters when a list is interrupted by prose and resumes. reversed counts down, for a countdown or a top-ten listed from ten.

HTML Example 2 4 lines
<ol start="4">
  <li>The fourth step, after an interruption.</li>
  <li>The fifth.</li>
</ol>

Open this example in the editor and change it. Nothing you type is saved.

Numbers are content here, not decoration - that is why they come from ol and not from CSS. If what you want is a visual bullet or number on something that is not a list, that is a CSS ::before.

Description lists

dl pairs terms with descriptions: a glossary, a set of metadata, a specification table that is not really tabular.

HTML Example 3 10 lines
<dl>
  <dt>HTML</dt>
  <dd>The markup language that describes the structure of a web page.</dd>

  <dt>CSS</dt>
  <dd>The language that describes how that structure should look.</dd>

  <dt>Void element</dt>
  <dd>An element that cannot have content, such as img or br.</dd>
</dl>

Open this example in the editor and change it. Nothing you type is saved.

The rules are looser than they look. One dt can have several dd elements - a term with several definitions. Several dt elements can share one dd - synonyms with a single definition. Both are valid and both are announced correctly.

What dl is not is a layout tool. It gets reached for because the default styling indents the description, which is a bad reason: the indent is CSS and the meaning is "these terms have these descriptions".

Nesting

A list inside a list represents hierarchy, and there is one placement rule that catches everyone.

HTML Example 4 14 lines
<ul>
  <li>Foundations
    <ul>
      <li>Introduction</li>
      <li>Document structure</li>
    </ul>
  </li>
  <li>Forms and media
    <ul>
      <li>Forms</li>
      <li>Audio and video</li>
    </ul>
  </li>
</ul>

Open this example in the editor and change it. Nothing you type is saved.

The nested ul goes inside the li, not between two li elements. Put it between them and it is a child of the outer ul, which is invalid, and the parser will attach it to the preceding item anyway - producing something that looks right and is not what you wrote.

Keep nesting shallow. Three levels is usually a sign the information wants a different shape.

Navigation is a list

The single most common real use, and worth writing out in full because it combines three lessons.

HTML Example 5 8 lines
<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/courses">Courses</a></li>
    <li><a href="/playground" aria-current="page">Playground</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

Open this example in the editor and change it. Nothing you type is saved.

A nav landmark so it can be jumped to, a ul so its length is announced, and aria-current="page" on the link to the page you are already on - which is the piece almost always missing, and the one that tells a screen reader user where they are.

The bullets and the vertical stacking are removed in CSS with list-style: none and a flex row. Removing the bullets visually does not remove the list semantics, with one caveat worth knowing: Safari with VoiceOver stops announcing a list whose list-style is none, and the fix is role="list" on the ul.

Things that are lists but do not look like them

A row of cards, a gallery, a set of search results, a comment thread: each is a set of similar things, so each is a list, however it is laid out.

HTML Example 6 15 lines
<h2>Courses</h2>
<ul class="card-grid">
  <li>
    <article>
      <h3><a href="/courses/html">HTML</a></h3>
      <p>Thirty topics, every example runnable.</p>
    </article>
  </li>
  <li>
    <article>
      <h3><a href="/courses/css">CSS</a></h3>
      <p>Layout, typography and the cascade.</p>
    </article>
  </li>
</ul>

Open this example in the editor and change it. Nothing you type is saved.

The CSS turns that into a grid of cards. The markup still says "list, 2 items", so a screen reader user knows how many courses there are before reading any of them - which a set of sibling article elements would not tell them.

The one element inside a list

li is the only child a ul or an ol may have. Anything else - a div wrapping two items, a stray paragraph between them, a heading - is invalid, and the parser moves it out of the list rather than keeping it where you wrote it.

HTML Example 7 10 lines
<ul>
  <li>
    <h3>Foundations</h3>
    <p>Eight topics covering structure, text, links and tables.</p>
  </li>
  <li>
    <h3>Forms and media</h3>
    <p>Six topics on collecting input and embedding media.</p>
  </li>
</ul>

Open this example in the editor and change it. Nothing you type is saved.

Note where the headings and paragraphs are: inside the li, which is allowed and useful. An li can contain anything - headings, paragraphs, images, another list, a whole article. What it cannot do is have a sibling that is not another li.

That is why a list of cards wraps each card in an li rather than giving the ul a row of div children. The structure survives; a screen reader still counts the items.

Removing the bullets without removing the list

Almost every list on a real site has its markers removed in CSS. That is normal and it does not change the markup's meaning - with one exception worth knowing about, because it silently costs you the thing the list was for.

HTML Example 8 7 lines
<nav aria-label="Footer">
  <ul role="list" style="list-style: none; padding-left: 0">
    <li><a href="/privacy">Privacy</a></li>
    <li><a href="/terms">Terms</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Open this example in the editor and change it. Nothing you type is saved.

Safari with VoiceOver stops announcing a list once list-style is none, on the reasoning that a list without markers is being used for layout. Often it is - but not here. Adding role="list" restores the announcement, and it is harmless everywhere else because it only states what the element already is.

The same applies to ol. If the numbers are decoration you are free to remove them; if they are content, consider whether the list should be a ul of items that each contain their own label.

When something is not a list

Two cases to resist:

Indentation. A ul with one item, used because its margin happens to indent. That is a CSS margin.

Prose with bullets for emphasis. Three sentences that flow into each other are a paragraph. Bulleting them because bullets look organised produces a list of fragments that read badly aloud.

The test: could the items be reordered, or counted, and would either operation make sense? If yes, it is a list.

Practice task

Take the navigation on a page you have built and rewrite it as a nav containing a ul, with aria-current="page" on the current page's link. Then add a nested list somewhere - a table of contents with sections and subsections - and check in the Elements panel that the inner ul really is inside an li and not a sibling of one.