Search topics
Lists built from divs, icons and typed numbers
A plan card draws its features as divs with check icons and its setup steps as paragraphs with typed numbers, so a screen reader hears loose lines: no list, no count, and nothing to jump to or skip past.
<div class="feature"><svg class="icon-check" …>Unlimited projects</div> The problem
Screen reader users hear the plan's features and steps as loose lines, with no count, no end, and no way to jump to them or past them.
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.
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
divwith 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
pwhose 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.
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.
2 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision A screen reader announces a list and how many items it holds before reading them, and says when it ends. Here the features and steps are read as unrelated lines, the list command finds nothing, and there is no way to skip past them.
- Limited vision People who use a screen reader with magnification hear the same loose lines. Reader views and user style sheets rebuild the page from its markup, drop the check icons, and leave lines with nothing to show they belong together.
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.
Try it
Listen to the plan card, then jump to its list the way a screen reader user would.
The panel shows what NVDA or JAWS reads from the top of the card. “Press L” is the key that jumps to the next list. Does anything say a list starts, how long it is, or where it ends? Broken 2 is the steps, with numbers typed into paragraphs.
Broken Can you fix this?
Reading the card from the top, a screen reader says:
Team plan, heading level 3Unlimited projects50 GB of storageShared calendarsPriority supportA recreated demo. The broken version is intentionally inaccessible.
The fix
Mark each group of items as a list, with ul and ol or with list roles, and number ordered steps once.
- 1Anything drawn as a list is a list: ul, ol or dl, or role="list" with role="listitem" items.
- 2Use an ordered list when the order matters, and number it once.
- 3Keep only items directly inside the list, and no role on it that overrides it.
Features drawn with icons
<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>Ul and ol elements
<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>Why this fix: Use native HTML first
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.
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.
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.
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
ulorolmay hold onlylielements, so a component that wraps each item in adivmust move the wrapper inside theli. - An item without its list. Conditional rendering that drops the
ulbut keeps itslielements leaves items that belong to nothing. - Terms and descriptions outside a
dl. For pairs such as "Storage: 50 GB", use adlwithdtandddelements, each pair optionally in onediv, and nothing else directly inside. - A role on the list.
role="presentation"orrole="navigation"on aulstops it being a list. Put a navigation list inside anavelement 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.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
4 checks, no mouse.
Test with: Screen reader
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.
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.
References
- WCAG Understanding: info-and-relationships (opens in a new tab)
- WCAG Technique H48 (opens in a new tab)
- developer.mozilla.org: ul (opens in a new tab)
- developer.mozilla.org: ol (opens in a new tab)
- developer.mozilla.org: list_role (opens in a new tab)
- dequeuniversity.com: list (opens in a new tab)
- dequeuniversity.com: listitem (opens in a new tab)
- dequeuniversity.com: definition-list (opens in a new tab)
- dequeuniversity.com: dlitem (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.