Skip to main content
Published / updated

Semantic Layout, Accessibility and SEO

Before you start

You need: tables and forms (Article 02).

Time: about 40 minutes, plus the practice.

Learning objective

Structure a page so a keyboard user, a screen-reader user and a search engine can all understand it.

Topics

  • Semantic layout elements
  • Landmarks and how they are navigated
  • Keyboard access and focus
  • ARIA — and when not to use it
  • Colour contrast
  • Metadata and structured data
  • Auditing a page

Semantic layout

<body>
<a href="#main" class="skip-link">Skip to main content</a>

<header>
<img src="/img/logo.svg" alt="NexCoding Academy">

<nav aria-label="Main">
<ul>
<li><a href="/students/">Students</a></li>
<li><a href="/teachers/">Teachers</a></li>
<li><a href="/fees/" aria-current="page">Fees</a></li>
</ul>
</nav>
</header>

<main id="main">
<h1>Fee collection</h1>

<article>
<h2>Outstanding fees — Class 10</h2>
<p>Twelve students have fees outstanding for 2024-25.</p>
</article>

<section aria-labelledby="recent-heading">
<h2 id="recent-heading">Recent payments</h2>
<!-- ... -->
</section>
</main>

<aside aria-label="Related links">
<h2>Quick links</h2>
<!-- ... -->
</aside>

<footer>
<p>&copy; 2026 NexCoding Academy</p>
</footer>
</body>
ElementUse for
<header>Introductory content — page or section
<nav>A major navigation block
<main>The page's primary content — one per page
<article>Self-contained content that would make sense alone
<section>A thematic grouping, with a heading
<aside>Tangentially related content
<footer>Footer for the page or a section

These are not styled differently from <div>. Their entire value is meaning — which is exactly why they get skipped, and exactly why they matter.

Landmarks

Screen-reader users navigate by landmark, jumping straight to main, nav or search rather than reading from the top. A page built entirely from <div> offers no landmarks at all, so every visit starts at the logo.

One <main> per page. More than one, and the "jump to main content" shortcut becomes ambiguous.

Several of the same landmark need distinguishing labels:

<nav aria-label="Main">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>

Without the labels a user hears "navigation, navigation, navigation".

section versus div

<!-- section: a thematic group with a heading -->
<section aria-labelledby="fees-heading">
<h2 id="fees-heading">Fee summary</h2>
</section>

<!-- div: a styling hook with no meaning -->
<div class="card-grid">...</div>

If it has no heading, it is probably a <div>. <section> without a heading gives a screen reader a region it cannot name — worse than a plain <div>, which is silently ignored.

aria-labelledby points at the heading's id, so the region is announced by its heading text.

article

<article>
<h2>Ravi Kumar</h2>
<p>Class 10, Section A — Roll number NCA-2024-0012</p>
</article>

Use <article> when the content would still make sense pulled out of the page — a student card, a blog post, a comment. Use <section> when it only makes sense in context.

Keyboard access

Every interactive element must be reachable and operable by keyboard. This matters for screen-reader users, for people who cannot use a mouse, and for anyone whose trackpad has died.

<!-- Focusable and operable for free -->
<a href="/students/">Students</a>
<button type="button">Save</button>
<input type="text">
<select>...</select>

<!-- Not focusable, not operable, no role -->
<div onclick="save()">Save</div>

A <div> with a click handler cannot be tabbed to, does not respond to Enter or Space, and is announced as nothing. Making it work needs four additions:

<div role="button" tabindex="0"
onclick="save()"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); save(); }">
Save
</div>

Or just use a <button>. That is the entire lesson: native elements are accessible by default, and every re-implementation is worse.

<a href="#main" class="skip-link">Skip to main content</a>
.skip-link {
position: absolute;
left: -9999px;
}

.skip-link:focus {
left: 0;
top: 0;
padding: 0.75rem 1rem;
background: #fff;
z-index: 100;
}

Hidden until focused, so the first Tab press reveals it. Without it, a keyboard user tabs through twenty navigation links on every page before reaching the content.

Focus must be visible

/* Never do this */
*:focus { outline: none; }

/* Do this instead */
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}

Removing the focus outline because it "looks untidy" makes the page unusable by keyboard — the user has no idea where they are. :focus-visible shows the ring for keyboard focus and not for mouse clicks, which is the behaviour designers actually want.

Tab order

<!-- Follows document order — correct -->
<input tabindex="0">

<!-- Removed from tab order, still focusable by script -->
<div tabindex="-1">

<!-- Jumps ahead of everything — almost always wrong -->
<input tabindex="5">

A positive tabindex creates a separate tab sequence that runs before every natural element, and it is nearly impossible to maintain. Fix the DOM order instead.

ARIA

ARIA adds roles and properties where HTML has none.

<div role="alert">Payment recorded successfully.</div>

<button aria-expanded="false" aria-controls="filters">Filters</button>
<div id="filters" hidden>...</div>

<input aria-describedby="phoneHelp" aria-invalid="true">
<small id="phoneHelp">10 digits, starting 6 to 9.</small>

<span aria-hidden="true"></span>
<button aria-label="Close">×</button>
AttributePurpose
roleWhat the element is
aria-labelAn accessible name when no visible text exists
aria-labelledbyNames it from another element's text
aria-describedbyAdditional description
aria-expandedWhether a disclosure is open
aria-current="page"The current item in a set
aria-liveAnnounce changes in this region
aria-hidden="true"Hide from assistive technology

The first rule of ARIA is not to use ARIA. A <button> needs no role="button"; a <nav> needs no role="navigation". Adding a role to an element that already has it is redundant, and adding the wrong role actively breaks it:

<!-- Now announced as a heading, not a link -->
<a href="/students/" role="heading">Students</a>

Incorrect ARIA is worse than none. Reach for it only when HTML genuinely has no element for what you are building — a tab panel, a combo box, a live region.

Live regions

<div aria-live="polite" role="status" id="saveStatus"></div>
document.getElementById('saveStatus').textContent = 'Student saved.';

A screen reader announces the new content without moving focus. Essential for anything that updates without a page reload — a save confirmation, a search result count, a validation error.

polite waits for a pause; assertive interrupts. Use assertive only for genuine errors.

Colour contrast

ContentWCAG AA minimum
Body text4.5:1
Large text (18pt+, or 14pt bold)3:1
UI components and graphics3:1

Check with DevTools — the colour picker in the Styles pane shows the contrast ratio and whether it passes.

/* 1.6:1 — fails badly, and is a common "muted" choice */
.muted { color: #ccc; background: #fff; }

/* 4.6:1 — passes AA, still visibly muted */
.muted { color: #6b7280; background: #fff; }

Colour must never be the only signal. A red border on an invalid field is invisible to a colour-blind user; add an icon or text:

<input aria-invalid="true" aria-describedby="rollError">
<p id="rollError" class="error" role="alert">Roll number must look like NCA-2024-0012.</p>

Around 8% of men have some form of colour vision deficiency. Status shown only by a red or green dot is unreadable for them.

Metadata and SEO

<head>
<title>Student Records — NexCoding Academy</title>
<meta name="description" content="Manage student admissions, classes, attendance and fees at NexCoding Academy.">
<link rel="canonical" href="https://nexcoding.in/students/">

<meta property="og:title" content="Student Records — NexCoding Academy">
<meta property="og:description" content="Manage student admissions, classes, attendance and fees.">
<meta property="og:image" content="https://nexcoding.in/img/og-students.png">
<meta property="og:url" content="https://nexcoding.in/students/">
<meta property="og:type" content="website">

<meta name="twitter:card" content="summary_large_image">
</head>
TagUsed for
<title>The search result heading and the browser tab
descriptionThe search result snippet
canonicalThe preferred URL when several serve the same content
og:*The preview card when shared on social media or messaging

<title> under about 60 characters and description under about 155, or search engines truncate them.

The canonical link matters more than it looks: /students, /students/, /students/index.html and /students?page=1 are four URLs serving one page, and without a canonical the ranking signal is split across them.

Structured data

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "EducationalOrganization",
"name": "NexCoding Academy",
"url": "https://nexcoding.in",
"telephone": "+91-9951510727",
"address": {
"@type": "PostalAddress",
"addressLocality": "Hyderabad",
"addressRegion": "Telangana",
"addressCountry": "IN"
}
}
</script>

JSON-LD describes the page's content to search engines, which use it for rich results. Validate it with Google's Rich Results Test — malformed structured data is ignored entirely, silently.

Semantic HTML is most of technical SEO

Search engines read headings for structure, alt for image content, link text for destination relevance, and landmarks for what is content versus navigation. A page built from correct elements needs very little SEO work beyond a good <title> and description.

Accessibility and SEO are largely the same task. Both reward describing content honestly in markup.

Auditing

Lighthouse — DevTools, Lighthouse tab, run Accessibility and SEO. It catches missing alt, contrast failures, missing labels, missing lang, and duplicate ids.

Keyboard test — put the mouse away and Tab through the whole page. Every interactive element must be reachable, in a sensible order, with visible focus. This finds more real problems than any automated tool.

Screen reader — NVDA on Windows (free), VoiceOver on Mac (built in). Ten minutes with one changes how you write HTML permanently.

HTML validatorvalidator.w3.org.

Automated tools catch roughly a third of accessibility issues. The keyboard test catches most of the rest.

Errors you will hit

What you seeCauseFix
Screen reader announces nothing usefulEverything is a <div>Use landmarks: header, nav, main, footer
Tab order jumps aroundDOM order does not match visual orderFix the DOM, not with tabindex
Focus is invisibleoutline: none with no replacementProvide a visible focus style
A clickable <div> cannot be reached by keyboardNot a real buttonUse <button>
Several <main> elementsOnly one is allowedKeep one

If you find yourself adding tabindex to fix tab order, the DOM order is wrong. Fix that instead.

Common mistakes

  • A page built entirely from <div>, offering no landmarks
  • More than one <main>
  • Several <nav> elements with no distinguishing labels
  • <section> with no heading
  • outline: none on focus
  • A <div> with a click handler instead of a <button>
  • Positive tabindex values
  • Redundant ARIA on elements that already have the role
  • Wrong ARIA, which is worse than none
  • Colour as the only status signal
  • Grey-on-white text failing contrast
  • No alt, or a decorative image with descriptive alt
  • Missing canonical on a page reachable by several URLs
  • Trusting Lighthouse as a complete accessibility check

Practice

  1. Rebuild your student page with <header>, <nav>, <main>, <section>, <aside> and <footer>.
  2. Add a skip link and confirm it appears on first Tab.
  3. Tab through the entire page. List every element you could not reach or could not see focused.
  4. Add outline: none globally, repeat the tab test, then remove it and use :focus-visible.
  5. Replace a <button> with a clickable <div>. Confirm it cannot be tabbed to or activated with Enter, then restore the button.
  6. Add two <nav> elements without labels, listen with a screen reader, then add aria-label to each.
  7. Add a <section> with no heading. Confirm the screen reader announces an unnamed region.
  8. Set body text to #ccc on white. Check the ratio in DevTools, then fix it to pass 4.5:1.
  9. Add a live region and update it from JavaScript. Confirm a screen reader announces the change without focus moving.
  10. Run Lighthouse Accessibility and fix every finding. Then do the keyboard test and record what Lighthouse missed.
  11. Add title, description, canonical and Open Graph tags. Paste the URL into a messaging app and confirm the preview.
  12. Add JSON-LD and validate it with the Rich Results Test.

Exercise 10 is the one that makes the point: automated tools are a floor, not a ceiling.

You can now

  • Structure a page with correct landmarks
  • Operate the whole page with the keyboard alone
  • Keep DOM order and visual order aligned
  • Use <button> rather than a clickable <div>
  • Keep focus visible

Review questions

  1. What do semantic layout elements provide that a <div> does not?
  2. Why is outline: none on focus a serious problem?
  3. Why is incorrect ARIA worse than no ARIA?
  4. Why is colour alone insufficient to indicate an error?

Next: CSS fundamentals