`<ul>` and `<ol>` must only directly contain `<li>`, `<script>` or `<template>` elements
A `<ul>` or `<ol>` element contains children other than `<li>`, `<script>`, or `<template>` elements. This breaks the semantic list structure and confuses screen readers.
Rule Description
This rule ensures <ul> and <ol> elements only contain valid children according to HTML specifications.
What This Rule Checks
- Direct children of
<ul>are only<li>,<script>, or<template> - Direct children of
<ol>are only<li>,<script>, or<template> - No other elements appear as direct children
What This Rule Does Not Check
- Content inside
<li>elements (can be anything) - Whether list semantics are appropriate for the content
- Visual styling of lists
Best Practices
- Only li elements - Use
<li>as direct children - Nest properly - Put nested lists inside
<li> - Wrap inside li - Place divs, spans inside
<li>, not outside - Semantic structure - Use lists for actual lists of items
- Allowed exceptions - Only
<script>and<template>besides<li>
Why It Matters
Impact on Users
- Screen reader users rely on proper list structure to navigate and understand content
- Keyboard navigation users use list shortcuts that may not work correctly
- All users benefit from semantic HTML that conveys document structure
- SEO - Search engines use list markup to understand content organization
Real-World Scenario
A screen reader announces: "List with 5 items" but only reads 3 items because div elements break the structure. The user is confused about missing items and cannot navigate the list properly using list shortcuts.
How to Fix
Solution 1: Only Use <li> as Direct Children
List elements should only directly contain list items.
Bad Example:
<!-- FAIL - div directly in ul --> <ul> <div> <li>Item 1</li> <li>Item 2</li> </div> <li>Item 3</li> </ul>
Good Example:
<!-- PASS - Only li elements --> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>
Solution 2: Move Wrapper Elements Inside <li>
Place wrapper elements inside list items, not outside.
Bad Example:
<!-- FAIL - span wrapping li --> <ul> <span class="group"> <li>Item 1</li> <li>Item 2</li> </span> </ul>
Good Example:
<!-- PASS - span inside li --> <ul> <li><span class="group">Item 1</span></li> <li><span class="group">Item 2</span></li> </ul> <!-- Or wrap content, not li elements --> <ul class="group"> <li>Item 1</li> <li>Item 2</li> </ul>
Solution 3: Complex List Items
Put all content inside <li> elements.
Bad Example:
<!-- FAIL - heading and p outside li --> <ul> <h3>Section Title</h3> <p>Description</p> <li>Item 1</li> <li>Item 2</li> </ul>
Good Example:
<!-- PASS - All content in li --> <ul> <li> <h3>Section Title</h3> <p>Description</p> </li> <li>Item 1</li> <li>Item 2</li> </ul> <!-- Or use proper heading structure --> <section> <h3>Section Title</h3> <p>Description</p> <ul> <li>Item 1</li> <li>Item 2</li> </ul> </section>
Solution 4: Nested Lists
Nest lists inside <li> elements.
Bad Example:
<!-- FAIL - ul directly in ul --> <ul> <li>Item 1</li> <ul> <li>Nested 1</li> <li>Nested 2</li> </ul> </ul>
Good Example:
<!-- PASS - Nested ul inside li --> <ul> <li>Item 1 <ul> <li>Nested 1</li> <li>Nested 2</li> </ul> </li> <li>Item 2</li> </ul>
Solution 5: Scripts and Templates are Allowed
<script> and <template> are valid direct children.
Good Example:
<!-- PASS - script and template are allowed --> <ul> <li>Item 1</li> <li>Item 2</li> <script> // Analytics or tracking code </script> <template id="list-item-template"> <li></li> </template> </ul>
Common Mistakes
1. Text Directly in List
<!-- FAIL --> <ul> Some text <li>Item 1</li> <li>Item 2</li> </ul> <!-- PASS --> <ul> <li>Some text</li> <li>Item 1</li> <li>Item 2</li> </ul>
2. Div Wrapper Around List Items
<!-- FAIL --> <ul> <div class="wrapper"> <li>Item 1</li> <li>Item 2</li> </div> </ul> <!-- PASS --> <ul class="wrapper"> <li>Item 1</li> <li>Item 2</li> </ul>
3. Headings in Lists
<!-- FAIL --> <ul> <h4>List Title</h4> <li>Item 1</li> <li>Item 2</li> </ul> <!-- PASS - Option 1 --> <section> <h4>List Title</h4> <ul> <li>Item 1</li> <li>Item 2</li> </ul> </section> <!-- PASS - Option 2 --> <ul> <li> <h4>List Title</h4> </li> <li>Item 1</li> <li>Item 2</li> </ul>
4. Links or Buttons Outside List Items
<!-- FAIL --> <ul> <a href="/more">View More</a> <li>Item 1</li> <li>Item 2</li> </ul> <!-- PASS --> <ul> <li>Item 1</li> <li>Item 2</li> <li><a href="/more">View More</a></li> </ul> <!-- Or separate from list --> <ul> <li>Item 1</li> <li>Item 2</li> </ul> <a href="/more">View More</a>
5. Improper Nesting
<!-- FAIL --> <ul> <li>Item 1</li> <ol> <li>Nested 1</li> </ol> </ul> <!-- PASS --> <ul> <li>Item 1 <ol> <li>Nested 1</li> </ol> </li> </ul>
Correct List Structures
Simple Unordered List
<ul> <li>First item</li> <li>Second item</li> <li>Third item</li> </ul>
Simple Ordered List
<ol> <li>Step one</li> <li>Step two</li> <li>Step three</li> </ol>
Nested Lists
<ul> <li>Main item 1 <ul> <li>Sub item 1.1</li> <li>Sub item 1.2</li> </ul> </li> <li>Main item 2 <ul> <li>Sub item 2.1</li> <li>Sub item 2.2</li> </ul> </li> </ul>
Complex List Items
<ul> <li> <h4>Product Name</h4> <p>Product description goes here.</p> <a href="/product">View details</a> </li> <li> <h4>Another Product</h4> <p>Another description.</p> <a href="/product2">View details</a> </li> </ul>
Lists with Icons/Images
<ul> <li> <img src="icon1.png" alt="Feature icon"> <span>Feature one</span> </li> <li> <img src="icon2.png" alt="Feature icon"> <span>Feature two</span> </li> </ul>
Navigation Lists
<nav aria-label="Main navigation"> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/services">Services</a> <ul> <li><a href="/services/web">Web Design</a></li> <li><a href="/services/seo">SEO</a></li> </ul> </li> <li><a href="/contact">Contact</a></li> </ul> </nav>
Lists with Scripts
<ul> <li>Item 1</li> <li>Item 2</li> <script> // Allowed: tracking, analytics, etc. console.log('List rendered'); </script> </ul>
Lists with Templates
<ul id="dynamic-list"> <li>Static item</li> <template id="item-template"> <li class="dynamic-item"> <span class="title"></span> <button class="delete">Delete</button> </li> </template> </ul>
Description Lists (Different Rule)
Note: <dl> (description lists) have different requirements:
<!-- Definition list - different structure --> <dl> <dt>Term 1</dt> <dd>Definition 1</dd> <dt>Term 2</dt> <dd>Definition 2</dd> </dl>
Testing
Manual Testing
- Inspect list elements in the page
- Verify only
<li>,<script>, or<template>are direct children - Check nested lists are inside
<li>elements - Ensure all list content is within
<li>elements
Screen Reader Testing
NVDA/JAWS: Navigate to list
Expected: "List with X items" (correct count)
Press L to navigate by lists:
Expected: All lists announced with correct item counts
Inside list, press I to navigate by items:
Expected: All items are navigable
Automated Testing
// Check list structure const lists = document.querySelectorAll('ul, ol'); lists.forEach(list => { Array.from(list.children).forEach(child => { const tagName = child.tagName.toLowerCase(); if (!['li', 'script', 'template'].includes(tagName)) { console.error( `Invalid direct child in ${list.tagName}:`, tagName, child ); } }); }); // Using axe-core const results = await axe.run(); const listViolations = results.violations.filter( v => v.id === 'list' );
Browser DevTools
// Find invalid list structures Array.from(document.querySelectorAll('ul, ol')).forEach(list => { const invalidChildren = Array.from(list.children) .filter(child => !['LI', 'SCRIPT', 'TEMPLATE'].includes(child.tagName)); if (invalidChildren.length) { console.warn('Invalid list structure:', list, invalidChildren); } }); // Get list statistics $$('ul, ol').forEach(list => { console.log({ type: list.tagName, totalChildren: list.children.length, liCount: list.querySelectorAll(':scope > li').length, invalidCount: Array.from(list.children) .filter(c => !['LI', 'SCRIPT', 'TEMPLATE'].includes(c.tagName)).length }); });
External Resources
Automate Your Accessibility Testing
Our tool automatically checks for this rule and hundreds of other accessibility issues.
Start Your Free Trial