This article page has two sets of navigation: the site menu and the section list. They look different, but without names they can sound identical to someone moving by landmarks. That is the sort of bug a screenshot will never show. The HTML itself has to explain the page. We'll use a lesson page and a small community-news exercise to make the choices concrete: where main starts, when a story earns article, what a heading level means inside a reusable component, and what an invalid form field should actually say. You can copy the examples, but the useful part is learning how to decide when the next page is different.
Start with the page, not a list of tags
Before touching JSX or HTML, write down the visitor's task. On a lesson page, they need to identify the course, read one lesson, try an example, and move on. That gives us a page boundary: global controls in the site header, the lesson in main, and a link to the next lesson after the lesson body. If we have a table of contents, it is navigation within this page. A marketing promotion may sit in an aside; it does not belong in the middle of the lesson just because the layout has an empty column.
The decision that gets missed most often is the humble div. It is perfectly fine for a grid wrapper or a row of cards. It becomes a problem when it replaces a control or a meaningful region. section is for a thematic part of the document, usually with a heading. article is for content that can stand on its own—this post, a forum answer, or an individual lesson. The W3C page-structure tutorial explains those relationships in terms of navigation, which is more useful than memorizing a list of tags.
<a href="#main-content">Skip to content</a>
<header class="site-header">
<a href="/">SovranCode</a>
<nav aria-label="Primary">
<a href="/courses">Courses</a>
<a href="/exercises">Exercises</a>
</nav>
</header>
<main id="main-content">
<article aria-labelledby="lesson-title">
<header>
<p>HTML · Lesson 4</p>
<h1 id="lesson-title">Build a contact form</h1>
</header>
<section aria-labelledby="requirements-title">
<h2 id="requirements-title">What the form needs</h2>
<p>Start with a label for each field.</p>
</section>
</article>
</main>
<footer class="site-footer">...</footer>The outer header belongs to the site. The header inside article introduces this lesson and does not create a second banner landmark. A CSS wrapper could be added around the lesson without changing its meaning.
Use landmarks to help people move, not to decorate code
Open this page's landmark list and you should find one main, one site banner, and navigation for both the main menu and the article contents. The two navigation regions need different names. “Primary” and “In this article” work because they describe destinations, not visual position. “Left navigation” would stop making sense when the sidebar moves above the article on a phone.
Two menus need two names
- On screen
- A site menu and a list of article sections
- Without labels
- Navigation; Navigation
- With labels
- Primary; In this article
- Verify
- Read the landmark list at desktop and phone widths
Do not wrap a single “Read next” link in nav just to get another landmark. Repeated sections, sidebars, and forms can make a landmark list crowded. The W3C landmarks pattern recommends a deliberate high-level structure. A named section is useful when someone would want to jump to it; the little container holding its icon and paragraph probably remains a div.
The same element changes meaning with context. A top-level header may be announced as the site banner. A header nested inside article is only the article's introduction. A top-level footer can be site-wide content information; an article footer is the end of that article. This is why reviewing the rendered tree beats scanning a file for tag names.
Repair one page: the community-garden story
Our semantic news article exercise begins with a short story about a community garden. Its starter markup uses generic containers. That is intentional: the visible page is readable, but there is no reliable document outline. “Community garden opens” is a div, so it will not appear in a heading list. “Opening details” is another div, so the in-page link has a target but no section heading to explain it.
Give the story a document shape
Keep the same story and date. Replace only the containers that have a job: site introduction, navigation, the story, its details section, and publication time.
<div>
<p>Neighborhood Journal</p>
<a href="#details">Opening details</a>
</div>
<div>
<div>Community garden opens</div>
<p>Published September 9, 2026</p>
<p>Residents transformed an empty lot.</p>
<div id="details">Opening details</div>
<p>The garden opens every Saturday.</p>
</div><header>
<p>Neighborhood Journal</p>
<nav aria-label="Article navigation">
<a href="#details">Opening details</a>
</nav>
</header>
<main>
<article>
<h1>Community garden opens</h1>
<p>Published <time datetime="2026-09-09">
September 9, 2026
</time></p>
<p>Residents transformed an empty lot.</p>
<section id="details">
<h2>Opening details</h2>
<p>The garden opens every Saturday.</p>
</section>
</article>
</main>Why it works. The story becomes one self-contained article inside the page's main content. The link still points to `#details`, but that target now begins a headed section. `time` retains the readable date while giving software an unambiguous date value. None of these changes requires a new visual design.
This is a small example, but it exposes a useful rule: repair meaning where it is missing, not every div on the page. A decorative card wrapper around the story can remain a div. A card title that is the story's actual headline should be a heading. The exercise provides a complete reference solution and checks for the essential structure; try changing the markup first, then compare why each element was chosen.
Make the heading outline tell the truth
Read only the headings of the garden story. “Community garden opens” should be the page's primary heading; “Opening details” sits below it. A browser can style both at any size. For a page with one primary topic, a clear h1 followed by h2 sections makes the outline easy to follow. Put an h3 inside the h2 whose subject it narrows. A visual size change is not a reason to jump from h2 to h4.
<h1>HTML course</h1>
<h2>Forms</h2>
<h3>Labels and instructions</h3>
<h3>Validation feedback</h3>
<h2>Page structure</h2>
<h3>Landmarks</h3>If ‘Validation feedback’ becomes a major chapter rather than part of ‘Forms’, change its level because the content relationship changed—not because the font needs to be bigger.
Reusable components are where this slips. Suppose a CourseCard hardcodes its title as h3. It is fine under the catalog's h2 “Courses” heading. Reuse it as the main recommendation inside a page headed by h1 and its level may no longer fit. Let the parent supply the heading element or a headingLevel prop. The component owns the card's appearance; the page owns its place in the outline. Avoid making every card an h2 just to simplify the API.
In DevTools, inspect the accessibility tree or run a heading-outline check. If you see h1 → h3 → h2, ask which relationship the skipped level is supposed to express. If the same heading says “More” eight times, replace it with a specific subject. The W3C heading tutorial describes the rank and nesting rules without tying them to font sizes.
Form semantics matter most when something goes wrong
A placeholder is an example or hint, not a durable label. It disappears when the visitor types. Give each control a visible label and connect it to the field with matching for and id values. That association lets assistive technology announce the field's purpose and makes the label itself a usable click target. The W3C form-label tutorial covers the common variations.
Now put the markup inside a real account form. Someone enters mira@ and presses Continue. A red border tells them little; “Enter a valid email address” next to the field tells them what to repair. The example below is the invalid state. On the initial render, do not set aria-invalid="true", and do not point aria-describedby at an error element that has not been rendered. When validation fails, render the error, connect it to the input, and consider moving focus to the first invalid field or a concise error summary.
<label for="email">Email address</label>
<p id="email-help">We'll send your receipt here.</p>
<input
id="email"
name="email"
type="email"
required
aria-invalid="true"
aria-describedby="email-help email-error"
>
<p id="email-error">Enter a valid email address.</p>
<button type="submit">Continue</button>This is the error state. On first render, omit aria-invalid="true" and the error message. After a failed submit, move focus to the first invalid field or to an error summary when that better suits the form.
The browser's required and type="email" checks can help before submission, but they are not the application's only validation. The server still decides whether the submitted address is acceptable and whether the action succeeded. Return server-side errors to the same field pattern, and include a general error when the failure is not tied to one field. W3C's notification tutorial shows both inline and page-level feedback. Test the sequence with a keyboard: submit, hear or read the error, repair the value, submit again, and receive a clear success state.
Run a five-minute review on the real page
Review the rendered page, not only the CourseCard file. The parent decides whether its heading is an h2 or h3; the entire document decides whether there are two main regions after a layout component is added. Check at the phone width where the table of contents moves above the article. Its accessible name should still describe destinations, not its old position. This is also a good time to test a skip link: does it land at the actual main content, and is the next focused element sensible?
Five checks before shipping
Run these against the rendered page, not just the source file.
- Read the heading list without looking at the visual design. Does it tell a coherent story?
- Use only Tab, Shift+Tab, Enter, and Space. Can you reach and operate each control?
- Inspect the landmark list. Is there one main region and a clear name for repeated navigation?
- Submit every form with missing or invalid input. Is the correction obvious and announced?
- Zoom to 200% and check that labels, focus, and reading order still make sense.
Run an automated accessibility check, then do the parts it cannot judge. A checker may find an input without a label. It cannot know whether eight links named “Read more” identify eight different stories, or whether an error message tells a person how to continue. Turn off CSS for a moment. The order of the story, navigation, form instructions, and footer should still be understandable. If the page becomes a random pile of text, the visual grid has been carrying meaning that the document lacks.
For a repeatable exercise, use the community-garden story. First complete it without referring to the solution. Then open the provided answer and compare each tag with a reason: why article around the story, why time for the date, why the h2 inside the details section. That comparison is more useful than simply matching the markup character for character.
The payoff is a page that explains itself
Semantic markup makes the page easier to navigate and maintain. It does not guarantee search rankings. Search engines still need a useful answer, an accurate title, descriptive links, and a page that loads well. The same restraint applies to accessibility claims: a perfectly nested heading tree does not make a confusing form understandable. Structure gives you a sound base for the rest of the work.
If you are working through the HTML course, pick one page you already wrote. Print its heading list and landmark list. Fix the first thing that would confuse a person who cannot see the layout. Then submit its form incorrectly and make the error useful. Those are small edits, but you can verify their effect in the browser today. A wholesale “replace divs with semantic tags” pass is much harder to review and often creates new noise.