Topic 7.7
HTML Best Practices and Common Mistakes
In one line
Good HTML picks the element that matches the meaning, keeps structure and order logical, avoids deprecated and fake controls, and stays lean for speed; these are the mistakes reviewers spot first.
Think of it like this
A well-organised kitchen. Knives in the knife block, spoons in the spoon drawer, labels on every jar. Anyone, even a guest, can cook there. A kitchen where everything is thrown into identical unlabelled boxes still "contains" all the tools, but nobody can find anything. A page built only from divs is that kitchen.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Div soup
- HTML made almost only of divs and spans, with no meaningful elements.
- Semantic HTML
- Choosing the element that matches the meaning of the content:
navfor navigation,buttonfor actions. - Deprecated element
- An old element the HTML standard no longer allows, like
<center>or<font>. - Void element
- An element that can't have content or an end tag, like
<img>or<br>. - Layout shift
- Content jumping on screen as images or ads load in. Measured as CLS (Cumulative Layout Shift).
- Content Security Policy (CSP)
- A security header that tells the browser which scripts and styles it may run; strict policies block inline ones.
- Link text
- The clickable words of a link. It should say where the link goes, even out of context.
Step by step
01Start from div soup
This page renders, but the accessibility tree has no landmarks, no heading, no list and no buttons. A screen reader user hears a long run of text with "clickable" items they can't reach with Tab.
<div class="header">
<div class="logo">Bake Club</div>
<div class="menu">
<div class="item" onclick="go('/')">Home</div>
<div class="item" onclick="go('/recipes')">Recipes</div>
</div>
</div>
<div class="content">
<div class="big-title">Easy bread</div>
<div class="text">Four ingredients, one bowl.</div>
<div class="btn" onclick="save()">Save recipe</div>
</div>
<div class="footer">© 2026 Bake Club</div>02Name the regions
Swap the outer wrappers for landmark elements. Each becomes a jump point in screen readers' landmark list.
03Use real links, headings and buttons
Navigation items go to pages, so they're links in a list. The title is the page's h1. Saving is an action, so it's a button. No click handlers on divs are left.
<header>
<a href="/" class="logo">Bake Club</a>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/recipes" aria-current="page">Recipes</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Easy bread</h1>
<p>Four ingredients, one bowl.</p>
<button type="button" id="save">Save recipe</button>
</article>
</main>
<footer><p>© 2026 Bake Club</p></footer>04Replace deprecated markup with CSS
Looks belong in CSS. The result is shorter, valid and easy to restyle.
<!-- old -->
<center><font color="red" size="5">Sale!</font></center>
<table border="1" bgcolor="#eee" align="center">…</table>
<!-- new -->
<p class="sale">Sale!</p>
<table class="data">…</table>
<style>
.sale { text-align: center; color: #b91c1c; font-size: 1.5rem; }
.data { margin-inline: auto; border-collapse: collapse; background: #eee; }
.data td, .data th { border: 1px solid #999; }
</style>05Make it fast
Reserve space for images, defer scripts, and load only what's above the fold eagerly.
<head>
<link rel="stylesheet" href="/site.css">
<script src="/app.js" defer></script>
</head>
…
<img src="hero.avif" alt="Fresh loaf on a wooden board." width="1200" height="600" fetchpriority="high">
<img src="step-3.webp" alt="Dough after the first rise." width="600" height="400" loading="lazy">06Review with a checklist
Before merging HTML, check: one clear h1 and no skipped levels; landmarks present; every image has a deliberate alt; every field has a label; links go places, buttons do things; no deprecated elements; validator and axe clean; images sized; scripts deferred.
Try it yourself
- 1
Fix the trap
In the first example, change
<div class="card" />to<div class="card"></div>. Predict the three log lines, then run. (false,(none),0.) - 2
Turn soup into structure
In the third example, change only the
soupsection until both log lines match, using a heading, a button and a list. Notice you can keep the classes for styling. - 3
Replace the old tags
In the second example's HTML, replace
<center>and<font>with a<p class="note">and add.note { text-align: center; color: red; }to the CSS. Same look, valid HTML.
Code & diagrams
The dashed border wraps everything: the browser ignored the slash and the div stayed open until the end of the body. It even swallowed the `<script>` that runs this example. Write `<div class="card"></div>`.
Preview
Console
Nothing logged yet.Expected console output
p inside .card? true
children of .card: P, IMG, P, SCRIPT
img has children? 0`center`, `font` and `marquee` are obsolete but still supported for old pages; `blink` is gone entirely. All four are invalid in modern HTML. Use CSS for looks.
Preview
Console
Nothing logged yet.Expected console output
center -> HTMLElement
font -> HTMLFontElement
marquee -> HTMLMarqueeElement
blink -> HTMLUnknownElement
button -> HTMLButtonElementThe two halves look alike. Only one has structure a screen reader can navigate: "Easy bread, heading level 2", "Save recipe, button", "list, 4 items".
Preview
Console
Nothing logged yet.Expected console output
soup: 0 heading, 0 button, 0 list
semantic: 1 heading, 1 button, 1 listBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a link as a button
<a href="#" onclick="openCart()">Cart</a>
Break #2
Choose headings by size
Use <h4> for a sidebar title because it "looks the right size", right after the page's h1.
Break #3
Lazy-load the hero image
Add loading="lazy" to the large image at the top of the page.
Myth vs fact
Myth
<div /> is a valid way to write an empty div.
Fact
In HTML the slash is ignored on non-void elements, so the div stays open. Only void elements like <img> and <br> are written without an end tag.
Myth
Semantic HTML is only for SEO.
Fact
It also gives keyboard support, screen reader navigation, reader modes, translation tools and cleaner CSS hooks, all for free.
Myth
<b> and <i> are deprecated.
Fact
They're valid HTML with defined meanings (text stylistically offset, alternate voice, like a ship's name or a foreign phrase). Use <strong> and <em> when you mean importance or stress emphasis.
Myth
If it works in my browser, it's fine.
Fact
Test with the keyboard, a screen reader, a phone, zoom, and the validator. "Works" means works for everyone.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Modern browsers treat
target="_blank"on links as ifrel="noopener"were set (Chrome, Firefox and Safari since about 2021), so the new page can't control yours viawindow.opener. Addingrel="noopener"explicitly is still harmless and protects very old browsers. Tell users a link opens a new tab, in the text or an icon with a text alternative. - ▸
Each DOM element costs memory and work during style recalculation and layout; deep nesting makes selector matching and layout slower. Lighthouse flags an "excessive DOM size" above about 1,400 elements on the page, with a deeper warning for depth over 32 and parents with more than 60 children.
- ▸
A strict CSP (
script-src 'self'without'unsafe-inline') blocks inlineonclickattributes and<script>blocks;style-srcwithout'unsafe-inline'blocksstyle="…". Writing HTML without inline handlers from the start makes adopting CSP (a key XSS defence, see Topic 5.5 and the System Design course) painless. - ▸
Custom elements (
<my-card>) are valid when the name contains a hyphen; unknown names without a hyphen becomeHTMLUnknownElement. Custom elements have no semantics by default, so give them real HTML inside or use theElementInternalsAPI to set a role.
Remember this
- 1
Div soup is a page built almost entirely from
<div>and<span>with classes. It renders, but it has no landmarks for screen reader users to jump between, no headings to skim, no buttons for keyboard users, and nothing for search engines to understand. Useheader,nav,main,article,section(with a heading),aside,footer, real headings, lists for lists,buttonfor actions andafor navigation (Phase 2). Usedivonly when no element has the right meaning, usually for styling wrappers. - 2
Links versus buttons: a link (
<a href>) goes somewhere (a new page or a place in this page); a button does something (submits, opens, toggles, deletes). Links respond to Enter, can be opened in new tabs, and appear in screen readers' link lists; buttons respond to Enter and Space.<a href="#" onclick="…">and<a href="javascript:void(0)">are buttons pretending to be links: the page jumps to the top, "open in new tab" opens nonsense, and Space doesn't work. Link text must make sense alone: "Download the 2024 report (PDF, 2 MB)", never "click here". - 3
Structure mistakes: skipping heading levels or choosing headings by font size (pick the level by structure, style it with CSS); several
h1s fighting each other; missinglang,charsetortitle; duplicateids; missing labels andalt(Topics 7.2 and 7.5);<br><br>for spacing (use CSS margin); tables for layout (Topic 3.4); empty elements for decoration (use CSS); text in images. - 4
Deprecated and obsolete elements and attributes still appear in old tutorials and copied code:
<center>,<font>,<big>,<tt>,<strike>,<acronym>,<marquee>,<blink>,<frame>/<frameset>, and presentational attributes likealign,bgcolor,borderon tables andwidthon table cells. Some still render for backwards compatibility, but they're non-conforming (the validator flags them), mix looks into content and have accessible replacements: CSS for looks,<abbr>for<acronym>,<s>or<del>for<strike>. - 5
Self-closing myths: only void elements (
img,br,hr,input,meta,link,source,track,wbr,area,base,col,embed) have no end tag. In HTML, the slash in<div />is ignored: it opens a div that swallows everything after it. JSX and XML allow<div />; HTML does not. Also: attributes are lowercase by convention, quote attribute values, and don't put block elements inside inline ones likeporspan. - 6
Performance is part of quality: give images
widthandheightto prevent layout shift (Topic 5.2), lazy-load below-the-fold images (loading="lazy") but never the main hero image, use modern formats andsrcset, load scripts withdeferortype="module"(Topic 6.5), keep the DOM small (Lighthouse warns at around 1,400 elements; every node costs memory, style and layout time), avoid inline SVG repeated hundreds of times (use<use>or an image), and don't load fonts and libraries you don't use. Inlinestyle="…"andonclick="…"attributes also block a strict Content Security Policy (a security header that blocks unexpected scripts), so keep styles in CSS files and handlers in JavaScript.
Explain it without notes
What is div soup, and why is it a problem beyond accessibility?
When should you use a link and when a button?
Why is <div /> a bug in HTML but fine in React?
Name five HTML habits that improve performance.
Practice
Rewrite this as semantic HTML: <div class="nav"><span onclick="location='/'">Home</span> <span onclick="location='/blog'">Blog</span></div><div class="h">My Blog</div>.
Find and fix four mistakes: <img src="chart.png"><a href="report.pdf">Click here</a><br><br><br><font size="2">Updated 2024</font>.
Trade-offs
- ↔
Strict semantic HTML sometimes needs extra CSS resets (buttons and lists have default styles), but that small CSS cost is far cheaper than rebuilding focus, keyboard and roles in JavaScript.
- ↔
Deleting every legacy element at once on a big old site risks visual regressions; replace them gradually, page template by page template, with visual tests.
Done when you can
I can turn div soup into landmarks, headings, lists, links and buttons.
I choose links for navigation and buttons for actions.
I avoid deprecated elements and presentational attributes.
I know which elements are void and never write
<div />in HTML.I size images, defer scripts and keep the DOM lean.
I can review HTML with a checklist before merging.