How to Add ARIA Landmarks to a WordPress Theme (Header, Nav, Main, Footer)

Robert Halley

Screen reader users do not scroll. They jump. Press D in NVDA or open the VoiceOver rotor and the page collapses into a short list of regions: banner, navigation, main, complementary, contentinfo. If your theme does not expose those regions, that list is empty and the only way through your page is the Tab key, one link at a time.

This guide is a practical walkthrough of ARIA landmarks in a WordPress theme: where each role belongs in classic template files, how to do the same thing in a block theme, and how to fix the landmark errors that audit tools flag most often (duplicate main regions and unlabeled navigation menus top the list). Anyone digging further should read Correcting Missing or Incorrectly Labeled Landmarks.

What ARIA Landmarks Actually Do

Landmarks mark the major regions of a document so assistive technology can navigate them directly. They do not change how the page looks. They change how fast a non-visual user can reach your content.

The important detail that trips up most theme developers: HTML5 sectioning elements already carry implicit landmark roles. You rarely need to write role="banner" by hand. You need to use the right element in the right place, and label it when there is more than one of a kind.

Landmark Reference Table

HTML element Implicit ARIA role Use it for Needs a label?
<header> banner (only when not inside article, aside, main, nav or section) Site header: logo, tagline, primary nav No (one per page)
<nav> navigation Menus, breadcrumbs, pagination Yes, if more than one
<main> main The unique content of the page No (exactly one per page)
<aside> complementary Sidebars, widget areas Yes, if more than one
<footer> contentinfo (same nesting rule as header) Site footer: copyright, legal, credits No (one per page)
<form role="search"> search Site search form Yes, if more than one
<section> region only when it has an accessible name Named content blocks you want in the landmark list Yes, always

Rule of thumb: use the native element, add aria-label only to disambiguate, and never add a redundant role such as <nav role="navigation">.

screen reader accessibility laptop

The 6 Landmark Mistakes Audit Tools Flag Most Often

  1. Two or more main regions. Usually the theme template outputs one and a page builder, WooCommerce template override or a plugin outputs another.
  2. Unlabeled navigation menus. Three <nav> elements all announced as “navigation” is useless to a screen reader user.
  3. No main at all. Content sits in <div id="content">, so the skip link jumps to a non-landmark and the rotor shows nothing.
  4. Content outside landmarks. Breadcrumbs, notification bars or cookie banners floating between </header> and <main> are invisible in landmark navigation.
  5. Nested banner or contentinfo. A <footer> inside a post <article> is not a contentinfo landmark, but a <footer> placed inside a wrapper div that is itself inside <main> loses its landmark status too.
  6. Landmark soup. Twelve role="region" wrappers on one homepage. Landmarks are a table of contents, not a fence around every block.
screen reader accessibility laptop

Part 1: ARIA Landmarks in a Classic WordPress Theme

Classic themes give you direct control because you are editing PHP templates. Here is the correct placement, file by file. For the wider picture, see What are ARIA Landmarks: The Guide You Need.

header.php: skip link, banner and primary nav

<body <?php body_class(); ?>>
<?php wp_body_open(); ?>

<a class="skip-link screen-reader-text" href="#primary">
  <?php esc_html_e( 'Skip to content', 'ppp' ); ?>
</a>

<header id="masthead" class="site-header">

  <div class="site-branding">
    <?php the_custom_logo(); ?>
  </div>

  <?php
  wp_nav_menu(
    array(
      'theme_location'       => 'primary',
      'container'            => 'nav',
      'container_id'         => 'site-navigation',
      'container_class'      => 'main-navigation',
      'container_aria_label' => __( 'Primary', 'ppp' ),
      'menu_id'              => 'primary-menu',
    )
  );
  ?>

</header><!-- #masthead -->

Notes that matter:

  • container_aria_label has been available in wp_nav_menu() since WordPress 5.5 and outputs aria-label on the <nav> wrapper. Use it instead of hand-rolling a nav element.
  • Do not write “Primary navigation” as the label. Screen readers already append the role, so the user hears “Primary navigation navigation”. Label it Primary.
  • The skip link target must be the main element and it needs tabindex="-1" for reliable focus in Safari and older browsers.

index.php, single.php, page.php, archive.php: exactly one main

<?php get_header(); ?>

<main id="primary" class="site-main" tabindex="-1">

  <?php if ( have_posts() ) : ?>

    <?php while ( have_posts() ) : the_post(); ?>
      <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
        <header class="entry-header">
          <?php the_title( '<h2 class="entry-title">', '</h2>' ); ?>
        </header>
        <div class="entry-content"><?php the_content(); ?></div>
      </article>
    <?php endwhile; ?>

    <?php
    the_posts_pagination(
      array(
        'aria_label' => __( 'Posts', 'ppp' ),
      )
    );
    ?>

  <?php endif; ?>

</main>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

The <header class="entry-header"> inside the article is intentionally not a banner landmark, because a header nested inside article loses its implicit role. That is correct behaviour, leave it alone.

the_posts_pagination() outputs a <nav class="navigation pagination">, so pass aria_label or you get a second unlabeled navigation landmark. Same for the_post_navigation().

sidebar.php: complementary

<?php if ( ! is_active_sidebar( 'sidebar-1' ) ) { return; } ?>

<aside id="secondary" class="widget-area" aria-label="<?php esc_attr_e( 'Blog sidebar', 'ppp' ); ?>">
  <?php dynamic_sidebar( 'sidebar-1' ); ?>
</aside>

One sidebar needs no label. Two or more (main sidebar plus a shop filter sidebar, for example) must be distinguished. The subject deserves a deeper dive than we can give it here.

footer.php: contentinfo plus a labeled footer nav

<footer id="colophon" class="site-footer">

  <?php
  wp_nav_menu(
    array(
      'theme_location'       => 'footer',
      'container'            => 'nav',
      'container_class'      => 'footer-navigation',
      'container_aria_label' => __( 'Footer', 'ppp' ),
      'depth'                => 1,
    )
  );
  ?>

  <div class="site-info">
    <p>&copy; <?php echo esc_html( date_i18n( 'Y' ) ); ?> <?php bloginfo( 'name' ); ?></p>
  </div>

</footer>

<?php wp_footer(); ?>
</body>
</html>

Keep <footer> as a direct child of <body> or of a plain layout wrapper. If it ends up inside <main>, the contentinfo landmark disappears.

searchform.php: the search landmark

<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
  <label for="search-field-header" class="screen-reader-text">
    <?php esc_html_e( 'Search this site', 'ppp' ); ?>
  </label>
  <input type="search" id="search-field-header" name="s" value="<?php echo get_search_query(); ?>" />
  <button type="submit"><?php esc_html_e( 'Search', 'ppp' ); ?></button>
</form>

If the same form appears in the header and in the footer, give each one an aria-label (“Header” and “Footer”), and make sure the id values stay unique. It is the sort of thing a careful web development studio handles once and never revisits.

Turn on HTML5 markup support

Without this, core widgets and forms fall back to older markup that skips <nav> wrappers entirely:

add_action( 'after_setup_theme', function () {
  add_theme_support( 'html5', array(
    'search-form',
    'navigation-widgets',
    'comment-form',
    'comment-list',
    'gallery',
    'caption',
    'style',
    'script',
  ) );
} );

Part 2: ARIA Landmarks in a Block Theme

Block themes build the same structure from HTML template files and block markup. The key is the tagName attribute on Group and Template Part blocks, and the ariaLabel attribute on the Navigation block.

parts/header.html

<!-- wp:group {"tagName":"header","className":"site-header","layout":{"type":"constrained"}} -->
<header class="wp-block-group site-header">

  <!-- wp:site-logo /-->
  <!-- wp:site-title /-->

  <!-- wp:navigation {"ariaLabel":"Primary","layout":{"type":"flex"}} /-->

</header>
<!-- /wp:group -->

In the editor, the ariaLabel value is set in the Navigation block sidebar under Advanced > Label (older releases called it “Menu name”). Set it on every navigation you add.

templates/index.html

<!-- wp:template-part {"slug":"header","area":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">

  <!-- wp:query {"queryId":1,"query":{"inherit":true}} -->
  <div class="wp-block-query">
    <!-- wp:post-template -->
      <!-- wp:post-title {"isLink":true,"level":2} /-->
      <!-- wp:post-excerpt /-->
    <!-- /wp:post-template -->
    <!-- wp:query-pagination {"layout":{"type":"flex"}} -->
      <!-- wp:query-pagination-previous /-->
      <!-- wp:query-pagination-numbers /-->
      <!-- wp:query-pagination-next /-->
    <!-- /wp:query-pagination -->
  </div>
  <!-- /wp:query -->

</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","area":"footer"} /-->

The block theme duplicate-landmark trap

This is the single most common block theme error, and it produces exactly the “duplicate banner” and “nested contentinfo” warnings that axe and WAVE report:

  • You set {"tagName":"header"} on the Group inside parts/header.html, and {"tagName":"header"} on the Template Part block in the template. Result: <header><header>...</header></header>.
  • Pick one place. Either the template part wrapper carries the tag, or the group inside does. Using "area":"header" on the template part without a tagName renders a plain <div> wrapper, which is safe when the inner group is already a <header>.
  • The same applies to footers: never let a Query Loop or a Post Content block sit inside a group tagged main while a page template also outputs main.

parts/footer.html

<!-- wp:group {"tagName":"footer","className":"site-footer","layout":{"type":"constrained"}} -->
<footer class="wp-block-group site-footer">

  <!-- wp:navigation {"ariaLabel":"Footer","overlayMenu":"never"} /-->

  <!-- wp:paragraph -->
  <p>&copy; 2026 Pixel Perfect Portfolios</p>
  <!-- /wp:paragraph -->

</footer>
<!-- /wp:group -->

Naming a region in a block theme

If you genuinely need a named region (a pricing section users should be able to jump to), use a Group with tagName set to section and add an aria-label through Advanced > Additional attributes in a custom block, or wrap it with a block variation. A <section> without an accessible name is not a landmark at all, so do not count on it.

screen reader accessibility laptop

Part 3: Labeling Multiple Navigation Menus Correctly

A typical WordPress page ends up with four or five navigation landmarks. Here is a labeling scheme that works:

Menu Recommended label How to set it
Main menu Primary container_aria_label / ariaLabel
Utility or top bar Utility container_aria_label
Breadcrumbs Breadcrumb Hand-coded <nav aria-label="Breadcrumb">
Post pagination Posts the_posts_pagination( array( 'aria_label' => ... ) )
Next / previous post Post the_post_navigation( array( 'aria_label' => ... ) )
Footer links Footer container_aria_label / ariaLabel

If a visible heading already names the region, reference it instead of duplicating text:

<nav aria-labelledby="footer-nav-heading">
  <h2 id="footer-nav-heading" class="screen-reader-text">Footer</h2>
  <!-- menu markup -->
</nav>

Part 4: Hunting Down a Duplicate main Landmark

When an audit reports two main regions, work through this list in order:

  1. View source and search for <main. Count the matches.
  2. Check child theme templates that were copied before you refactored the parent, especially page.php, single.php and woocommerce.php.
  3. Check plugin template overrides in /wp-content/themes/your-theme/woocommerce/ or /templates/.
  4. Check page builder settings. Several builders wrap their canvas in a <main> element by default and offer a “content wrapper tag” option you can set back to div.
  5. In block themes, check whether both the Template Part and an inner Group carry tagName.
  6. Check header or footer scripts pasted into the Customizer or a code snippets plugin.

If two main areas genuinely exist in your layout, keep one <main> and turn the second into a named <section aria-label="..."> or <aside>.

screen reader accessibility laptop

Part 5: How to Test Your Landmarks

Keyboard and screen reader checks

  • NVDA + Firefox (Windows): press D to cycle landmarks, or Insert + F7 for the elements list.
  • VoiceOver + Safari (macOS): VO + U opens the rotor, then arrow to the Landmarks list.
  • TalkBack (Android): set the reading control to Landmarks and swipe.

Automated and visual tools

  • axe DevTools browser extension: catches duplicate banner, duplicate contentinfo, and content outside landmarks.
  • WAVE by WebAIM: the Structure tab draws landmark boundaries directly on the page.
  • Landmarks extension for Firefox and Chrome: lists every landmark and highlights it, ideal for spotting the nesting bugs.
  • Lighthouse in Chrome DevTools: quick pass/fail signal, not exhaustive.
  • Accessibility Checker or similar WordPress plugins: useful for scanning every post at scale, but plugins cannot restructure your template files. Fix the theme, then scan.

The one-minute manual test

  1. Open any page and run document.querySelectorAll('main').length in the console. It must return 1.
  2. Run document.querySelectorAll('nav').length, then check that every one has an aria-label or aria-labelledby.
  3. Confirm header and footer are not nested inside main, article or section.
  4. Tab once from the address bar. The skip link should appear and point at your main.

Copy-Ready Landmark Checklist

  • Exactly one <main> per page, with tabindex="-1" and a matching skip link
  • One top-level <header> and one top-level <footer>, both outside <main>
  • Every <nav> labeled, with no word “navigation” inside the label
  • Site search wrapped in role="search" with a labeled input
  • Sidebars in <aside>, labeled when there is more than one
  • No redundant roles on native elements
  • All visible content sits inside some landmark
  • add_theme_support( 'html5', ... ) declared in classic themes
  • Verified with one screen reader and one automated tool before release
screen reader accessibility laptop

Why This Matters More in 2026

The European Accessibility Act has applied to new consumer-facing digital products since June 2025, WCAG 2.2 AA is now the reference level in most public procurement rules, and landmark structure is one of the first things any manual auditor checks because it takes thirty seconds to evaluate. Getting ARIA landmarks right in your WordPress theme is one of the cheapest accessibility wins available: a handful of tags, no design change, no performance cost.

FAQ

Do I still need role=”banner” if I use the header element?

No. A <header> that is not nested inside article, aside, main, nav or section already exposes the banner role. Adding role="banner" is harmless but redundant, and it can mask a nesting problem you should fix instead.

Can a page have two main landmarks?

No. The HTML specification allows only one main element to be visible and exposed per document. Additional ones are reported as errors by axe, WAVE and screen readers, and they break landmark navigation because users cannot tell which region holds the real content.

Does the WordPress Navigation block add an aria-label automatically?

It renders a <nav> element, but the label is empty unless you set it. Open the block, go to the Advanced panel, and fill in the Label field. Do this for every navigation instance in your header, footer and any template part.

Can a plugin add ARIA landmarks without editing my theme?

Plugins can inject attributes and flag issues, and that is genuinely useful for auditing. They cannot reliably move a <footer> out of <main> or delete a duplicate <main> generated by a page builder. Treat plugins as a detection layer and fix the structure in the theme templates.

Should I use section with role=”region” for homepage blocks?

Only for a small number of genuinely important areas, and always with an accessible name. Too many regions turn the landmark list into noise, which defeats the purpose. Three to seven landmarks per page is a healthy range.

Do ARIA landmarks help SEO?

Not directly as a ranking factor, but clean semantic HTML helps crawlers identify primary content versus boilerplate, improves the odds of useful featured snippets, and reduces the legal and reputational risk of an inaccessible site. The real return is usability.

How do I fix landmarks in a theme I bought and cannot edit safely?

Create a child theme, copy only the template files you need to change (usually header.php, footer.php, sidebar.php and one or two content templates), and apply the markup above. For block themes, edit the templates and template parts in the Site Editor, which stores your changes in the database and leaves the parent theme files untouched.

Need a second pair of eyes on your theme? Our team at Pixel Perfect Portfolios audits WordPress themes for landmark structure, keyboard operability and WCAG 2.2 AA conformance, then ships the corrected templates back to you ready to deploy.

Leave a Comment