How to fix: Lists must be marked up as lists
Related items should use real <ul> or <ol> markup so assistive tech announces how many there are and that they belong together.
The fix
Ensure <ul> and <ol> contain only <li> (plus <script>/<template>) as direct children.
Who this affects
Entering a correctly marked list makes a screen reader announce the type and the size, for example "list, six items", and repeat the position as the user moves down. A <div> or a loose line of text sitting directly inside the <ul> is read out inside the list without being one of the counted items, so what an NVDA user hears in a filter panel or a navigation menu no longer matches what is on screen, and the list navigation keys, L and I in NVDA and JAWS, step straight past that content. Where a role on the container replaces the list semantics, the count and the position go with it.
Before and after
<ul><div>Item</div></ul>
<ul><li>Item</li></ul>
Where this usually comes from
- A wrapper <div> added inside the <ul> to hold flex or grid layout classes.
- A child component whose root element is a <div> rather than an <li>, used inside a list component.
- Virtual scrolling libraries that insert a sizing container between the list and its rendered rows.
- Rich-text output from a CMS where an editor left a stray text node or a <p> between the items.
- Loading spinners, empty-state text or ad slots rendered inside the <ul> instead of after it.
Checking it yourself
Select the <ul> or <ol> in the Chrome DevTools Elements panel and check every direct child is an <li>, with <script> and <template> the only other things allowed. In the console, [...document.querySelectorAll('ul,ol')].filter(l => [...l.children].some(c => !['LI','SCRIPT','TEMPLATE'].includes(c.tagName) && c.getAttribute('role') !== 'listitem')) returns the element-level offenders, though stray text between items only shows up by eye in the Elements panel. With NVDA or VoiceOver, arrow through the list and check that every visible item is announced with its position; anything read out inside the list with no position is the stray content.
A scanner finds this one automatically. What it cannot judge, and how the score is built, is on how we test.
Related fixes
Check your site for this issue
Scan any page free against WCAG 2.2 AA and get every automatically detected issue with its fix.