Guide

CSS fundamentals explained

Cascading Style Sheets (CSS) is the language browsers use to style HTML documents — colors, spacing, typography, and layout. Where HTML describes what content means, CSS describes how it looks and flows on screen. Every modern site, from a landing page to a complex dashboard, rests on a handful of core ideas: selectors that target elements, the cascade that resolves conflicting rules, inheritance that passes values down the tree, and the box model that turns each element into a measurable rectangle. Master these before reaching for Flexbox and Grid or a utility framework. This guide covers selectors, specificity, the cascade, inheritance, the box model, display modes, units, colors, custom properties, typography basics, a Harbor product-card worked example, a property decision table, pitfalls, and a production checklist.

How CSS attaches to HTML

Browsers load CSS through three mechanisms:

  • External stylesheet<link rel="stylesheet" href="/style.css"> in the document head. One file can style many pages; browsers cache it across navigations.
  • Embedded rules — a <style> block in the head or body. Useful for page-specific tweaks or critical above-the-fold CSS.
  • Inline declarationsstyle="color: red" on an element. Highest specificity but hardest to maintain; reserve for email templates or one-off overrides.

A CSS rule has two parts: a selector (which elements match) and a declaration block of property–value pairs:

article.card h2 {
  font-size: 1.25rem;
  font-weight: 600;
  margin-block: 0 0.5rem;
}

The browser builds a CSS Object Model (CSSOM) from all loaded rules, then combines it with the DOM to produce the render tree that gets laid out and painted. Understanding fundamentals makes those later performance optimizations intelligible instead of magical.

Selectors — how rules find elements

Selectors are patterns that match nodes in the DOM. Common types:

  • Type (element)p, button matches all elements of that tag.
  • Class.card matches any element with class="card". Reusable; prefer classes over IDs for styling.
  • ID#checkout matches one element with that id. Unique per page; higher specificity than classes.
  • Attributeinput[type="email"], a[href^="https"].
  • Combinators — descendant (.nav a), child (ul > li), adjacent sibling (h2 + p), general sibling (h2 ~ p).
  • Pseudo-classes — state selectors like :hover, :focus-visible, :nth-child(2n), :not(.disabled).
  • Pseudo-elements — generated boxes like ::before, ::after, ::placeholder.

Modern selectors include :is() and :where() for grouping without exploding specificity, and :has() for parent selection (e.g., form:has(:invalid) to style a form containing invalid fields). Keep selectors shallow — deep chains like body div.main section article .content p span are brittle and slow to override.

The cascade and specificity

When two rules set the same property on the same element, the browser does not pick randomly. The cascade resolves conflicts in order:

  1. Origin and importance — author styles beat user-agent defaults; !important flips priority (use sparingly).
  2. Specificity — more specific selectors win.
  3. Source order — later rules win if specificity ties.

Specificity is often written as (inline, IDs, classes/attributes/pseudo-classes, elements/pseudo-elements). Examples:

  • p — (0, 0, 0, 1)
  • .card — (0, 0, 1, 0)
  • #hero .card — (0, 1, 1, 0)
  • style="..." inline — (1, 0, 0, 0)

:where(.a, .b) contributes zero specificity; :is(.a, #b) takes the highest specificity of its arguments. Layers (@layer base, components, utilities) let teams control cascade order without specificity arms races — utilities in a later layer can override components with equal specificity.

Inheritance

Some properties inherit from parent to child by default — notably color, font-family, line-height, and cursor. Others do not — margin, padding, border, and background reset on each element unless you set them explicitly.

Use inherit to force inheritance (border-color: inherit), or initial / unset to reset. Setting typography on body or :root and letting children inherit keeps font stacks consistent without repeating declarations on every paragraph.

The box model

Every element is a rectangular box with four regions:

  • Content — text, images, or child boxes.
  • Padding — space between content and border.
  • Border — visible edge around padding.
  • Margin — space outside the border, separating siblings.

width and height traditionally applied to the content box only; padding and border added on top — the source of countless "why is my div 300px when I set 250px?" bugs. The fix:

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

With border-box, declared width includes padding and border. Most reset stylesheets and frameworks apply this globally. Vertical margins between block siblings collapse — the larger margin wins instead of stacking — which surprises newcomers but simplifies vertical rhythm.

Logical properties (margin-block, padding-inline, inline-size) replace physical top/left/right/bottom and adapt automatically to writing mode and RTL layouts.

Display, positioning, and overflow

The display property defines how an element participates in layout:

  • block — starts on a new line, stretches to container width (div, p, h1).
  • inline — flows with text, ignores width/height (span, a).
  • inline-block — flows inline but respects box dimensions.
  • none — removes from layout entirely (unlike visibility: hidden, which keeps space).
  • flex / grid — enable modern layout modes (covered in depth in the layout guide).

position values (relative, absolute, fixed, sticky) take elements out of normal flow or anchor them to scroll containers. z-index only competes within the same stacking context — a common source of "why won't my modal appear on top?" bugs. overflow: hidden clips children; auto adds scrollbars when needed.

Units, colors, and custom properties

CSS units fall into absolute and relative families:

  • px — device pixels; fine for borders and shadows.
  • rem — relative to root font size (usually 16px); preferred for spacing and type scale.
  • em — relative to the element's own font size; useful inside components where nested scaling is intentional.
  • % — relative to parent (meaning varies by property).
  • vw / vh — viewport width/height percentages; watch mobile browser chrome quirks.
  • ch — width of the "0" character; handy for readable line lengths.

Colors accept named values, hex (#1a2b3c), rgb(), hsl(), and the newer oklch() perceptual space. Alpha transparency uses slash syntax: rgb(0 0 0 / 0.5).

Custom properties (CSS variables) live on the cascade like any property and inherit:

:root {
  --color-text: #1a1a1a;
  --space-md: 1rem;
  --radius-card: 12px;
}

.card {
  color: var(--color-text);
  padding: var(--space-md);
  border-radius: var(--radius-card);
}

Variables enable theming (light/dark mode via prefers-color-scheme), design tokens, and runtime updates from JavaScript (element.style.setProperty('--accent', '#3b82f6')).

Typography and readability

Core text properties:

  • font-family — stack with fallbacks: Inter, system-ui, sans-serif.
  • font-size — use rem for accessible scaling when users change browser defaults.
  • font-weight — 400 regular, 600 semibold; avoid faux-bold from missing font files.
  • line-height — 1.4–1.7 for body copy improves readability.
  • letter-spacing and text-transform — use sparingly on UI labels.

Limit line length to roughly 45–75 characters (max-width: 65ch). Pair with sufficient contrast (WCAG AA: 4.5:1 for normal text) — a concern shared with the accessibility guide, not just aesthetics.

Worked example: Harbor product card

Harbor Outfitters sells a 40L daypack. The HTML from the HTML fundamentals guide provides semantic structure; CSS makes it shoppable:

.product-card {
  --card-bg: rgba(255, 255, 255, 0.72);
  --card-border: rgba(255, 255, 255, 0.4);
  box-sizing: border-box;
  max-width: 22rem;
  padding: 1.25rem;
  border: 1px solid var(--card-border);
  border-radius: 1rem;
  background: var(--card-bg);
  backdrop-filter: blur(12px) saturate(1.4);
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
}

.product-card h1 {
  font-size: 1.375rem;
  font-weight: 600;
  margin-block: 0 0.5rem;
  line-height: 1.3;
}

.product-card .price {
  font-size: 1.125rem;
  font-weight: 600;
  color: #0f766e;
  margin-block: 0.75rem;
}

.product-card button[type="submit"] {
  width: 100%;
  padding: 0.75rem 1rem;
  border: none;
  border-radius: 0.5rem;
  font-weight: 600;
  color: #fff;
  background: linear-gradient(135deg, #10b981, #8b5cf6);
  cursor: pointer;
}

.product-card button[type="submit"]:hover {
  filter: brightness(1.05);
}

.product-card button[type="submit"]:focus-visible {
  outline: 2px solid #8b5cf6;
  outline-offset: 2px;
}

Custom properties centralize the glass-surface tokens. border-box makes max-width predictable. State pseudo-classes (:hover, :focus-visible) handle interaction without JavaScript. On narrow viewports, a single media query widens the card to 100% — layout grids handle multi-card rows in the Flexbox/Grid guide.

Property choice decision table

You needUseAvoid
Reusable component stylingClass selectors (.btn-primary)Styling by tag alone (button { ... } everywhere)
Consistent spacing scalerem + custom propertiesRandom pixel values per component
Theme colorsCSS variables on :rootHard-coded hex repeated in 50 rules
Override a single propertyMore specific selector or layer!important as default tool
Predictable widthsbox-sizing: border-boxContent-box + mental math on padding
Hide from everyonedisplay: none or hidden attributeopacity: 0 (still focusable/clickable)
Accessible focus rings:focus-visible outlineoutline: none without replacement
Two-dimensional page layoutCSS Grid (see layout guide)Float-based column hacks

Anti-patterns

  • Specificity wars — chaining IDs and !important to beat a library rule creates unmaintainable sheets. Prefer layers or a single extra class.
  • Universal selector abuse* { margin: 0 } without box-sizing or thoughtful resets breaks form controls.
  • Pixel-only typography — fixed px font sizes ignore user accessibility settings; rem respects them.
  • Layout with tables or floats — obsolete for new work; Grid and Flexbox are universally supported.
  • Deeply nested SCSS mirroring markup — compiles to selectors too specific to override.
  • Animating layout propertieswidth, height, and top trigger expensive reflows; prefer transform and opacity.
  • Removing focus outlines — keyboard users cannot see where they are; style :focus-visible instead.

Production checklist

  • Apply box-sizing: border-box globally (or via reset).
  • Define design tokens as custom properties on :root.
  • Set base typography on body with rem sizes.
  • Prefer classes for styling; keep specificity flat.
  • Use logical properties for spacing in internationalized UIs.
  • Test focus states with keyboard-only navigation.
  • Validate contrast ratios for text and interactive elements.
  • Minify and cache external stylesheets; avoid render-blocking unused CSS.
  • Lint with stylelint or browser DevTools coverage for dead rules.
  • Document component tokens so HTML and CSS stay in sync across pages.

Key takeaways

  • Selectors match DOM nodes; keep them shallow and class-based for maintainability.
  • The cascade resolves conflicts by importance, specificity, and source order — layers tame complexity at scale.
  • The box model plus border-box is the foundation of every layout calculation.
  • Custom properties turn CSS into a themable token system without a preprocessor.
  • Fundamentals unlock everything else — layout modules, animations, and rendering performance all assume you understand how rules become pixels.

Related reading