list-children-valid

Lists must only directly contain list items

automaticWCAG 1.3.1 (A)confidence: highseverity: seriousperceivable

Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.

Applies to. Applies to <ul>/<ol> elements that have at least one direct element child or non-whitespace text directly inside them, and whose role is list: no role attribute, a role attribute resolving to list (its first token naming a known role, in any case), or one naming no known ARIA role. A <ul>/<ol> given another role (listbox, menubar, tablist, none, ...) is not a list, so its children follow that role's rules instead.

Expectation. Every direct element child is <li>, <script>, or <template>. UNLESS it has an explicit role (its role attribute's first token naming a known role, matched in any case), in which case the explicit role wins over the tag entirely: a child is valid iff that role is "listitem" (so <li role="presentation">/<li role="menuitem"> are invalid despite the <li> tag, and conversely a non-<li> element explicitly given role="listitem" or role="foo LISTITEM" is valid). A role attribute naming no known role leaves the tag to decide. A wrapper <div> used for styling (no role at all) still breaks list semantics the same as before. Non-whitespace text directly inside the list is an invalid child too: HTML allows only <li> and script-supporting elements there, and the text belongs to no list item. A child that does not belong FAILS only where the page shows an item the markup loses: the child is drawn as a list item (display list-item with a marker), or it wraps <li> elements that are, which HTML-AAM then maps to no list item. That is a relationship shown and not programmatically determinable (SC 1.3.1). Anything else (a heading or a separator between the items, loose text, an <li> whose role replaces listitem with no marker drawn) breaks HTML's content model, and H48 is only a sufficient technique: it is reported at CANTTELL, for a person to say whether the page shows it as one of the items. The marker is read from the computed style; jsdom does not resolve the list-style shorthand, so there list-style: none set that way still reads as a marker, and the finding fails as it would have before.

What a finding reports.

  • reasonCode: LIST_ITEM_ROLE_OVERRIDDEN when every child that does not belong is an <li> whose explicit role replaces listitem (<li role="presentation">, role="none", role="group"...), and LIST_INVALID_CHILD for any other child: an element that is not an <li>, or text. A list with both is LIST_INVALID_CHILD.
  • element: the list's tag name, ul or ol.
  • invalidChildren: the children that do not belong in the list, one tag name per child (#text for text placed directly inside).
  • overriddenRoles: with LIST_ITEM_ROLE_OVERRIDDEN, the role of each such <li>, in order.
  • element is a stable contract, unlike the rest of data.details.

These data.details fields help to read and reproduce a finding. Apart from reasonCode and the few fields Output schema names as stable, they are not a stable contract: a field may be renamed or dropped in a minor release, so don’t build on them.

Examples

pass
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>

<ul>'s only direct children are <li>.

fail
<ul>
<div>
<li>Elderberry</li>
</div>
</ul>

A <div> between the <ul> and its <li> leaves the bulleted item in no list.

can't tell
<ul>
<li>Elderberry</li>
<div>wrapper</div>
</ul>

A <div> is a direct child of <ul> alongside the <li>, but it is not drawn as a list item: whether a WCAG criterion fails depends on whether the page shows it as one.

How to check

When this rule can’t decide, it reports cantTell and leaves the question to a person. Each question it can ask is listed here under its reason code, with the steps to answer it. A finding carries the same steps as howToCheck, in the scan’s language. See Output schema.

LIST_INVALID_CHILD

  1. Inspect the list the report names and note the child that is not a list item, script or template, or the text placed directly inside it.
  2. Look at how the page shows the content that does not belong: as one of the list's items (with a bullet or number, or styled like the items), or as something else such as a heading, a separator or loose text.
  3. With a screen reader, read the list and note how many items it announces and whether that content is announced as one of them.

Pass. The content is not shown as one of the list's items, so no item the page shows is missing from the markup. Still move it inside an <li> or out of the list: HTML does not allow it there.

Fail. The page shows the content as one of the list's items, but the markup does not make it one, so that relationship is not programmatically determinable (WCAG 1.3.1).

LIST_ITEM_ROLE_OVERRIDDEN

  1. Inspect the list the report names and note each <li> whose role attribute replaces listitem.
  2. Look at whether the page still shows that <li> as one of the list's items, with a bullet or number or styled like the others.
  3. With a screen reader, read the list and note how many items it announces and whether that content is announced as one of them.

Pass. The content is not shown as one of the list's items, so no item the page shows is missing from the markup. Still move it inside an <li> or out of the list: HTML does not allow it there.

Fail. The page shows the content as one of the list's items, but the markup does not make it one, so that relationship is not programmatically determinable (WCAG 1.3.1).

View source