Skip to content

CSS & Tailwind

CSS (Cascading Style Sheets) is a declarative language for describing how a document is presented: colour, size, spacing, position, typography, and motion. HTML says what a thing is; CSS says how it looks and where it sits.

CSS is not a programming language. There are no loops and no imperative statements. You write rules that say “elements matching this pattern should have these property values”, and the browser figures out the rest — which rule wins, what the computed value is, and how the page lays out.

The browser turns your HTML into a DOM tree (a tree of element nodes). It then:

  1. Parses every stylesheet into a list of rules.
  2. For each element, finds every declaration that could apply to it, and resolves conflicts using the cascade and specificity — this produces the computed style.
  3. Builds a box for each visible element and runs layout to decide its size and position.
  4. Paints those boxes, in an order determined by stacking rules.

Almost every CSS problem is a problem in one of those four steps: the rule didn’t match, the wrong rule won, the box model wasn’t what you assumed, or the paint order was not what you expected.

There are three places CSS can live. Only one of them is a good default.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<h1 class="title">Hello</h1>
</body>
</html>
styles.css
.title {
color: rebeccapurple;
font-size: 2rem;
}

A separate file is cacheable, reusable across pages, and keeps content separate from presentation. <link rel="stylesheet"> is render-blocking by design — the browser will not paint until it has the CSS, which avoids a flash of unstyled content.

<head>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
</style>
</head>

Useful for a single-file demo, for critical above-the-fold CSS inlined to avoid an extra request, or for email templates. Not for application code.

<p style="color: red;">Danger</p>

Applies to exactly one element, cannot express :hover or media queries, and has very high priority in the cascade, so it is hard to override. Legitimate uses are narrow: values computed at runtime by JavaScript (a progress bar width, a drag position), and setting a custom property from a template.

@import "typography.css";

Valid, but each @import is discovered only after the importing file is downloaded and parsed, so imports serialise network requests. In a build pipeline (Vite, PostCSS, Sass) @import is resolved at build time and is fine — Tailwind v4 relies on exactly this. In a raw browser stylesheet, prefer multiple <link> tags.

/* selector */ /* declaration block */
h1, .title {
color: #222; /* property: value; -> one declaration */
margin-block-end: 1rem;
}
  • Selector — the pattern that decides which elements the block applies to. h1, .title is a selector list: the block applies to anything matching either.
  • Declaration block — everything between the braces.
  • Declaration — a property: value pair, terminated by a semicolon. The last semicolon is optional but always write it.
  • Comment/* ... */. There is no // line comment in CSS.

An invalid declaration is discarded silently and the rest of the block still applies. An invalid selector, however, invalidates the whole rule — which is why one typo can appear to break a large block.

.card {
colour: red; /* dropped: not a real property */
color: red; /* still applies */
padding: 10; /* dropped: length needs a unit */
}

Everything about “which style actually wins” reduces to three ideas. Each gets full treatment in selectors and the cascade; here is the shape of it.

Multiple declarations can target the same property on the same element. The cascade is the algorithm that picks one. Simplified, in order:

  1. Origin and importance — browser default styles lose to your styles; !important flips the order.
  2. Specificity — the more specific selector wins.
  3. Source order — if everything else ties, the declaration that appears later wins.
p { color: blue; }
p { color: green; } /* wins: same specificity, appears later */

A selector’s specificity is a triple (id, class, type) compared left to right.

Selector Specificity Notes
* 0,0,0 universal selector counts for nothing
p 0,0,1 element (type) selector, also ::before
.card 0,1,0 class, attribute [href], pseudo-class :hover
#main 1,0,0 id
#main .card p 1,1,1 sum of the parts

1,0,0 beats 0,99,0. Specificity is not a number in base 10 — a hundred classes never outrank one id.

Some properties pass from parent to child automatically; most do not.

body {
color: #333; /* inherited: every descendant gets it */
font-family: system-ui;/* inherited */
border: 1px solid red; /* NOT inherited: only body gets a border */
}

Inherited properties are mostly text-related: color, font-*, line-height, letter-spacing, text-align, visibility, cursor, list-style. Box properties (margin, padding, border, background, width, display) are not inherited.

This is why setting font-family once on body styles the whole page, and why you still have to set border-collapse on the table itself.

Every element generates a rectangular box made of four nested layers:

┌─────────────────── margin ───────────────────┐
│ ┌───────────────── border ───────────────┐ │
│ │ ┌───────────── padding ───────────┐ │ │
│ │ │ │ │ │
│ │ │ content │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘

By default, width sets the content width only — padding and border are added on top, so a width: 200px box with padding: 20px and border: 1px occupies 242px. That is almost never what you want:

Put this at the top of every project
*, *::before, *::after {
box-sizing: border-box;
}

With border-box, width means the total width including padding and border. Full detail in the box model and layout.

Tailwind CSS is not a different language and not a component library. It is a generator of single-purpose CSS classes (utilities), each mapping to one or a few CSS declarations.

What Tailwind generates
.flex { display: flex; }
.items-center{ align-items: center; }
.gap-4 { gap: calc(var(--spacing) * 4); } /* 1rem */
.text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); }

So this markup:

<div class="flex items-center gap-4 text-sm"></div>

is exactly equivalent to writing that CSS by hand and giving the element one class. What you gain is that you never invent a class name, never grow a stylesheet that only ever gets bigger, and never wonder whether deleting a rule breaks something else. What you trade is verbose markup.

Two consequences matter:

  • You still need to know CSS. justify-content and align-items behave identically whether you typed them or typed justify-between items-center. Every page in this section before the Tailwind ones is a prerequisite, not an alternative.
  • Tailwind v4 is configured in CSS. Since v4 (January 2025) the design tokens live in a @theme block in your stylesheet rather than in tailwind.config.js:
app.css (Tailwind v4)
@import "tailwindcss";
@theme {
--color-brand: oklch(0.62 0.19 259);
--font-display: "Inter", sans-serif;
}

That single --color-brand token generates bg-brand, text-brand, border-brand, ring-brand, and so on. The v3 equivalent was a JavaScript object under theme.extend.colors plus a content array; v4 removes both. Both are covered, with the differences called out, in Tailwind fundamentals.

Page What it covers
Selectors & the cascade Every selector type, combinators, pseudo-classes and pseudo-elements, specificity maths, inheritance keywords, custom properties
Box model & layout Content/padding/border/margin, border-box, display, margin collapsing, position, stacking contexts, overflow, normal flow
Flexbox One-dimensional layout: axes, container and item properties, flex shorthand, centring, gotchas
Grid Two-dimensional layout: tracks, fr, repeat(), minmax(), template areas, implicit grid, responsive grids without media queries
Responsive & modern CSS Units, media queries, mobile-first, container queries, clamp(), responsive images, logical properties
Visual styling & animation Colour spaces, gradients, shadows, typography, @font-face, transforms, transitions, keyframes
Tailwind fundamentals Utility-first reasoning, v4 setup, the scales, responsive/state/dark variants
Tailwind in practice Real layouts, @theme customisation, arbitrary values, @apply, component extraction, gotchas
  • CSS resolves, for every element and every property, exactly one winning declaration — via origin, specificity, then source order.
  • Prefer external stylesheets; reserve inline styles for runtime-computed values.
  • Set box-sizing: border-box globally before you do anything else.
  • Inheritance covers text properties only; box properties never inherit.
  • Tailwind emits ordinary CSS. Learning CSS is learning Tailwind; the reverse is not true.