Structure, content and forms in HTML
The idea
Picture a newspaper fresh off the press. Before you read a single word, the page already tells you where things are: a masthead with the paper's name at the top, one big headline, columns of articles below it, a short box of smaller stories off to the side, and a line of fine print at the bottom with the publisher's address. You don't have to read the fine print to know it's fine print — its place on the page already says what it is.
A web page can work the same way, or it can refuse to. Two pages can look identical in a browser and still be built completely differently underneath: one with parts a screen reader, a search engine, or another program can recognize by name, and one that is just an unlabeled stack of boxes stacked by eye. This lesson is about building the first kind, and about the other three jobs a page's markup does besides looking right: linking out, holding real data, and collecting answers from a visitor.
You'll keep using the same editor and folder habits as before. Nothing new to install — just a text file ending in .html and the browser you already have open.
Document structure and semantic elements
Stay with the newspaper. A masthead, a headline, a sidebar and a footer are all boxes of text, but they are not interchangeable boxes — swap the fine print into the headline slot and the page stops making sense, even though every word is still there. HTML gives you tags named after exactly these kinds of slots, so the type of box travels with the content instead of living only in your head.
Every HTML file starts with the same three lines of plumbing:
<!DOCTYPE html>
<html lang="en">
<head>
<!DOCTYPE html> tells the browser "read this as modern HTML," so it doesn't guess. <html lang="en"> wraps the whole page and names its language — a screen reader uses lang to pick the right pronunciation rules, and a translator tool uses it to know what it's translating from. <head> opens a section that holds information about the page rather than anything a visitor reads on screen.
Here is a full skeleton for a one-page bookshop site:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page & Ink Bookshop</title>
</head>
<body>
<header>
<h1>Page & Ink Bookshop</h1>
</header>
<nav>
<ul>
<li><a href="#catalog">Catalog</a></li>
<li><a href="#hours">Hours</a></li>
</ul>
</nav>
<main>
<section id="catalog">
<h2>New arrivals</h2>
<p>A shelf of staff picks, restocked every Friday.</p>
</section>
</main>
<footer>
<p>© 2026 Page & Ink Bookshop</p>
</footer>
</body>
</html>
<meta charset="UTF-8"> tells the browser which table of symbols to read the file's bytes as, so accents, currency signs and emoji show up correctly instead of as garbled boxes. <meta name="viewport" ...> tells a phone's browser to use the screen's real width instead of pretending it's a wide desktop monitor and shrinking everything to fit — without it, mobile pages look zoomed out. <title> is the text in the browser tab; it is metadata, not page content, so it never appears inside <body>.
Inside <body>, <header> is the masthead, <nav> is a group of navigation links, <main> is the one article that matters right now, and <footer> is the fine print. <section> groups related content under its own heading, the way a newspaper groups several stories under one column label. A plain <div> has none of this meaning — it's a box with no name tag, useful for styling hooks later but invisible to anything trying to understand the page's parts. Use a named landmark tag whenever one fits; reach for <div> only when none of them do.
One more rule carries real weight: use exactly one <h1> per page, for the single most important heading, the way a newspaper runs exactly one banner headline. Everything under it steps down one level at a time — <h2> for each major section, <h3> only inside a section that already has an <h2>. Skipping a level is like jumping from a chapter title straight to a sub-sub-heading with nothing in between; a reader scanning just the headings loses the thread.
Links, images and media
A link is a doorway: walk through it and you're somewhere else. An <a href="..."> tag is that doorway, and the text between its tags is the sign on the door — it should say where the door leads, not just "click here," because a screen reader user often jumps from link to link hearing only that text, out of context.
<p>
Browse the <a href="#catalog">full catalog</a>, or read about us on
<a href="https://example.com/about" target="_blank" rel="noopener noreferrer">our partner site</a>.
</p>
href="#catalog" points at an element with id="catalog" further down the same page — the browser scrolls there instead of loading anything new. href="https://example.com/about" is a full address to a different site. target="_blank" opens that link in a new tab; when you use it, add rel="noopener noreferrer" too, because without it the new tab gets a live handle back to your original page's window object, which a malicious destination could use to quietly redirect it — one attribute closes that door.
An image is a delivery, not a doorway, and it needs its own description for anyone who can't see the picture:
<figure>
<img src="shelf.jpg" alt="A wooden bookshelf stacked two rows deep with paperbacks">
<figcaption>This week's staff picks shelf.</figcaption>
</figure>
src is the file to load. alt is read aloud by a screen reader and shown if the image fails to load — write what the image shows, not its filename. <figure> groups an image with its own caption, and <figcaption> is that caption, shown to everyone, sighted or not. A purely decorative image — a swirl divider with no information in it — gets alt="" (present but empty) rather than a made-up description, which tells a screen reader to skip it silently instead of reading out something meaningless.
Audio and video follow the same shape, with a fallback for browsers that can't play the file:
<video controls width="480">
<source src="tour.mp4" type="video/mp4">
Your browser can't play this video. <a href="tour.mp4">Download it instead</a>.
</video>
controls adds the play/pause/volume bar — without it, the video plays but a visitor has no way to pause it. <source> names the file and its type; a browser that can read neither simply shows whatever text and links you put between the opening and closing <video> tags.
Lists and tables
A grocery list and a spreadsheet are both lists of things, but you'd never write one the other's way. A grocery list is just items, in whatever order you think of them. A spreadsheet is a grid where the column a value sits in is part of its meaning — "12" means nothing until you know it's under the "Quantity" column, not "Price."
HTML has one tag family for each shape. An unordered list is the grocery list — order doesn't matter:
<ul>
<li>Fiction</li>
<li>Science & nature</li>
<li>
Children's
<ul>
<li>Picture books</li>
<li>Early readers</li>
</ul>
</li>
</ul>
<ul> holds <li> items; nesting a whole <ul> inside one <li> makes a sub-list, like "Children's" splitting into two kinds. An ordered list is for steps where the order is the meaning, like a recipe — same shape, <ol> instead of <ul>, and the browser numbers it for you.
A table is the spreadsheet grid, and it needs its headers marked, not just styled to look bold:
<table>
<thead>
<tr>
<th scope="col">Day</th>
<th scope="col">Opens</th>
<th scope="col">Closes</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Monday</th>
<td>9:00 AM</td>
<td>6:00 PM</td>
</tr>
<tr>
<th scope="row">Sunday</th>
<td>11:00 AM</td>
<td>4:00 PM</td>
</tr>
</tbody>
</table>
<thead> holds the column headers, <tbody> holds the data rows. <th> marks a cell as a header instead of data, and scope="col" or scope="row" says which direction it heads — a screen reader uses that to announce "Day: Monday, Opens: 9:00 AM" instead of just reading three bare words in a row. Using <td> everywhere, with bold text to look like a header, gives sighted visitors the right picture and everyone else nothing to go on.
Forms, inputs and built-in validation
A paper sign-up sheet at a front desk has blank lines, each with a small label telling you what to write there, and sometimes a note like "required" next to a line you can't skip. An HTML form is that sheet, and the browser enforces some of its own rules before anything gets sent anywhere.
<form>
<div>
<label for="newsletter-email">Email for the newsletter</label>
<input type="email" id="newsletter-email" name="email" required>
</div>
<div>
<label for="newsletter-genre">Favorite genre</label>
<select id="newsletter-genre" name="genre">
<option value="">Choose one</option>
<option value="fiction">Fiction</option>
<option value="nonfiction">Nonfiction</option>
</select>
</div>
<button type="submit">Sign up</button>
</form>
<label for="newsletter-email"> is tied to <input id="newsletter-email"> by that matching for/id pair — click the label's text and focus jumps to the input, and a screen reader announces the label the moment the input is focused. A placeholder attribute, which shows faint text inside an empty input, is not a substitute: it disappears the moment you start typing and most screen readers either skip it or read it only once.
type="email" does two things: it changes the on-screen keyboard on a phone to one with @ and . within easy reach, and it makes the browser itself check the typed text looks like an email address before letting the form proceed. required means the browser blocks submission and shows a built-in message — something like "Please fill out this field" — if the input is empty; the exact wording differs by browser, but the behavior doesn't. Other common checks: min/max bound a type="number" input, maxlength caps how many characters a text input accepts, and pattern takes a precise rule the typed text must match, such as a phone number shaped like 555-123-4567.
Checkboxes and radio buttons are both built from <input>, and the difference is one word: radios sharing the same name let you pick only one of the group, checkboxes let you pick any number. A radio group needs one more wrapper so a screen reader can announce what the whole group is for, not just each option on its own:
<fieldset>
<legend>Preferred contact method</legend>
<label for="contact-email"><input type="radio" id="contact-email" name="contact" value="email"> Email</label>
<label for="contact-phone"><input type="radio" id="contact-phone" name="contact" value="phone"> Phone</label>
</fieldset>
<fieldset> draws a box around the related group, and <legend> is its title, read out before the first option.
Accessibility basics
Every piece above was already pulling double duty: the right tag doesn't just organize the page, it tells anyone or anything that isn't looking at it with eyes exactly what each part is. That's the whole idea behind accessibility — not a separate pass at the end, but the natural result of picking the tag that matches the content instead of the tag that merely looks right once it's styled.
Four things to check, in the order you've already met them:
- Headings: one
<h1>, then<h2>s, then<h3>s only nested under an<h2>— never skipped, so a user jumping heading-to-heading gets a real outline of the page instead of a broken one. - Landmarks:
<header>,<nav>,<main>,<footer>and friends let a screen reader user jump straight to "main content" or "navigation" the way you'd read a building's wall directory instead of walking every hallway to find the right office. - Alt text: every
<img>that carries information gets a real description; every purely decorative one getsalt=""so it's silently skipped instead of announced as "image" with nothing useful attached. - Labels: every form input has a
<label for="...">pointing at itsid— no exceptions, not even for a field that "obviously" only takes one kind of answer.
None of this is guesswork you have to do by eye. Chrome and Edge ship an Accessibility pane right next to Elements in the developer tools; selecting an element there shows its "accessible name" — the exact text a screen reader would announce for it — which is a fast way to catch a label that silently points at the wrong id.
Metadata, SEO basics and Open Graph
A book's cover and back-cover blurb aren't part of the story, but they're what convince someone to open it: the title in big letters, a line or two about what's inside, maybe a quote. Nobody reading page 50 ever sees that blurb again — it only works where the book is encountered, not where it's read. A page's <head> holds exactly that kind of information: nothing shown in the page itself, everything shown wherever the page shows up elsewhere.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page & Ink Bookshop</title>
<meta name="description" content="An independent bookshop with new arrivals every Friday, author events, and a cafe counter in the back.">
<link rel="icon" href="favicon.ico">
<link rel="canonical" href="https://pageandink.example/">
<meta property="og:title" content="Page & Ink Bookshop">
<meta property="og:description" content="New arrivals every Friday, author events, and a cafe counter in the back.">
<meta property="og:image" content="https://pageandink.example/storefront.jpg">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
</head>
<title> is also the blue link text a search engine shows in its results, and <meta name="description"> is the gray summary line underneath it — write it like a blurb aimed at a stranger deciding whether to click, not a restatement of the title. <link rel="icon"> is the small picture in the browser tab. <link rel="canonical"> names the one true address for this content, which matters once the same page might be reachable at more than one URL.
The og: tags are Open Graph — a shared set of names that Facebook, Slack, Discord, iMessage and most other apps read when you paste in a link, to build that preview card with a title, a line of text and an image. Without them, a pasted link falls back to whatever <title> and the first scrap of visible text happen to be, which is often the wrong summary. og:image needs a full https:// address, not a relative path like storefront.jpg on its own — the app fetching the preview isn't running from your page, so it has no folder to resolve a relative path against. twitter:card asks for a specific large-image layout on that one platform; most others are happy with the og: tags alone.
Common mistakes
- Reaching for
<div>first. If a landmark tag —header,nav,main,footer,section,article,aside— fits the content, use it. Save<div>for the cases none of them cover. - More than one
<h1>, or a skipped heading level. Treat headings as a strict outline, not a font-size shortcut for "make this text bigger" — that's a job for CSS, later. - Alt text that repeats the filename (
alt="IMG_4821.jpg") or over-describes a decoration. Describe what matters about the picture, or mark italt=""if nothing does. - A
placeholderinstead of a<label>. Placeholder text vanishes the moment someone starts typing, and isn't reliably announced by a screen reader. target="_blank"with norel="noopener noreferrer". The new tab shouldn't get a live handle back to the page that opened it.- A table of
<div>s styled to look gridded. It may look right, but nothing can tell a screen reader which column a cell belongs to. - An empty or missing
<meta name="description">. Search engines and link previews then have to guess, usually badly.
Try it
- Build the skeleton. Save a
bookshop.htmlwith the full document skeleton from the Document structure section —<!DOCTYPE html>,<html lang="en">, the<head>metadata, and<header>/<nav>/<main>/<footer>in<body>. Open it in your browser, then open the developer tools (F12) and look at the Elements tab. Find the<nav>element and confirm it's a sibling of<header>, not nested inside it. - Add a broken link on purpose. Inside
<main>, add<img src="does-not-exist.jpg" alt="A photo that will fail to load">. With the Network tab open, reload the page and find the row for that image — you should see a404status, same as the one you caused on purpose in the last lesson. Then fix thesrcto point at a real image file (any small picture you have) and confirm the row turns to200. - Add the media. Add a
<figure>with that image and a<figcaption>, then one external link withtarget="_blank" rel="noopener noreferrer"and one internal link with anhref="#..."that jumps to your<main>'sid. Click the internal link and watch the page scroll instead of reload. - Add lists and a table. Add a
<ul>of at least three items (nest a smaller<ul>inside one<li>), and the store-hours<table>from the Lists and tables section, with<th scope="col">headers and<th scope="row">for each day. - Add the form and break it on purpose. Add the newsletter form from the Forms section with
requiredon the email field. Click submit with the field empty and read the browser's own validation message. Then delete theforattribute from the<label>, open the Accessibility pane in developer tools, select the input, and check whether it still reports a label as its accessible name. - Fill in the
<head>. Add<title>,<meta name="description">and all five Open Graph/Twitter tags from the Metadata section, describing your own fictional shop. View the page's source (right-click → View Page Source) and confirm every tag landed inside<head>, not<body>. - Hunt your own mistakes. Re-read the Common mistakes list against your file. Find at least one you accidentally made, fix it, and say in one sentence why the original version would have caused a problem for someone using a screen reader or a search engine.