Give it a try
-
To see your bookmarks bar, press the keys
(Ctrl/Cmd + Shift + B). - Drag the "SpotCheck - Landmark Visualizer" 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.
Example of the popup that will appear in the bottom right corner of your browser with a summary of the results.
What the colors mean
Each landmark gets an outline and a small label showing its role.
A uniquely identifiable landmark, or one with a clear accessible name.
More than one landmark of the same type exists but this one has
no aria-label to tell it apart.
A duplicate <main>, a
landmark nested inside another of the same role, or a
region/form with no accessible name at all, so it isn't exposed
as a landmark.
Try it here
Run the check against the sample markup below without leaving this page.
Applicable WCAG 2.2 success criteria
Landmarks give assistive technology a programmatic map of the page, so people can jump straight to the content they need.
-
1.3.1
Info and Relationships(Level A)
Structure that is visually obvious, like a header or a sidebar, must also be conveyed programmatically, which is exactly what landmark roles do.
-
2.4.1
Bypass Blocks(Level A)
A properly labeled main and nav landmark let screen reader and keyboard users skip repeated blocks of content on every page.
-
1.3.6
Identify Purpose(Level AAA)
Landmark roles help user-agent features and personalization tools recognize the purpose of each region.
Read the source
The bookmarklet contains the TL;DR version of the code. Here's the readable version.
(function () {
var NS = '__a11yLandmarkViz';
if (window[NS] && window[NS].active) {
window[NS].teardown();
return;
}
var SEL = 'header,nav,main,aside,footer,form,section,[role]';
var LANDMARK_ROLES = {
banner: 1, navigation: 1, main: 1, complementary: 1, contentinfo: 1,
search: 1, form: 1, region: 1
};
var TAG_ROLE = {
HEADER: 'banner', NAV: 'navigation', MAIN: 'main',
ASIDE: 'complementary', FOOTER: 'contentinfo', FORM: 'form', SECTION: 'region'
};
function isTopLevel(el) {
// banner/contentinfo only count as landmarks when not nested in another
// sectioning element; keep it simple and just track nesting of same role.
return true;
}
function getRole(el) {
var explicit = el.getAttribute('role');
if (explicit && LANDMARK_ROLES[explicit]) return explicit;
var byTag = TAG_ROLE[el.tagName];
if (byTag) {
if ((el.tagName === 'HEADER' || el.tagName === 'FOOTER') && el.closest('article,aside,main,nav,section')) {
return null; // header/footer nested in sectioning content isn't a landmark
}
return byTag;
}
return null;
}
function getAccessibleName(el) {
var labelledby = el.getAttribute('aria-labelledby');
if (labelledby) {
var text = labelledby.split(/\s+/).map(function (id) {
var ref = document.getElementById(id);
return ref ? ref.textContent.trim() : '';
}).join(' ').trim();
if (text) return text;
}
var label = el.getAttribute('aria-label');
if (label && label.trim()) return label.trim();
return '';
}
var els = Array.prototype.filter.call(document.querySelectorAll(SEL), function (el) {
return !!getRole(el);
});
var byRole = {};
els.forEach(function (el) {
var role = getRole(el);
(byRole[role] = byRole[role] || []).push(el);
});
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 = '2px';
if (getComputedStyle(el).position === 'static') {
el.style.position = 'relative';
}
var badge = document.createElement('div');
badge.textContent = label;
badge.setAttribute('aria-hidden', 'true');
badge.setAttribute('data-a11y-bm-badge', 'landmark');
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]++;
}
els.forEach(function (el) {
var role = getRole(el);
var group = byRole[role];
var name = getAccessibleName(el);
var nestedSame = el.parentElement && el.parentElement.closest(SEL) &&
getRole(el.parentElement.closest(SEL)) === role;
if (role === 'main' && group.length > 1) {
mark(el, 'red', 'main (duplicate)');
} else if (nestedSame) {
mark(el, 'red', role + ' (nested in same role)');
} else if ((role === 'region' || role === 'form') && !name) {
mark(el, 'red', role + ' (no accessible name, not exposed as landmark)');
} else if (group.length > 1 && !name) {
mark(el, 'gold', role + ' (ambiguous, add aria-label)');
} else {
mark(el, 'green', role + (name ? ': ' + name : ''));
}
});
var panel = document.createElement('div');
panel.setAttribute('data-a11y-bm-panel', 'landmark');
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;">Landmark Visualizer</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
-
Only
<header>/<footer>elements that are not nested inside<article>,<aside>,<main>,<nav>, or<section>count as banner/contentinfo landmarks. -
A
<section>or<form>only becomes a landmark once it has an accessible name; otherwise it is invisible to landmark navigation. -
The "Try it here" demo uses plain, labeled placeholder boxes
rather than real
<header>,<nav>, and<main>elements, so trying it out doesn't create real duplicate landmarks on this page. The actual bookmarklet checks real landmark elements on whatever page you run it on.