If you have ever opened "view source" on a webpage, you saw it: a nested forest of angle brackets. <div>s wrapping <div>s wrapping a <p> somewhere deep inside. That tag soup is what the browser actually receives. CSS and JavaScript come along later to dress it up and make it move. Without HTML, neither of them has anything to grab onto.
HTML is the part that everyone thinks they know in five minutes and most developers (yes, even seniors) get wrong for years.
That is the gap HTML fills, and the discipline it asks for.
What is HTML, really
Think of HTML as the bones of a webpage. CSS is the clothes. JavaScript is the muscles. Without bones, you cannot stand up. Without good bones, your CSS is constantly compensating, your JS is constantly querying the wrong nodes, and your screen reader users have no idea what is going on.
HTML is declarative and semantic. You do not write "draw a heading 32 pixels tall in bold". You write "this is a heading", and the browser, the assistive tech, and the search engine all agree on what it means. CSS handles the looks. The meaning stays in the HTML.
Two rules define the whole language:
-
Tags describe meaning. A
<button>is for clicking. A<nav>is navigation. A<p>is a paragraph. Pick the tag that matches the meaning, not the one that "looks right". - Tags nest into a tree. The browser parses your text into a tree of nodes called the DOM. CSS targets the tree. JavaScript walks it. Everything starts here.
That is the whole vibe.
Let's pretend we are building one
We want a simple way to describe documents that link to each other, with text, images, and forms. We will call it HTML (HyperText Markup Language). For the running example, we are building a tiny personal blog page.
Decision 1: Every page has the same skeleton
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mia's Blog</title>
<meta name="description" content="A small blog about cats and code." />
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<!-- the visible page -->
</body>
</html>
A few things every page needs, no exceptions:
-
<!DOCTYPE html>at the very top. Anything else triggers "quirks mode", which you do not want. -
<html lang="en">so screen readers and translators know the language. Pick the right one. -
<meta charset="utf-8">so accents, emojis, non Latin alphabets render correctly. -
<meta name="viewport" ...>so mobile devices do not zoom the page out to 800 pixels wide. Without this line, your page is unusable on phones. -
<title>is the tab title and the search result heading. -
<meta name="description">is the search snippet.
For social previews you want Open Graph tags too:
<meta property="og:title" content="Mia's Blog" />
<meta property="og:description" content="A small blog about cats and code." />
<meta property="og:image" content="https://example.com/cover.png" />
<meta name="twitter:card" content="summary_large_image" />
These are the lines the rest of the world judges your site by before they ever see it.
Decision 2: Use real semantic landmarks for layout
A page should describe itself. There is one tag for each landmark. Use them.
<body>
<header>
<h1>Mia's Blog</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<article>
<header>
<h2>Why my cat steals socks</h2>
<p><time datetime="2026-04-12">April 12, 2026</time></p>
</header>
<p>It started on a Tuesday...</p>
<section>
<h3>The investigation</h3>
<p>...</p>
</section>
<footer>
<p>Tagged: <a href="/tag/cats">cats</a></p>
</footer>
</article>
</main>
<aside>
<h2>About me</h2>
<p>I write about silly little things.</p>
</aside>
<footer>
<p>© 2026 Mia.</p>
</footer>
</body>
The senior level rules:
-
One
<main>per page. It is the unique main content. -
<article>for self contained content (a blog post, a card, a comment). -
<section>for thematic grouping that has a heading. -
<nav>for primary navigation, not every group of links. A footer with three links does not need to be a nav. -
<aside>for related but tangential stuff (sidebar, callouts). -
<header>and<footer>can be used inside<article>and<section>too. They mean "header of this thing".
If you find yourself writing <div class="nav"> or <div class="header">, stop. There is a real tag for that.
Decision 3: Headings build the document outline
Headings are not just big text. They are the table of contents the page exposes to assistive tech, search engines, and reader modes.
<h1>Mia's Blog</h1>
<h2>Why my cat steals socks</h2>
<h3>The investigation</h3>
<h3>The verdict</h3>
<h2>How to bake mochi</h2>
Rules that matter:
-
One
<h1>per page in modern practice. It is the page's title. -
Do not skip levels. No
<h1>straight to<h3>. - Pick by meaning, not size. If you want big text on a paragraph, that is what CSS is for.
A screen reader user can jump heading by heading. If your headings are nonsense, they are lost.
Decision 4: Text content has rich semantics
Most developers reach for <div> and <span> reflexively. The language has many smaller, more meaningful tags:
<p>I really like <strong>mochi cookies</strong>.</p>
<p>The recipe says <em>do not skip the milk</em>.</p>
<p>Press <kbd>Cmd</kbd>+<kbd>K</kbd> to search.</p>
<p>The variable is named <code>userId</code>.</p>
<pre><code>const user = { id: 1 };</code></pre>
<p>Result: <samp>Welcome, Mia!</samp></p>
<p>Highlighted: <mark>this is important</mark>.</p>
<p>Posted on <time datetime="2026-04-12">April 12</time>.</p>
<blockquote cite="https://example.com/source">
<p>The best code is no code at all.</p>
</blockquote>
<p>Old price <s>$20</s> new price $15.</p>
<p>Footnote ref<sup>1</sup>, water H<sub>2</sub>O.</p>
<p>An <abbr title="Application Programming Interface">API</abbr> is...</p>
Each one is a small win for accessibility, search, and reader modes.
A small but important distinction: <strong> and <b> look the same by default, but <strong> means "important". <b> means "stylistically offset, no extra meaning". Use <strong> for the meaning, <b> for the rare cosmetic case. Same story for <em> (emphasis) versus <i> (italic for technical terms or names).
Decision 5: Lists are everywhere
<!-- unordered list, order does not matter -->
<ul>
<li>Mochi</li>
<li>Whiskers</li>
<li>Pepper</li>
</ul>
<!-- ordered list, order matters -->
<ol>
<li>Wash the rice</li>
<li>Boil the water</li>
<li>Stir gently</li>
</ol>
<!-- description list -->
<dl>
<dt>Mochi</dt>
<dd>The cat that steals socks.</dd>
<dt>Whiskers</dt>
<dd>The cat that loves boxes.</dd>
</dl>
If you have a group of related items, use a list. Navigation menus are <ul>s of <li>s with <a>s inside, almost always. Do not lay out lists as a soup of <div>s.
Decision 6: Links and images, the two oldest features
<a href="/about" rel="prev">About me</a>
<a href="https://other-site.com" target="_blank" rel="noopener noreferrer">
External link
</a>
<a href="mailto:hi@example.com">Email</a>
<a href="tel:+15551234567">Call</a>
<a href="#section-2">Jump to section</a>
Rules:
-
target="_blank"always needsrel="noopener noreferrer"for security and performance. Modern browsers do this by default, but be explicit. - Link text should describe the destination. "Click here" is bad for screen reader users who often jump link to link.
Images:
<img
src="/mochi.jpg"
alt="A small white cat sitting on a stack of books"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
Rules every senior breaks at least once:
-
altis mandatory. Decorative images getalt=""(empty). Meaningful images get a real description. There is no "skip the alt". -
Always set
widthandheight(matching the intrinsic ratio). Prevents layout shift while loading. -
loading="lazy"on offscreen images saves bandwidth. -
For different screens, use
srcsetandsizesso phones get small images and laptops get big ones:
<img
src="/mochi-800.jpg"
srcset="/mochi-400.jpg 400w, /mochi-800.jpg 800w, /mochi-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
alt="A small white cat..."
/>
For art direction (different crops, not just sizes) use <picture>:
<picture>
<source media="(max-width: 600px)" srcset="/mochi-portrait.jpg" />
<img src="/mochi-landscape.jpg" alt="A small white cat..." />
</picture>
Decision 7: Forms, the part most devs underestimate
Forms are where accessibility, validation, and user experience all meet. HTML gives you a lot for free if you let it.
<form action="/api/subscribe" method="post">
<fieldset>
<legend>Subscribe to my blog</legend>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
required
autocomplete="email"
placeholder="you@example.com"
/>
<label for="freq">How often?</label>
<select id="freq" name="freq">
<option value="weekly">Weekly</option>
<option value="monthly" selected>Monthly</option>
</select>
<label>
<input type="checkbox" name="news" />
Also send news
</label>
<button type="submit">Subscribe</button>
</fieldset>
</form>
The senior level rules:
-
Every input needs a
<label>linked byforandid, or wrapped around the input. Click on the label, the input focuses. Screen readers read the label. This is non negotiable. -
Use the right
type.email,url,tel,number,date,time,password,search,color. The browser changes the keyboard, validation, and UI accordingly. -
Always set
nameon form fields. Without it, the field is not submitted. -
autocompleteis your friend.email,current-password,new-password,one-time-code,cc-number, etc. Password managers and browsers help users dramatically. -
required,min,max,pattern,minlength,maxlengthgive you free client side validation. Pair it with server side validation, never replace it. -
Use
<button type="button">for buttons that do not submit. Default<button>inside a form submits. -
<fieldset>and<legend>group related inputs and label the group. Especially important for radios and checkboxes.
Decision 8: Tables are for tabular data, nothing else
A short history lesson, because this trips up everyone. In the 1990s and early 2000s, CSS was weak. Web designers wanted side by side columns and grids for their page layouts (a sidebar next to the content, a header row above three feature boxes), and the only HTML element that could line things up in rows and columns was <table>. So people built whole websites out of nested <table> tags. The header was a table. The sidebar was a cell. It was the worst.
Then CSS grew up. We got Flexbox, then CSS Grid. Today you can build any layout you can imagine without a single <table> tag.
So the modern rule has two halves:
Use <table> only for actual data that has rows and columns of meaning. Leaderboards, price comparisons, schedules, financial tables, anything that would naturally print as a spreadsheet. Screen readers have special navigation for tables, search engines index them as data, and CSS gives you table specific styling for free.
For visual layout, use <div> (or <section>, <header>, <main>, etc) plus CSS Grid or Flexbox. No <table>, no <tr>, no <td>. Those elements come with screen reader semantics that lie about your layout being data.
A quick side by side. The wrong way (layout with a table):
<!-- DON'T do this for layout -->
<table>
<tr>
<td>Sidebar</td>
<td>Main content goes here</td>
</tr>
</table>
The right way (layout with semantic elements + CSS Grid):
<div class="page">
<aside>Sidebar</aside>
<main>Main content goes here</main>
</div>
<style>
.page { display: grid; grid-template-columns: 240px 1fr; gap: 1rem; }
</style>
Same look, completely different meaning to the browser, the screen reader, and Google.
Now for an actual data table, the right markup looks like this:
<table>
<caption>Top selling books last month</caption>
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Sold</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">The Little Prince</th>
<td>Saint-Exupery</td>
<td>231</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td></td>
<td>892</td>
</tr>
</tfoot>
</table>
Rules:
-
<caption>describes the table. First child of<table>. -
<thead>,<tbody>,<tfoot>group rows. Helps screen readers and printing. -
scope="col"andscope="row"on<th>link headers to cells.
Decision 9: Modern HTML you should know
A handful of tags and features that came in HTML5 era and are widely supported in 2026:
<!-- a native disclosure widget -->
<details>
<summary>Show ingredients</summary>
<ul><li>Rice flour</li><li>Sugar</li></ul>
</details>
<!-- a native modal dialog -->
<dialog id="confirm">
<p>Are you sure?</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="ok">OK</button>
</form>
</dialog>
<script>document.getElementById("confirm").showModal();</script>
<!-- progress and meter -->
<progress value="32" max="100">32%</progress>
<meter value="0.6">60%</meter>
<!-- multimedia -->
<video src="/intro.mp4" controls poster="/intro.jpg" preload="metadata"></video>
<audio src="/intro.mp3" controls></audio>
<!-- canvas / svg for graphics -->
<canvas width="400" height="200"></canvas>
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M..." />
</svg>
<dialog> in particular replaces the homemade modal pattern that everyone wrote with position: fixed and a role="dialog". The native one handles focus trap, backdrop, and the escape key for you.
Decision 10: Accessibility is a first class citizen
Most accessibility wins do not need ARIA. They come from picking the right tag.
A short senior level checklist:
-
Use real
<button>for buttons, real<a>for links. A<div onclick>cannot be focused with a keyboard, has no role, and screen readers ignore it. ARIA cannot fully fix this. - Every form input has a
<label>. -
Every meaningful image has alt text. Decorative images use
alt="". - Color contrast is sufficient (WCAG AA: 4.5:1 for normal text).
- Focus is visible. Do not delete the outline without giving it a replacement.
- Skip links to main content at the top of the page for keyboard users:
<a href="#main" class="skip-link">Skip to main content</a>
-
ARIA only when you cannot use a real element.
aria-label,aria-describedby,aria-current,aria-expanded,role="status",role="alert"are the most useful.role="button"on a div is the worst smell. - Test with the keyboard alone. If you cannot tab to and use every control, neither can a lot of users.
-
Test with a screen reader at least once. VoiceOver on Mac (
Cmd + F5), NVDA on Windows (free).
Decision 11: A word on <div> and <span>
<div> and <span> are the meaningless tags. <div> is block, <span> is inline. They have no semantics. They exist for the rare case when no real tag fits.
You will still use <div> plenty (a layout wrapper, a card, a flex container). That is fine. The rule is: if a real tag fits, use the real tag first. Reach for <div> when nothing else does.
Decision 12: Performance basics in HTML
Where you put things in the document changes how fast the page loads.
<head>
<!-- preconnect to origins you know you will hit -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<!-- preload critical assets -->
<link rel="preload" href="/hero.jpg" as="image" />
<!-- async / defer scripts -->
<script src="/analytics.js" async></script>
<script src="/app.js" defer></script>
</head>
Rules:
-
deferruns the script after the HTML is parsed, in order. Default for app scripts. -
asyncruns whenever it loads, no order. Default for independent third party scripts. -
No
deferorasyncblocks parsing while it loads and runs. Avoid. -
Lazy load images below the fold (
loading="lazy"). -
Set
widthandheighton images and iframes to prevent layout shift.
A peek under the hood
What really happens when a browser opens your page:
- The browser starts streaming the HTML.
- The HTML parser builds the DOM node by node.
- Whenever it hits a
<link rel="stylesheet">, it kicks off a CSS download. CSS is render blocking. - Whenever it hits a
<script>withoutdefer/async/module, it pauses parsing, downloads, runs. - Once the DOM and CSSOM are ready, the browser builds the render tree, runs layout (figures out where everything goes), then paint (fills in pixels), then composite (puts layers on screen).
- Images, fonts, and async scripts arrive in parallel and trigger smaller updates.
Three implications for senior work:
-
CSS in
<head>is good. CSS at the bottom of<body>flashes unstyled content. -
<script defer>in<head>is the modern default. It downloads in parallel with the parser without blocking it. - A big DOM is slow. 5,000 nodes is fine, 50,000 nodes is sluggish on phones.
Tiny tips that will save you later
-
Use the right tag for the meaning.
<button>, not<div onclick>. - Always set
lang,charset, andviewport. - Always label form inputs.
- Always set
alton images. - One
<h1>, no skipped levels. - Validate your HTML with the W3C validator or your linter.
- Test with the keyboard.
-
Use
<dialog>,<details>,<picture>,<time>when they fit. - Read MDN. It is the only documentation that matters and it is excellent.
- Inspect real sites. A site you admire has lessons in its markup.
Wrapping up
So that is the whole story. We needed a way to describe documents that the browser, search engines, and assistive technologies could all understand. We invented HTML, a tree of meaningful tags. The browser parses it into a DOM, layout and paint follow, and CSS and JavaScript hang their work off this tree.
We learned that the wins come from picking the right tag (<button>, <nav>, <article>, <section>, <main>), labeling forms, writing alt text, structuring headings, and using modern features (<dialog>, <details>, <picture>) instead of building them by hand.
Once that map is in your head, every webpage you read is full of small lessons. HTML stops feeling like a beginner topic you skipped and starts feeling like the foundation that every senior frontend developer keeps coming back to.
Happy marking up, and may your <div> count be smaller than your <section> count.
Top comments (0)