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 declarations —
style="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,buttonmatches all elements of that tag. - Class —
.cardmatches any element withclass="card". Reusable; prefer classes over IDs for styling. - ID —
#checkoutmatches one element with that id. Unique per page; higher specificity than classes. - Attribute —
input[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:
- Origin and importance — author styles beat user-agent defaults;
!importantflips priority (use sparingly). - Specificity — more specific selectors win.
- 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— useremfor 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-spacingandtext-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 need | Use | Avoid |
|---|---|---|
| Reusable component styling | Class selectors (.btn-primary) | Styling by tag alone (button { ... } everywhere) |
| Consistent spacing scale | rem + custom properties | Random pixel values per component |
| Theme colors | CSS variables on :root | Hard-coded hex repeated in 50 rules |
| Override a single property | More specific selector or layer | !important as default tool |
| Predictable widths | box-sizing: border-box | Content-box + mental math on padding |
| Hide from everyone | display: none or hidden attribute | opacity: 0 (still focusable/clickable) |
| Accessible focus rings | :focus-visible outline | outline: none without replacement |
| Two-dimensional page layout | CSS Grid (see layout guide) | Float-based column hacks |
Anti-patterns
- Specificity wars — chaining IDs and
!importantto beat a library rule creates unmaintainable sheets. Prefer layers or a single extra class. - Universal selector abuse —
* { margin: 0 }withoutbox-sizingor thoughtful resets breaks form controls. - Pixel-only typography — fixed
pxfont sizes ignore user accessibility settings;remrespects 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 properties —
width,height, andtoptrigger expensive reflows; prefertransformandopacity. - Removing focus outlines — keyboard users cannot see where they
are; style
:focus-visibleinstead.
Production checklist
- Apply
box-sizing: border-boxglobally (or via reset). - Define design tokens as custom properties on
:root. - Set base typography on
bodywithremsizes. - 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-boxis 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
- HTML fundamentals explained — semantic structure that CSS styles
- CSS Flexbox and Grid layout explained — one- and two-dimensional layout on top of the box model
- Responsive web design explained — media queries, fluid type, and mobile-first CSS
- Browser critical rendering path explained — how CSSOM joins the DOM before paint