🎨 Making the Web Beautiful: CSS
You have spent four modules building web pages with HTML. Every heading, paragraph, list, form, table, and video is in place. But everything still looks like a plain document from the 1990s — black text on a white background, Times New Roman, no spacing, no color. That is because HTML only describes what content is. It says nothing about how it should look.
CSS — Cascading Style Sheets — is the language that controls appearance. Colors, fonts, spacing, layout, animations — every visual detail you see on any modern website is CSS. In this module, you learn the core mechanics that power it all.
🎯 By the end of this module, you will:
- Link a CSS stylesheet to an HTML page
- Write selectors that target any element on the page
- Understand specificity and predict which rule wins
- Use the box model to control spacing and sizing
- Choose the right unit for every situation (
px,rem,%,vw) - Define colors in hex, RGB, and HSL — and know when to use each
- Create reusable design tokens with CSS custom properties
⚙️ How CSS Works
A CSS rule tells the browser: "Find these elements, then apply these styles." Every rule has two parts: a selector (what to style) and a declaration block (how to style it).
Anatomy of a CSS Rule
CSS Rule Structure
h1, .card, #hero).
color, font-size, margin).
blue, 24px, 2rem).
/* selector { property: value; } */ h1 { color: navy; font-size: 2rem; }
Three Ways to Add CSS
There are three places you can write CSS. Each has its place, but one is clearly the best for real projects.
Where to Write CSS
style attribute directly on an element. Quick but unmaintainable. Avoid in production.
<style> block inside <head>. Good for single-page demos but styles can't be shared across pages.
.css file linked with <link>. The standard approach. One file styles an entire site.
1 — Inline Styles
Write CSS directly in the HTML element's style attribute. This has the highest specificity (it overrides almost everything) and is impossible to reuse. Use it only for quick tests or JavaScript-driven dynamic values.
<p style="color: red; font-weight: bold;">This text is red and bold.</p>
2 — Internal Stylesheet
Place a <style> element inside <head>. The styles apply only to that page. Good for single-page experiments, but the CSS is not reusable across other pages.
<head> <style> h1 { color: navy; font-size: 2rem; } p { line-height: 1.6; } </style> </head>
3 — External Stylesheet ✅
Create a separate .css file and link it in <head> with the <link> element. This is the standard approach for every real website. One CSS file can style hundreds of HTML pages. The browser also caches it, so returning visitors load it instantly.
<!-- In your HTML file --> <head> <link rel="stylesheet" href="style.css"> </head>
/* style.css — your separate CSS file */ h1 { color: navy; font-size: 2rem; } p { line-height: 1.6; color: #333; }
🎯 Selectors
A selector is the targeting system of CSS. It tells the browser which elements to style. Master selectors, and you can reach any element on any page without changing the HTML.
Basic Selectors
The Four Foundation Selectors
* — Selects every element. Used mainly in resets.
h1 p div — Selects every element of that tag name.
.card .active — Selects elements with that class attribute. Reusable — many elements can share a class.
#hero #nav — Selects the one element with that id. Must be unique per page. High specificity — prefer classes instead.
/* Universal — resets margin on everything */ * { margin: 0; padding: 0; } /* Type — styles every <p> on the page */ p { line-height: 1.6; } /* Class — styles any element with class="card" */ .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; } /* ID — styles the one element with id="hero" */ #hero { background: #f0f4ff; }
id. In practice, experienced developers use classes for styling and reserve IDs for JavaScript hooks and anchor links.
Attribute Selectors
Target elements based on their HTML attributes — or even partial attribute values. This is powerful for styling links, inputs, and data attributes.
/* Any element that has a "title" attribute */ [title] { cursor: help; } /* Input whose type is exactly "email" */ input[type="email"] { border-color: #3b82f6; } /* Links that start with "https" */ a[href^="https"] { color: green; } /* Files that end with ".pdf" */ a[href$=".pdf"] { color: crimson; } /* Elements whose class contains "btn" anywhere */ [class*="btn"] { cursor: pointer; }
Combinators
Combinators describe the relationship between two selectors. They let you target elements based on their position in the HTML tree.
Four Combinators
div p — Any <p> inside a <div>, at any depth.
ul > li — Only direct children. Does not reach nested descendants.
h2 + p — The <p> that comes immediately after an <h2>.
h2 ~ p — All <p> elements that follow an <h2>, at the same level.
/* Descendant: any <a> inside .nav */ .nav a { text-decoration: none; } /* Child: only direct <li> of .menu */ .menu > li { display: inline-block; } /* Adjacent sibling: the first <p> after any <h2> */ h2 + p { font-size: 1.1rem; color: #555; } /* General sibling: all <p> after an <h2> */ h2 ~ p { margin-left: 1rem; }
Pseudo-classes
Pseudo-classes select elements based on their state or position — things you cannot express with a simple attribute. They start with a single colon :.
/* User interaction states */ a:hover { color: crimson; } input:focus { outline: 2px solid #3b82f6; } button:active { transform: scale(0.97); } /* Positional pseudo-classes */ li:first-child { font-weight: bold; } li:last-child { border-bottom: none; } tr:nth-child(even) { background: #f8f9fa; }
Pseudo-elements
Pseudo-elements create virtual elements that do not exist in the HTML. They use a double colon :: to distinguish them from pseudo-classes.
/* Add a bullet before each list item */ li::before { content: "→ "; color: #3b82f6; } /* Style the first line of a paragraph */ p::first-line { font-weight: bold; } /* Enlarge the first letter (drop cap) */ p::first-letter { font-size: 2em; float: left; margin-right: 0.25rem; }
Grouping Selectors
When multiple selectors share the same styles, separate them with commas. This avoids duplicating declarations.
/* Without grouping — repetitive */ h1 { font-family: 'Inter', sans-serif; } h2 { font-family: 'Inter', sans-serif; } h3 { font-family: 'Inter', sans-serif; } /* With grouping — one rule for all */ h1, h2, h3 { font-family: 'Inter', sans-serif; }
⚖️ Specificity & The Cascade
When two CSS rules target the same element with conflicting values, the browser needs a tiebreaker. That tiebreaker is called the cascade — a three-step algorithm that determines which rule wins.
The Cascade — Three Tiebreakers (in order)
!important rules beat normal rules (but avoid using it).
#id beats .class beats element.
Specificity Scoring
Every selector gets a four-digit score: (Inline, IDs, Classes, Elements). Compare from left to right — the first number that differs wins. One ID (0,1,0,0) always beats any number of classes (0,0,99,0).
Specificity Score Examples
| Selector | Inline | IDs | Classes | Elements | Score |
|---|---|---|---|---|---|
p |
0 | 0 | 0 | 1 | 0,0,0,1 |
.card |
0 | 0 | 1 | 0 | 0,0,1,0 |
.nav .link |
0 | 0 | 2 | 0 | 0,0,2,0 |
#hero .title |
0 | 1 | 1 | 0 | 0,1,1,0 |
div#main .card p |
0 | 1 | 1 | 2 | 0,1,1,2 |
style="..." |
1 | 0 | 0 | 0 | 1,0,0,0 |
Cascade in Action
In this example, three rules target the same paragraph. Which color wins?
p { color: black; /* specificity: 0,0,0,1 */ } .intro { color: navy; /* specificity: 0,0,1,0 — wins! */ } body p { color: gray; /* specificity: 0,0,0,2 */ }
The paragraph appears navy. Even though body p has two element selectors (0,0,0,2), one class (0,0,1,0) outweighs any number of elements. Specificity compares column by column from left to right.
Inheritance
Some CSS properties are inherited by child elements automatically. Text properties — color, font-family, font-size, line-height — flow down from parent to children. Box properties — margin, padding, border, background — do not.
/* Set font on body — all text inherits it */ body { font-family: 'Inter', sans-serif; color: #333; line-height: 1.6; } /* No need to repeat font-family on <p>, <li>, etc. */ /* They inherit it from body automatically. */
!important
!important forces a rule to win regardless of specificity. It seems convenient, but it creates a specificity arms race — once you use it, the only way to override it is with another !important. If you find yourself reaching for !important, your selectors are probably too complex. Simplify them instead.
📦 The Box Model
Every element on a web page is a rectangular box. Even a round button, a circular avatar, or a line of text — the browser treats them all as boxes. The box model defines how space is calculated around and inside each element.
The Four Layers
Box Model Layers (inside → outside)
width and height.
.card { width: 300px; /* content width */ padding: 20px; /* inner space */ border: 2px solid #ddd; /* visible edge */ margin: 16px; /* outer space */ } /* Total rendered width (default box-sizing): 300 + 20+20 + 2+2 + 16+16 = 376px */
box-sizing: The Math Fix
By default, width: 300px sets only the content width. Padding and border are added on top, making the actual box wider than 300px. This default behavior is called content-box — and it is confusing.
The solution: box-sizing: border-box. It makes width include padding and border. If you set width: 300px with border-box, the box is exactly 300px wide — padding and border are subtracted from the inside.
/* The modern reset — add this to every project */ *, *::before, *::after { box-sizing: border-box; }
width: 300px means the box is 300px wide, period. Every CSS framework (Bootstrap, Tailwind, etc.) includes this reset.
Shorthand Properties
Margin and padding accept 1 to 4 values in a single declaration. The values go clockwise: top, right, bottom, left (think of a clock starting at 12).
/* 1 value: all four sides */ .a { padding: 16px; } /* 2 values: top/bottom left/right */ .b { padding: 16px 24px; } /* 3 values: top left/right bottom */ .c { padding: 16px 24px 8px; } /* 4 values: top right bottom left (clockwise) */ .d { padding: 16px 24px 8px 12px; }
Margin Collapse
When two vertical margins touch, they do not add up — they collapse into the larger of the two. If a heading has margin-bottom: 24px and the following paragraph has margin-top: 16px, the space between them is 24px, not 40px.
/* These margins collapse — gap is 24px, not 40px */ h2 { margin-bottom: 24px; } p { margin-top: 16px; }
📏 Units
CSS offers dozens of units for sizing. Some are fixed (absolute), others scale with context (relative). Choosing the right unit is crucial for responsive, accessible design.
Absolute Units
Absolute units have a fixed size regardless of context. In practice, only px (pixels) matters for screen design. The others (cm, mm, in, pt) are for print stylesheets.
.fixed-box { width: 320px; font-size: 16px; border: 1px solid #ccc; }
Relative Units
Relative units scale based on something else — the parent element, the root font size, or the viewport. They are the key to responsive design.
Relative Units Reference
| Unit | Relative To | Best For |
|---|---|---|
em |
Parent's font size | Padding/margin that scales with text |
rem |
Root (<html>) font size |
Font sizes, spacing — predictable scaling |
% |
Parent element's size | Widths, layout proportions |
vw |
1% of viewport width | Full-width sections, hero text |
vh |
1% of viewport height | Full-screen hero sections |
ch |
Width of the "0" character | Optimal reading width (~60-80ch) |
em vs rem — The Key Difference
em is relative to the parent element's font size. This means em values compound when nested — a 1.5em inside a 1.5em becomes 2.25× the base size. rem (root em) is always relative to the <html> element, so it stays predictable regardless of nesting.
/* The root font size (browser default: 16px) */ html { font-size: 16px; } /* rem — always relative to root (16px) */ h1 { font-size: 2rem; /* = 32px, always */ } h2 { font-size: 1.5rem; /* = 24px, always */ } /* Spacing with rem — predictable everywhere */ .section { padding: 2rem; /* = 32px */ margin-bottom: 1.5rem; /* = 24px */ }
rem for font sizes and spacing. Use % or vw for widths and layout. Use px only for things that should never scale (borders, shadows, tiny details). Use em only when you intentionally want a value to scale with its parent's font size.
Viewport Units
Viewport units let you size elements relative to the browser window. 100vw = full width of the viewport, 100vh = full height.
/* Full-screen hero section */ .hero { width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; } /* Responsive text that scales with viewport */ .big-title { font-size: 5vw; }
🎨 Colors
CSS gives you multiple ways to define colors. Each format has strengths — hex is compact, RGB is familiar, and HSL is the most intuitive for design work.
Named Colors
CSS has 140+ built-in color names like red, navy, tomato, cornflowerblue. Handy for quick prototyping but limited for precise design.
h1 { color: navy; } .warning { color: crimson; } .success { background: mediumseagreen; }
Hexadecimal
Hex codes are the most common color format on the web. They use #RRGGBB — two digits for red, green, and blue, each from 00 (none) to FF (full). You can shorten to #RGB when each pair is a double digit.
Hex Color Examples
#FF0000 → Pure red (short: #F00)
#00FF00 → Pure green (short: #0F0)
#3B82F6 → A pleasant blue (no shorthand)
#333333 → Dark gray (short: #333)
body { background: #f8f9fa; /* light gray */ color: #1a1a2e; /* near-black */ } .brand { color: #3b82f6; /* blue */ }
RGB & RGBA
rgb(R, G, B) uses decimal values 0–255 for each channel. Add an alpha channel (0–1) with rgba() or the modern syntax rgb(R G B / A) for transparency.
/* Solid colors */ .heading { color: rgb(59, 130, 246); /* blue */ } /* With transparency (alpha) */ .overlay { background: rgba(0, 0, 0, 0.5); /* 50% black */ } /* Modern syntax (same result) */ .overlay-modern { background: rgb(0 0 0 / 0.5); }
HSL — The Designer's Format
hsl(H, S%, L%) uses Hue (0–360° on the color wheel), Saturation (0% gray to 100% vivid), and Lightness (0% black to 100% white). HSL is the most intuitive format because you can create variations of a color by adjusting one number.
HSL Color Wheel
hsl(0, 100%, 50%) — Red (0°)
hsl(120, 100%, 40%) — Green (120°)
hsl(220, 90%, 56%) — Blue (220°)
hsl(220, 90%, 76%) — Same hue, lighter (+20% lightness)
hsl(220, 90%, 36%) — Same hue, darker (−20% lightness)
/* Create a color palette from one hue */ .btn-primary { background: hsl(220, 90%, 56%); /* base blue */ } .btn-primary:hover { background: hsl(220, 90%, 46%); /* darker on hover */ } .btn-light { background: hsl(220, 90%, 96%); /* very light tint */ color: hsl(220, 90%, 40%); /* dark text */ }
CSS Custom Properties (Variables)
Custom properties let you define reusable values once and reference them everywhere. Define them with --name and use them with var(--name). The most common use: a centralized color palette you can change in one place.
/* Define your palette on :root */ :root { --color-primary: hsl(220, 90%, 56%); --color-primary-dark: hsl(220, 90%, 40%); --color-text: #1a1a2e; --color-bg: #ffffff; --spacing: 1rem; } /* Use them anywhere */ body { color: var(--color-text); background: var(--color-bg); } .btn { background: var(--color-primary); padding: var(--spacing); } .btn:hover { background: var(--color-primary-dark); }
[data-theme="dark"] selector. Flip one attribute on <html> and the entire site switches themes. This is exactly how this course page implements dark mode.
currentColor
The keyword currentColor inherits the element's computed color value. It is useful for borders, shadows, and SVG fills that should match the text color automatically.
.tag { color: #3b82f6; border: 1px solid currentColor; /* border matches text color */ box-shadow: 0 0 0 3px currentColor; /* shadow too */ }
✏️ Practice: CSS Fundamentals
Test your understanding of selectors, specificity, the box model, and colors.
Exercise 1: Write the Selector
For each description, write the CSS selector that targets the correct elements.
1. All paragraphs inside an element with class article.
.article p — descendant combinator. Selects any <p> at any depth inside .article.
2. Only the direct <li> children of an element with class menu (not nested sublists).
.menu > li — child combinator. Stops at one level — nested <li> inside sublists are excluded.
3. All links that open a PDF file.
a[href$=".pdf"] — attribute selector with $= (ends with). Matches any <a> whose href ends in .pdf.
4. Every other row in a table (even rows).
tr:nth-child(even) — pseudo-class. Commonly used for zebra-striped tables.
Exercise 2: Rank the Specificity
Rank these selectors from lowest to highest specificity.
p /* A */ .card p /* B */ #main .card p /* C */ .card .content p:hover /* D */
A: p → (0,0,0,1) — one element selector.
B: .card p → (0,0,1,1) — one class + one element.
D: .card .content p:hover → (0,0,3,1) — two classes + one pseudo-class + one element.
C: #main .card p → (0,1,1,1) — one ID + one class + one element. IDs always outweigh classes.
✅ Correct order: A < B < D < C. Remember: one ID column beats any number in the class column.
Exercise 3: Fix the Box Model
This card is supposed to be exactly 300px wide, but it renders wider. Find the three problems.
.card { width: 300px; padding: 20px; border: 3px solid #ccc; margin: 16px; }
❌ Missing box-sizing: border-box — with the default content-box, padding (20×2 = 40px) and border (3×2 = 6px) are added on top of the 300px width, making the box 346px wide.
💡 Fix: Add box-sizing: border-box; to the rule — or better, use the universal reset *, *::before, *::after { box-sizing: border-box; } at the top of your stylesheet.
📐 With border-box, width: 300px means the box is exactly 300px wide. Padding and border are subtracted from the inside. Margin is always outside and does not count toward the width.
Exercise 4: Debug the Cascade
Given this HTML and CSS, what color is the heading? Explain why.
<div id="page"> <h1 class="title">Hello</h1> </div>
h1 { color: red; } .title { color: blue; } #page h1 { color: green; } h1.title { color: orange; }
🔴 h1 → specificity (0,0,0,1)
🔵 .title → specificity (0,0,1,0)
🟢 #page h1 → specificity (0,1,0,1) — wins!
🟠 h1.title → specificity (0,0,1,1)
✅ The heading is green. The #page h1 selector has an ID (column 2 = 1), which outweighs any combination of classes and elements in the other selectors.
📋 Module Summary
CSS rules = selector + declarations. Use external stylesheets (<link>) for every real project. Inline and internal styles exist but are rarely the right choice.
Type (p), class (.card), ID (#hero), attribute ([type="email"]), pseudo-classes (:hover), pseudo-elements (::before), and combinators (>, +, ~).
Score: (Inline, IDs, Classes, Elements). Compare column by column. One ID beats any number of classes. Avoid !important.
Content → Padding → Border → Margin. Always use box-sizing: border-box so width includes padding and border. Vertical margins collapse.
rem for fonts and spacing, %/vw for widths, px for borders. em compounds when nested — rem does not.
Hex (#3b82f6), RGB (rgb(59,130,246)), HSL (hsl(220,90%,56%)). Use HSL for palettes, custom properties for reusable design tokens.