Accessibility pattern · Layout and structure

Lists and groups

A screen reader announces a list and its length before the first item, so people know how much is coming and can skip past it. Styling can quietly take that away; role="list" and CSS counters keep it.

WCAG criteria
4
Keyboard rules
2
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

What's in the box

ul

  • Steel kettle, 1.5 litres
  • Power base with a 1 m cord
  • Spare parts
    • Mesh filter
    • Lid seal
  • Quick-start card

Set up your kettle

ol

  1. Rinse the kettle and fill it to the MAX line.
  2. Boil once and pour that water away.
  3. Set it on the base and switch on at the wall.
  4. Press the lever; it clicks off when the water boils.

Order details

dl

Order number
40215
Placed on
3 October 2026
Arrives
Thursday, 8 October
Paid
₹1,899 by UPI

Features

ul

  • Auto shut-off
  • Boil-dry protection
  • 360° base
  • BIS certified
  • 2-year warranty

More kettles

ul

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves through the links in the product cards. Lists are not tab stops; they need no keyboard handling at all.
L or IScreen reader keys, not the page's: L moves to the next list and I to the next list item, in NVDA and JAWS. The markup is all they need.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
A screen reader reaches What's in the boxList, 4 items. Steel kettle, 1.5 litres
It moves into Spare partsSpare parts. List, 2 items, nested. Mesh filter
It reaches the setup stepsList, 4 items. 1 Rinse the kettle and fill it to the MAX line.
It reaches the order detailsDescription list, 4 items. Order number, 40215
Broken version: it reaches the featuresAuto shut-off. Boil-dry protection. 360° base (no list, no count)

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-lists" data-ap-lists>
  <div class="ap-lists__card" data-ls-example>
    <div class="ap-lists__top">
      <h3 class="ap-lists__name" id="ls-box-name">What's in the box</h3>
      <code class="ap-lists__tag" translate="no">ul</code>
    </div>
    <p class="ap-lists__readout" data-ls-readout></p>
    <ul class="ap-lists__bullets">
      <li>Steel kettle, 1.5 litres</li>
      <li>Power base with a 1 m cord</li>
      <li>Spare parts
        <ul class="ap-lists__bullets">
          <li>Mesh filter</li>
          <li>Lid seal</li>
        </ul>
      </li>
      <li>Quick-start card</li>
    </ul>
  </div>

  <div class="ap-lists__card" data-ls-example>
    <div class="ap-lists__top">
      <h3 class="ap-lists__name" id="ls-steps-name">Set up your kettle</h3>
      <code class="ap-lists__tag" translate="no">ol</code>
    </div>
    <p class="ap-lists__readout" data-ls-readout></p>
    <!-- list-style: none hides the markers; role="list" keeps the list in Safari. -->
    <ol class="ap-lists__steps" role="list">
      <li class="ap-lists__step">Rinse the kettle and fill it to the MAX line.</li>
      <li class="ap-lists__step">Boil once and pour that water away.</li>
      <li class="ap-lists__step">Set it on the base and switch on at the wall.</li>
      <li class="ap-lists__step">Press the lever; it clicks off when the water boils.</li>
    </ol>
  </div>

  <div class="ap-lists__card" data-ls-example>
    <div class="ap-lists__top">
      <h3 class="ap-lists__name" id="ls-order-name">Order details</h3>
      <code class="ap-lists__tag" translate="no">dl</code>
    </div>
    <p class="ap-lists__readout" data-ls-readout></p>
    <dl class="ap-lists__facts">
      <div class="ap-lists__fact"><dt class="ap-lists__dt">Order number</dt><dd class="ap-lists__dd">40215</dd></div>
      <div class="ap-lists__fact"><dt class="ap-lists__dt">Placed on</dt><dd class="ap-lists__dd">3 October 2026</dd></div>
      <div class="ap-lists__fact"><dt class="ap-lists__dt">Arrives</dt><dd class="ap-lists__dd">Thursday, 8 October</dd></div>
      <div class="ap-lists__fact"><dt class="ap-lists__dt">Paid</dt><dd class="ap-lists__dd">₹1,899 by UPI</dd></div>
    </dl>
  </div>

  <div class="ap-lists__card" data-ls-example>
    <div class="ap-lists__top">
      <h3 class="ap-lists__name" id="ls-features-name">Features</h3>
      <code class="ap-lists__tag" translate="no">ul</code>
    </div>
    <p class="ap-lists__readout" data-ls-readout></p>
    <ul class="ap-lists__chips" role="list">
      <li class="ap-lists__chip">Auto shut-off</li>
      <li class="ap-lists__chip">Boil-dry protection</li>
      <li class="ap-lists__chip">360° base</li>
      <li class="ap-lists__chip">BIS certified</li>
      <li class="ap-lists__chip">2-year warranty</li>
    </ul>
  </div>

  <div class="ap-lists__card ap-lists__card--wide" data-ls-example>
    <div class="ap-lists__top">
      <h3 class="ap-lists__name" id="ls-more-name">More kettles</h3>
      <code class="ap-lists__tag" translate="no">ul</code>
    </div>
    <p class="ap-lists__readout" data-ls-readout></p>
    <ul class="ap-lists__products" role="list">
      <li class="ap-lists__product">
        <span class="ap-lists__swatch" aria-hidden="true"></span>
        <a class="ap-lists__link" href="#ls-mini">Tapri mini, 1 litre</a>
        <span class="ap-lists__meta">Steel · ₹1,499</span>
      </li>
      <li class="ap-lists__product">
        <span class="ap-lists__swatch ap-lists__swatch--glass" aria-hidden="true"></span>
        <a class="ap-lists__link" href="#ls-glass">Tapri glass, 1.7 litres</a>
        <span class="ap-lists__meta">Glass · ₹2,250</span>
      </li>
      <li class="ap-lists__product">
        <span class="ap-lists__swatch ap-lists__swatch--gooseneck" aria-hidden="true"></span>
        <a class="ap-lists__link" href="#ls-pour">Tapri pour-over</a>
        <span class="ap-lists__meta">Gooseneck · ₹2,990</span>
      </li>
    </ul>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Groups of items are ul, ol and dl elements, so their count, order and nesting are exposed rather than drawn with bullets and spacing.

  • 1.3.2 Meaningful Sequence Level A

    The setup steps are an ordered list, so the sequence is part of the markup, and the numbers come from the list itself.

  • 1.4.10 Reflow Level AA

    The tags wrap and the cards fall into one column in a narrow container, so nothing scrolls sideways at 320 pixels.

  • 2.4.4 Link Purpose (In Context) Level A

    Each card's link is the product's name, so a list of links still says where each one goes.

Usage

When to use it

Use it

  • Any set of similar things: features, search results, cards, steps, tags, navigation links.
  • Name and value pairs, like order details or specifications: a description list.

Use something else

  • A single item: a list of one adds an announcement and nothing else.
  • Layout for things that are not a set, like a logo beside a menu.

Common failures

How it usually goes wrong

  • Divs with drawn bullets

    Bullets and numbers drawn on divs look like a list but are read as separate lines, with no count and no way to skip to the end.

  • list-style: none and nothing else

    Safari drops the list role from a ul or ol whose markers are removed, so VoiceOver reads plain text. role="list" puts it back.

  • Numbers lost in the styling

    Step numbers drawn as background images, or hidden with the markers, disappear for screen reader users. CSS counters in ::before are read as text.

  • One list per item

    Wrapping each card in its own ul announces list, 1 item, five times. Put the cards in one list.

  • Typed-in numbers in an ol

    "1. Rinse the kettle" inside an ol that also numbers itself can be read as 1, 1. Let the list number, and keep the text plain.

  • Anything else between dt and dd

    A description list may hold dt and dd, optionally wrapped in a div per pair. Paragraphs or spans in between break the pairs.

Notes

Building it

  • Lists need no script. The one on this page only builds the readouts, counting items the way a screen reader does and checking the Safari case.
  • role="list" on a ul or ol repeats what the element already says everywhere except Safari, which drops list semantics when list-style is none. It costs nothing to add.
  • The step numbers come from counter(list-item) in ::before. Browsers expose generated content as text, so the numbers are read, and the ol's own start and reversed attributes still drive them.
  • In a description list, a div around each dt and its dd is allowed and makes styling easier; nothing else may sit between the pairs.
  • In the broken version, the script swaps every list for divs with the same classes, so it looks the same; only the readouts, and screen readers, notice.

Sources: WAI tutorial: Content structure, lists · WCAG technique H48: ol, ul and dl for lists · Scott O'Hara: Fixing lists in Safari

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction