The header’s HTML structure decides more than most people expect before ever touching CSS. A header built with a proper landmark element and correctly nested navigation, using a real heading hierarchy underneath it, reads cleanly to a screen reader and a search engine crawler alike. A header built as a pile of generic divs technically renders the same in a browser and quietly fails both. This covers what the markup should actually look like, why each piece is there, and the specific places people get it wrong.
What the Header Actually Needs to Contain, Structurally
Site branding, usually a logo or site title linking back to the homepage, comes first in the source order. Primary navigation follows. Beyond that, a header commonly carries a search trigger, an account or cart icon, sometimes a phone number or a secondary utility menu, but these are additions to the core two, not replacements for them.
The specific visual layout, logo left versus centered, navigation inline versus a hamburger menu, is a design decision that doesn’t change the underlying HTML requirement: a header element wrapping the whole thing, with genuinely semantic children inside it rather than a stack of interchangeable divs.
A Clean Baseline Structure
<header id=”masthead” class=”site-header”>
<div class=”site-branding”>
<?php if ( has_custom_logo() ) : ?>
<?php the_custom_logo(); ?>
<?php else : ?>
<p class=”site-title”><a href=”<?php echo esc_url( home_url( ‘/’ ) ); ?>” rel=”home”><?php bloginfo( ‘name’ ); ?></a></p>
<?php endif; ?>
</div>
<nav id=”site-navigation” class=”main-navigation” aria-label=”Primary”>
<?php
wp_nav_menu( array(
‘theme_location’ => ‘primary’,
‘menu_id’ => ‘primary-menu’,
) );
?>
</nav>
</header>
A few details here matter more than they look. The site title is wrapped in a paragraph, not an h1, deliberately: only one h1 should exist per page, and that slot belongs to the actual content title, not the site name repeated in every header across the entire site. A theme that puts an h1 in the header is creating a duplicate-heading problem on every single page. The nav element carries an aria-label distinguishing it from any other nav elements the page might have (a footer nav, a mobile-specific nav), since a page with multiple unlabeled nav landmarks is genuinely confusing for anyone navigating by landmark with a screen reader.
Why header, Not div, Is the Right Element
The header element is a landmark role in the accessibility tree, which means assistive technology can jump directly to it, and to nothing else that isn’t also marked as a landmark. A div styled to look identical carries no such meaning; a screen reader user navigating by landmark simply won’t find it, since visually looking like a header and being semantically a header are two separate things the browser doesn’t infer from CSS.
This isn’t a purely accessibility-only concern either. Search engines use semantic HTML as a signal for understanding page structure, and a properly marked header, nav, and main separate crawlable content from chrome more reliably than an unmarked div soup, where the crawler has to guess which parts of the page are navigation and which are actual content.
The practical difference shows up clearest when comparing two otherwise identical headers, one built with real landmarks and one built entirely from divs, in a browser’s own accessibility inspector rather than by eye. Both look pixel-identical on screen. Only one of them shows up correctly when a screen reader user brings up the page’s list of landmarks, which is often the very first thing an experienced assistive technology user does on an unfamiliar site to orient themselves before reading anything else.
Nesting the Logo and Navigation Correctly
A common mistake is putting the navigation menu inside the same wrapping element as the logo without a clear semantic boundary between them, which works visually but muddies the actual document structure. Keep site-branding and the nav element as clear siblings inside the header, not nested inside each other, so a screen reader or a crawler can distinguish “this is the brand” from “this is the navigation” without ambiguity.
If the visual design calls for the logo and nav sitting on the same horizontal line, that’s a job for flexbox on the parent header element, not a reason to collapse the underlying markup structure into one ambiguous wrapper.
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
}
Building a Search Bar Into the Header Correctly
WordPress’s built-in get_search_form() function outputs a properly labeled form with an actual label element (visually hidden if the design calls for an icon-only trigger), which is worth using instead of hand-rolling a custom search input that skips the label entirely.
<div class=”header-search”>
<?php get_search_form(); ?>
</div>
If the header uses a collapsed, icon-triggered search (a magnifying glass that expands into an input on click), the underlying form still needs a real label, visually hidden with a standard screen-reader-only CSS class rather than removed from the markup entirely. A placeholder attribute alone is not a substitute for a label; placeholders disappear the moment a user starts typing and screen readers handle them inconsistently as a stand-in for a real label.
Widget Areas in the Header
Registering a widget area specifically for the header gives site owners a way to add content (a phone number, a small promotional banner, social icons) without editing header.php directly every time something needs to change.
// In functions.php
function register_header_widget_area() {
register_sidebar( array(
‘name’ => ‘Header Widget Area’,
‘id’ => ‘header-widget’,
‘before_widget’ => ‘<div class=”header-widget”>’,
‘after_widget’ => ‘</div>’,
) );
}
add_action( ‘widgets_init’, ‘register_header_widget_area’ );
// In header.php
<?php if ( is_active_sidebar( ‘header-widget’ ) ) : ?>
<div class=”header-widgets”>
<?php dynamic_sidebar( ‘header-widget’ ); ?>
</div>
<?php endif; ?>
Wrapping the dynamic_sidebar() call in an is_active_sidebar() check matters, without it, an empty widget area still outputs its wrapping div, which can leave a stray empty box in the layout that’s confusing to debug months later when nobody remembers a widget area exists there at all.
Block Themes: The Header Template Part
On a block theme, this same structure lives in a header.html template part inside the theme’s parts folder, edited through the Site Editor rather than a PHP file directly. The underlying semantic requirements don’t change, a Site Title block still needs to render at an appropriate heading level (configurable in the block’s own settings, defaulting to a level that avoids competing with the page’s actual h1), and a Navigation block still needs a clear, singular purpose rather than being duplicated confusingly across multiple header variations.
Editing directly in the Site Editor rather than PHP means less risk of accidentally breaking semantic structure through a stray unclosed tag, the block editor enforces valid nesting more than freehand PHP templating does, but it’s still worth checking the rendered output’s actual heading levels and landmark structure using a browser’s accessibility inspector rather than assuming the block editor got it perfectly right by default.
Sticky Headers Need Their Own Consideration
A header that becomes fixed to the top of the viewport on scroll needs two things beyond the base structure: a height reserved in the page’s layout so content doesn’t jump the moment the header goes fixed, and a skip link that still functions correctly once the header is pinned in place.
Skip links, a hidden link at the very top of the page that becomes visible on keyboard focus and jumps directly to the main content, are easy to overlook entirely and critical for keyboard users who’d otherwise have to tab through the entire header and navigation menu on every single page just to reach the actual content.
<a class=”skip-link screen-reader-text” href=”#main”>Skip to content</a>
This needs to sit as the very first focusable element in the document, before the header itself, and the target it jumps to (id=”main” on the main content wrapper) needs to actually exist and receive focus correctly, including accounting for a sticky header’s height so the skip link doesn’t jump to a spot that’s then immediately hidden underneath the fixed header.
A Real Troubleshooting Scenario: A Screen Reader Skips the Whole Header
A site passes every automated accessibility scanner cleanly, then a manual screen reader test reveals the navigation menu never gets announced at all, VoiceOver jumps straight from the logo to the main page content as if the nav simply doesn’t exist.
The automated scanner missed this because the markup is technically valid, nothing is broken in a way a linter catches. The actual cause in this pattern is usually a nav element with role=”presentation” or aria-hidden=”true” applied somewhere upstream, often by a page builder plugin that added the attribute to suppress a duplicate mobile menu from being announced twice, but which ended up hiding the desktop menu’s landmark entirely by mistake, a copy-paste error in the builder’s own template rather than anything wrong with the site owner’s content.
Inspect the actual rendered DOM, not the block editor’s source view, for any aria-hidden or role=”presentation” attribute sitting on or above the nav element. If a page builder’s own header template is the source, check for a “hide on desktop” or “mobile only” toggle on that specific nav instance, since these often apply the hiding attribute in a way that’s easy to overlook when scanning the visual builder interface, which shows the element as visually present even while it’s aria-hidden from assistive technology.
A Cart Icon or Account Link Needs Real Text, Not Just an Icon
An icon-only cart or account link in the header is visually compact and, without a text alternative, meaningless to a screen reader user, who hears nothing more descriptive than “link” or “button” with no indication of what it actually does.
<a href=”<?php echo esc_url( wc_get_cart_url() ); ?>” class=”header-cart-link”>
<span class=”cart-icon” aria-hidden=”true”></span>
<span class=”screen-reader-text”>View your shopping cart</span>
</a>
The aria-hidden=”true” on the decorative icon span tells assistive technology to skip it entirely, while the visually hidden screen-reader-text span provides the actual accessible name. This pattern, a decorative icon paired with hidden real text, applies equally to a search trigger, a menu toggle, or any other icon-only control in the header, and it’s worth building as a standard pattern reused across every icon button rather than solving it differently each time one gets added.
A visible item count badge on the cart icon needs the same treatment. A number alone (“3”) sitting next to an icon reads as just “3” to a screen reader with no context, wrapping it with hidden text (“3 items in cart”) gives it actual meaning rather than an ambiguous number floating in the announcement.
RTL and Multilingual Headers Need Direction-Aware Markup, Not Just Translated Text
A header that works correctly in English can break in a genuinely structural way once the page direction flips for Arabic or Hebrew, not just a visual mirroring issue but an actual markup consideration. WordPress handles the base dir=”rtl” attribute on the html element automatically when an RTL language is active, and flexbox-based layouts respect that direction natively, reversing row order without any extra work.
Where this breaks: any hardcoded margin-left or padding-right value in the header’s CSS doesn’t flip automatically the way margin-inline-start or padding-inline-end would. A header built entirely with physical CSS properties (left, right specifically) rather than logical properties looks visually correct in English and subtly broken, icons overlapping text, uneven spacing, once the same header renders in an RTL language.
.site-branding {
margin-inline-end: 16px;
}
Using logical properties from the start avoids maintaining two separate CSS versions, one for LTR and one for RTL, and it’s a genuinely small change in how the CSS gets written that prevents a real, visible bug for a meaningful slice of a multilingual site’s audience.
Common Mistakes Worth Naming Directly
Using an h1 for the site title in the header, creating a duplicate heading conflict with the page’s actual content title.
Building the header entirely from generic divs with no header, nav, or landmark roles at all, which technically renders correctly but is invisible to landmark-based navigation.
Skipping the label on a header search form, relying on a placeholder attribute that disappears the moment someone starts typing.
Forgetting a skip link entirely, forcing keyboard users to tab through the full header and navigation on every page load before reaching actual content.
Verifying the Structure Is Actually Correct
Chrome DevTools’ Accessibility panel shows the computed accessibility tree directly, confirm the header shows up as a banner landmark and the nav shows up as a navigation landmark with the expected label, rather than assuming the visual result implies correct underlying roles.
Tab through the header using only the keyboard. Confirm the skip link appears first and the logo is reachable and correctly labeled, then check that every navigation item receives a visible focus indicator in a sensible order. This single pass catches most of the structural problems that a purely visual review misses entirely, since a sighted review of a header naturally skips right past the parts that only matter to someone navigating without a mouse.
FAQ
Should the site logo be an image tag or a background image in CSS?
An actual img tag (or WordPress’s the_custom_logo() output, which generates one) with meaningful alt text is the more accessible and more search-engine-friendly choice. A CSS background image carries no alt text and is invisible to both screen readers and image search.
Does the header need role=”banner” explicitly?
No, the header element gets an implicit banner role automatically when it’s a direct child of body (not nested inside another sectioning element like article or aside), so an explicit role attribute is redundant in the standard case.
Can I have more than one nav element in the header?
Yes, a primary menu and a secondary utility menu (account links, a language switcher) can both live in the header as separate nav elements, as long as each carries its own distinct aria-label so assistive technology can tell them apart when a user is navigating by landmark.
Is it a problem if my theme’s header markup has extra wrapping divs?
Not inherently, extra non-semantic wrapper divs used purely for layout purposes are fine and common. The actual requirement is that the meaningful semantic elements, header, nav, the branding link, exist somewhere in that structure with correct roles, not that every div be eliminated entirely.
How do I check what heading level a Site Title block is actually rendering at?
Inspect the rendered HTML directly, either through browser dev tools or a page’s view-source, rather than trusting the block editor’s settings panel alone. The block’s setting controls what level it should output, but a theme’s own CSS or a conflicting style variation can sometimes override the visual size in a way that makes the actual semantic level easy to misjudge just by looking at the page.
Does adding aria-label to the nav element affect SEO?
Not directly as a ranking factor, but it improves how assistive technology and some crawlers interpret the page’s structure, which supports the same broader goal as clean semantic HTML generally: making the page’s actual content and navigation unambiguous to anything parsing it, human or automated.
Where This Leaves You
Use header and nav as real landmark elements, not divs styled to look the part. Keep the site title out of h1, and give every nav element a clear, distinct label.
Add a working skip link, and verify the result with a keyboard and an actual screen reader, not just a visual glance or an automated scanner alone. A header that looks finished and a header that’s actually structurally sound are two different bars to clear, and only one of them shows up in a quick look at the rendered page.