HTML and CSS Basics: How the Browser Builds the DOM, Applies the Cascade and Lays Out Your First Page

Key takeaways

HTML is parsed into a DOM tree, CSS is matched against that tree through the cascade, and layout happens on boxes. Understanding those three steps explains most beginner bugs: styles that do not apply, widths that overflow, margins that vanish and pages that look tiny on phones.

Introduction

HTML describes what is on the page (a heading, a navigation list, an article). CSS describes how it should look. JavaScript, which comes later in the series, adds behavior. That split is easy to memorize, but it does not help much when a style refuses to apply or a box ends up 44 pixels wider than you asked for.

What does help is knowing roughly what the browser does with your files:

  1. It parses the HTML text into a tree of objects called the DOM (Document Object Model).
  2. It parses the CSS and, for every element in the DOM, decides which declarations win. This is the cascade.
  3. It turns each element into one or more boxes and computes their sizes and positions (layout), then paints pixels.

Almost every beginner bug lives in one of those three steps. This article builds a small page and explains each step along the way, with the specific traps I see most often.


Tools you need

You need a text editor and a browser. VS Code is a common choice; the Live Server extension reloads the page on save, and Prettier keeps indentation consistent, which matters more than it sounds when you are hunting for an unclosed tag. Any modern browser works, and all of them ship DevTools (F12, or Cmd+Option+I on macOS).

Opening a file by double-clicking it uses a file:// URL. That is fine for this article, but some features (fetching JSON, ES modules, some fonts) are blocked on file:// for security reasons, which is one more reason a local server like Live Server is worth installing early.


The minimal document and why each line exists

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My first page</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Hello!</h1>
  <p>This is my first web page.</p>
</body>
</html>
  • <!DOCTYPE html> is not decoration. Without it, browsers render in quirks mode, which emulates old browser bugs for legacy pages. In quirks mode, for example, percentage heights and some table sizing behave differently. If a layout works on one page and not another, a missing doctype is worth checking first.
  • lang="en" tells screen readers which pronunciation rules to use, and helps browsers choose fonts and hyphenation.
  • <meta charset="UTF-8"> must appear early in the document (the spec requires it within the first 1024 bytes) so the browser decodes the bytes correctly. When it is missing or wrong, characters like é or — turn into garbage such as é.
  • <meta name="viewport"> is covered in section 7; skipping it is the single most common reason a page “looks broken on mobile.”
  • <title> is what appears in the browser tab, in bookmarks, and usually as the headline in search results.

Everything in <head> is metadata; everything in <body> is rendered content.


How the parser builds the DOM (and silently fixes your mistakes)

The HTML parser is extremely forgiving. It never stops with an error; instead it follows detailed recovery rules defined in the HTML standard. That is friendly, but it means the DOM you get can differ from the HTML you wrote, and CSS and JavaScript work on the DOM, not on your source file.

A classic example: a <p> element cannot contain block elements like <div>. If you write

<p class="intro">
  <div>Welcome</div>
</p>

the parser closes the paragraph as soon as it sees <div>. The resulting DOM is effectively:

<p class="intro"></p>
<div>Welcome</div>
<p></p>

The stray </p> creates a second, empty paragraph. Now .intro div { color: red; } matches nothing, because the div is no longer inside .intro. I have lost real time on exactly this: the CSS was correct, the markup looked correct in the editor, and only the Elements panel in DevTools (which shows the DOM, not the source) revealed that the element had been moved.

Two habits catch this class of bug:

  • Inspect the Elements panel rather than trusting “View Source.”
  • Run your HTML through the W3C Markup Validation Service (validator.w3.org). For the snippet above it reports an error along the lines of “No p element in scope but a p end tag seen.” Fix validator errors before debugging CSS.

Semantic elements and accessibility

Visually, <header>, <nav>, <main>, <article>, <section> and <footer> look exactly like <div>: block-level, no styling. The difference is meaning. Assistive technology exposes several of them as landmarks; a screen reader user can press a key to jump straight to <main> or to the navigation instead of listening to every link in the header.

A few rules that pay off immediately:

  • Use one <h1> for the page topic and do not skip levels to get a smaller font (h2 then h4). Headings form an outline that screen reader users navigate by. Change size with CSS instead.
  • Use <button> for actions and <a href> for navigation. A <div onclick> is not focusable with the keyboard and is not announced as a button.
  • Every meaningful <img> needs an alt that says what the image conveys. A purely decorative image should have alt="" so screen readers skip it; leaving alt out entirely often makes them read the file name.
  • Every form field needs a <label> linked by for/id, which also makes the label clickable.

Adding CSS and how the cascade picks a winner

There are three ways to attach CSS:

<!-- 1. Inline: applies to one element -->
<p style="color: red;">Red text</p>

<!-- 2. Internal: a style block in head -->
<style>
  p { color: blue; }
</style>

<!-- 3. External: a separate file, shared across pages -->
<link rel="stylesheet" href="style.css">

External stylesheets are the default for real sites because the browser can cache one file for every page. Inline styles are hard to override (see below) and should be rare.

A CSS rule is a selector plus a block of declarations:

selector {
  property: value;
}

When several rules set the same property on the same element, the cascade decides. Simplified, it compares in this order:

  1. Importance: declarations marked !important beat normal ones.
  2. Specificity of the selector.
  3. Order: if everything else ties, the rule that appears later wins.

Specificity is counted as three numbers (IDs, classes/attributes/pseudo-classes, element types), compared left to right:

SelectorSpecificity
p0-0-1
.note0-1-0
nav a:hover0-1-2
#header .title1-1-0

Inline style="" beats any selector in a stylesheet. So in this example the paragraph is green, not blue, even though the blue rule comes later:

<style>
  #intro { color: green; }  /* 1-0-0 */
  p.lead { color: blue; }   /* 0-1-1, lower, so it loses */
</style>
<p id="intro" class="lead">Which color?</p>

This is where the typical “specificity fight” starts. Someone styles the header with #header a, and later a .button class cannot change link colors inside the header. The quick fix is a longer selector, then an ID, then !important, and each step makes the next override harder. I have inherited stylesheets where half the declarations carried !important for exactly this reason, and changing a single color meant searching for which of four competing rules actually won. The sustainable approach is boring: style with classes, avoid IDs in selectors, keep selectors short, and let source order do the rest.

Inheritance is separate from the cascade. Some properties, mostly text-related ones like color, font-family and line-height, are inherited from the parent when an element has no value of its own. Layout properties like margin, padding, border and width are not. That is why setting font-family on body changes the whole page, while setting padding on body does not pad every paragraph.


The box model: box-sizing and margin collapse

Every element becomes a rectangle made of content, padding, border and margin. Two behaviors surprise almost everyone.

box-sizing

By default (box-sizing: content-box), width sets only the content width:

.card {
  width: 300px;
  padding: 20px;
  border: 2px solid #ccc;
}
/* rendered width: 300 + 20*2 + 2*2 = 344px */

Put two of these side by side in a 600px container and the second one wraps to the next line. With box-sizing: border-box, width includes padding and border, so the card is exactly 300px and the content area shrinks to 256px. Most projects set this globally:

*, *::before, *::after {
  box-sizing: border-box;
}

Margin collapse

Vertical margins between block elements do not add up; the larger one wins:

h2 { margin-bottom: 24px; }
p  { margin-top: 16px; }
/* gap between an h2 and the following p: 24px, not 40px */

The more confusing case is parent and child. If a parent has no border, padding or other content separating it from its first child, the child’s top margin escapes the parent:

<section class="hero">
  <h1>Title</h1>
</section>
.hero { background: #2c3e50; }
.hero h1 { margin-top: 40px; }

You would expect 40px of dark background above the heading. Instead the whole section moves down by 40px and the heading sits flush against the top of the dark area. Adding padding-top to .hero (or a border, or making it a flex/grid container, or display: flow-root) stops the collapse. Margins never collapse inside flex and grid containers, which is one reason modern layouts run into this less often, and why I tend to use padding on containers and gap in flex/grid layouts instead of margins on children.


The viewport meta tag

Mobile browsers were designed to display desktop sites. Without instructions, they lay the page out on a wide virtual viewport (commonly about 980px) and zoom out to fit the screen. Your text shrinks to unreadable size, and a media query such as

@media (max-width: 600px) {
  nav { flex-direction: column; }
}

never matches, because as far as layout is concerned the viewport is 980px wide. The line

<meta name="viewport" content="width=device-width, initial-scale=1">

tells the browser to use the device’s real width in CSS pixels. Avoid adding maximum-scale=1 or user-scalable=no: they block pinch zoom, which is an accessibility problem for people with low vision.

To test, open DevTools and toggle the device toolbar (Ctrl+Shift+M, or Cmd+Shift+M on macOS). Removing the viewport tag while in that mode shows the “tiny desktop page” effect immediately.


Putting it together: a small portfolio page

my-website/
├── index.html
└── style.css

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Jane Doe | Web Developer</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <h1>Jane Doe</h1>
    <p>Web developer</p>
    <nav aria-label="Main">
      <ul class="nav-list">
        <li><a href="#about">About</a></li>
        <li><a href="#skills">Skills</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main class="content">
    <section id="about">
      <h2>About</h2>
      <p>I am learning web development one layer at a time.</p>
    </section>

    <section id="skills">
      <h2>Skills</h2>
      <ul>
        <li>HTML</li>
        <li>CSS</li>
        <li>JavaScript (in progress)</li>
      </ul>
    </section>

    <section id="contact">
      <h2>Contact</h2>
      <p><a href="mailto:[email protected]">[email protected]</a></p>
    </section>
  </main>

  <footer class="site-footer">
    <p>&copy; 2026 Jane Doe</p>
  </footer>
</body>
</html>

style.css

*, *::before, *::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.6;
  color: #222;
}

.site-header {
  background: #2c3e50;
  color: #fff;
  text-align: center;
  padding: 2rem 1rem;   /* padding, not a child margin, so nothing collapses out */
}

.site-header h1 {
  margin: 0 0 0.25rem;
  font-size: 2.25rem;
}

.nav-list {
  display: flex;
  justify-content: center;
  gap: 1rem;
  list-style: none;
  padding: 0;
  margin: 1rem 0 0;
}

.nav-list a {
  color: #fff;
}

.content {
  max-width: 45rem;
  margin: 2rem auto;
  padding: 0 1rem;
}

.content h2 {
  border-bottom: 2px solid #3498db;
  padding-bottom: 0.25rem;
}

.site-footer {
  background: #34495e;
  color: #fff;
  text-align: center;
  padding: 1rem;
}

@media (max-width: 600px) {
  .nav-list {
    flex-direction: column;
    gap: 0.25rem;
  }
}

A few choices here are deliberate. Everything is styled through classes, so specificity stays flat and any rule can be overridden by a later one. max-width plus margin: 2rem auto centers the content and keeps line length readable on wide screens without breaking on narrow ones, which a fixed width: 720px would. Link colors are set explicitly inside the dark header, because links do not inherit color from their parent: browsers give <a> its own default blue.

Notice what is missing: the older habit of * { margin: 0; padding: 0; }. It works, but it also strips the default spacing from lists, headings and paragraphs everywhere, and you then re-add it piece by piece. Resetting body’s margin and setting spacing where you need it is usually less work.


Debugging with DevTools

Right-click an element and choose Inspect. The panels you will use most:

  • Elements / Styles: every rule that matches the selected element, sorted by cascade priority. Overridden declarations are struck through. A declaration with a warning icon is invalid (a typo like colr: red, or a value the property does not accept), and the browser ignored it entirely.
  • Computed: the final value of each property and, when expanded, which rule it came from. This answers “why is this 16px?” faster than reading the stylesheet.
  • Box model diagram (inside Computed or Layout): shows content, padding, border and margin sizes. Hovering an element in the page also highlights margins in orange, which makes a collapsed or escaped margin obvious.
  • Device toolbar: simulate phone widths to test the viewport tag and media queries.

When a style does not apply, I go through the same short list: is the element in the DOM where I think it is, does the selector match it, is the declaration valid, and which rule beats it. That order follows the browser’s own pipeline, and it finds the problem nearly every time.