# Lists built from divs, icons and typed numbers

Source: https://easeweb.dev/learn/list-markup
Topics: Page structure > Lists are marked up as lists (WCAG 1.3.1)
The fix: Mark every group of items that looks like a list as one. Either rebuild the features as a ul and the steps as an ol, letting the ol number them, or, where only attributes can change, give the existing container role="list" and each item role="listitem", keeping the typed numbers. Either way, keep the list markup valid: only items directly inside the list, and no role that overrides it.
Test with: screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html https://www.w3.org/WAI/WCAG22/Techniques/html/H48 https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ul https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ol https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role https://dequeuniversity.com/rules/axe/4.10/list https://dequeuniversity.com/rules/axe/4.10/listitem https://dequeuniversity.com/rules/axe/4.10/definition-list https://dequeuniversity.com/rules/axe/4.10/dlitem

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A pricing page has a card for its Team plan. Under the plan's name are four features, each with a green check mark in front of it: unlimited projects, 50 GB of storage, shared calendars and priority support. Below them, under Getting started, are three numbered steps for setting the plan up. Anyone looking at the card sees a list of four features and a list of three steps. A screen reader user hears something else. Reading down the card, it says "Team plan, heading level 3", then each feature as a line of its own, with nothing before the first to say a list starts or how long it is, and nothing after the last to say it has ended. Pressing L, the key that jumps to the next list in NVDA and JAWS, gets "no next list". The steps are read with their numbers, but again as separate lines, not as a list of three.

## Why it fails

1.3.1 needs the structure people see to be in the code too. The check marks, the alignment and the numbers all show that these lines are items of one group, and of how many. The markup says nothing of the kind, in two different ways:

- Each feature is a `div` with an inline SVG check mark. The icon is hidden from screen readers, as decorative icons should be, so what remains is four unrelated blocks of text.
- Each step is a `p` whose number is typed into its text. The number is read, but the three paragraphs are not grouped, so there is no count and nothing for list navigation to find. Some reviewers accept typed numbers as the "available in text" route of 1.3.1, because the order can be heard. The Trusted Tester process and most audits don't: the grouping and the count still aren't there.

## Who is affected

The card looks right in every browser, so nobody who checks it by eye sees a problem. It reaches the people who get a page's structure from its markup.

## What automation and AI miss

axe's list rules check the markup of lists that already exist: a `div` between a `ul` and its items (`list`), or an `li` with no list around it (`listitem`). A card with no list markup at all gives them nothing to check, so both forms here pass every axe scan. EaseWeb's audit reports lines that start with a typed bullet or number in sequence, and bullets drawn with CSS on elements that aren't list items. Rows that start with an icon are sent for review rather than reported, because a row of icons can as easily be a set of links or a toolbar. AI fixes go wrong in their own ways. Wrapping the steps in an `ol` and leaving the typed numbers in reads every step twice, as "1. 1. Create your team.". Adding `role="list"` to the container but leaving a wrapper `div` around each item makes a list with no items. Whether a group of lines is meant as a list, and whether its order matters, is a judgement against what the page shows.

## Before: features drawn with icons

```html
<h3>Team plan</h3>
<div class="features">
  <div class="feature"><svg class="icon-check" aria-hidden="true">…</svg>Unlimited projects</div>
  <div class="feature"><svg class="icon-check" aria-hidden="true">…</svg>50 GB of storage</div>
  <div class="feature"><svg class="icon-check" aria-hidden="true">…</svg>Shared calendars</div>
  <div class="feature"><svg class="icon-check" aria-hidden="true">…</svg>Priority support</div>
</div>
```

The check marks make a list on screen, but they are the only part of it, and they are hidden from screen readers. The four `div` elements carry no relationship, so they are read as four lines that happen to follow each other.

## Before: steps with typed numbers

```html
<h3>Getting started</h3>
<p>1. Create your team.</p>
<p>2. Invite up to 10 people.</p>
<p>3. Connect your calendar.</p>
```

The numbers are text, so the order is heard, but the steps are three paragraphs, not a list of three. Nothing tells a screen reader where the steps start or end, and an editor who adds a step has to renumber the rest by hand.

## After: ul and ol elements

```html
<h3>Team plan</h3>
<ul class="features">
  <li><svg class="icon-check" aria-hidden="true">…</svg>Unlimited projects</li>
  <li><svg class="icon-check" aria-hidden="true">…</svg>50 GB of storage</li>
  <li><svg class="icon-check" aria-hidden="true">…</svg>Shared calendars</li>
  <li><svg class="icon-check" aria-hidden="true">…</svg>Priority support</li>
</ul>

<h3>Getting started</h3>
<ol class="steps">
  <li>Create your team.</li>
  <li>Invite up to 10 people.</li>
  <li>Connect your calendar.</li>
</ol>
```

```css
.features {
  list-style: none;
  padding: 0;
}
```

The features are an unordered list and the steps an ordered one, so a screen reader announces "list, 4 items" and "list, 3 items" before reading them, says when each ends, and stops at both when someone presses L. The `ol` numbers the steps itself, so the typed numbers are gone and a new step is numbered without anyone editing the rest. The check marks stay, with the default bullets turned off in CSS so the card looks exactly as it did.

## After: list roles on the existing elements

```html
<h3>Team plan</h3>
<div class="features" role="list">
  <div class="feature" role="listitem"><svg class="icon-check" aria-hidden="true">…</svg>Unlimited projects</div>
  <div class="feature" role="listitem"><svg class="icon-check" aria-hidden="true">…</svg>50 GB of storage</div>
  <div class="feature" role="listitem"><svg class="icon-check" aria-hidden="true">…</svg>Shared calendars</div>
  <div class="feature" role="listitem"><svg class="icon-check" aria-hidden="true">…</svg>Priority support</div>
</div>

<h3>Getting started</h3>
<div class="steps" role="list">
  <p role="listitem">1. Create your team.</p>
  <p role="listitem">2. Invite up to 10 people.</p>
  <p role="listitem">3. Connect your calendar.</p>
</div>
```

The containers become lists and their children list items, so a screen reader announces both lists with their counts, exactly as with `ul` and `ol`. ARIA has no ordered list role, so the steps keep their typed numbers: they are how the order reaches the listener, and they still have to be renumbered by hand. Every child of a `role="list"` element must be a `role="listitem"`, so a wrapper added between them later breaks the list again. Reader views, copy and paste, and anything else that reads the HTML rather than the accessibility tree still sees `div` and `p` elements.

## Which option to use

Both make each group a list with the right count and meet 1.3.1. Use `ul` and `ol` wherever you can change the markup: they do the same job with no attributes to keep in step, the `ol` numbers the steps for you, and every tool that reads the page, not only screen readers, sees a list. Keep the roles for markup you can add attributes to but not change, such as a component library's grid, and switch to list elements when you can.

## Implementation decisions

Turning the bullets off in CSS is fine, with one exception. Safari has long stopped announcing a `ul` or `ol` as a list in VoiceOver when its CSS removes the markers (`list-style: none`), unless it sits inside a `nav`. If the count matters there, as it does for a list of plan features, add `role="list"` to that `ul`. It is redundant in other browsers and does no harm, so keep it out of lists whose markers show.

List markup only helps while it stays valid. Each of these breaks a list that was built correctly:

- A wrapper between the list and its items. A `ul` or `ol` may hold only `li` elements, so a component that wraps each item in a `div` must move the wrapper inside the `li`.
- An item without its list. Conditional rendering that drops the `ul` but keeps its `li` elements leaves items that belong to nothing.
- Terms and descriptions outside a `dl`. For pairs such as "Storage: 50 GB", use a `dl` with `dt` and `dd` elements, each pair optionally in one `div`, and nothing else directly inside.
- A role on the list. `role="presentation"` or `role="navigation"` on a `ul` stops it being a list. Put a navigation list inside a `nav` element instead.

Choose the list by its content, not its look. Use an `ol` when the order matters, such as steps or a ranking, and a `ul` when it doesn't, even if the design shows numbers. Style either one with CSS: check marks, custom counters and horizontal layouts all keep the list. Nest a list inside the `li` it belongs to, never between items.

Not every group needs to be a list. A single item, two links side by side, or a sentence that names three things are fine as they are. Groups of cards, navigation links and search results gain from being lists, because the count tells a listener how much there is before they start.

Typed bullets and numbers usually come from a CMS, where text is pasted in from a document. Turn on the editor's list buttons, and have it convert a line that starts with "1." or "•" into a list as it is typed.

## Verify the fix

1. In the browser's accessibility tree, find each group drawn as a list: it is a list whose children are list items, and the number of items matches what is on screen.
2. Check every list whose order matters is ordered, and that each item has one number, with none typed in beside the list's own.
3. With NVDA or JAWS, read each list from the line above it: it is announced as a list with its count, the items follow, and leaving it announces the end. Press L to move from list to list: it stops at each one and nowhere else.
4. In Safari with VoiceOver, read each list whose markers are hidden by CSS and confirm it is still announced as a list. If it isn't, add `role="list"` to it.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever a shared list component, the CMS editor or the styles that remove markers change.

## Limits

This is a starting pattern, not a guarantee for every application. Class names and the `…` inside the icons are placeholders. Screen readers word lists differently, "list, 4 items", "list with 4 items" or "4 items", so listen for the count and the end rather than the exact phrasing. This guide covers whether content shown as a list is marked up as one. It doesn't cover tables, headings, or lists that can be reordered by dragging. Test the complete journey with your target assistive technology.
