Web Development

Complete HTML Course

Learn HTML from scratch, step by step: beginner, intermediate and advanced lessons on text, links, images, semantic layout, tables, forms, media, accessibility, SEO and modern HTML, with practical code examples and projects.

  • Beginner
  • 15 modules
  • 69 lessons
  • About 5 h 36 min
  • Free, no sign-up

Module 1 of 15

Beginner: Getting Started with HTML

Lesson 1.1 4 min read

What Is HTML? A Simple Guide for Complete Beginners

HTML is the language that gives every web page its structure. In this guide you will learn what HTML is, how it works with CSS and JavaScript, and how a browser reads it.

What is HTML?

HTML stands for HyperText Markup Language. Let's break that name into small pieces.

  • HyperText means text that can link to other text. When you click a link and jump to a new page, that is hypertext.
  • Markup means you "mark" parts of your content to say what they are. You say "this is a heading", "this is a paragraph", "this is an image".
  • Language means there are rules that everyone (and every browser) agrees on.

HTML is not a programming language. It does not do maths or make decisions. It only describes content. Think of it like the labels on boxes when you move house: "kitchen", "books", "fragile". The labels don't change what is inside, but they tell people what each box is.

Here is a tiny piece of HTML:

<h1>My Favourite Recipes</h1>
<p>These are the meals I cook every week.</p>

The browser shows "My Favourite Recipes" as a big heading and the next line as a normal paragraph. The words inside the angle brackets, like <h1> and <p>, are called tags. You will learn all about them later in this module.

HTML vs CSS vs JavaScript: what is the difference?

Almost every website uses three languages together. A house is a good way to picture them.

LanguageJobHouse comparison
HTMLStructure and contentThe skeleton: walls, rooms, doors
CSSLooks and layoutThe paint, wallpaper and furniture
JavaScriptBehaviour and interactionThe electricity: lights, doorbell, alarm

You can build a house with no paint and no electricity. It is plain, but you can still walk inside. In the same way, a page with only HTML works. It is plain, but people can read it. CSS makes it look nice, and JavaScript makes it react when you click or type. This course starts with HTML because everything else is built on top of it.

Note: In this module we only write HTML. Your pages will look simple, and that is fine. CSS handles the looks, and you will learn it later.

How does a browser read HTML?

A browser is the program you use to visit websites, such as Chrome, Firefox, Safari or Edge. When you open a web page, this happens:

  1. The browser asks a computer somewhere on the internet (a server) for a file.
  2. The server sends back an HTML file. It is just plain text.
  3. The browser reads the HTML from top to bottom.
  4. It builds a picture of the page in memory, and then draws it on your screen.

The browser is very forgiving. If you make a small mistake, it usually tries to guess what you meant instead of showing an error. That is friendly, but it also means mistakes can hide. Later you will learn how to check your code.

What is a web page, really?

A web page is a text file that ends in .html. You can open it in a simple text editor and read every word. Here is a complete, very small web page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Sam's Bakery</title>
  </head>
  <body>
    <h1>Sam's Bakery</h1>
    <p>Fresh bread every morning from 7 am.</p>
  </body>
</html>

If you save this as bakery.html and open it in a browser, you will see a large heading "Sam's Bakery" and one line of text below it. The browser tab will also say "Sam's Bakery". Don't worry about every line yet. We will explain each one in the boilerplate lesson.

Images, videos and styles are usually separate files. The HTML file points to them, and the browser loads them too. So a "web page" is really an HTML file plus everything it links to.

Practice exercise

Visit any website you like. Write down three things on the page that you think are headings, two that are paragraphs, and one that is a link. This trains your eyes to see the structure behind every page.

Hint: headings are usually bigger and bolder text. Links often change your mouse pointer into a hand.

Frequently asked questions

Is HTML hard to learn?

No. HTML is one of the easiest computer languages to start with. You can write your first working page in a few minutes, and you can learn the most common tags in a few days.

Is HTML a programming language?

No. HTML is a markup language. It describes content but cannot make decisions or do calculations. For that, websites use JavaScript.

Do I need to install anything to use HTML?

You only need a text editor and a web browser. Both are free. The next lesson shows you how to set them up.

Key takeaways

  • HTML stands for HyperText Markup Language and describes the structure of a web page.
  • HTML is the skeleton, CSS is the paint, and JavaScript is the electricity.
  • A browser downloads an HTML file, reads it from top to bottom and draws the page.
  • A web page is a plain text file ending in .html, plus the files it links to.

Open this lesson on its own page ↗

Module 2 of 15

Beginner: Working with Text

Lesson 2.1 4 min read

HTML Headings and Paragraphs

HTML headings and paragraphs are the most common elements on the web. Learn how to use <h1> to <h6> and <p> correctly so your page is clear for readers, screen readers and search engines.

How do HTML headings work (h1 to h6)?

HTML has six levels of headings, from <h1> (the most important) to <h6> (the least important).

<h1>Heading level 1</h1>
<h2>Heading level 2</h2>
<h3>Heading level 3</h3>
<h4>Heading level 4</h4>
<h5>Heading level 5</h5>
<h6>Heading level 6</h6>

In the browser, each heading is bold, and the text gets smaller from h1 down to h6. But size is not the point. Headings describe the outline of your page, like the chapter and section titles in a book.

  • <h1> is the title of the whole page.
  • <h2> elements are the main sections.
  • <h3> elements are sub-sections inside an h2, and so on.

Why use only one h1 per page?

Your <h1> tells everyone what the page is about. It is like the title on the cover of a book. A book has one title, so a page should normally have one <h1>.

Search engines like Google read your headings to understand the topic of your page. A clear <h1> that matches what people search for helps your page appear in the right results. Screen reader users also often jump straight to the <h1> to find out where they are.

Tip: Make your <h1> describe the page, and usually match it to your <title>. For example, "Easy Banana Bread Recipe" for both.

Keep headings in order for readers and SEO

Don't skip levels when you go down. After an <h2>, the next smaller level should be <h3>, not <h5>. You can go back up to <h2> whenever a new main section starts.

Many screen reader users move around a page by jumping from heading to heading, like using a table of contents. If the levels jump around, the page feels confusing. Search engines also use this outline to understand which ideas belong together.

Here is a realistic blog post with good heading order:

<h1>Easy Banana Bread Recipe</h1>
<p>This banana bread takes 10 minutes to prepare and uses only one bowl.</p>

<h2>Ingredients</h2>
<p>You need three ripe bananas, flour, sugar, butter, eggs and baking soda.</p>

<h2>Method</h2>
<h3>Preparing the mix</h3>
<p>Mash the bananas with a fork, then stir in the melted butter.</p>
<h3>Baking</h3>
<p>Pour the mix into a tin and bake for 60 minutes at 175°C.</p>

<h2>Storing your bread</h2>
<p>Wrap it in foil. It stays fresh for three days.</p>

You will see a big title, then three main sections. The "Method" section has two smaller headings inside it. If you read only the headings, you understand the whole page.

How to write paragraphs in HTML

The <p> element is for a paragraph: a block of text about one idea. Browsers put a little space above and below each paragraph automatically.

<h2>About our café</h2>
<p>Corner Café opened in 2019 in a small street near the station. We serve coffee from local roasters and cakes baked every morning.</p>
<p>On Saturdays we host a book swap. Bring a book you have finished and take a new one home.</p>

You will see a heading and two separate blocks of text with space between them. Note that pressing Enter inside your code does not create a new paragraph in the browser. You need a new <p> element for each paragraph. You will learn more about this in the whitespace lesson.

Common mistake: Using empty paragraphs like <p></p> to add space between things. This adds nothing useful for screen readers. Spacing is a job for CSS.

Why you should not use headings just for big text

Sometimes beginners use an <h3> because they want slightly big, bold text, even when it is not a heading. Please don't. Headings tell browsers, search engines and assistive technology "a new section starts here". If you use them only for looks, you break the page's outline.

Choose the heading level based on meaning (how important the section is), not size. Later, CSS lets you make any text any size you want.

Practice exercise

Write a short page about your favourite city. Use one <h1> with the city's name, then three <h2> sections: "Things to see", "Food to try" and "Getting around". Put at least one paragraph under each. Add an <h3> inside "Food to try" for one special dish.

Hint: read only your headings out loud. They should sound like a table of contents.

Key takeaways

  • Use <h1> to <h6> to build an outline of your page, like chapters in a book.
  • Use one <h1> per page to say what the whole page is about.
  • Don't skip heading levels; good order helps screen reader users and SEO.
  • Use <p> for each paragraph, and choose headings for meaning, never just for size.

Open this lesson on its own page ↗

Module 3 of 15

Beginner: Links and Images

Lesson 3.1 4 min read

How to Create Links in HTML

HTML links connect your pages to each other and to the rest of the web. Learn how to create links with the <a> tag and href, write good link text, and link to sections, email, phones and downloads.

A link is made with the <a> element. The "a" stands for anchor. The href attribute (short for "hypertext reference") says where the link goes. The text between the tags is what people click.

<p>Read the full guide on <a href="https://developer.mozilla.org/en-US/docs/Web/HTML">MDN Web Docs</a>.</p>
<p>See our <a href="menu.html">full menu</a> or go back to the <a href="index.html">home page</a>.</p>

In the browser, "MDN Web Docs", "full menu" and "home page" appear underlined and usually blue. Clicking the first one opens the MDN website. The other two open files called menu.html and index.html in the same folder as your page. You will learn more about these paths in the next lesson.

Link text is the clickable words. Good link text tells people where the link goes before they click.

  • Bad: "To see our menu, click here."
  • Good: "See our full lunch menu."

Why does this matter? Many screen reader users ask for a list of all the links on a page. A list that says "click here, click here, read more" is useless. Search engines also use link text to understand what the target page is about.

Tip: Read your link text on its own, without the sentence around it. If it still makes sense, it is good link text.

The target="_blank" attribute opens the link in a new browser tab.

<p>Find us on <a href="https://maps.example.com/corner-cafe" target="_blank" rel="noopener">the map (opens in a new tab)</a>.</p>

Clicking the link opens the map in a new tab, and your page stays open in the old one. The rel="noopener" attribute is a safety setting. It stops the new page from controlling your page through JavaScript. Modern browsers now do this automatically for target="_blank", but adding it is still a good, clear habit.

Use new tabs only when there is a good reason, such as keeping a form open while someone reads help. Opening new tabs without warning can confuse people, so it is kind to say "(opens in a new tab)".

You can link to a specific part of a page. First, give the target element an id attribute. An id is a unique name, used only once on the page. Then link to it with # plus the id.

<nav>
  <a href="#menu">Menu</a>
  <a href="#opening-hours">Opening hours</a>
  <a href="#contact">Contact</a>
</nav>

<h2 id="menu">Menu</h2>
<p>Coffee, tea, cakes and sandwiches.</p>

<h2 id="opening-hours">Opening hours</h2>
<p>Every day from 8 am to 6 pm.</p>

<h2 id="contact">Contact</h2>
<p>Call us or send an email.</p>

Clicking "Opening hours" jumps the page down to that heading. (If the page is short, you may not see much movement. Add more text to try it.) You can also link to a section on another page, like href="about.html#team".

Common mistake: Using spaces in an id, like id="opening hours". Use dashes instead: id="opening-hours". Also, ids are case-sensitive, so #Menu and #menu are different.

The href does not have to be a web address.

<p>Email us: <a href="mailto:hello@cornercafe.example">hello@cornercafe.example</a></p>
<p>Call us: <a href="tel:+441179460000">0117 946 0000</a></p>
<p><a href="files/menu-2026.pdf" download>Download our menu (PDF, 200 KB)</a></p>
  • mailto: opens the visitor's email program with your address already filled in.
  • tel: starts a phone call on mobile phones. Write the number with the country code and no spaces.
  • The download attribute tells the browser to save the file instead of opening it. It works for files on your own website.

Note: Telling people the file type and size, like "PDF, 200 KB", helps them decide whether to click, especially on mobile data.

Practice exercise

Make a small "Contact us" page. Add a list of three section links at the top, three sections with matching id values, an email link, a phone link and one link to an external website that opens in a new tab.

Hint: each href="#something" needs an element with id="something".

Frequently asked questions

Add target="_blank" to the <a> element, and also add rel="noopener" for safety. Tell users it opens a new tab.

The most common reasons are a typo in the file name, a wrong folder path, a missing https:// on an external link, or a missing closing quote in the href. Check the path carefully.

Yes. Put the <img> element inside the <a> element. You will see an example in the images lesson.

Key takeaways

  • Links use the <a> element, and href sets the destination.
  • Write link text that makes sense on its own; avoid "click here".
  • Use target="_blank" with rel="noopener" only when a new tab really helps.
  • Link to a section with href="#id" and a matching id on the target.
  • mailto:, tel: and download create email, phone and download links.

Open this lesson on its own page ↗

Module 4 of 15

Beginner: Publishing Your Website

Lesson 4.1 7 min read

How to Publish Your Website for Free: GitHub Pages and Netlify

Learn how to publish your website for free with GitHub Pages or Netlify, so anyone can visit it. We prepare your files, put them online step by step, and test the site on a phone.

What are web hosting and a domain name?

Right now your website lives only on your computer. To let other people see it, you need web hosting: a computer that is always on and connected to the internet, which sends your files to anyone who asks for them. That computer is called a server. Hosting is like renting a shop: your files are the goods, and the server keeps the door open day and night.

A domain name is the address people type to find your site, like example.com. It is like the street address of your shop. Free hosts give you a free address that includes their name, for example sam.github.io or sam-portfolio.netlify.app. You can buy your own domain later if you want one.

For a site made only of HTML, CSS, images and maybe a little JavaScript (called a static site), hosting can be completely free. Two popular, beginner-friendly options are GitHub Pages and Netlify. Both give you free HTTPS, which means the padlock icon in the address bar.

How to prepare your files before you publish

A few small checks now will save you from broken pages later.

  1. Name your home page index.html. Servers look for this file first. When someone visits your address, index.html is the page they see.
  2. Use lowercase file names with no spaces. Write about-me.html and team-photo.jpg, not About Me.html. Web servers treat Photo.jpg and photo.jpg as different files.
  3. Use relative paths for your own files. Write images/logo.png, never C:\Users\Sam\Desktop\logo.png. A path to your own hard drive does not exist on the server.
  4. Put everything in one folder. Your HTML, CSS and images all go inside one project folder, with index.html at the top level (not hidden inside a sub-folder).

A tidy project folder looks like this:

my-website/
├── index.html
├── about.html
├── 404.html
├── css/
│   └── style.css
└── images/
    ├── logo.png
    └── team-photo.jpg

And here is an index.html that is ready to publish:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sam Lee - Web Design Student</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <img src="images/logo.png" alt="Sam Lee logo" width="120" height="120">
  <h1>Hi, I'm Sam</h1>
  <p>I'm learning HTML and building my first website.</p>
  <a href="about.html">Read more about me</a>
</body>
</html>

Every path is relative and lowercase, so the logo, the stylesheet and the "About" link work on your computer and on the server. The viewport meta tag makes the page fit small phone screens.

Both GitHub Pages and Netlify also look for a file called 404.html. If someone visits a page that doesn't exist, they see this friendly page instead of a plain error:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Page not found - Sam Lee</title>
</head>
<body>
  <h1>Sorry, this page does not exist</h1>
  <p>The link may be old, or the address may have a typo.</p>
  <a href="/">Go to the home page</a>
</body>
</html>

The link uses /, which means "the root of the site", so it works from any address on your site.

Common mistake: Everything works on your computer but images are broken online. Usually a file is called Logo.PNG while your HTML says logo.png. Windows and macOS ignore the difference; web servers do not.

How to publish with GitHub Pages, step by step

GitHub is a website for storing code. GitHub Pages turns a GitHub project into a live website for free.

  1. Go to github.com and create a free account. Your username becomes part of your web address.
  2. Click the + button at the top right and choose New repository. A repository (or "repo") is a project folder stored on GitHub.
  3. Give it a name like my-website, make it Public, and click Create repository.
  4. On the new page, click uploading an existing file (or Add file, then Upload files). Drag in everything inside your project folder, including the css and images folders. Then click Commit changes to save.
  5. Open the repo's Settings tab and click Pages in the left menu.
  6. Under "Build and deployment", set the source to Deploy from a branch, choose the main branch and the / (root) folder, then click Save.
  7. Wait one or two minutes and refresh. GitHub shows your address, for example https://sam-lee.github.io/my-website/.

To update your site later, upload the changed files to the same repo. The live site updates by itself after a minute or so.

Tip: If you name the repository exactly your-username.github.io, your site lives at https://your-username.github.io/ with no folder name at the end. Each account can have one site like this.

How to publish with Netlify drag and drop

Netlify is even faster if you don't want to learn GitHub yet.

  1. Go to netlify.com and sign up for a free account. You can sign up with your email or your GitHub account.
  2. Find the area that says you can drag and drop your project folder to deploy.
  3. Drag your whole my-website folder onto it. Netlify uploads it and gives you a live address in a few seconds, like https://shiny-cupcake-12ab34.netlify.app.
  4. In the site settings, change the site name to something nicer, such as sam-lee-portfolio. Your address becomes https://sam-lee-portfolio.netlify.app.

To update the site, open the site's Deploys page and drag the folder in again. Netlify keeps old versions, so you can go back if something breaks.

Using your own custom domain

If you want an address like samlee.com, buy it from a domain registrar (a company that sells domain names); most cost about 10 to 20 US dollars a year. Then open the custom domain section in GitHub Pages settings or in Netlify's domain settings and type your domain. The host shows you some DNS records, which are like signposts that tell the internet where your domain points. Copy them into your registrar's DNS settings. It can take from a few minutes to a day to start working, and both hosts then add free HTTPS for you.

How to check that your website works on a phone

Most visitors will use a phone, so always test there.

  • Open your live address on your phone, or send the link to yourself in a message.
  • Check that the text is readable without zooming and that nothing is wider than the screen.
  • Tap every link and check that every image loads.
  • Visit a made-up page, like /test-page, to see your 404 page.
  • On your computer, open DevTools (F12) and turn on the device toolbar to try many screen sizes quickly.

Note: After you upload changes, your browser may still show the old version from its memory (the cache). Do a hard refresh with Ctrl + Shift + R (Cmd + Shift + R on a Mac) to load the newest files.

Practice exercise

Publish your personal profile page from the beginner project. Rename the main file to index.html if needed, make all file names lowercase, and add a simple 404.html. Publish it with GitHub Pages or Netlify, then open the live link on your phone and test every link and image.

Hint: if an image is broken online, compare the file name in your HTML with the real file name, letter by letter, including capital letters and the file extension.

Frequently asked questions

Is GitHub Pages really free?

Yes. GitHub Pages is free for public repositories and is meant for personal, project and portfolio sites. It does not run server code like PHP or databases, only static files.

Should I choose GitHub Pages or Netlify?

Both are great. Netlify drag and drop is the quickest start. GitHub Pages is a good choice if you also want to learn GitHub, which most developers use every day.

Why does my site show a 404 error after publishing?

Usually there is no index.html at the top level of what you uploaded. Make sure index.html is not inside an extra folder, and that its name is all lowercase.

Key takeaways

  • Hosting stores your files on a server; a domain name is the address people type.
  • Before publishing, use index.html, lowercase names without spaces, and relative paths.
  • GitHub Pages: upload files to a public repo, then turn on Pages in Settings.
  • Netlify: drag and drop your folder to get a live site in seconds.
  • Always test the live site on a real phone and do a hard refresh after updates.

Open this lesson on its own page ↗

Module 5 of 15

Intermediate: Page Structure and Semantic HTML

Lesson 5.1 5 min read

The HTML Head Section: Meta Tags, Title and Links

The HTML head section holds information about your page: the title, meta tags like charset, viewport and description, and links to stylesheets, icons and scripts.

What goes in the HTML head?

Every HTML page has two big parts. The <body> holds what people see. The <head> holds information about the page. Think of a book: the body is the story, and the head is the cover, the back-cover summary and the barcode. Readers don't read the barcode, but the shop needs it.

Nothing inside <head> appears on the page itself. Instead, the browser, search engines and social media sites read it. Here are the most common things you will put there:

  • <meta charset="utf-8"> tells the browser which character set the file uses.
  • <meta name="viewport"> makes the page work well on phones.
  • <title> sets the text in the browser tab and in search results.
  • <meta name="description"> gives a short summary for search results.
  • <link> connects other files, like a CSS stylesheet or a favicon.
  • <script> loads JavaScript (you will usually add defer).

Charset, viewport and title: the three must-haves

A character set (charset) is the list of letters and symbols a computer can show. UTF-8 covers almost every language and emoji, so always use it. Put it first in the head, because the browser needs to know it before it reads any other text.

The viewport is the visible area of the screen. Without the viewport meta tag, phones pretend to be a wide desktop screen and shrink your page until the text is tiny. With width=device-width, initial-scale=1, the page uses the real width of the phone.

The <title> is short text that names the page. It shows in the browser tab, in bookmarks and as the blue link in Google. Make each page title unique and clear, for example "Contact Us - Green Leaf Cafe". Aim for about 50–60 characters.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Fresh Coffee and Cakes - Green Leaf Cafe</title>
</head>
<body>
  <h1>Welcome to Green Leaf Cafe</h1>
</body>
</html>

Open this file in a browser. You will see only the heading on the page, but look at the browser tab: it shows the title. On a phone, the text appears at a normal size because of the viewport tag.

Common mistake: Forgetting the viewport meta tag. Your page may look fine on a laptop but tiny and hard to read on a phone. Add it to every page.

How to write a good meta description

The meta description is a summary of the page in one or two sentences. Search engines often show it under the title in search results. It does not directly raise your ranking, but a clear description makes people more likely to click.

  • Keep it under about 155 characters so it is not cut off.
  • Describe what the reader will get from this page.
  • Write a different description for every page.

Linking a stylesheet, a favicon and scripts

The <link> element connects your page to other files. It has no closing tag. The rel attribute says what kind of file it is, and href says where it is.

A favicon is the small icon in the browser tab, like a logo stamp. Modern browsers accept SVG and PNG icons. The apple-touch-icon is used when someone saves your site to an iPhone home screen.

For JavaScript, use the <script> element. If you put it in the head, add the defer attribute. defer means "download this now, but run it after the page HTML is ready". Without it, the browser stops building the page until the script has loaded. The other common option is to put the script at the very end of the body.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Fresh Coffee and Cakes - Green Leaf Cafe</title>
  <meta name="description" content="Green Leaf Cafe serves fresh coffee, homemade cakes and light lunches in the city centre. See our menu and opening hours.">

  <link rel="stylesheet" href="css/styles.css">
  <link rel="icon" href="images/favicon.svg" type="image/svg+xml">
  <link rel="icon" href="images/favicon-32.png" sizes="32x32" type="image/png">
  <link rel="apple-touch-icon" href="images/apple-touch-icon.png">

  <script src="js/menu.js" defer></script>
</head>
<body>
  <h1>Welcome to Green Leaf Cafe</h1>
  <p>Coffee, cake and good company.</p>
</body>
</html>

This is a complete, professional head. The browser loads styles.css to style the page, shows your icon in the tab, and runs menu.js after the HTML is ready. If the files don't exist yet, the page still works; the browser just skips them.

Tip: Keep the order simple: charset first, then viewport, title, description, stylesheets, icons, and scripts last. It is easy to read and works well. You can learn more on MDN: The head element.

Practice exercise

Create a page called about.html for a small bookshop. Give it a full head section with charset, viewport, a unique title, a meta description under 155 characters, a link to css/style.css, and a favicon link. Add one <h1> in the body.

Hint: Start from the second example above and change the text. Check the browser tab to make sure your title appears.

Key takeaways

  • The head holds information about the page; nothing in it shows on the page itself.
  • Always include <meta charset="utf-8"> first and the viewport meta tag.
  • Write a unique <title> and meta description for every page.
  • Use <link> for stylesheets and favicons, and <script defer> for JavaScript in the head.

Open this lesson on its own page ↗

Module 6 of 15

Intermediate: HTML Tables

Lesson 6.1 4 min read

How to Create a Table in HTML

To create a table in HTML, you use table, tr, th and td, plus caption, thead, tbody, tfoot and scope to make the data clear and accessible.

When should you use an HTML table?

A table shows tabular data: information that has rows and columns, like a spreadsheet. Think of a train timetable, a price list or a sports league. If it makes sense to read the data both across and down, a table is the right tool.

Tables are for data only, not for page layout. You will learn more about that in the next lesson.

The basic table elements: table, tr, th and td

An HTML table is built from a few simple elements:

  • <table> wraps the whole table.
  • <tr> is a table row.
  • <th> is a table header cell, a title for a row or column. Browsers show it in bold and centred by default.
  • <td> is a table data cell, a normal cell with data.

You build the table row by row. Each <tr> holds its cells from left to right.

<table>
  <tr>
    <th>Plan</th>
    <th>Price per month</th>
    <th>Storage</th>
  </tr>
  <tr>
    <td>Basic</td>
    <td>£4</td>
    <td>50 GB</td>
  </tr>
  <tr>
    <td>Pro</td>
    <td>£9</td>
    <td>500 GB</td>
  </tr>
</table>

You will see a small grid with three columns. The first row is bold because it uses <th>. There are no borders yet; you add those with CSS. To see the grid while you practise, you can add a quick CSS rule later like border: 1px solid on cells.

Common mistake: Putting text directly inside <tr> or <table>. Text must always go inside a <td> or <th> cell, or the browser will move it outside the table.

Adding a caption, thead, tbody and tfoot

A <caption> is the title of the table. It must be the first thing inside <table>. It helps everyone, especially screen reader users, understand what the table is about before they read it.

You can also split the table into three groups of rows:

  • <thead> holds the header row or rows.
  • <tbody> holds the main data rows.
  • <tfoot> holds summary rows, like totals.

These groups make the code easier to read, help screen readers, and give you easy hooks for CSS. When a long table is printed, browsers can repeat the <thead> on each page.

Using scope to connect headers to cells

The scope attribute on a <th> says which cells it is the header for:

  • scope="col" means "I am the header for this column".
  • scope="row" means "I am the header for this row".

With scope, a screen reader can read a cell as "Tuesday, 10:00, Yoga" instead of just "Yoga". It is a small addition that makes a big difference.

Here is a full, real-world class timetable for a gym:

<table>
  <caption>Bright Fitness weekly class timetable</caption>
  <thead>
    <tr>
      <th scope="col">Time</th>
      <th scope="col">Monday</th>
      <th scope="col">Tuesday</th>
      <th scope="col">Wednesday</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">08:00</th>
      <td>Spinning</td>
      <td>Yoga</td>
      <td>Spinning</td>
    </tr>
    <tr>
      <th scope="row">12:30</th>
      <td>Pilates</td>
      <td>Strength</td>
      <td>Pilates</td>
    </tr>
    <tr>
      <th scope="row">18:00</th>
      <td>Boxing</td>
      <td>Spinning</td>
      <td>Yoga</td>
    </tr>
  </tbody>
</table>

The caption appears above the table. The days across the top are column headers, and the times down the left side are row headers. Both are bold. Every class cell is now linked to a time and a day.

And here is a price table with a total in the footer:

<table>
  <caption>Your order</caption>
  <thead>
    <tr>
      <th scope="col">Item</th>
      <th scope="col">Quantity</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Coffee beans (500 g)</th>
      <td>2</td>
      <td>£16.00</td>
    </tr>
    <tr>
      <th scope="row">Ceramic mug</th>
      <td>1</td>
      <td>£12.00</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td></td>
      <td>£28.00</td>
    </tr>
  </tfoot>
</table>

This shows an order summary with the total row at the bottom. The empty <td> keeps the columns lined up. In the next lesson you will learn how to merge cells so you don't need empty ones.

Tip: Always add a <caption> and use <th> with scope for headers. It takes ten extra seconds and makes your table accessible. See MDN: The table element for more details.

Practice exercise

Create a table that shows the opening hours of a library for each day of the week. Use a <caption>, a <thead> with "Day", "Opens" and "Closes", and a <tbody> with seven rows. Make each day name a row header.

Hint: Day names go in <th scope="row">, and the column titles go in <th scope="col">.

Key takeaways

  • Use tables only for data with rows and columns.
  • Build tables with <table>, <tr>, <th> and <td>.
  • Add a <caption> and group rows with <thead>, <tbody> and <tfoot>.
  • Use scope="col" and scope="row" on header cells so screen readers can connect data to headers.

Open this lesson on its own page ↗

Module 7 of 15

Intermediate: HTML Forms

Lesson 7.1 5 min read

HTML Forms Basics: form, label, input and button

HTML forms let users send information to a website. Learn the form, label, input and button elements, GET vs POST, the name attribute, and build a complete contact form.

What is an HTML form?

A form is the part of a web page where users type or choose information and send it. Login boxes, search bars, sign-up pages and checkout pages are all forms. A paper form at the doctor's office is a good comparison: it has labelled boxes, you fill them in, and you hand it in at the desk.

In HTML, a form is made of a few key elements:

  • <form> wraps all the fields and says where to send the data.
  • <label> is the text that describes a field.
  • <input> is a field the user types into or clicks.
  • <button> sends the form.

The form element: action and method

The <form> element has two important attributes.

action is the URL where the data is sent, usually a page or script on your server that handles it. HTML alone cannot save or email data. You need a server-side program (written in PHP, Node.js, Python and so on) or a form service to receive it.

method says how the data is sent:

  • method="get" puts the data in the URL, like search?q=shoes. Use it for searches and filters, where it is useful to bookmark or share the result. Never use it for passwords or private data.
  • method="post" sends the data hidden inside the request, not in the URL. Use it for anything that creates or changes something: contact forms, sign-ups, logins, orders.

If you leave out method, the browser uses GET.

<form action="/search" method="get">
  <label for="search-box">Search products</label>
  <input type="search" id="search-box" name="q">
  <button type="submit">Search</button>
</form>

You will see a label, a text box and a "Search" button. If you type "mugs" and press the button, the browser goes to /search?q=mugs. The q comes from the field's name.

Labels, ids and the name attribute

Every field needs a label. To connect them, give the input an id and give the label a matching for attribute. Now clicking the label puts the cursor in the field, and screen readers read the label when the field gets focus.

The name attribute is different. It is the key that is sent to the server with the value, like a label on a box in the post. Without a name, the field's value is not sent at all.

  • id connects the field to its <label> (and to CSS or JavaScript). It must be unique on the page.
  • name is the field's name in the data sent to the server.

They often have the same value, and that's fine.

Common mistake: Forgetting the name attribute. The form looks perfect, but the server receives nothing from that field. Check every input has a name.

Button types: submit, reset and button

The <button> element has a type attribute:

  • type="submit" sends the form. This is the default inside a form.
  • type="reset" clears every field back to its starting value. It is rarely helpful and easy to click by accident, so most sites don't use it.
  • type="button" does nothing by itself. It is used with JavaScript.

Always write the type so your intent is clear. You may also see <input type="submit">, which works, but <button> is more flexible because it can contain other HTML, like an icon.

A complete HTML contact form example

Here is a real contact form you can copy and use.

<form action="/contact" method="post">
  <p>
    <label for="name">Your name</label><br>
    <input type="text" id="name" name="name" autocomplete="name">
  </p>

  <p>
    <label for="email">Email address</label><br>
    <input type="email" id="email" name="email" autocomplete="email">
  </p>

  <p>
    <label for="subject">Subject</label><br>
    <input type="text" id="subject" name="subject">
  </p>

  <p>
    <label for="message">Message</label><br>
    <textarea id="message" name="message" rows="6"></textarea>
  </p>

  <p>
    <button type="submit">Send message</button>
  </p>
</form>

You will see four labelled fields stacked on top of each other and a "Send message" button. When the user clicks the button, the browser sends name, email, subject and message to /contact using POST. The <textarea> is a multi-line text box; you will learn more about it soon. The <br> tags only put each label above its field until you add CSS.

Tip: You can also wrap the input inside the label: <label>Your name <input type="text" name="name"></label>. This connects them without for and id. Both ways are correct; the for/id way gives you more freedom for layout.

Practice exercise

Build a newsletter sign-up form that uses POST and sends data to /subscribe. It should have a "First name" field, an "Email" field and a "Subscribe" button. Every field needs a label connected with for and id, and a name.

Hint: Click each label in the browser. If the cursor jumps into the right field, your labels are connected correctly.

Frequently asked questions

Can HTML send an email by itself?

No. HTML only collects the data and sends it to the action URL. A program on the server, or an online form service, must receive the data and send the email.

What is the difference between id and name?

id is for the page: it connects a label and must be unique. name is for the server: it is the key sent with the field's value.

Should I use GET or POST for a login form?

Always POST. With GET, the password would appear in the URL, the browser history and server logs.

Key takeaways

  • The <form> element's action says where data goes, and method says how (GET or POST).
  • Use GET for searches and POST for anything private or that changes data.
  • Connect each <label> to its field with for and id.
  • Every field needs a name, or its value is not sent.
  • Write type="submit" or type="button" on every button.

Open this lesson on its own page ↗

Module 8 of 15

Intermediate: Media and Embedded Content

Lesson 8.1 4 min read

Adding Video and Audio to a Web Page

To add video and audio to a web page, use the HTML video and audio elements with controls, multiple sources, captions, a poster image and smart preload settings.

How to add a video in HTML

The <video> element plays a video file that you host yourself. The most important attribute is controls: it shows the play button, the timeline and the volume. Without it, users have no way to play or pause the video.

<video src="videos/latte-art.mp4" controls width="640" height="360" poster="images/latte-art-poster.jpg">
  Your browser cannot play this video. <a href="videos/latte-art.mp4">Download the video</a> instead.
</video>

You will see a video player with a picture on it. The poster is the image shown before the video starts, like a cover on a DVD box. The width and height reserve space so the page doesn't jump while the video loads. The text inside the element appears only in very old browsers that can't play video.

Offering multiple sources

Not every browser supports every video format. Instead of src on the video, you can list several <source> elements. The browser goes down the list and plays the first one it can. Put the smaller, more modern format first.

  • MP4 (H.264 video) plays in every modern browser. It is the safe choice.
  • WebM (VP9 or AV1 video) files are often smaller, and support is now very good.

The type attribute tells the browser the format, so it doesn't download files it can't play.

Adding captions with the track element

Captions are the words spoken in the video, shown as text on the screen. They help deaf and hard-of-hearing users, people watching without sound on a train, and people learning a language. They are written in a simple text file format called WebVTT (with the .vtt extension).

Add them with the <track> element inside <video>:

  • kind="captions" (or kind="subtitles" for translations).
  • src is the .vtt file.
  • srclang is the language code, like en or es.
  • label is the name shown in the captions menu.
  • default turns the track on at the start.
<video controls width="640" height="360" poster="images/latte-art-poster.jpg" preload="metadata">
  <source src="videos/latte-art.webm" type="video/webm">
  <source src="videos/latte-art.mp4" type="video/mp4">
  <track kind="captions" src="captions/latte-art-en.vtt" srclang="en" label="English" default>
  <track kind="subtitles" src="captions/latte-art-es.vtt" srclang="es" label="Español">
  <p>Your browser cannot play this video. <a href="videos/latte-art.mp4">Download the MP4</a>.</p>
</video>

The browser picks the WebM file if it can, otherwise the MP4. English captions show by default, and users can switch to Spanish subtitles in the player menu. A WebVTT file looks like this:

WEBVTT

00:00:01.000 --> 00:00:04.000
First, pour the milk slowly into the centre.

00:00:04.500 --> 00:00:08.000
Then move the jug from side to side to make the pattern.

Note: Captions files usually need to come from the same website as your page. If you open your HTML file directly from your computer (with a file:// address), some browsers won't load them. Use a local web server to test.

Autoplay, muted and preload rules

Browsers block videos that start with sound by themselves, because surprise noise annoys users. A video can only autoplay if it is muted. On iPhones, add playsinline so the video plays inside the page instead of jumping to full screen. This is common for short background clips:

<video autoplay muted loop playsinline width="1280" height="720" poster="images/beach-poster.jpg">
  <source src="videos/beach-waves.mp4" type="video/mp4">
</video>

This video starts by itself, with no sound, and loops forever. Use autoplay rarely. Moving content can distract people, and it uses mobile data. If a video autoplays, keep controls or another way to pause it.

The preload attribute tells the browser how much to download before the user presses play:

  • preload="none": download nothing. Best for pages with many videos.
  • preload="metadata": download only the length and size. A good default.
  • preload="auto": the browser may download the whole file.

How to add audio in HTML

The <audio> element works just like <video>, but with no picture. Use it for podcasts, music or sound clips.

<figure>
  <figcaption>Episode 12: Starting your first vegetable garden (24 minutes)</figcaption>
  <audio controls preload="none">
    <source src="audio/episode-12.mp3" type="audio/mpeg">
    <source src="audio/episode-12.ogg" type="audio/ogg">
    <a href="audio/episode-12.mp3">Download episode 12 (MP3)</a>
  </audio>
  <p><a href="transcripts/episode-12.html">Read the transcript</a></p>
</figure>

You will see a podcast title and a small audio player with play, timeline and volume. MP3 works everywhere. Nothing downloads until the user presses play, because of preload="none".

Tip: For audio-only content, give users a written transcript (all the words as text). It helps deaf users and is also great for SEO, because search engines can read the text.

Practice exercise

Add a short product video to a page. Give it controls, a poster image, two <source> elements (WebM and MP4), English captions with default, and preload="metadata". Then add an <audio> player for a short welcome message below it.

Hint: Write your own .vtt file with two or three lines of captions, following the WebVTT example above, and test with a local server.

Key takeaways

  • Use <video> and <audio> with controls so users can play and pause.
  • List several <source> elements with type; MP4 is the safe fallback.
  • Add captions with <track kind="captions"> and a WebVTT file.
  • Autoplay only works when the video is muted; use it rarely.
  • Choose preload carefully to save users' data.

Open this lesson on its own page ↗

Module 9 of 15

Intermediate: More HTML Elements

Lesson 9.1 4 min read

Semantic Text Elements: abbr, cite, time, address, dfn and data

Semantic text elements like <abbr>, <cite>, <time>, <address>, <dfn> and <data> give extra meaning to words in your HTML, which helps readers, search engines and screen readers.

Why use semantic text elements?

You already know <strong> and <em> from the formatting lesson. They tell the browser why a word is special, not just how it should look. The elements in this lesson do the same for other kinds of text: short forms, titles of works, dates, contact details, definitions and values.

Think of them as labels on jars in a kitchen. Two jars may look the same, but the labels tell you which one is salt and which one is sugar. These elements label your words, so that browsers, search engines, translation tools and screen readers know what each piece of text really is.

abbr and dfn: abbreviations and definitions

An abbreviation is a short form of a longer word or phrase, like "HTML" or "NASA". Wrap it in <abbr> and put the full meaning in the title attribute.

A definition is the place where you explain a term for the first time. Wrap the term being defined in <dfn>. Use it only once per term, at the point where you explain it.

<article>
  <h2>How web pages travel</h2>
  <p>
    Every page is sent using <abbr title="HyperText Transfer Protocol Secure">HTTPS</abbr>.
  </p>
  <p>
    A <dfn>web server</dfn> is a computer that stores websites and sends pages to visitors when they ask for them.
  </p>
  <p>
    Your browser talks to the web server many times while a page loads.
  </p>
</article>

In the browser, "HTTPS" may get a dotted underline, and hovering over it shows the full name. "web server" appears in italics in its defining sentence. Later uses of "web server" are normal text, because they are not definitions.

Tip: The title tooltip does not appear on phones or for keyboard users. For important abbreviations, also write the full form in the text the first time: "HyperText Markup Language (<abbr>HTML</abbr>)". The <abbr> tag is still useful for tools and future readers.

cite, time and data: titles, dates and values

<cite> marks the title of a creative work: a book, film, song, article, game or painting. It is not for a person's name.

<time> marks a date or time. The visible text can be friendly ("next Saturday"), while the datetime attribute gives a clear, machine-readable version, like 2026-11-14 or 2026-11-14T19:30. Calendars, search engines and browser tools can read it.

<data> connects any visible text to a machine-readable value, for example a product code or an exact number.

<section>
  <h2>Book club: November meeting</h2>
  <p>
    This month we are reading <cite>The Hobbit</cite> by J.R.R. Tolkien.
  </p>
  <p>
    We meet on <time datetime="2026-11-14T19:30">Saturday 14 November at 7:30 pm</time>.
  </p>
  <p>
    Tickets: <data value="5.00">five pounds</data>, product
    <data value="BC-1114">November ticket</data>.
  </p>
</section>

The page looks like normal text, with the book title in italics. Behind the scenes, a script or search engine can read 2026-11-14T19:30 as an exact date and time, and 5.00 as an exact price. That is how semantic elements help computers understand your page without changing what people read.

address: contact information

<address> is for contact details for the person or organisation behind the page or an article: an email, phone number, social link or postal address. It is not for any random address in your text, like "the party is at 12 Oak Road".

<footer>
  <address>
    Green Leaf Cafe<br>
    12 Market Street, Leeds<br>
    <a href="mailto:hello@greenleaf.example">hello@greenleaf.example</a><br>
    <a href="tel:+441130000000">0113 000 0000</a>
  </address>
</footer>

Browsers show it in italics by default. Screen readers and search engines can recognise it as the contact block for the site.

del, ins and s: showing changes

Sometimes you want to show that text has changed.

  • <del> marks text that was removed. Browsers show it with a line through it.
  • <ins> marks text that was added. Browsers underline it.
  • <s> marks text that is no longer correct or relevant, but was not "deleted" as an edit. A classic use is an old price.

Both <del> and <ins> can have a datetime attribute to say when the change happened.

<ul>
  <li>Workshop starts at <del datetime="2026-10-01">6 pm</del> <ins datetime="2026-10-01">6:30 pm</ins></li>
  <li>Price: <s>£40</s> now £30</li>
</ul>

You see "6 pm" crossed out with "6:30 pm" underlined after it, and the old price crossed out next to the new price.

Note: Many screen readers do not say "deleted" or "inserted" by default. If the change matters, add words too, for example "Price: was <s>£40</s>, now £30".

Practice exercise

Write a short blog post about a film you watched. Use <cite> for the film title, <time> with a datetime for when you watched it, <abbr> for one abbreviation (like "UK" or "TV"), and <dfn> to define one word, such as "sequel". Finish with an <address> block with your email link.

Hint: the datetime value must use the format year-month-day, like 2026-10-01.

Key takeaways

  • Use <abbr title="..."> for abbreviations and <dfn> where a term is first defined.
  • <cite> is for titles of works, not names of people.
  • <time datetime="..."> and <data value="..."> add machine-readable values to friendly text.
  • <address> is only for contact details for the page or article author.
  • <del> and <ins> show edits; <s> shows text that is no longer correct.

Open this lesson on its own page ↗

Module 10 of 15

Advanced: Accessibility in HTML

Lesson 10.1 5 min read

Web Accessibility Basics: Writing HTML for Everyone

Web accessibility means building pages that everyone can use, including people who are blind, deaf, or who cannot use a mouse. In this lesson you will learn the HTML habits that make a page accessible from the start.

Who benefits from accessible HTML?

Accessibility (often shortened to a11y, because there are 11 letters between the "a" and the "y") is about removing barriers. Think of a building with a ramp next to the stairs. The ramp helps wheelchair users, but it also helps parents with buggies and people carrying heavy boxes. Accessible websites work the same way.

People who benefit include:

  • People who are blind and use a screen reader, a program that reads the page out loud or sends it to a braille display. Common screen readers are NVDA and JAWS on Windows, VoiceOver on Mac and iPhone, and TalkBack on Android.
  • People who cannot use a mouse and move around the page with the keyboard only.
  • People with low vision who zoom the page to 200% or more.
  • People who are deaf and need captions for videos.
  • People with dyslexia or memory problems who need clear, simple pages.
  • Everyone else: someone using a phone in bright sunlight, or someone with a broken arm.

A screen reader does not "see" your design. It reads your HTML. That is why good HTML is the most important accessibility tool you have.

Use semantic elements first

You already know semantic elements like <header>, <nav>, <main>, <button> and <label>. For accessibility, they are gold. A screen reader user can jump straight to the <main> area, list all the links in <nav>, or press a key to move from heading to heading. A <div> gives them none of this.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Green Leaf Café – Menu</title>
</head>
<body>
  <header>
    <p>Green Leaf Café</p>
    <nav aria-label="Main">
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/menu">Menu</a></li>
        <li><a href="/contact">Contact us</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <h1>Our menu</h1>
    <h2>Breakfast</h2>
    <p>Served every day from 8 to 11 am.</p>
    <h2>Lunch</h2>
    <h3>Soups</h3>
    <p>Tomato and basil, or lentil and carrot.</p>
  </main>
  <footer>
    <p>Open Monday to Saturday.</p>
  </footer>
</body>
</html>

In the browser this looks like a plain page. But a screen reader now knows the language is English, where the navigation is, where the main content starts, and how the headings are nested. The heading outline goes h1 → h2 → h3 with no skipped levels, like a table of contents.

Tip: Always set lang on the <html> element. Without it, a screen reader may read English text with a French or German accent, which is very hard to understand. If one paragraph is in another language, add lang to it too: <p lang="es">Hola</p>.

You learned the alt attribute in the media lessons. Here is the accessibility rule in one sentence: write what the image tells the reader, not what it looks like in detail. A chart image needs its main message ("Sales doubled between 2024 and 2026"). A purely decorative image needs an empty alt="" so screen readers skip it.

Link text matters just as much. Screen reader users often pull up a list of all links on a page. A list of ten "Click here" links is useless.

<!-- Bad: the link makes no sense on its own -->
<p>To see our prices, <a href="/pricing">click here</a>.</p>

<!-- Good: the link says where it goes -->
<p>See our <a href="/pricing">pricing plans</a>.</p>

<!-- Good: an image that is also a link -->
<a href="/">
  <img src="logo.svg" alt="Green Leaf Café home page">
</a>

<!-- Good: a decorative image -->
<img src="leaf-divider.svg" alt="">

The first link only says "click here". The second link reads well even when it is alone. The logo link uses alt to describe where the link goes, because the image is the only content of the link.

Colours are set with CSS, not HTML, but keep one rule in mind now: text needs enough contrast with its background. Light grey text on a white background is hard to read for many people. You will learn to check contrast in the CSS course.

Common mistake: Using colour alone to show meaning, like "required fields are red". Some people cannot see red. Add text too, such as the word "(required)".

What is WCAG?

WCAG (Web Content Accessibility Guidelines) is the international rulebook for accessibility, written by the W3C. The current version is WCAG 2.2. It has three levels: A (the minimum), AA (the normal target for most websites and the level many laws require) and AAA (the highest). Its ideas are grouped into four words: content must be perceivable, operable, understandable and robust. You don't need to memorise it. If you use semantic HTML, good alt text, clear links, headings in order and labels on every form field, you already meet many of its rules. You can read a friendly summary on MDN's accessibility guide.

Practice exercise

Take any page you built earlier in this course and check it: Does <html> have lang? Is there exactly one <h1> and are headings in order? Does every image have a sensible alt? Does every link make sense on its own? Fix anything that fails.

Hint: Try reading only your link texts and headings out loud, top to bottom. If it still makes sense, you are on the right track.

Frequently asked questions

Do I need to install a screen reader to test my page?

It helps a lot. VoiceOver is already built into Mac and iPhone, Narrator is built into Windows, and NVDA is free. Even five minutes of listening to your page will teach you more than any checklist.

Is accessibility required by law?

In many places, yes. The European Accessibility Act applies to many websites and apps in the EU since June 2025, and the US, UK and other countries have similar rules. Most of them point to WCAG level AA.

Does accessible HTML look different?

No. Accessible HTML usually looks exactly the same. The differences are in the structure that assistive technology reads.

Key takeaways

  • Accessibility helps everyone, not only people with disabilities.
  • Semantic elements give screen readers structure for free, so use them first.
  • Set lang, keep one <h1> and nest headings in order.
  • Write alt text that gives the image's message, and link text that makes sense alone.
  • WCAG 2.2 level AA is the usual target, and good HTML gets you much of the way.

Open this lesson on its own page ↗

Module 11 of 15

Advanced: SEO and Metadata

Lesson 11.1 5 min read

SEO-Friendly HTML: How to Help Google Understand Your Page

SEO-friendly HTML helps search engines like Google understand what your page is about. In this lesson you will learn how to write good titles, meta descriptions, headings, links and other HTML that improves SEO.

What is SEO and how does HTML help?

SEO (Search Engine Optimization) means making your pages easy for search engines to find, understand and show to the right people. Google uses a program called a crawler (or bot) that visits pages and reads their HTML. It cannot look at your page like a human. It reads the code. Clear, semantic HTML is like a well-labelled shop: the crawler quickly finds what is on each shelf.

Great content is still the most important part of SEO. HTML cannot make a bad page rank well. But bad HTML can hide a great page.

How to write a good title and meta description

The <title> is the blue clickable line in Google results, and the text in the browser tab. The meta description is the short grey text under it. Google does not always use your description, but a good one often gets used and makes people click.

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Easy Banana Bread Recipe (One Bowl, 10 Minutes Prep) | Bake Club</title>
  <meta name="description" content="Make soft, moist banana bread with one bowl and 7 simple ingredients. Ready in an hour, with tips for nuts, chocolate chips and vegan swaps.">
  <link rel="canonical" href="https://bakeclub.example/recipes/banana-bread">
</head>

This page shows up in Google with a clear title that matches what people type ("easy banana bread recipe") and a description that explains why it is worth clicking.

Rules for titles and descriptions:

  • Every page needs its own unique title and description. Never copy the same one to all pages.
  • Put the most important words first. Keep titles to about 50–60 characters, and descriptions to about 150–160, or Google may cut them.
  • Write for humans, not robots. No lists of keywords.

Google uses your page structure to understand which ideas are most important.

  • Use one <h1> per page that says what the page is about, usually similar to the title.
  • Use <h2> for main sections and <h3> for sub-sections, in order.
  • Use semantic tags like <main>, <article>, <nav> and <footer>, so the crawler knows which part is the real content.
  • Write descriptive link text. "Read our banana bread recipe" tells Google what the linked page is about. "Click here" tells it nothing.
  • Give images helpful alt text. Google Images uses it, and it helps your page too.
<main>
  <article>
    <h1>Easy Banana Bread Recipe</h1>
    <p>This one-bowl banana bread is soft, sweet and perfect for using old bananas.</p>
    <img src="/images/banana-bread-sliced.webp" alt="Sliced banana bread on a wooden board" width="1200" height="800">
    <h2>Ingredients</h2>
    <ul>
      <li>3 ripe bananas</li>
      <li>80 g melted butter</li>
    </ul>
    <h2>Method</h2>
    <p>Mash the bananas, then stir in the butter.</p>
    <h2>Variations</h2>
    <p>Want more? Try our <a href="/recipes/chocolate-banana-muffins">chocolate banana muffins</a>.</p>
  </article>
</main>

The crawler sees one clear main topic in the <h1>, three sections, a described image and a link whose text explains the next page. Nice clean URLs like /recipes/banana-bread also help. They are easier to read than /page.php?id=8812&cat=3.

Canonical, robots meta and hreflang

Sometimes the same page can be reached at several URLs, for example with tracking codes like ?utm_source=newsletter. The canonical link, <link rel="canonical" href="...">, tells Google which URL is the "real" one. Use the full address, including https://.

The robots meta tag tells search engines what they may do with a page:

<!-- Do not show this page in search results, and don't follow its links -->
<meta name="robots" content="noindex, nofollow">

<!-- Website in English and Spanish -->
<html lang="en">
<head>
  <link rel="alternate" hreflang="en" href="https://bakeclub.example/recipes/banana-bread">
  <link rel="alternate" hreflang="es" href="https://bakeclub.example/es/recetas/pan-de-platano">
  <link rel="alternate" hreflang="x-default" href="https://bakeclub.example/recipes/banana-bread">
</head>

Use noindex for pages like "thank you for signing up" or internal search results, which don't help searchers. You don't need a robots tag on normal pages, because "index, follow" is the default. The hreflang links tell Google which language version to show to which users. x-default is the fallback for everyone else. The lang attribute on <html> still states the language of the current page.

Common mistake: Leaving <meta name="robots" content="noindex"> on a live site after copying it from a test version. Google will remove the whole site from search. Always check this before launch.

Practice exercise

Write the <head> and main content for a page called "How to Care for a Snake Plant". Include a unique title, a meta description, a canonical link, one <h1>, three <h2> sections, an image with alt text and one descriptive link.

Hint: Imagine what someone would type into Google to find this page, and make sure those words appear naturally in the title, description and <h1>.

Frequently asked questions

Does the meta keywords tag help SEO?

No. Google has ignored <meta name="keywords"> for many years. You can leave it out.

Can I use more than one h1?

HTML allows it, and Google says it won't hurt you. But one clear <h1> per page is simpler for both users and screen readers, so it is still the best habit.

How long until Google shows my changes?

It can take from a few days to a few weeks. You can ask Google to re-crawl a page in Google Search Console, a free tool for site owners.

Key takeaways

  • SEO starts with good content, and clear HTML helps Google understand it.
  • Write a unique, specific <title> and meta description for every page.
  • Use one <h1>, headings in order, semantic tags, descriptive links and image alt text.
  • Use a canonical link to pick the main URL and noindex to hide pages from search.
  • Use lang and hreflang for sites in more than one language.

Open this lesson on its own page ↗

Module 12 of 15

Advanced: Modern HTML Features and Performance

Lesson 12.1 5 min read

Responsive Images with srcset, sizes and picture

Responsive images let the browser download the right image size for each screen. In this lesson you will learn srcset, sizes and the <picture> element for modern formats, art direction and faster loading.

Why do we need responsive images?

A large photo for a big desktop screen might be 2400 pixels wide and 500 KB. A phone with a small screen only needs about 800 pixels. If the phone downloads the big file, the page is slower and uses more mobile data for no reason. Responsive images let you offer several sizes of the same image, and the browser picks the best one. It's like a coffee shop offering small, medium and large cups instead of filling one huge cup for everyone.

srcset with w descriptors and sizes

The srcset attribute lists the image files and their real widths. The number with a w is the width of the file in pixels (a width descriptor). The sizes attribute tells the browser how wide the image will be shown on the page.

<img
  src="/images/mountain-800.jpg"
  srcset="/images/mountain-400.jpg 400w,
          /images/mountain-800.jpg 800w,
          /images/mountain-1600.jpg 1600w,
          /images/mountain-2400.jpg 2400w"
  sizes="(min-width: 1200px) 1100px, (min-width: 700px) 90vw, 100vw"
  alt="Snowy mountain peak at sunrise"
  width="2400" height="1600">

Here is what happens. The sizes value says: "On screens at least 1200px wide, the image is 1100px wide. On screens at least 700px wide, it is 90% of the screen width (90vw). Otherwise it fills the whole width (100vw)." The browser checks the screen, works out the size it needs, also considers the screen's pixel density, and downloads only one matching file. A phone may get the 800 version, and a large high-resolution laptop the 2400 version. The src is a fallback for very old browsers.

Tip: Always add width and height with the image's real size (or at least its correct ratio). The browser then reserves the right space before the image loads, so the text does not jump down. This jumping is called layout shift.

For a small image with a fixed size, like a 200px logo, use x descriptors instead: srcset="logo.png 1x, logo@2x.png 2x". The 2x file is for high-resolution screens.

The picture element for AVIF and WebP

AVIF and WebP are modern image formats. They are often 30–50% smaller than JPEG at the same quality. In 2026 all major browsers support both, but <picture> lets you offer the best format with a safe fallback.

<picture>
  <source type="image/avif" srcset="/images/team-800.avif 800w, /images/team-1600.avif 1600w" sizes="(min-width: 900px) 50vw, 100vw">
  <source type="image/webp" srcset="/images/team-800.webp 800w, /images/team-1600.webp 1600w" sizes="(min-width: 900px) 50vw, 100vw">
  <img src="/images/team-800.jpg" srcset="/images/team-800.jpg 800w, /images/team-1600.jpg 1600w" sizes="(min-width: 900px) 50vw, 100vw" alt="Our team of five smiling in the office kitchen" width="1600" height="1067">
</picture>

The browser reads the <source> elements from top to bottom and uses the first type it supports. Most browsers pick AVIF. Any browser that does not support it moves on to WebP, and finally to the JPEG in <img>. The <img> is required: it is the element that is actually shown, and it holds the alt, width and height.

Art direction: a different image on small screens

Sometimes you don't just want a smaller image, you want a different one. A wide landscape photo of a cyclist on a road becomes tiny on a phone. A tighter, taller crop works better. Choosing different crops for different screens is called art direction. You do it with the media attribute on <source>.

<picture>
  <source media="(min-width: 800px)" srcset="/images/cyclist-wide.webp" width="1600" height="700">
  <img src="/images/cyclist-tall.webp" alt="Cyclist riding up a mountain road" width="800" height="1000" fetchpriority="high">
</picture>

On screens at least 800px wide, the browser shows the wide photo. On smaller screens, it shows the tall crop from the <img>. Each <source> can have its own width and height because the shapes are different.

Note: Use media only for art direction. If it is the same picture at different sizes, use srcset and sizes, and let the browser choose.

Speed up the hero image with fetchpriority

The hero image is the big image at the top of a page. It is usually the largest thing visible when the page opens, so it decides how fast the page feels. Adding fetchpriority="high" to its <img> tells the browser "download this one first", as you can see in the example above. Use it on one or two images at most. If everything is high priority, nothing is.

Common mistake: Adding loading="lazy" to the hero image. Lazy loading delays the image, which makes the most important picture appear later. You'll learn when to use lazy loading in the next lesson.

Practice exercise

Take one large photo and create three sizes (for example 480, 960 and 1920 pixels wide) with a free tool like Squoosh. Save each as AVIF, WebP and JPEG. Write a <picture> element that offers all of them with a correct sizes value for an image that fills the full width on phones and half the width on screens wider than 1000px.

Hint: sizes="(min-width: 1000px) 50vw, 100vw". Open your browser's developer tools, Network tab, and resize the window to see which file loads.

Key takeaways

  • Responsive images save data and make pages faster on small screens.
  • srcset with w descriptors lists the files, and sizes says how wide the image is shown.
  • <picture> with type sources offers AVIF and WebP with a JPEG fallback.
  • <source media="..."> is for art direction: different crops for different screens.
  • Add width and height to every image, and fetchpriority="high" to the hero image.

Open this lesson on its own page ↗

Module 13 of 15

Advanced: Web Components and Web App Features

Lesson 13.1 5 min read

Web Components Explained: Custom Elements, Shadow DOM and slot

Web components let you create your own reusable HTML elements, like <info-card>. Learn custom elements, template and slot, and shadow DOM in plain words, with short examples.

What are web components?

Imagine you build the same "profile card" on ten pages. Each time you copy the same <div> structure, the same classes and the same CSS. Web components let you package that card into your own HTML tag, such as <profile-card>, and then use it like any built-in element.

Web components are built into every modern browser. You don't need a framework like React or Vue. They are made of three parts that work together:

  • Custom elements: your own tag names, with JavaScript that says how they behave.
  • The template element: HTML that is stored but not shown until you use it. You met <template> in the data attributes lesson.
  • Shadow DOM: a private area inside your element, with its own HTML and CSS that the rest of the page cannot accidentally change.

How to make a custom element with customElements.define

Here is the smallest useful custom element. It reads a name attribute and shows a greeting.

<greeting-badge name="Amira"></greeting-badge>
<greeting-badge></greeting-badge>

<script>
  class GreetingBadge extends HTMLElement {
    connectedCallback() {
      const name = this.getAttribute("name") ?? "guest";
      this.textContent = `Hello, ${name}!`;
    }
  }

  customElements.define("greeting-badge", GreetingBadge);
  console.log(customElements.get("greeting-badge") === GreetingBadge);
  // Output: true
</script>

The page shows "Hello, Amira!" and then "Hello, guest!". Line by line:

  1. class GreetingBadge extends HTMLElement creates a new kind of element. extends HTMLElement means "start with everything a normal HTML element can do".
  2. connectedCallback() is a special method. The browser runs it each time the element is added to the page.
  3. this is the element itself. this.getAttribute("name") reads its name attribute. ?? "guest" means "if there is no name, use guest".
  4. this.textContent = ... puts the greeting text inside the element, using a template literal.
  5. customElements.define("greeting-badge", GreetingBadge) registers the tag name and connects it to the class. After this line, every <greeting-badge> on the page comes to life.

Common mistake: Using a name without a dash, like <greeting>. Custom element names must contain a hyphen, such as greeting-badge. The dash guarantees your tag will never clash with a future built-in HTML element. They also need a closing tag; <greeting-badge /> does not work.

Template, slot and shadow DOM in plain words

The DOM is the browser's live family tree of every element on your page. Shadow DOM is a small, separate tree attached to one element. Think of a custom element as a house: the shadow DOM is the inside, with its own furniture and paint. CSS from outside does not leak in, and the CSS inside does not leak out. A rule like h2 { color: red; } on your page will not change the <h2> inside the component.

A slot is a gap in the component where you can put your own content, like a picture frame. The frame (the component) stays the same; you choose the picture. A named slot like <slot name="title"> is filled by a child with slot="title". An unnamed slot takes all the other content. Text inside the <slot> tag is the default, shown if you don't fill it.

<template id="info-card-template">
  <style>
    .card { border: 2px solid #6a5acd; border-radius: 12px; padding: 1rem; max-width: 300px; }
    h2 { margin: 0 0 0.5rem; color: #6a5acd; font-size: 1.25rem; }
  </style>
  <div class="card">
    <h2><slot name="title">Untitled</slot></h2>
    <slot>No description yet.</slot>
  </div>
</template>

<info-card>
  <span slot="title">HTML Basics</span>
  <p>Learn tags, elements and attributes in 30 minutes.</p>
</info-card>

<info-card>
  <span slot="title">CSS Flexbox</span>
</info-card>

<script>
  class InfoCard extends HTMLElement {
    constructor() {
      super();
      const template = document.querySelector("#info-card-template");
      const shadow = this.attachShadow({ mode: "open" });
      shadow.append(template.content.cloneNode(true));
    }
  }

  customElements.define("info-card", InfoCard);
</script>

You see two purple-bordered cards. The first shows "HTML Basics" as its heading and your paragraph below. The second shows "CSS Flexbox" and the default text "No description yet." Here is what the script does:

  1. super() must come first in a constructor; it sets up the normal element.
  2. document.querySelector(...) finds the template.
  3. this.attachShadow({ mode: "open" }) creates the private shadow DOM. open means your own scripts can still look inside if needed.
  4. template.content.cloneNode(true) makes a full copy of the template's HTML, and shadow.append(...) puts it inside the shadow DOM.

Note: Your slotted content (the <span> and <p>) stays in the normal page, so search engines and screen readers read it as usual. There is also a way to create shadow DOM with HTML only, using <template shadowrootmode="open"> inside the element, which works in all modern browsers.

When should you use web components?

Web components are a good choice when:

  • You want a reusable widget (a rating stars element, a share button, a card) that works on any site, with or without a framework.
  • Many teams or projects need the same piece of UI.
  • You want the component's CSS protected from the rest of the page.

They are not always needed. For a small site, plain HTML with a CSS class is simpler. And remember: content inside a custom element depends on JavaScript. Keep important text in the light DOM (inside the tags, through slots) and use real buttons, links and labels inside the component, so it stays accessible.

Practice exercise

Build a <user-avatar> custom element. It reads name and src attributes and shows an <img> with the name as alt text, followed by the name in a <strong>. Use it three times with different people.

Hint: in connectedCallback, build the HTML with a template literal and set this.innerHTML. Only do this with your own trusted values, never with text typed by visitors.

Key takeaways

  • Web components are built into browsers and let you create reusable custom HTML elements.
  • Register an element with customElements.define("name-with-dash", ClassName).
  • connectedCallback runs when the element is added to the page.
  • Shadow DOM keeps the component's HTML and CSS private; <slot> lets users insert their own content.
  • Use them for reusable widgets; plain HTML and CSS are fine for simple pages.

Open this lesson on its own page ↗

Module 14 of 15

Advanced: HTML Web APIs

Lesson 14.1 5 min read

HTML Web APIs: An Introduction

HTML Web APIs are tools built into the browser that let your page find a location, store data, run background tasks and more, using a little JavaScript.

So far you have used HTML to describe content. Browsers can do much more than show content, though. In this module you will meet the most useful browser APIs. You don't need to know JavaScript well: every script is short and explained line by line, and the JavaScript course teaches the language in depth.

What is a Web API?

API stands for Application Programming Interface. That sounds complicated, but the idea is simple: an API is a set of ready-made commands that one program offers to another.

Think of a restaurant menu. You don't go into the kitchen and cook. You choose from the menu, the waiter takes your order, and the kitchen does the hard work. A Web API is the browser's menu. Your page asks "please give me the user's location" or "please save this note", and the browser does the complex work for you.

Web APIs are built into every modern browser. You don't install anything. Some examples you may already know: document.querySelector() finds an element (part of the DOM API), and fetch() loads data from a server (the Fetch API).

How HTML and JavaScript work together with APIs

HTML gives the page its structure: buttons, text, lists. JavaScript listens for what the user does and calls an API. Then it puts the result back into the HTML. Here is a tiny but complete example that uses the Clipboard API to copy a discount code:

<p>Your discount code: <code id="code">SPRING25</code></p>
<button type="button" id="copy-btn">Copy code</button>
<p id="status" role="status"></p>

<script>
  const button = document.querySelector("#copy-btn");
  const status = document.querySelector("#status");

  button.addEventListener("click", async () => {
    const code = document.querySelector("#code").textContent;
    await navigator.clipboard.writeText(code);
    status.textContent = "Copied! Paste it at checkout.";
  });
</script>

Line by line:

  • document.querySelector("#copy-btn") finds the button by its id, using the DOM API.
  • addEventListener("click", ...) runs the code inside when the user clicks.
  • .textContent reads the text of the <code> element.
  • navigator.clipboard.writeText(code) asks the browser to copy the text. navigator is a built-in object that holds many APIs. await waits until copying is finished.
  • The last line shows a message in the page. role="status" makes screen readers announce it.

Click the button, then paste anywhere: you will see SPRING25.

Feature detection: check before you use an API

Not every browser supports every API, and older devices may miss newer ones. Before using an API, check that it exists. This is called feature detection.

<p id="support"></p>

<script>
  const message = document.querySelector("#support");

  if ("geolocation" in navigator) {
    message.textContent = "Good news: your browser can share your location.";
  } else {
    message.textContent = "Sorry, location is not available in this browser.";
  }
</script>

"geolocation" in navigator asks "does the navigator object have something called geolocation?" It gives true or false. Then if ... else shows the right message. This way the page never crashes; it simply offers a different path.

Tip: Don't check the browser's name ("is this Chrome?"). Browsers change all the time. Checking for the feature itself is always more reliable.

Permissions and secure contexts (HTTPS)

Some APIs can reach private things: the user's location, camera, microphone or clipboard. The browser protects the user in two ways.

1. Permissions. The first time a page asks for something private, the browser shows a pop-up like "example.com wants to know your location. Allow / Block". The user decides. If they block it, your code gets an error and must handle it politely. Ask only when the user clicks a button that needs it, never as soon as the page loads.

2. Secure contexts. Powerful APIs only work in a secure context: a page loaded over HTTPS. HTTPS encrypts the connection so nobody in the middle can spy on or change the page. For development, http://localhost and http://127.0.0.1 also count as secure, so your XAMPP or VS Code Live Server pages work. You can check it with one line:

console.log(window.isSecureContext); // Output: true (on https:// or localhost)

Some browsers also offer the Permissions API so you can see in advance if the user already said yes or no:

const result = await navigator.permissions.query({ name: "geolocation" });
console.log(result.state); // Output: "granted", "denied" or "prompt"

"prompt" means the browser will ask the user the next time you request it.

Common mistake: Opening your page by double-clicking the file, so the address starts with file://. Many APIs (workers, some storage and location features) don't work or behave strangely there. Always test with a local server like XAMPP (http://localhost/...) or Live Server.

APIWhat it doesNeeds HTTPS?Asks permission?
GeolocationGets the user's locationYesYes
Drag and DropLets users drag elements and filesNoNo
Web StorageSaves small data in the browser (localStorage)NoNo
Web WorkersRuns heavy scripts in the backgroundNoNo
Server-Sent EventsReceives live updates from the serverNoNo
ClipboardCopies and pastes textYesSometimes (for reading)
FetchLoads data from a serverNoNo
NotificationsShows system notificationsYesYes
Media DevicesUses the camera and microphoneYesYes

The first five are the lessons of this module. You can browse the full list on MDN: Web APIs.

Practice exercise

Make a page with three paragraphs that report whether the browser supports geolocation (in navigator), localStorage (in window) and Worker (in window), and whether the page is a secure context. Open it once with file:// and once through http://localhost and compare.

Hint: Copy the feature detection example three times and change the name you check. For the secure context, show window.isSecureContext in a fourth paragraph.

Key takeaways

  • A Web API is a ready-made set of commands the browser offers to your page.
  • HTML gives the structure; a little JavaScript calls the API and puts the result into the page.
  • Use feature detection ("geolocation" in navigator) before using an API.
  • Private features ask the user for permission and only work on HTTPS (or localhost).
  • Test API pages through a local server, not by opening the file directly.

Open this lesson on its own page ↗

Module 15 of 15

Advanced: Best Practices and Final Project

Lesson 15.1 5 min read

HTML Best Practices, Validation and Common Mistakes

Good HTML is valid, clean and easy to read. In this lesson you will learn how to validate HTML with the W3C validator, follow a best practices checklist, and fix the 10 most common HTML mistakes.

How to validate your HTML

Browsers are very forgiving. If you forget to close a tag, they guess what you meant and show the page anyway. That sounds nice, but different browsers may guess differently, and screen readers and search engines may get confused. A validator is a tool that checks your HTML against the official rules, like a spell checker for code.

The best known is the W3C Markup Validation Service at validator.w3.org. You can:

  1. Enter the address of a live page ("Validate by URI").
  2. Upload an HTML file ("Validate by File Upload").
  3. Paste your code ("Validate by Direct Input").

The validator lists errors (real rule breaks you should fix) and warnings (suggestions). Each message shows the line number. Fix the first error first, because one mistake, like an unclosed tag, can cause many errors further down.

Tip: Many code editors, like VS Code, can show HTML problems as you type with an extension such as HTMLHint. Combine that with the validator before you publish.

HTML best practices checklist

Use this checklist for every page:

  • Start with <!doctype html> and <html lang="...">.
  • Put <meta charset="utf-8"> first in <head>, then the viewport meta tag.
  • Write a unique <title> and meta description.
  • Use semantic elements: <header>, <nav>, <main>, <article>, <section>, <footer>.
  • One <h1>, then headings in order without skipping levels.
  • Every <img> has alt, width and height.
  • Every form field has a <label>.
  • Use <button> for actions and <a href> for navigation.
  • Keep styling in CSS, not in HTML attributes.
  • Use lowercase for elements and attributes, and double quotes for attribute values.
  • Indent nested elements consistently (2 spaces is common).
  • The page passes the W3C validator with no errors.

File naming matters too. Use lowercase letters, numbers and hyphens only: about-us.html, team-photo-2026.webp. Avoid spaces and capital letters. A space becomes %20 in the URL, and About.html and about.html are different files on most web servers. The home page is usually index.html.

10 common HTML mistakes (and how to fix them)

1. Missing doctype or lang. Without the doctype, browsers use old "quirks mode".

<!-- Wrong -->
<html>
<!-- Right -->
<!doctype html>
<html lang="en">

2. Block elements inside a paragraph. A <p> can only contain inline content, so the browser closes it early.

<!-- Wrong -->
<p>Our offers: <ul><li>Free delivery</li></ul></p>
<!-- Right -->
<p>Our offers:</p>
<ul><li>Free delivery</li></ul>

3. Duplicate id. An id must be unique on the page.

<!-- Wrong -->
<div id="card">Plan A</div>
<div id="card">Plan B</div>
<!-- Right -->
<div class="card" id="plan-a">Plan A</div>
<div class="card" id="plan-b">Plan B</div>

4. Wrong nesting order. Close tags in the reverse order you opened them.

<!-- Wrong -->
<p><strong>Sale ends <em>today</strong></em></p>
<!-- Right -->
<p><strong>Sale ends <em>today</em></strong></p>

5. Missing alt on images.

<!-- Wrong -->
<img src="ceo.jpg">
<!-- Right -->
<img src="ceo.jpg" alt="Sara Kim, founder and CEO" width="400" height="400">

6. Inputs without labels. A placeholder is not a label: it disappears when you type.

<!-- Wrong -->
<input type="email" placeholder="Email">
<!-- Right -->
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email">

7. Using headings for size. Headings show structure, not size. Use CSS for size.

<!-- Wrong: h4 used because it "looks small" -->
<h1>Pricing</h1>
<h4>Monthly plans</h4>
<!-- Right -->
<h1>Pricing</h1>
<h2>Monthly plans</h2>

8. A clickable div instead of a button.

<!-- Wrong -->
<div onclick="subscribe()">Subscribe</div>
<!-- Right -->
<button type="button" onclick="subscribe()">Subscribe</button>

9. Using <br> for spacing. Line breaks are for content like addresses or poems, not layout.

<!-- Wrong -->
<p>Welcome!</p>
<br><br><br>
<p>Our story</p>
<!-- Right: add space with CSS margin instead -->
<p>Welcome!</p>
<p>Our story</p>

10. Deprecated presentational tags. <center>, <font> and <marquee> are obsolete. Use CSS.

<!-- Wrong -->
<center><font color="red">Big sale!</font></center>
<!-- Right -->
<p class="sale-banner">Big sale!</p>

Common mistake: Thinking "it looks fine in my browser, so it's correct". Many of these mistakes look fine visually but break accessibility, SEO or other browsers. Always validate.

Indentation and readable code

Code is read by people much more often than it is written. Indent each nested element one level, put block elements on their own lines, and add blank lines between big sections. Short inline elements can stay on one line.

<article class="post">
  <header>
    <h2>Five tips for better sleep</h2>
    <p>By <a href="/authors/tom">Tom Reed</a> · <time datetime="2026-09-12">12 September 2026</time></p>
  </header>

  <p>Good sleep starts hours before bed.</p>

  <ol>
    <li>Keep the same bedtime every day.</li>
    <li>Avoid screens for the last hour.</li>
  </ol>
</article>

This code shows the page structure at a glance: you can see which elements are inside which. Editors can format code for you with tools like Prettier.

Practice exercise

Copy all ten "Wrong" examples above into one HTML file inside a basic page and run it through the W3C validator using Direct Input. Read each error message, then fix them one by one until the validator shows no errors.

Hint: Some mistakes (like a missing label or a clickable div) are not validator errors. The validator checks rules, not good design, so you still need the checklist.

Key takeaways

  • Validate every page with the W3C validator and fix the first error first.
  • Use a checklist: doctype, lang, charset, title, semantic tags, headings, alt, labels.
  • Name files in lowercase with hyphens, and indent nested code consistently.
  • Most common mistakes are wrong nesting, duplicate ids, missing alt or labels, and fake buttons.
  • "Looks fine" is not the same as "is correct".

Open this lesson on its own page ↗

Code copied to clipboard!