How to buildOctober 11, 202612 min read
Markup and layout: HTML and CSSStep 2 of 7

HTML: semantic markup that browsers, search engines and screen readers understand

In short

Semantic markup means choosing HTML elements for what the content is, not how it looks: the site header is header, the menu is nav, the main content is main, headings are h1–h6 without skipped levels, an action is a button, navigation is an a. From that markup the browser builds an accessibility tree that screen readers use to jump between regions and headings. You check the markup with a validator (Nu Html Checker or html-validate) and with the accessibility tree in DevTools.

FundamentalsLevel: BeginnerHTML

After this you will be able to

  • Mark up a page with header, nav, main, section, article, aside and footer and explain which of them become landmarks in the accessibility tree
  • Build an h1–h3 heading outline without skipped levels and explain why a level is not chosen by font size
  • Choose between a and button, give images alt, width and height, and associate form fields with label
  • Check a page with Nu Html Checker, html-validate and the accessibility tree in DevTools

Verified with

HTML Living Standard (WHATWG, 2026-10-09)html-validate 11.16.2Nu Html Checker 26.10.11Microsoft Edge 155 + Playwright 1.64

Checked against docs: October 11, 2026

Contents
  1. 01Semantics: elements by meaning, not by looks
  2. 02Document skeleton: doctype, lang, head and body
  3. 03Page regions: header, nav, main, section, article, aside, footer
  4. 04Headings: one h1 and no skipped levels
  5. 05Link or button: how to choose
  6. 06Images: alt and dimensions
  7. 07Forms: label, input, button
  8. 08Checking: validator and accessibility tree

Semantics: elements by meaning, not by looks

In programming, semantics means what a piece of code means. MDN gives a simple example: an h1 tells the browser that its text is a top-level heading, while a span with a large font only looks like one and carries no meaning. Appearance is the job of CSS; HTML describes what each piece of content is.

Below is a "div soup": a page built from anonymous div elements. CSS can make it look fine, but for machines there is no header, no menu, no main heading and no button.

bad.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Coffee machine repair</title>
</head>
<body>
  <div class="header">
    <img src="/img/logo.svg">
    <div class="menu">
      <a href="#" onclick="openServices()">Services</a>
    </div>
  </div>
  <div class="hero">
    <div class="title">Coffee machine repair in Almaty</div>
    <h3>Diagnostics and repair for coffee shops</h3>
    <div class="btn" onclick="sendForm()">Send request</div>
    <input placeholder="Phone">
  </div>
</body>
</html>
Don't do this. We will run this file through a validator in the last section.

Who reads markup besides people? The browser turns HTML into the DOM, a tree of elements, and from it builds the accessibility tree, where each node has a role (button, link, heading), a name and a state. Screen readers and other assistive technology work with that tree: they jump between page regions and headings and announce "button" or "link". According to MDN, browser reader modes look for main, headings and sections. MDN also lists search engines among those that benefit from meaningful markup. And a developer finds a block faster among nav and footer than among a dozen nested div elements.

Div soup

  • A screen reader sees loose text and a single link with no structure
  • A div "button" never receives keyboard focus
  • There is no level-one heading, so it is unclear what the page is about
  • Every behaviour has to be rebuilt in JavaScript

Semantic markup

  • Header, menu, main content and footer are separate regions
  • A button is reachable with Tab and fires on Enter and Space
  • Headings h1–h3 form the page outline
  • The browser provides behaviour for free: focus, form submission, navigation

Document skeleton: doctype, lang, head and body

Every page starts with the same skeleton. It is short, but each line switches something on or off in the browser.

skeleton.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Coffee machine repair in Almaty — Zerno service</title>
  <meta name="description" content="Diagnostics and repair of coffee machines for coffee shops and offices in Almaty.">
</head>
<body>
  <h1>Coffee machine repair in Almaty</h1>
</body>
</html>
A minimal valid document: no messages from Nu Html Checker or html-validate.

<!DOCTYPE html>

Why it is there
Turns on standards mode. Without it the browser falls back to quirks mode, a compatibility mode for old sites, and computes some CSS the old way

<html lang="en">

Why it is there
The page language. Screen readers use it to pick pronunciation; WCAG 3.1.1 requires the page language to be programmatically determinable

<meta charset="utf-8">

Why it is there
The document encoding; without it non-Latin text may turn into garbage characters

<meta name="viewport" …>

Why it is there
Without it a mobile browser renders the page in a virtual window wider than the screen and shrinks it as a whole

<title>

Why it is there
The name in the tab and in bookmarks; search engines use it as one of their signals

<meta name="description">

Why it is there
A short summary of the page content

<body>

Why it is there
Everything the user sees and hears

Page regions: header, nav, main, section, article, aside, footer

Large parts of the page are marked up with sectioning elements. Several of them become landmarks in the accessibility tree: a screen reader can list them and jump to one in a single step.

header

What it means
Introductory content: logo, menu, search. Inside an article or section it is that block's header
Role in the accessibility tree
banner when the header is not nested in main, article, section, aside or nav

nav

What it means
A major block of navigation links. Not every group of links is a nav
Role in the accessibility tree
navigation

main

What it means
The dominant content of the page, not repeated on other pages
Role in the accessibility tree
main; a document has only one visible main

section

What it means
A thematic section with a heading when no more specific element fits
Role in the accessibility tree
region, but only when the section has an accessible name, for example via aria-labelledby

article

What it means
A self-contained piece: a blog post, a comment, a product card
Role in the accessibility tree
article

aside

What it means
Indirectly related content: a call-out, related links
Role in the accessibility tree
complementary

footer

What it means
The footer of the page or a block: contacts, authorship, copyright
Role in the accessibility tree
contentinfo for the page footer

div does not go away. The HTML standard says plainly that section is not a generic container and that wrappers for styling or scripting should be a div. A quick test: if a block has its own heading and belongs in a table of contents, it is a section; if it exists only for a grid or a background, it is a div. The standard puts the difference between section and article like this: a section forms part of something else, an article is its own thing.

Let's build a landing page for a coffee machine repair service: header with menu, hero, services, benefits, request form and footer. The company is fictional; the address and email are placeholders.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Coffee machine repair in Almaty — Zerno service</title>
  <meta name="description" content="Diagnostics and repair of coffee machines for coffee shops and offices in Almaty.">
</head>
<body>
  <a href="#main">Skip to content</a>

  <header>
    <a href="/"><img src="/img/logo.svg" alt="Zerno — home" width="120" height="32"></a>
    <nav aria-label="Main menu">
      <ul>
        <li><a href="#services">Services</a></li>
        <li><a href="#benefits">Why us</a></li>
        <li><a href="#request">Request</a></li>
      </ul>
    </nav>
  </header>

  <main id="main">
    <section>
      <h1>Coffee machine repair in Almaty</h1>
      <p>Diagnostics, descaling and repair of espresso and bean-to-cup machines for coffee shops and offices.</p>
      <a href="#request">Request a repair</a>
      <img src="/img/master.jpg" alt="A technician replaces the group head gasket of an espresso machine" width="1200" height="800">
    </section>

    <section id="services">
      <h2>Services</h2>
      <article>
        <h3>Diagnostics</h3>
        <p>We find the cause of the fault and agree on the cost before any work starts.</p>
      </article>
      <article>
        <h3>Descaling</h3>
        <p>We flush the water system and replace filters as the manufacturer recommends.</p>
      </article>
      <article>
        <h3>Repair</h3>
        <p>We replace pumps, gaskets, sensors and control boards.</p>
      </article>
    </section>

    <section id="benefits">
      <h2>Why coffee shops trust us</h2>
      <ul>
        <li>On-site visits across the city</li>
        <li>Genuine spare parts</li>
        <li>Warranty on all work</li>
      </ul>
    </section>

    <section id="request">
      <h2>Repair request</h2>
      <form action="/api/request" method="post">
        <label for="name">Name</label>
        <input id="name" name="name" type="text" autocomplete="name" required>

        <label for="phone">Phone</label>
        <input id="phone" name="phone" type="tel" autocomplete="tel" required>

        <label for="problem">What is wrong with the machine</label>
        <textarea id="problem" name="problem" rows="4"></textarea>

        <button type="submit">Send request</button>
      </form>
    </section>
  </main>

  <footer>
    <address>
      1 Example St, Almaty · <a href="mailto:hello@example.com">hello@example.com</a>
    </address>
    <small>© 2026 Zerno service</small>
  </footer>
</body>
</html>
The full page: zero messages in Nu Html Checker and html-validate, checked in Microsoft Edge 155.
  • The first link, "Skip to content", points to main id="main": keyboard users skip the menu with one Enter. We checked that it is the first element Tab lands on.
  • The nav has an aria-label: when a page has several menus (header and footer), a screen reader tells them apart by name.
  • Service cards are article elements: each is self-contained and could live on its own page. Benefits are a plain ul list.
  • The logo inside the link has an alt that says where the link goes, not "logo".
  • address in the footer holds the page owner's contact details.

Headings: one h1 and no skipped levels

The h1–h6 elements define six heading levels. All headings in document order make up the page outline, and many screen reader users explore a page exactly that way, jumping from heading to heading. So levels follow the structure, not the font size: size is CSS's job.

  • The HTML standard: each following heading may be at the same level, higher, or exactly one level deeper than the previous one. h1 → h3 without an h2 is an error, not a style choice.
  • If a page has headings, at least one must be level 1.
  • MDN recommends one h1 per page that describes the content, much like title. The standard allows several h1 elements, but it is not a best practice.
  • The old advice "give every section its own h1" is outdated: browsers never implemented the algorithm that recalculated those levels, and nested h1 elements in sections are now non-conforming.

1

Heading
Coffee machine repair in Almaty

2

Heading
Services

3

Heading
Diagnostics · Descaling · Repair

2

Heading
Why coffee shops trust us

2

Heading
Repair request
The landing page outline as the browser sees it

Link or button: how to choose

The rule is short: a link a goes to an address — another page, an anchor, a file. A button performs an action on the current page — opens a dialog, shows reviews, submits a form. You can style them alike, but they behave differently.

links-buttons.html
<h1>Links and buttons</h1>
<!-- Going to another page or an anchor — a link -->
<a href="/prices">Repair prices</a>
<a href="#request">Request a repair</a>

<!-- An action on the same page — a button with type="button" -->
<button type="button" id="show-reviews">Show all reviews</button>

<!-- Submitting a form — a type="submit" button inside the form -->
<form action="/api/callback" method="post">
  <label for="cb-phone">Phone</label>
  <input id="cb-phone" name="phone" type="tel" autocomplete="tel" required>
  <button type="submit">Call me back</button>
</form>
The <body> fragment of the verified links-buttons.html page.

MDN specifically warns against "link buttons" such as href="#" or javascript:void(0) with a click handler: they break copying the link, opening it in a new tab and bookmarking, misbehave while JavaScript is loading, and expose the wrong role to screen readers. That is what button is for.

A div with onclick is even worse. We tabbed through both pages in Edge: on the bad one focus lands on the link and the input and skips the div "button", so it cannot be pressed from the keyboard. On the good one the button receives focus and fires on both Enter and Space. Adding role="button" to a div does not fix this: as web.dev explains, the role only changes what the screen reader announces, while focus and keys would have to be scripted by hand.

Images: alt and dimensions

The alt attribute is the text replacement for an image. A screen reader reads it, and the browser shows it if the image fails to load. MDN advises describing what the image means rather than the fact that it exists: not "photo", not the file name, but what matters in context. If an image is purely decorative, leave alt empty so assistive technology skips it. If the image sits inside a link or a button, alt describes the action or destination.

images.html
<h1>Images</h1>
<!-- Informative: alt conveys what matters in the photo -->
<img src="/img/master.jpg" alt="A technician replaces the group head gasket of an espresso machine" width="1200" height="800">

<!-- Decorative: empty alt, screen readers skip it -->
<img src="/img/beans-pattern.svg" alt="" width="400" height="80">

<!-- Image link: alt says where the link goes -->
<a href="/"><img src="/img/logo.svg" alt="Zerno — home" width="120" height="32"></a>
The <body> fragment; the page includes the CSS rule img { max-width: 100%; height: auto; }.

The accessibility tree of this page contains two images, not three: the browser dropped the decorative pattern with alt="", and the logo link took its name, "Zerno — home", from alt.

The width and height attributes are the file's intrinsic size in pixels, without units. From them the browser computes the aspect ratio in advance and reserves space, so text does not jump when the image arrives. Together with CSS max-width: 100%; height: auto the image stays fluid. We delayed an image by 3 seconds and measured its box before it loaded: on a 375 px wide screen a 1200×800 photo already had 375×250 px reserved, while the same image without the attributes had a height of 0.

Forms: label, input, button

Every field needs a label that is associated with it in code. There are two ways: explicit, where the label's for equals the field's id, and implicit, where the field sits inside the label. The association does two things: a screen reader announces the label when the field gets focus, and clicking the label focuses the field, which gives a checkbox a bigger hit area.

form.html
<h1>Descaling reminders</h1>
<form action="/api/subscribe" method="post">
  <!-- Explicit association: label's for = the field's id -->
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <!-- Implicit association: the field is inside the label -->
  <label>
    <input name="consent" type="checkbox" required>
    I agree to receive service emails
  </label>

  <button type="submit">Subscribe</button>
</form>
The <body> fragment of the verified form.html page.
  • name is the key the value is sent under. A field without name is not submitted.
  • type changes behaviour: email checks the address format before submission — in our check the form with not-an-email was not sent to the server, and with a valid address it was. On phones tel may bring up a numeric keypad.
  • autocomplete tells the browser what data is expected (name, email, tel) so it can offer saved values.
  • required blocks submitting an empty field. It is a convenience for the user, not protection: the server must validate data on its own anyway.
  • The submit button is a button type="submit" inside the form. Then the form submits both on click and on Enter in a field.

Which fields to keep, where to show errors and how to shorten a form are covered in the UX article on forms; here we deal only with markup.

Checking: validator and accessibility tree

  1. W3C validator

    Open Nu Html Checker at validator.w3.org/nu and paste the code or the page address. It finds syntax errors and violations of the standard: an image without alt, invalid nesting.

  2. html-validate in the project

    The same kind of checks locally and in CI: npm install --save-dev html-validate, a config file and a run via npx. The document preset adds heading-level checks.

  3. Accessibility tree in DevTools

    In Chrome DevTools: Elements → Accessibility tab → Show accessibility tree. The DOM is replaced with a tree of roles and names, so you see the regions, headings and field labels at once.

  4. Keyboard

    Walk through the page with Tab. Chrome's documentation says plainly that DevTools cannot check this for you: focus must move in order and reach every interactive element.

.htmlvalidate.json
{
  "extends": ["html-validate:recommended", "html-validate:document"]
}
html-validate config: recommended rules plus rules for complete documents.
terminal
$ npx html-validate bad.html
…\bad.html
   9:6  error  <img> is missing required "alt" attribute        wcag/h37
  16:6  error  Initial heading level must be <h1> but got <h3>  heading-level
  18:6  error  <input> is missing recommended "type" attribute  no-implicit-input-type
  18:6  error  <input> element does not have a <label>          input-missing-label

✖ 4 problems (4 errors, 0 warnings)

More information:
  https://html-validate.org/rules/wcag/h37.html
  https://html-validate.org/rules/heading-level.html
  https://html-validate.org/rules/no-implicit-input-type.html
  https://html-validate.org/rules/input-missing-label.html

$ echo $?
1

$ npx html-validate index.html

$ echo $?
0
Real html-validate 11.16.2 output: the bad page has 4 errors and exit code 1, the landing page has none. The file path is shortened.

Notice what the validator did not find: the div with onclick and the href="#" link. They are syntactically valid; only the accessibility tree and a keyboard check catch them. Here is the accessibility tree of our landing page, captured in Edge with Playwright's ariaSnapshot(); DevTools shows the same roles and names:

YAML
- link "Skip to content":
  - /url: "#main"
- banner:
  - link "Zerno — home":
    - /url: /
    - img "Zerno — home"
  - navigation "Main menu":
    - list:
      - listitem:
        - link "Services":
          - /url: "#services"
      - listitem:
        - link "Why us":
          - /url: "#benefits"
      - listitem:
        - link "Request":
          - /url: "#request"
- main:
  - heading "Coffee machine repair in Almaty" [level=1]
  - paragraph: Diagnostics, descaling and repair of espresso and bean-to-cup machines for coffee shops and offices.
  - link "Request a repair":
    - /url: "#request"
  - img "A technician replaces the group head gasket of an espresso machine"
  - heading "Services" [level=2]
  - article:
    - heading "Diagnostics" [level=3]
    - paragraph: We find the cause of the fault and agree on the cost before any work starts.
  - article:
    - heading "Descaling" [level=3]
    - paragraph: We flush the water system and replace filters as the manufacturer recommends.
  - article:
    - heading "Repair" [level=3]
    - paragraph: We replace pumps, gaskets, sensors and control boards.
  - heading "Why coffee shops trust us" [level=2]
  - list:
    - listitem: On-site visits across the city
    - listitem: Genuine spare parts
    - listitem: Warranty on all work
  - heading "Repair request" [level=2]
  - text: Name
  - textbox "Name"
  - text: Phone
  - textbox "Phone"
  - text: What is wrong with the machine
  - textbox "What is wrong with the machine"
  - button "Send request"
- contentinfo:
  - text: 1 Example St, Almaty ·
  - link "hello@example.com":
    - /url: mailto:hello@example.com
  - text: © 2026 Zerno service
The landing page accessibility tree: banner, a named navigation, main with level 1–3 headings, contentinfo.
  • <!DOCTYPE html>, lang on html, charset, viewport and a meaningful title are in place
  • Header, menu, main content and footer use header, nav, main, footer
  • One h1, heading levels without gaps
  • Navigation uses links with real addresses, actions use button with the right type
  • Informative images have a meaningful alt, decorative ones an empty alt; width and height are set
  • Every field has an associated label, a name and a fitting type
  • The validator reports no errors, Tab reaches every interactive element

Next comes layout: placing these blocks on a grid with Flexbox and Grid without touching the markup. Accessibility in code — focus, ARIA and screen reader testing — has its own lesson in this track.

How this was verified3 · passedchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.