Semantic HTML and CSS: Build an Accessible First Webpage
Introduction
Semantic HTML and CSS let you build an accessible webpage with a clear structure and predictable styles. This lesson belongs to course 1 of the Meta Front-End Developer pathway: HTML describes what content is, and CSS describes how it looks. React components, which come later, ultimately produce HTML. If your HTML is wrong, no framework will rescue it, and the people who suffer most are those using screen readers, keyboards, or search engines.
In this lesson you build the first page of the Harbor Workshops project: an accessible tutor profile for a community workshop. You will not copy a course assignment; the brief below is original to OnLevelUp.
After this lesson you will be able to write a well-structured HTML document, choose elements for their meaning, write useful alternative text, and apply the basics of the CSS cascade and box model.
Learning objectives
- Write a complete HTML5 document with correct boilerplate.
- Choose headings, lists, links, and landmark elements by meaning, not appearance.
- Write alternative text appropriate to an image's purpose.
- Explain the CSS cascade and identify when specificity and source order decide among ordinary author rules.
- Predict the size of a box using the box model and
box-sizing. - Build and review a small accessible profile page.
1. The HTML document
HTML (HyperText Markup Language) wraps content in elements. Most elements have an opening tag, content, and a closing tag. Void elements such as <img>, <input>, and <meta> have no content or closing tag. Elements can have attributes, name/value pairs inside the opening tag.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maya Chen, Pottery Tutor | Harbor Workshops</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<h1>Maya Chen</h1>
<p>Pottery tutor at Harbor Workshops.</p>
</body>
</html>What each piece does:
| Piece | Purpose |
|---|---|
<!doctype html> |
Puts the browser in standards mode |
lang="en" |
Declares the page language; screen readers choose pronunciation from it, and translation tools rely on it |
charset="utf-8" |
Makes accented characters and symbols display correctly |
| viewport meta | Lets mobile browsers use the real device width (more in lesson 1.3) |
<title> |
Names the document in the tab and helps users identify it; search engines may rewrite the displayed search title |
For the next practical step, study browser requests and URL resolution.
The <head> contains information about the page. The <body> contains what visitors see. Browsers are forgiving about mistakes and quietly repair them, so you cannot judge correctness by appearance. Use the W3C Nu HTML Checker to validate.
2. Semantic elements: meaning over appearance
A semantic element communicates its role. <button> says "this is clickable and does something". A <div> says nothing. Browsers, search engines, and assistive technologies use meaning to build navigation tools: a screen-reader user can jump from heading to heading, list to list, or landmark to landmark.
Headings
Headings <h1> to <h6> build the outline of the page. Rules of thumb:
- Prefer one clear
<h1>describing the page's main topic. This is a useful authoring convention, not a claim that every document with two<h1>elements is invalid. - Do not skip levels (
<h2>then<h4>) because you like the font size. Use CSS for size. - Headings are not for making text bold.
Text, lists, and links
<p>Maya has taught ceramics for <strong>twelve years</strong>.</p>
<h2>Classes this term</h2>
<ul>
<li>Wheel basics (Mondays)</li>
<li>Glazing workshop (Saturdays)</li>
</ul>
<h2>Preparation steps</h2>
<ol>
<li>Wear clothes you can get dirty.</li>
<li>Arrive ten minutes early.</li>
<li>Bring a towel.</li>
</ol>
<p>Read the <a href="/workshops/pottery">pottery workshop details</a>.</p><ul>is an unordered list;<ol>is ordered, so use it when sequence matters.<strong>marks strong importance;<em>marks emphasis. Screen readers may change tone.<b>and<i>are for stylistic offsets without added importance.- Link text must make sense out of context. Screen-reader users often list all links on a page. "Click here" repeated five times is useless; "pottery workshop details" is meaningful.
- Links navigate; buttons act. If clicking changes the page's URL, use
<a>. If it submits or toggles something, use<button>.
Landmarks
Landmark elements divide the page into regions that assistive technology can jump between.
| Element | Role | Use |
|---|---|---|
<header> |
Introductory content | Site logo and title (or the intro of a section) |
<nav> |
Navigation links | Main menu; label it with aria-label if you have several |
<main> |
Primary content | One main element that is not hidden |
<section> |
A thematic group, with a heading | Classes, reviews |
<article> |
Self-contained content | A workshop card, a blog post |
<aside> |
Related but secondary | Tips, side notes |
<footer> |
Closing content | Contact info, legal links |
Not every semantic element automatically creates a landmark. A <section> needs an accessible name, such as aria-labelledby referencing its heading, to expose a region landmark. Page-level headers and footers can expose banner and contentinfo landmarks; those inside an article or section do not. An <article> describes self-contained content rather than a generic landmark.
Overusing <div> is the classic beginner pattern called "div soup". A <div> is fine when no semantic element fits, typically for grouping elements for layout.
Worked example 1: converting div soup
Problem. A previous developer wrote this:
<div class="top">
<div class="big">Harbor Workshops</div>
<div class="links">
<span onclick="go('home')">Home</span>
<span onclick="go('classes')">Classes</span>
</div>
</div>
<div class="main">
<div class="big">Maya Chen</div>
<div>Pottery tutor</div>
</div>Visually it works. Convert it to semantic HTML.
Reasoning, step by step.
- The top block is introductory content with the site name and navigation: use
<header>, with the site name in a paragraph or heading and a<nav>for links. - The "links" are
<span>s with click handlers. A span is not focusable by keyboard and is not announced as a link. Each destination is a new page, so use<a href>. - The main block is the primary content:
<main>. "Maya Chen" is the page's topic:<h1>. - "Pottery tutor" is a paragraph:
<p>.
Solution.
<header>
<p class="site-name">Harbor Workshops</p>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/classes">Classes</a></li>
</ul>
</nav>
</header>
<main>
<h1>Maya Chen</h1>
<p>Pottery tutor</p>
</main>Why other options are worse. Adding role="link" and tabindex="0" to the spans would recreate part of an anchor's behavior by hand, still without right-click, middle-click, or status-bar previews. The first rule of ARIA is: if a native element does the job, use it.
For the next practical step, study accessible forms and validation.
3. Images and alternative text
<img src="/img/maya-wheel.jpg" alt="Maya guiding a student's hands on a spinning clay pot" width="640" height="427">altis required. It is read by screen readers and displayed when the image fails to load.- Write alt text according to the image's purpose:
| Image role | Example | Alt text |
|---|---|---|
| Informative | Photo of the studio | Describe what matters: "Studio with six pottery wheels and shelves of glazed bowls" |
| Functional | Icon that provides a link or button name | Describe the action: "Search", not "magnifying glass"; use empty alt if adjacent text already supplies the same name |
| Decorative | Ornamental divider | Empty alt, alt="", so assistive technology skips it |
| Complex | Chart | Short alt plus a full description nearby in text |
- Do not start with "image of" or "picture of"; screen readers already announce an image.
- Always give
widthandheightattributes (or an equivalent CSS aspect ratio). The browser reserves space before the image loads, preventing the page from jumping, which is called layout shift. - Use
loading="lazy"for images far below the fold.
4. CSS basics
CSS (Cascading Style Sheets) is a set of rules. Each rule has a selector and declarations.
h1 {
color: #1f3a5f;
font-size: 2rem;
}You can attach CSS in three ways: an external file via <link> (preferred, cacheable), a <style> element, or a style attribute (hard to override, avoid).
Selectors
| Selector | Example | Matches |
|---|---|---|
| Type | p |
All paragraphs |
| Class | .card |
Elements with class="card" |
| ID | #intro |
The element with id="intro" (IDs must be unique) |
| Descendant | nav a |
Any link inside a nav |
| Child | ul > li |
Direct children only |
| Attribute | a[target="_blank"] |
Links that open a new tab |
| Pseudo-class | a:hover, button:focus-visible |
A state |
Prefer classes for styling; they are reusable and have moderate specificity.
The cascade
The full cascade considers relevance, origin and importance (including cascade layers), specificity, scope proximity for scoped rules, and finally source order. Transitions and animations also occupy defined priority levels; !important is not a universal trump card.
For the beginner examples below, assume ordinary, unlayered author declarations with no inline styles, animations, transitions, or scoped rules. Within that scope:
- Importance and origin: normal author styles beat normal browser defaults; important declarations follow a separate origin and layer order.
- Specificity: compare IDs, then classes/attributes/pseudo-classes, then type selectors. Inline declarations have special precedence within their origin and importance; they are not selectors.
- Source order: if everything else ties, the later rule wins.
p { color: black; }
.note { color: darkgreen; }
p.note { color: navy; }For <p class="note">, all three match. p has specificity (0,0,1), .note is (0,1,0), p.note is (0,1,1). The result is navy. Reordering the rules would not change it, because specificity is compared before order.
Inheritance: some properties (color, font-family, line-height) pass from parent to children; others (border, margin, padding) do not. This is why setting font-family on body styles the whole page.
Units
pxis a CSS pixel, an absolute CSS length unit rather than necessarily one physical screen pixel.remis relative to the root font size (usually 16px). Preferremfor text and spacing; users who increase their default font size are respected.emusually refers to the element's own font size; in itsfont-sizeproperty it refers to the inherited parent font size.%uses a reference defined by the property, such as containing-block width forwidth; it is not universally a percentage of the parent.
The box model
Every element is a rectangular box: content, then padding, then border, then margin (outside space).
By default (box-sizing: content-box), width sets only the content width. Consider:
.card {
width: 300px;
padding: 20px;
border: 5px solid #ccc;
}Total horizontal space: 300 + 2×20 + 2×5 = 350px. With box-sizing: border-box, width includes padding and border, so the card is exactly 300px wide and the content shrinks to 250px. Most projects start with:
*, *::before, *::after {
box-sizing: border-box;
}Eligible vertical margins in normal block flow can collapse (not between flex or grid items): a 20px bottom margin above a 30px top margin results in a 30px gap, not 50px.
Block and inline
With their normal block display, div, p, h1, and section start on a new line and an auto-width block generally fills available width. CSS can change their display behavior. Inline elements (a, strong, span, img) flow within a line and ignore width and height (for non-replaced ones). display: inline-block combines behaviors, and display: none removes an element from layout and from the accessibility tree.
5. Building the tutor profile page
Brief (original). Harbor needs a profile page for tutor Maya Chen with: her name, a photo, a short biography, a list of classes, and a "Book a class" link. It must work with the keyboard, have good contrast, and look fine at 360px width.
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maya Chen, Pottery Tutor | Harbor Workshops</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<a class="skip-link" href="#content">Skip to main content</a>
<header>
<p class="site-name">Harbor Workshops</p>
<nav aria-label="Main">
<ul class="nav-list">
<li><a href="/">Home</a></li>
<li><a href="/tutors">Tutors</a></li>
</ul>
</nav>
</header>
<main id="content" tabindex="-1">
<article class="profile">
<h1>Maya Chen</h1>
<img src="/img/maya.jpg" alt="Maya smiling beside a shelf of glazed bowls" width="320" height="320">
<p>Maya has taught ceramics for twelve years and loves helping first-timers shape their first bowl.</p>
<h2>Classes this term</h2>
<ul>
<li>Wheel basics (Mondays, 18:00)</li>
<li>Glazing workshop (Saturdays, 10:00)</li>
</ul>
<a class="button" href="/book?tutor=maya">Book a class</a>
</article>
</main>
<footer><p>Harbor Workshops, Quay Street 12</p></footer>
</body>
</html>styles.css:
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.5;
color: #1a1a1a;
background: #fff;
}
.skip-link {
position: absolute;
left: -999px;
}
.skip-link:focus { left: 1rem; top: 1rem; z-index: 10; background: #fff; padding: .5rem; }
header, main, footer { padding: 1rem; }
.nav-list { display: flex; flex-wrap: wrap; gap: 1rem; list-style: none; padding: 0; }
.profile { max-width: 40rem; margin: 0 auto; }
.profile img { max-width: 100%; height: auto; border-radius: .5rem; }
.button {
display: inline-block;
padding: .75rem 1.25rem;
background: #1f3a5f;
color: #fff;
text-decoration: none;
border-radius: .25rem;
}
.button:hover, .button:focus-visible { background: #14263f; }
a:focus-visible { outline: 3px solid #8a4b00; outline-offset: 2px; }Notes on decisions:
- The skip link lets keyboard users bypass the navigation. It is hidden off-screen but becomes visible on focus. Do not use
display: nonefor it, because it would then never receive focus. Itstabindex="-1"target can receive focus when the link is activated. max-width: 100%; height: autokeeps the image inside narrow screens.- The visible focus outline is retained and made clearer. Removing it with
outline: noneand no replacement is an accessibility failure. #1f3a5fon white has a contrast ratio well above 4.5:1, the WCAG AA minimum for normal text. White text on the button gives a similar ratio.- Apply
aria-current="page"only to the link for the actual current page. This is Maya's profile, so the parent Tutors index is not marked current. tabindex="-1"lets the skip-link destination receive focus without adding it to the normal Tab sequence. Supply the referenced image and linked pages when building this original practice project.
Predicted behavior. At 360px width the nav items sit in a row, the image shrinks to the container, and the button remains easy to tap. Pressing Tab first reaches the skip link, then "Home", "Tutors", and "Book a class", each with a visible outline.
6. Common pitfalls and study tips
- Using headings for styling or skipping levels.
- Using
<br>for spacing between paragraphs (use margin). - Placing non-phrasing elements such as
<div>or<section>inside<p>, which causes parser repair. HTML content rules depend on element categories, not their CSS display value. - Missing
altor writing "image of…". - Removing focus outlines.
- Relying on color alone to convey meaning (for example, red text for errors without an icon or text).
- Setting fixed pixel heights that clip text when users enlarge fonts.
- Selectors that are too specific (
#page .content ul li a) and hard to override. Prefer a single class.
Study tip. When asked which element to use, choose the one whose meaning matches the content. "Which is best for a set of related links?" is <nav> containing a list. "Which text is read by screen readers when an image fails?" is alt.
Study tip. To compute total box width, add content, padding, border (both sides), and margin where relevant, and note the box-sizing value first.
Mini case study: the clickable span
Situation. A usability test shows that a keyboard-only participant cannot reach the "Book a class" control on a draft profile. The control is <span class="button" onclick="book()">Book a class</span>. Also, the photo has alt="image" and the page has two <h1> elements.
Task. List the problems and propose fixes.
Solution.
- Span with click handler. A span is not in the tab order and exposes no role. If booking navigates to a new page, use
<a href="/book?tutor=maya">; if it triggers an action in the page, use<button type="button">. Both are focusable and activated by keyboard natively. alt="image". It conveys nothing. Replace it with a purposeful description such as "Maya smiling beside a shelf of glazed bowls".- Two
<h1>s. Keep one<h1>for the page topic (the tutor name); demote the other to<h2>, adjusting size with CSS.
Check. Tab reaches the control; a screen reader announces "link, Book a class" (or "button"); the outline lists one <h1> followed by <h2>s.
Key terms
- Element / attribute: building block of HTML / extra information on an element.
- Semantic HTML: elements chosen for meaning.
- Landmark: region (header, nav, main, footer) used for navigation by assistive tech.
- Alt text: text alternative for an image.
- Selector / declaration: what a CSS rule targets / the property-value pair.
- Cascade / specificity: how conflicting rules are resolved.
- Box model: content, padding, border, margin.
- Focus: the element currently receiving keyboard input.
Summary checklist
- My document has doctype,
lang, charset, viewport, and a descriptive title. - I used one
<h1>, no skipped heading levels, and landmark elements. - Links have meaningful text and buttons are real buttons.
- Every image has a purposeful
alt(empty if decorative) and dimensions. - I can predict which of several CSS rules wins and why.
- I can calculate box width with and without
border-box. - Keyboard focus is visible and the page is usable by Tab alone.
Official sources
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 1: Introduction to Front-End Development (opens in a new tab)
- MDN: HTML elements reference (opens in a new tab)
- MDN: Cascade, specificity, and inheritance (opens in a new tab)
- W3C WAI: Images tutorial (opens in a new tab)
- W3C: Non-text contrast (opens in a new tab)
- W3C WAI: Landmark regions (opens in a new tab)
- CSS cascade (opens in a new tab)
- CSS length units (opens in a new tab)
- WCAG 2.2 Quick Reference (opens in a new tab)
