Frameworks come and go and all of them compile down to the same three things: markup, styles and a browser that renders them. Engineers who skip this layer write components that work and cannot explain why the page jumps, why the modal traps focus in the wrong place, or why Google shows a blank description.
This post is the part of the platform you need before a framework makes sense. It is not a CSS course — that is 21 posts over here and HTML is 12 more. This is the shape of it and the bits that matter most.
What the browser actually does
Between a URL and pixels there are five steps, and knowing their names makes performance work possible:
| Step | What happens |
|---|---|
| Parse | HTML becomes the DOM, a tree of nodes. CSS becomes the CSSOM. |
| Style | Every node is matched against every rule to decide its final computed style. |
| Layout | The browser computes where each box goes and how big it is. Also called reflow. |
| Paint | Boxes become actual pixels — text, colours, borders, shadows. |
| Composite | Painted layers are combined, sometimes on the GPU. |
Two consequences you will use constantly:
- CSS blocks rendering; scripts block parsing. A stylesheet in the
<head>stops anything painting until it loads, which is why an enormous CSS file shows a white screen. A plain<script>in the head stops HTML parsing dead — hencedeferandtype="module". - Changing geometry costs more than changing appearance. Animating
widthortopforces layout again on every frame. Animatingtransformandopacitycan skip to composite. That is the whole reason "animate transform, not position" is repeated everywhere.
Semantic HTML is not a style preference
You can build any interface out of <div>. The reason not to is that the
element you choose is the only thing that tells everything except your eyes what the page
means — screen readers, search engines, the browser's own keyboard handling, and the
test-automation tools in post 11.
Compare these. They can be made to look identical:
<!-- Announces nothing, not focusable, ignores Enter and Space -->
<div class="btn" onclick="submit()">Sign in</div>
<!-- Focusable, keyboard-operable and announced as "Sign in, button" — for free -->
<button type="submit">Sign in</button>To make the first one equivalent you would add tabindex, a role, a
keydown handler for Enter and Space, and a disabled state. That is four bugs waiting to
happen in place of one element.
The handful that carry most of the weight:
| Element | What you get |
|---|---|
<button> | Focus, Enter/Space, the button role, a disabled state. |
<a href> | Navigation, middle-click, "open in new tab", crawlable. If it goes somewhere, it is a link — not a button with an onclick. |
<form> | Enter submits, browsers offer autofill and password saving. |
<label for> | Clicking the label focuses the input, and the input gets an accessible name. |
<h1>–<h6> | The document outline screen-reader users navigate by. In order, no skipped levels. |
<main>, <nav>, <header>, <footer> | Landmarks, so a user can jump straight to the content. |
<ul>/<li> | "List, 6 items" — announced count. |
The demo app leans on this. Its Playwright tests find things the way a screen reader would —
getByRole('button', { name: 'Sign in' }) — which only works because it is a real
button with a real label.
The head section decides how you appear elsewhere
Nobody sees the <head> and everybody sees its consequences — the tab title,
the Google result, the card that renders when your link is pasted into Slack.
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Order pizza — Pizza</title>
<meta name="description" content="Build your own pizza and have it delivered.">
<link rel="canonical" href="https://example.com/menu">
<meta property="og:title" content="Order pizza">
<meta property="og:image" content="https://example.com/og.png">The viewport line is the one that matters most and the one most often missing. Without it a phone renders the page at desktop width and scales it down, so every media query you wrote is ignored and your text is unreadable.
The HTML track goes further on this.
The CSS you will use every day
CSS is enormous and you will use a small slice of it constantly.
The box model
Every element is a box: content, then padding, then border, then margin. The single most useful
line of CSS ever written is the one that makes width mean what you expect:
*, *::before, *::after { box-sizing: border-box; }By default, width: 300px plus 20px of padding gives you a 340px box. With
border-box it gives you a 300px box. Every CSS framework sets this, including
Bootstrap, which is why you may never have hit the problem.
The cascade and specificity
When two rules touch the same property, the winner is decided by specificity first and source order second. Roughly: inline style beats id, id beats class, class beats element.
The practical advice is not "learn to compute specificity" — it is keep it low and
flat. Style with single class names. A rule like
.page .card ul li a.link wins every fight today and is impossible to override
tomorrow, and the usual escape hatch — !important — just restarts the war one level
up. Detail here.
Flexbox and grid
Almost every layout is one of these two, and the choice is simple:
- Flexbox — content laid out along one axis. Navbars, button rows, a label and a value pushed apart.
- Grid — two axes at once. Card galleries, page shells, anything with real rows and columns.
/* One axis: push the last item to the right, centre everything vertically. */
.navbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
/* Two axes: as many columns as fit, each at least 16rem. No media query needed. */
.menu { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 1.5rem; }That auto-fill/minmax line is worth memorising — it is a responsive
card grid with no breakpoints at all. More on grid,
more on layout.
Custom properties
Custom properties are variables the browser understands, which makes them different from Sass variables in a way that matters:
:root { --brand: #d8102a; --radius: 0.75rem; }
.button { background: var(--brand); border-radius: var(--radius); }
/* Overridable per subtree at runtime — a Sass variable cannot do this. */
.promo { --brand: #0a7d34; }The demo app uses both and documents the rule of thumb in
src/styles/_tokens.scss: Sass variable if only the build needs it, custom property
if the browser does. Sass can do arithmetic on a colour to derive a hover shade; only a custom
property can be reassigned live for one part of the page, or flipped for a dark theme.
Responsive by default
Write the narrow layout first and add complexity as the screen grows. Going the other way means every breakpoint is undoing something.
.menu { grid-template-columns: 1fr; }
@media (min-width: 48em) {
.menu { grid-template-columns: repeat(2, 1fr); }
}Two more modern tools worth knowing exist: clamp() for type that scales without
breakpoints, and container queries, which let a component respond to its own width rather
than the window's — much closer to how components actually get reused.
How styles get into a component app
| Approach | What it is | Watch out for |
|---|---|---|
| Global stylesheet | One file, plain CSS. | Every name is global; collisions grow with the team. |
| Sass/SCSS | CSS plus nesting, variables and mixins, compiled at build time. | Nesting too deep recreates the specificity problem. |
| CSS Modules | Class names are hashed per file, so they cannot collide. | Sharing a token needs a deliberate mechanism. |
| Utility CSS (Tailwind) | Compose from tiny single-purpose classes. | Markup gets noisy; the team must actually agree. |
| CSS-in-JS | Styles declared in the component. | Runtime cost, and awkward with server rendering. |
| Component library | Bootstrap, MUI — someone else's components. | Fast start; customising deeply can be a fight. |
All six are in production somewhere. Do not let a preference here become an identity — learn the box model and the cascade and you can work in any of them.
The one thing to take from this post
The element you pick is an API, not decoration. A <button> is a promise to
the keyboard, the screen reader, the search crawler and your own test suite, and a
<div> with an onclick is a promise to none of them. Get the markup
right and accessibility, testability and SEO stop being separate projects.