Landmarks should have a unique role or role/label/title (i.e. accessible name) combination

When a page has several landmarks of the same type, such as two navigation menus, screen readers can only tell them apart by their names. Without unique names, customers hear "navigation" twice and cannot tell the main menu from the footer menu or the breadcrumbs.

Rule id
landmark-unique
Source
axe-core 4.13.0
Impact
Moderate
Fix tier
ASSISTED
on Shopify, Shopware, WooCommerce and Magento
Status
runs

What the check does

Ensure landmarks are unique

The rule collects the page’s landmarks: elements with a landmark role, such as navigation or search, and <header>, <footer>, <nav>, <main>, <aside>, <form> and <section> elements whose computed role is a landmark. Without a role attribute, a <section> or <form> counts only when aria-label or aria-labelledby gives it a name, and a <header> or <footer> inside <article>, <aside>, <nav>, <section> or <main> does not count. Landmarks hidden from assistive technology are left out.

axe-core compares each landmark’s role and its accessible name, in lower case, across the page and its iframes. Two landmarks with the same role fail when neither has a name or when their names match, so “Menu” and “menu” count as a match. axe-core reports one landmark from each such group as the failure; the others appear only as its related elements.

This best-practice rule maps to no WCAG success criterion. Validiscan’s scanner runs it apart from the conformance rules, keeps only its failures and leaves its findings out of remediation progress. The rule does not judge whether the names help a customer tell the landmarks apart.

How to fix it

Give landmarks that share a role distinct accessible names with aria-label or aria-labelledby, such as aria-label="Main menu" and aria-label="Footer menu" on two <nav> elements. Use short, translated names.

WCAG success criteria

Best practice: not tied to a WCAG success criterion.

Recipes that list this rule

This rule has the tier ASSISTED. In a project, a finding of it takes the ASSISTED way, and these recipes are not applied to it.

Show the before and after (1)
Before and after Example data

assets/base.css.liquid Changes to assets/base.css.liquid: 30 added, 0 removed

Changes to assets/base.css.liquid: 30 added, 0 removed
Line before Line after Change Code
1866 1866 }
1867 1867
1868 1868 /* ----------------------------------------------------------------- motion */
1869 Added
1870 Added /* validiscan:skip-link-main */
1871 Added .skip-to-content-link:not(:focus) {
1872 Added position: absolute !important;
1873 Added overflow: hidden;
1874 Added width: 1px;
1875 Added height: 1px;
1876 Added margin: -1px;
1877 Added padding: 0;
1878 Added border: 0;
1879 Added clip: rect(0 0 0 0);
1880 Added white-space: nowrap;
1881 Added }
1882 Added
1883 Added .skip-to-content-link:focus {
1884 Added z-index: 9999;
1885 Added position: fixed !important;
1886 Added top: 0.5rem;
1887 Added left: 0.5rem;
1888 Added overflow: visible;
1889 Added width: auto;
1890 Added height: auto;
1891 Added margin: 0;
1892 Added padding: 0.75rem 1.25rem;
1893 Added clip: auto;
1894 Added background-color: #ffffff;
1895 Added color: #121212;
1896 Added text-decoration: underline;
1897 Added }
1898 Added /* /validiscan:skip-link-main */

layout/checkout.liquid Changes to layout/checkout.liquid: 4 added, 3 removed

Changes to layout/checkout.liquid: 4 added, 3 removed
Line before Line after Change Code
18 18 </head>
19 19
20 20 <body class="template-checkout">
21 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a>
21 22 <header class="checkout-header">
22 23 <div class="page-width checkout-header__inner">
23 24 <a href="{{ routes.root_url }}" class="checkout-header__logo"><img src="{{ 'logo.svg' | asset_url }}" width="140" height="32"></a>
24 Removed <nav class="checkout-breadcrumb">
25 Added <nav class="checkout-breadcrumb" aria-label="{{ 'accessibility.breadcrumbs' | t | escape }}">
25 26 <ol class="checkout-breadcrumb__list" role="list">
26 27 <li><a href="{{ routes.cart_url }}" class="checkout-breadcrumb__link">Cart</a></li>
27 28 <li><span class="checkout-breadcrumb__current">Information</span></li>
31 32 </nav>
32 33 </div>
33 34 </header>
34 Removed <div class="main checkout" id="MainContent">
35 Added <main class="main checkout" id="MainContent" tabindex="-1">
35 36 {{ content_for_layout }}
36 Removed </div>
37 Added </main>
37 38
38 39 <footer class="checkout-footer">
39 40 <div class="page-width">

layout/theme.focused.liquid Changes to layout/theme.focused.liquid: 1 added, 0 removed

Changes to layout/theme.focused.liquid: 1 added, 0 removed
Line before Line after Change Code
22 22 </head>
23 23
24 24 <body class="gradient template-{{ template.name | handleize }} layout-focused">
25 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a>
25 26 {% section 'header' %}
26 27
27 28 <main id="MainContent" class="content-for-layout focus-none" role="main" tabindex="-1">

layout/theme.liquid Changes to layout/theme.liquid: 1 added, 0 removed

Changes to layout/theme.liquid: 1 added, 0 removed
Line before Line after Change Code
28 28 </head>
29 29
30 30 <body class="gradient template-{{ template.name | handleize }}">
31 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a>
31 32
32 33 {% sections 'header-group' %}
33 34

locales/en.default.json Changes to locales/en.default.json: 3 added, 1 removed

Changes to locales/en.default.json: 3 added, 1 removed
Line before Line after Change Code
34 34 "total_reviews": "{{ count }} reviews",
35 35 "link_messages": {
36 36 "new_window": "Opens in a new window."
37 Removed }
37 Added },
38 Added "breadcrumbs": "Breadcrumbs",
39 Added "mobile_menu": "Mobile menu"
38 40 },
39 41 "sections": {
40 42 "header": {

sections/footer.liquid Changes to sections/footer.liquid: 1 added, 1 removed

Changes to sections/footer.liquid: 1 added, 1 removed
Line before Line after Change Code
17 17 {%- endform -%}
18 18 </div>
19 19
20 Removed <nav class="footer__menus">
20 Added <nav class="footer__menus" aria-label="{{ 'sections.footer.menu' | t | escape }}">
21 21 {%- for handle in menu_handles %}
22 22 {%- assign footer_menu = linklists[handle] %}
23 23 <div class="footer-block footer-block--menu">

sections/header.liquid Changes to sections/header.liquid: 2 added, 2 removed

Changes to sections/header.liquid: 2 added, 2 removed
Line before Line after Change Code
11 11 {%- render 'icon', icon: 'menu', size: icon_size -%}
12 12 </summary>
13 13 <div class="menu-drawer">
14 Removed <nav class="menu-drawer__navigation">
14 Added <nav class="menu-drawer__navigation" aria-label="{{ 'accessibility.mobile_menu' | t | escape }}">
15 15 <ul class="menu-drawer__menu list-unstyled" role="list">
16 16 {%- for link in menu.links %}
17 17 <li><a href="{{ link.url }}" class="menu-drawer__menu-item">{{ link.title | escape }}</a></li>
25 25 <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo"></a>
26 26 {% if logo_is_heading %}</h1>{% else %}</div>{% endif %}
27 27
28 Removed <nav class="header__inline-menu">
28 Added <nav class="header__inline-menu" aria-label="{{ 'sections.header.main_menu' | t | escape }}">
29 29 <ul class="list-menu list-menu--inline" role="list">
30 30 {%- for link in menu.links %}
31 31 <li>
Show the before and after (1)
restores the skip links, adds <main> and labels the navigation of the fixture theme Example data

404.php Changes to 404.php: 2 added, 2 removed

Changes to 404.php: 2 added, 2 removed
Line before Line after Change Code
9 9 ?>
10 10
11 11 <div id="primary" class="content-area">
12 Removed <div id="main" class="site-main">
12 Added <main id="main" class="site-main">
13 13 <div class="error-404 not-found">
14 14 <div class="page-content">
15 15 <header class="page-header">
22 22 </section>
23 23 </div>
24 24 </div>
25 Removed </div><!-- #main -->
25 Added </main><!-- #main -->
26 26 </div><!-- #primary -->
27 27
28 28 <?php

archive-product.php Changes to archive-product.php: 2 added, 2 removed

Changes to archive-product.php: 2 added, 2 removed
Line before Line after Change Code
22 22
23 23 <div id="primary" class="content-area">
24 24 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?>
25 Removed <div id="main" class="site-main">
25 Added <main id="main" class="site-main">
26 26
27 27 <header class="woocommerce-products-header">
28 28 <h1 class="woocommerce-products-header__title page-title"><?php woocommerce_page_title(); ?></h1>
46 46 <?php do_action( 'woocommerce_no_products_found' ); ?>
47 47 <?php endif; ?>
48 48
49 Removed </div><!-- #main -->
49 Added </main><!-- #main -->
50 50 </div><!-- #primary -->
51 51
52 52 <?php // The sidebar div gets role="complementary" but no label (VALIDISCAN-SEED landmark-unique on pages with two). ?>

front-page.php Changes to front-page.php: 2 added, 2 removed

Changes to front-page.php: 2 added, 2 removed
Line before Line after Change Code
18 18
19 19 <div id="primary" class="content-area">
20 20 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: the content wrapper is a <div>, not <main>. ?>
21 Removed <div id="main" class="site-main">
21 Added <main id="main" class="site-main">
22 22
23 23 <?php get_template_part( 'template-parts/hero-slider' ); ?>
24 24
62 62 ?>
63 63 </section>
64 64
65 Removed </div><!-- #main -->
65 Added </main><!-- #main -->
66 66 </div><!-- #primary -->
67 67
68 68 <?php

functions.php Changes to functions.php: 1 added, 4 removed

Changes to functions.php: 1 added, 4 removed
Line before Line after Change Code
32 32 * Storefront pieces the child theme does not use.
33 33 */
34 34 function kb_remove_storefront_parts() {
35 Removed // VALIDISCAN-SEED vs-headings-landmarks: Storefront's "Skip to navigation / Skip to content"
36 Removed // links are removed because "they flash on the first Tab"; nothing replaces them.
37 Removed remove_action( 'storefront_before_header', 'storefront_skip_links', 0 );
38 35 remove_action( 'storefront_footer', 'storefront_handheld_footer_bar', 999 );
39 36 remove_action( 'storefront_header', 'storefront_product_search', 40 );
40 37 }
212 209 $defaults['delimiter'] = '<span class="breadcrumb-separator"> / </span>';
213 210 // VALIDISCAN-SEED landmark-unique + vs-headings-landmarks: the breadcrumb <nav> has no aria-label,
214 211 // like the primary and the footer navigation.
215 Removed $defaults['wrap_before'] = '<div class="storefront-breadcrumb"><div class="col-full"><nav class="woocommerce-breadcrumb">';
212 Added $defaults['wrap_before'] = '<div class="storefront-breadcrumb"><div class="col-full"><nav class="woocommerce-breadcrumb" aria-label="' . esc_attr__( 'Breadcrumb', 'kestrel-birch' ) . '">';
216 213 $defaults['wrap_after'] = '</nav></div></div>';
217 214 return $defaults;
218 215 }

header-checkout.php Changes to header-checkout.php: 1 added, 0 removed

Changes to header-checkout.php: 1 added, 0 removed
Line before Line after Change Code
18 18
19 19 <body <?php body_class(); ?>>
20 20 <?php wp_body_open(); ?>
21 Added <a class="skip-link screen-reader-text" href="#content"><?php esc_html_e( 'Skip to content', 'kestrel-birch' ); ?></a>
21 22
22 23 <div id="page" class="hfeed site">
23 24 <header id="masthead" class="site-header kb-checkout-header" role="banner">

header.php Changes to header.php: 1 added, 1 removed

Changes to header.php: 1 added, 1 removed
Line before Line after Change Code
74 74 <div class="storefront-primary-navigation">
75 75 <div class="col-full">
76 76 <?php // VALIDISCAN-SEED landmark-unique + vs-headings-landmarks: the primary <nav> has no aria-label. ?>
77 Removed <nav id="site-navigation" class="main-navigation">
77 Added <nav id="site-navigation" class="main-navigation" aria-label="<?php esc_attr_e( 'Primary navigation', 'kestrel-birch' ); ?>">
78 78 <?php
79 79 wp_nav_menu(
80 80 array(

page-contact.php Changes to page-contact.php: 2 added, 2 removed

Changes to page-contact.php: 2 added, 2 removed
Line before Line after Change Code
24 24
25 25 <div id="primary" class="content-area">
26 26 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?>
27 Removed <div id="main" class="site-main">
27 Added <main id="main" class="site-main">
28 28 <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
29 29 <header class="entry-header"><?php the_title( '<h1 class="entry-title">', '</h1>' ); ?></header>
30 30 <div class="entry-content">
85 85 </div>
86 86 </div>
87 87 </article>
88 Removed </div><!-- #main -->
88 Added </main><!-- #main -->
89 89 </div><!-- #primary -->
90 90
91 91 <?php

page.php Changes to page.php: 2 added, 2 removed

Changes to page.php: 2 added, 2 removed
Line before Line after Change Code
13 13
14 14 <div id="primary" class="content-area">
15 15 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?>
16 Removed <div id="main" class="site-main">
16 Added <main id="main" class="site-main">
17 17
18 18 <?php
19 19 while ( have_posts() ) :
35 35 endwhile;
36 36 ?>
37 37
38 Removed </div><!-- #main -->
38 Added </main><!-- #main -->
39 39 </div><!-- #primary -->
40 40
41 41 <?php

search.php Changes to search.php: 2 added, 2 removed

Changes to search.php: 2 added, 2 removed
Line before Line after Change Code
11 11
12 12 <div id="primary" class="content-area">
13 13 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?>
14 Removed <div id="main" class="site-main">
14 Added <main id="main" class="site-main">
15 15 <header class="woocommerce-products-header">
16 16 <h1 class="woocommerce-products-header__title page-title">
17 17 <?php
36 36 <?php else : ?>
37 37 <?php wc_no_products_found(); ?>
38 38 <?php endif; ?>
39 Removed </div><!-- #main -->
39 Added </main><!-- #main -->
40 40 </div><!-- #primary -->
41 41
42 42 <?php

single-product.php Changes to single-product.php: 2 added, 2 removed

Changes to single-product.php: 2 added, 2 removed
Line before Line after Change Code
13 13
14 14 <div id="primary" class="content-area">
15 15 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?>
16 Removed <div id="main" class="site-main">
16 Added <main id="main" class="site-main">
17 17 <?php
18 18 while ( have_posts() ) :
19 19 the_post();
20 20 wc_get_template_part( 'content', 'single-product' );
21 21 endwhile;
22 22 ?>
23 Removed </div><!-- #main -->
23 Added </main><!-- #main -->
24 24 </div><!-- #primary -->
25 25
26 26 <?php
Show the before and after (1)
fixes the vs-headings-landmarks finding and validates Example data

app/design/frontend/Validiscan/fixture/Magento_Cms/layout/cms_index_index.xml Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/layout/cms_index_index.xml: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/layout/cms_index_index.xml: 1 added, 1 removed
Line before Line after Change Code
6 6 <div>, so the home page has no <main> landmark (and, with the page title hidden by
7 7 display:none in _theme.less, no h1 either).
8 8 Fixed variant: keep htmlTag="main"; hide the title visually only (clip), not with display:none. -->
9 Removed <referenceContainer name="main.content" htmlTag="div" htmlId="maincontent" htmlClass="page-main"/>
9 Added <referenceContainer name="main.content" htmlTag="main" htmlId="maincontent" htmlClass="page-main"/>
10 10 <referenceBlock name="breadcrumbs" remove="true"/>
11 11 </body>
12 12 </page>

app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/footer.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/footer.phtml: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/footer.phtml: 1 added, 1 removed
Line before Line after Change Code
19 19 ?>
20 20 <!-- VALIDISCAN-SEEDED list + landmark-unique: list items wrapped in a div inside the ul; second unlabelled nav -->
21 21 <?php /* Fixed variant: <nav class="footer-nav" aria-label="Footer links"> with the <li> elements directly in the <ul>. */ ?>
22 Removed <nav class="footer-nav">
22 Added <nav class="footer-nav" aria-label="<?= $escaper->escapeHtmlAttr(__('Footer links')) ?>">
23 23 <ul class="footer links">
24 24 <div class="footer-col">
25 25 <?php foreach ($links as $link): ?>

app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/header.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/header.phtml: 1 added, 0 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/header.phtml: 1 added, 0 removed
Line before Line after Change Code
13 13 <header class="page-header">
14 14 <div class="panel wrapper">
15 15 <div class="panel header">
16 Added <a class="action skip contentarea" href="#contentarea"><span><?= $escaper->escapeHtml(__('Skip to Content')) ?></span></a>
16 17 <!-- VALIDISCAN-SEEDED vs-headings-landmarks: the skip link was removed from the header panel -->
17 18 <?php /* Fixed variant: <a class="action skip contentarea" href="#contentarea"><span>Skip to Content</span></a> */ ?>
18 19 <?php if ($linksMoved): ?>

app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/topmenu.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/topmenu.phtml: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/html/topmenu.phtml: 1 added, 1 removed
Line before Line after Change Code
15 15 <div class="section-item-content nav-sections-item-content" id="store.menu" data-role="content">
16 16 <!-- VALIDISCAN-SEEDED aria-required-children + landmark-unique: menubar whose children are plain list items; nav without a label -->
17 17 <?php /* Fixed variant: <nav ... aria-label="Main menu"> and a plain <ul> (no menubar role). */ ?>
18 Removed <nav class="navigation" data-action="navigation">
18 Added <nav class="navigation" data-action="navigation" aria-label="<?= $escaper->escapeHtmlAttr(__('Main menu')) ?>">
19 19 <ul role="menubar" data-mage-init='{"menu":{"responsive":true,"expanded":true,"position":{"my":"left top","at":"left bottom"}}}'>
20 20 <?php foreach ($items as $item): ?>
21 21 <li class="level0 <?= $escaper->escapeHtmlAttr($item['class']) ?> level-top"><a href="<?= $escaper->escapeUrl($item['url']) ?>" class="level-top"><span><?= $escaper->escapeHtml($item['label']) ?></span></a></li>

app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 24 added, 0 removed

Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 24 added, 0 removed
Line before Line after Change Code
1264 1264 height: 18px;
1265 1265 stroke-width: 3;
1266 1266 }
1267 Added
1268 Added /* validiscan:skip-link-main */
1269 Added .action.skip:not(:focus) {
1270 Added position: absolute;
1271 Added width: 1px;
1272 Added height: 1px;
1273 Added margin: -1px;
1274 Added padding: 0;
1275 Added overflow: hidden;
1276 Added clip: rect(0 0 0 0);
1277 Added white-space: nowrap;
1278 Added border: 0;
1279 Added }
1280 Added .action.skip:focus {
1281 Added position: absolute;
1282 Added top: 4px;
1283 Added left: 4px;
1284 Added z-index: 100;
1285 Added padding: 8px 12px;
1286 Added background: #fff;
1287 Added color: #1a1a1a;
1288 Added text-decoration: underline;
1289 Added }
1290 Added /* /validiscan:skip-link-main */

app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 24 added, 0 removed

Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 24 added, 0 removed
Line before Line after Change Code
1232 1232 height: 18px;
1233 1233 stroke-width: 3;
1234 1234 }
1235 Added
1236 Added /* validiscan:skip-link-main */
1237 Added .action.skip:not(:focus) {
1238 Added position: absolute;
1239 Added width: 1px;
1240 Added height: 1px;
1241 Added margin: -1px;
1242 Added padding: 0;
1243 Added overflow: hidden;
1244 Added clip: rect(0 0 0 0);
1245 Added white-space: nowrap;
1246 Added border: 0;
1247 Added }
1248 Added .action.skip:focus {
1249 Added position: absolute;
1250 Added top: 4px;
1251 Added left: 4px;
1252 Added z-index: 100;
1253 Added padding: 8px 12px;
1254 Added background: #fff;
1255 Added color: #1a1a1a;
1256 Added text-decoration: underline;
1257 Added }
1258 Added /* /validiscan:skip-link-main */

Start with a free check of your shop.

Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.

Request a free check

Or write to info@validiscan.com.

What the free check covers, and what it leaves out
You send
your shop’s address
Pages scanned
up to 15
You get
every failure the scan can prove, with the page and the code
On your shop
public pages only; no login, nothing bought, no forms sent
What it is not
a full audit

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice