Give it a try
-
To see your bookmarks bar, press the keys
(Ctrl/Cmd + Shift + B). - Drag the "SpotCheck - List Identifier" link to your bookmarks bar or bookmark folder.
- Open any page, then click the bookmark to run it.
- Click it again or click the close button in the popup to clear the results.
What the colors mean
Every list gets a dashed outline and a label describing what was found.
A properly formed list with one or more list items.
The list is structurally valid but contains one or more empty
<li> elements.
The list is empty, has a direct child that isn't an
<li>, or is an
<li> that isn't inside a
list at all.
Try it here
Run the check against the sample markup below without leaving this page.
Valid list
- Item one
- Item two
Empty list
Malformed list
- Item
Orphaned item
Applicable WCAG 2.2 success criteria
List semantics tell assistive technology how many items there are and where the list starts and ends, which only works when the markup is well formed.
-
1.3.1
Info and Relationships(Level A)
Content that is visually a list needs to be marked up as a real list so screen readers announce item counts and boundaries correctly.
Read the source
The bookmarklet contains the TL;DR version of the code. Here's the readable version.
(function () {
var NS = '__a11yListIdentifier';
if (window[NS] && window[NS].active) {
window[NS].teardown();
return;
}
var lists = Array.prototype.slice.call(document.querySelectorAll('ul,ol'));
var records = [];
var counts = { green: 0, gold: 0, red: 0 };
function mark(el, level, label) {
records.push({ el: el, outline: el.style.outline, outlineOffset: el.style.outlineOffset, position: el.style.position, badge: null });
var color = level === 'green' ? '#1a7d4f' : level === 'gold' ? '#8b6800' : '#be412a';
el.style.outline = '3px solid ' + color;
el.style.outlineOffset = '4px';
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var badge = document.createElement('div');
badge.textContent = label;
badge.setAttribute('aria-hidden', 'true');
badge.style.cssText = [
'position:absolute', 'top:0', 'left:0', 'transform:translateY(-100%)',
'background:' + color, 'color:#fff', 'font:600 14px monospace',
'padding:2px 6px', 'border-radius:3px 3px 0 0', 'z-index:2147483000',
'pointer-events:none', 'white-space:nowrap'
].join(';');
el.insertBefore(badge, el.firstChild);
records[records.length - 1].badge = badge;
counts[level]++;
}
lists.forEach(function (list) {
var directChildren = Array.prototype.slice.call(list.children);
var liChildren = directChildren.filter(function (c) { return c.tagName === 'LI'; });
var otherElementChildren = directChildren.filter(function (c) {
return c.tagName !== 'LI' && c.tagName !== 'TEMPLATE' && c.tagName !== 'SCRIPT';
});
var strayText = Array.prototype.slice.call(list.childNodes).some(function (n) {
return n.nodeType === 3 && n.textContent.trim().length > 0;
});
if (liChildren.length === 0) {
mark(list, 'red', list.tagName.toLowerCase() + ': empty list');
} else if (otherElementChildren.length > 0 || strayText) {
mark(list, 'red', list.tagName.toLowerCase() + ': non-<li> content as a direct child');
} else {
var emptyItems = liChildren.filter(function (li) { return !li.textContent.trim() && !li.children.length; });
if (emptyItems.length) {
mark(list, 'gold', list.tagName.toLowerCase() + ': ' + emptyItems.length + ' empty <li>');
} else {
mark(list, 'green', list.tagName.toLowerCase() + ': ' + liChildren.length + ' items');
}
}
});
// Orphaned <li> not inside a list/menu at all
var orphanLis = Array.prototype.filter.call(document.querySelectorAll('li'), function (li) {
var parent = li.parentElement;
return !parent || !/^(UL|OL|MENU)$/.test(parent.tagName);
});
orphanLis.forEach(function (li) {
mark(li, 'red', 'li: not inside <ul>/<ol>/<menu>');
});
var panel = document.createElement('div');
panel.setAttribute('data-a11y-bm-panel', 'list-identifier');
panel.setAttribute('role', 'status');
panel.setAttribute('aria-live', 'polite');
panel.style.cssText = [
'position:fixed', 'bottom:16px', 'right:16px', 'z-index:2147483001',
'background:#1b2430', 'color:#e7eaed', 'font:13px/1.5 -apple-system,sans-serif',
'padding:12px 32px 12px 16px', 'border-radius:6px', 'box-shadow:0 4px 16px rgba(0,0,0,.3)',
'max-width:260px'
].join(';');
panel.innerHTML =
'<strong style="display:block;margin-bottom:4px;">List Identifier</strong>' +
'<span style="color:#3fbf7f;">●</span> ' + counts.green + ' correct ' +
'<span style="color:#e0b03f;">●</span> ' + counts.gold + ' flagged ' +
'<span style="color:#e06a4f;">●</span> ' + counts.red + ' incorrect';
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.textContent = '\u00d7';
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.style.cssText = [
'position:absolute', 'top:4px', 'right:6px',
'background:transparent', 'border:none', 'color:#e7eaed',
'font:16px/1 -apple-system,sans-serif', 'cursor:pointer',
'padding:2px 6px', 'border-radius:4px', 'line-height:1'
].join(';');
closeBtn.addEventListener('click', function () { window[NS].teardown(); });
panel.appendChild(closeBtn);
document.body.appendChild(panel);
function teardown() {
records.forEach(function (r) {
r.el.style.outline = r.outline;
r.el.style.outlineOffset = r.outlineOffset;
r.el.style.position = r.position;
if (r.badge && r.badge.parentNode) r.badge.parentNode.removeChild(r.badge);
});
if (panel.parentNode) panel.parentNode.removeChild(panel);
}
window[NS] = {
active: true,
teardown: function () {
teardown();
window[NS].active = false;
}
};
})();
FYI
-
An empty
<ul>or<ol>usually means content failed to load, or a list wrapper was left behind after items were removed by script. -
Only element children other than
<li>,<template>, and<script>are flagged as malformed content. -
A stray
<li>not inside a<ul>,<ol>, or<menu>loses its list-item semantics entirely and is announced as plain text.