skip to content

In HTML, which elements are allowed as direct children of <ul> and <ol>, and where must a nested sublist be placed?

level: juniorimportance: must knowfreq 60%

answer

  1. only one element type may sit directly inside
  2. wrappers belong one level deeper
  3. the item is permissive, the list is not
  4. sublists go inside the parent item
  5. script and template are the only exceptions

basics

~10 s

Only <li> elements may be direct children of <ul> and <ol>, plus the script-supporting elements <script> and <template>. A nested sublist belongs inside the <li> it hangs off, never between two <li> elements.

solid answer

~40 s

The content model of `<ul>` and `<ol>` is zero or more `<li>` elements, optionally intermixed with `<script>` and `<template>`. Nothing else is allowed as a direct child — no `<div>` row wrapper, no bare `<a>`, no visible text node. An `<li>`, by contrast, accepts flow content, so almost anything can go *inside* an item, including another list. That is why a submenu is written as `<li>Products<ul>…</ul></li>`: the sublist sits inside the parent item, so assistive tech reports it as a level-two list belonging to "Products". Put that same `<ul>` between two `<li>` elements and it becomes an orphan list at the same level, and the parent item no longer owns it. If you need a per-row wrapper for layout, put the `<div>` inside the `<li>`, not around it.

code

html · 9 lines
html
<ul>
  <li>Products
    <ul>
      <li><a href="/phones">Phones</a></li>
      <li><a href="/laptops">Laptops</a></li>
    </ul>
  </li>
  <li><a href="/pricing">Pricing</a></li>
</ul>

go deeper

for a junior

Be ready to state plainly that only <li> may sit directly inside <ul> or <ol>, and to write a two-level menu with the sublist inside the parent <li>.

for a middle

Explain the asymmetry: the list container has a strict content model while <li> takes flow content, and describe what nesting position changes about how a list and its levels are exposed.

for a senior

Show that you catch this in generated markup — loops that emit wrapper elements or sibling sublists — and that you can justify the fix in terms of announced item counts rather than validation for its own sake.

for a principal

Own the guardrail: decide whether HTML validation and accessibility-tree checks run in CI so structural list defects are caught by the pipeline instead of a screen-reader audit months later.

## The content model, stated exactly The HTML specification gives `<ul>` and `<ol>` an unusually narrow content model: zero or more `<li>` elements, optionally intermixed with the *script-supporting elements* `<script>` and `<template>`. That is the complete list of legal direct children. A `<div>`, a `<p>`, a bare `<a>`, or a visible text node directly inside `<ul>` is a conformance error. Inter-element whitespace (the newlines and indentation between tags) is fine and is not counted as content. `<li>` is the opposite: its content model is *flow content*, meaning practically anything — paragraphs, headings, images, buttons, form controls, and another list. So the rule is easy to remember as "the list is strict about its children; the item is permissive about its own". ```html <!-- invalid: div is not allowed as a child of ul --> <ul> <div class="row"><a href="/a">A</a></div> </ul> <!-- valid: the wrapper lives inside the item --> <ul> <li><div class="row"><a href="/a">A</a></div></li> </ul> ``` ## "But it renders fine" is not a defence Browsers are famously forgiving, and a `<div>` written directly inside `<ul>` will still appear on screen. The HTML parser does not move it or drop it — it simply becomes a child element of the list in the DOM. What you lose is not pixels, it is meaning: the list's accessibility semantics rest on the assumption that its children are list items, so an interloper can disrupt how the list and its item count are exposed, and it makes the markup fail validation. Since the fix costs one extra element, there is no reason to argue the point in an interview. ## Why nesting position matters so much A nested list is the case people get wrong most often, because both versions look identical once styled: ```html <!-- wrong: the sublist is a sibling of the items, not part of one --> <ul> <li>Products</li> <ul> <li>Phones</li> </ul> </ul> <!-- right: the sublist is inside the item it belongs to --> <ul> <li>Products <ul> <li>Phones</li> </ul> </li> </ul> ``` In the wrong version the inner `<ul>` is not associated with "Products" at all. A screen reader encounters two lists at the same level, so the relationship a sighted user reads from the indentation — these phones belong under Products — is simply absent for someone listening. In the right version the inner list is a descendant of the item, and assistive technology can report nesting level along with position, e.g. entering a level-two list of one item. A useful detail: the `</li>` end tag is optional. An `<li>` is closed implicitly by the next `<li>` or by the parent's end tag, which means `<li>Products <ul>…</ul>` still places the sublist inside the item — the item has not ended yet. Omitting end tags is legal but makes the structure harder to read, so most teams close items explicitly. ## What correct list markup actually buys `<ul>` and `<ol>` carry the implicit ARIA role `list`, and `<li>` carries `listitem`. From that, screen readers derive an announcement along the lines of "list, five items" on entry, a position for each item as you move through it, and the nesting level for sublists. None of that exists for a stack of links in a `<div>`: the user gets no count, no sense of progress, and no boundary telling them the group has ended. It is the cheapest accessibility win in HTML, and it is free with the correct element. ## The bugs this rule prevents Three ship regularly. First, the `<div>` row wrapper added when someone needs a flex container per row — move it inside the `<li>`. Second, the sublist placed as a sibling of the items, usually produced by a template that emits children after the parent item's closing tag. Third, a template that renders a comment banner, a text node, or a framework-injected element straight into the list container. Whenever markup is generated in a loop, check the *generated* HTML rather than the template: the loop body is where the stray direct child appears.

  • What can an <li> itself contain?
    Flow content — effectively anything that can appear in the body: paragraphs, headings, images, buttons, form controls, and further lists. That asymmetry is the point: the list container is strict so its structure stays machine-readable, while each item is free to hold whatever the design needs.
  • If the </li> end tag is optional, when does an <li> actually end?
    It ends at the next `<li>` start tag or at the parent list's end tag, whichever comes first. So markup like `<li>Products <ul>…</ul>` still nests the sublist inside the item, because the item has not closed yet. It is legal but easy to misread, so most teams close `<li>` explicitly.
  • Does the parser move a <div> that you write directly inside a <ul>?
    No. Unlike misplaced content in table markup, there is no relocation rule for lists — the `<div>` stays exactly where you put it, as a child element of the list in the DOM. It renders, it fails validation, and it undermines the assumption that a list's children are list items.

Think of the list as a bookshelf that accepts only shelves, while each shelf holds anything you like — including a smaller shelf standing on it.

saying these in an interview costs you the question

  • Thinks any element can be a direct child of ul
  • Puts a nested ul between two li elements
  • Wraps each li in a div for layout
  • Believes browser rendering proves the markup is valid
  • Adds text directly inside ul as a heading for the group

context