Web Development

Complete CSS Course

Learn CSS from scratch, step by step: beginner, intermediate and advanced lessons on selectors, colors, fonts, the box model, Flexbox, Grid, responsive design, animations and modern CSS, with practical code examples and projects.

  • Beginner
  • 17 modules
  • 77 lessons
  • About 6 h 15 min
  • Free, no sign-up

Module 1 of 17

Beginner: Getting Started with CSS

Lesson 1.1 4 min read

What Is CSS? A Simple Guide for Complete Beginners

CSS is the language that controls how a web page looks: colors, fonts, spacing and layout. In this guide you will learn what CSS is and how it works with HTML.

What does CSS do?

CSS stands for Cascading Style Sheets. A style sheet is a list of rules that tell the browser how to show things on the screen. For example, a rule can say "make every heading dark blue" or "put 20 pixels of space around this box".

Without CSS, every web page would look like a plain text document: black text, white background, blue underlined links, and everything stacked from top to bottom. CSS is what turns that plain page into a real website with a design.

With CSS you can:

  • Change colors of text and backgrounds
  • Choose fonts and text sizes
  • Add space inside and around elements
  • Add borders, rounded corners and shadows
  • Place elements side by side, in columns or in a grid
  • Make the same page look good on phones and large screens

What is the difference between HTML and CSS?

Think of a house. HTML is the structure: the walls, doors and windows. CSS is the decoration: the paint, the curtains and the furniture. You need the structure first, and then you make it look nice.

HTMLCSS
Describes what the content isDescribes how the content looks
Uses tags like <h1>, <p>, <img>Uses rules like color: red;
Saved in .html filesUsually saved in .css files
A heading, a paragraph, a listBlue text, big font, 20px of space

Keeping these two jobs separate is a big advantage. You can change the whole design of a website by editing one CSS file, without touching the HTML content.

What does "cascading" mean in CSS?

"Cascading" means that styles flow down and combine, like water in a waterfall. Many rules can apply to the same element. When two rules disagree, the browser follows a clear order to choose a winner: more specific rules usually beat general ones, and if two rules are equally specific, the one that comes later wins. You do not need to master this today. Just remember that CSS rules can stack on top of each other, and the browser has a fair system to decide which one to use.

Note: You will meet the cascade again many times. For now, a simple rule works well: if a style does not seem to apply, another rule may be winning.

CSS before and after: a simple example

Here is a small HTML page with no CSS at all.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Blog</title>
</head>
<body>
  <h1>My First Blog Post</h1>
  <p>Today I started learning CSS. It is easier than I thought!</p>
</body>
</html>

In the browser you will see a big black heading and a normal paragraph in the default font, pressed against the left edge of the window. It works, but it looks plain.

Now let's add a few lines of CSS inside a <style> tag in the <head>.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Blog</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f1ea;
      padding: 40px;
    }
    h1 {
      color: #2a4d69;
    }
    p {
      font-size: 18px;
      line-height: 1.6;
    }
  </style>
</head>
<body>
  <h1>My First Blog Post</h1>
  <p>Today I started learning CSS. It is easier than I thought!</p>
</body>
</html>

The HTML content is exactly the same. But now the page has a soft cream background, a clean font, a dark blue heading, bigger and easier-to-read text, and space around the edges. That is the power of CSS: same content, new look.

Tip: Save the file as index.html and open it in your browser. Then change #2a4d69 to tomato and refresh. Playing with values is the fastest way to learn.

Practice exercise

Create an HTML page with a heading and two paragraphs about your favorite food. Then add a <style> tag and change the background color of the page, the color of the heading, and the font size of the paragraphs.

Hint: copy the second example above and change the values. Try color names like darkgreen, navy or orange.

Frequently asked questions

Is CSS a programming language?

Not in the usual sense. CSS is a style sheet language: you describe how things should look, and the browser does the work. You don't write loops or logic like in JavaScript.

Do I need to learn HTML before CSS?

Yes, a little. CSS styles HTML elements, so you need to know basic tags like <h1>, <p>, <a> and <div>. You don't need to be an HTML expert.

Is CSS hard to learn?

The basics are friendly. You can make visible changes in your first five minutes. Layout takes more practice, but you will learn it step by step in this course.

Key takeaways

  • CSS controls how a web page looks; HTML controls what is on the page.
  • CSS stands for Cascading Style Sheets.
  • "Cascading" means many rules can apply to one element, and the browser has a fixed order to choose which one wins.
  • The same HTML can look completely different with different CSS.

Open this lesson on its own page ↗

Module 2 of 17

Beginner: CSS Selectors

Lesson 2.1 4 min read

Basic CSS Selectors: Element, Class and ID

CSS selectors tell the browser which HTML elements to style. This lesson covers the basic CSS selectors: element, class, ID, universal and grouped selectors.

Element selectors (type selectors)

An element selector, also called a type selector, uses the tag name. It styles every element of that type on the page.

p {
  color: #333;
  line-height: 1.6;
}

h2 {
  color: darkgreen;
}

Every <p> on the page gets dark gray text with comfortable line spacing, and every <h2> turns dark green. Element selectors are great for setting the basic look of your whole site.

Class selectors: .class

A class is a name you give to elements with the class attribute. In CSS, you select a class with a dot . in front of the name. Many elements can share the same class, like many students wearing the same team shirt.

<p>Our shop is open every day.</p>
<p class="highlight">Today only: 20% off all books!</p>
<p>Free delivery over $30.</p>
<p class="highlight">New arrivals every Friday.</p>
.highlight {
  background-color: #fff3a3;
  font-weight: bold;
}

Only the two paragraphs with class="highlight" get a yellow background and bold text. The other paragraphs stay normal. You can also add the same class to other tags, like a <span> or <li>, and they will get the same style.

Using multiple classes on one element

An element can have more than one class. Separate the class names with a space inside the class attribute.

<button class="btn btn-primary">Save</button>
<button class="btn btn-danger">Delete</button>
.btn {
  padding: 10px 18px;
  border: none;
  border-radius: 6px;
  color: white;
  font-size: 16px;
}

.btn-primary {
  background-color: #1e88e5;
}

.btn-danger {
  background-color: #e53935;
}

Both buttons share the .btn styles (padding, rounded corners, white text). Then each one adds its own color: blue for Save, red for Delete. This "base class plus variation class" pattern is used on almost every real website.

Note: Class names are case-sensitive and cannot start with a number. Use lowercase words joined with hyphens, like card-title or btn-primary.

ID selectors: #id

An ID is a unique name for one element on the page. In CSS, you select it with a hash #. An ID should be used only once per page, like a passport number.

<header id="site-header">
  <h1>Green Leaf Café</h1>
</header>
#site-header {
  background-color: #2e7d32;
  color: white;
  padding: 20px;
}

The header gets a green background, white text and some space inside.

The universal selector and grouping selectors

The universal selector * selects every element on the page. It is often used for small resets.

* {
  margin: 0;
}

Grouping lets you give the same styles to several selectors at once. Separate them with commas.

h1, h2, h3 {
  font-family: Georgia, serif;
  color: #1a237e;
}

All three heading levels now share the same font and color. Without grouping, you would write the same two lines three times.

Class vs ID: which should you use?

For styling, use classes almost all the time. Here's why:

Class .nameID #name
How many per page?As many as you wantOnly one
Reusable?YesNo
Strength in the cascadeMediumVery strong
Best used forStylingLinks to page sections (href="#contact"), JavaScript, form labels

IDs are very strong in the cascade. An ID rule beats many class rules, which can make it hard to change a style later. Keep IDs for jobs that need a unique name, and use classes for design.

Common mistake: Writing the dot or hash in the HTML. It's class="card", not class=".card". The . and # only belong in CSS.

Practice exercise

Build a simple product list with three <div> elements. Give all of them the class product, and give one of them a second class sale. Style .product with a light gray background and padding, and make .sale add a red border. Then group h1, h2 to share one color.

Hint: the sale item's HTML should look like class="product sale". A red border is border: 2px solid red;.

Key takeaways

  • Element selectors (p) style every element of that tag.
  • Class selectors (.card) style any element with that class, and an element can have several classes.
  • ID selectors (#header) target one unique element, but are too strong for everyday styling.
  • * selects everything; commas group selectors that share the same styles.
  • Prefer classes for styling.

Open this lesson on its own page ↗

Module 3 of 17

Beginner: Colors, Text and Fonts

Lesson 3.1 5 min read

CSS Colors: Named, HEX, RGB and HSL

CSS colors can be written as named colors, HEX codes, RGB or HSL values. This lesson explains every format, transparency, and how to choose readable color contrast.

color and background-color

Two properties do most of the color work in CSS:

  • color sets the text color.
  • background-color sets the color behind the element.
<div class="notice">Your order has been shipped!</div>
.notice {
  color: #1b5e20;
  background-color: #e8f5e9;
  padding: 16px;
}

You will see a light green box with dark green text. Using a light and dark version of the same color is a simple trick that always looks clean.

Named colors

CSS has around 150 named colors, like red, navy, tomato, gold and rebeccapurple. They are easy to remember and perfect for testing.

h1 { color: tomato; }
body { background-color: whitesmoke; }

The problem is that you only get those 150 choices. Real designs usually need an exact shade, so you will use the other formats below.

HEX colors

A HEX code starts with # followed by six characters, using the numbers 0–9 and the letters a–f. The six characters are three pairs: red, green and blue.

#  ff  80  00
   |   |   |
  red green blue      (00 = none, ff = full)
  • #000000 is black (no light at all).
  • #ffffff is white (all colors at full).
  • #ff0000 is pure red.
  • #ff8000 is orange.

There is also a short form with three characters: #f80 means the same as #ff8800. HEX is the most common format you'll see, because design tools like Figma show colors in HEX.

RGB and HSL colors

RGB describes a color with three numbers from 0 to 255, for red, green and blue light. It's like mixing three flashlights. Modern CSS separates the numbers with spaces: rgb(30 136 229) is a bright blue. You will also see the older comma style, rgb(30, 136, 229), which still works everywhere.

HSL stands for hue, saturation and lightness. Many people find it the easiest format to understand.

  • Hue is the position on a color wheel, from 0 to 360 degrees. 0 is red, 120 is green, 240 is blue.
  • Saturation is how strong the color is. 0% is gray, 100% is full color.
  • Lightness is how light it is. 0% is black, 50% is normal, 100% is white.
<a class="btn" href="/checkout">Go to checkout</a>
.btn {
  display: inline-block;
  padding: 12px 20px;
  color: rgb(255 255 255);
  background-color: hsl(210 80% 45%);   /* blue */
  text-decoration: none;
}

.btn:hover {
  background-color: hsl(210 80% 35%);   /* same blue, darker */
}

You will see a blue button with white text that turns darker blue when your mouse is over it. The big advantage of HSL: to make a darker or lighter version of a color, you only change the last number. Doing that with HEX is much harder.

Transparency: alpha, rgba and opacity

You can make a color see-through by adding an alpha value. Alpha goes from 0 (fully transparent) to 1 (fully solid). Write it after a slash.

.overlay {
  background-color: rgb(0 0 0 / 0.5);       /* black, 50% see-through */
}

.badge {
  background-color: hsl(210 80% 45% / 0.2); /* light blue tint */
}

The older functions rgba() and hsla() do the same thing, for example rgba(0, 0, 0, 0.5). HEX can also have alpha, using eight characters: #00000080.

The opacity property is different. It makes the whole element transparent, including its text, images and children.

.card-a { background-color: rgb(0 0 0 / 0.5); color: white; }  /* only the background fades */
.card-b { background-color: black; color: white; opacity: 0.5; } /* everything fades, text too */

Common mistake: Using opacity to fade a background and then wondering why the text is also faded. If you only want a see-through background, use a color with alpha instead.

How to choose readable color contrast

Contrast is the difference between the text color and the background color. Low contrast, like light gray text on white, is hard to read, especially for people with poor eyesight or on a phone in the sun.

The WCAG accessibility guidelines ask for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. You don't need to calculate this yourself: in Chrome, Edge or Firefox DevTools, click a color swatch in the Styles panel to see its contrast ratio.

Tip: Dark text on a light background is the safest choice for long text. Use #222 or #333 instead of pure black for a softer look that is still very readable.

Practice exercise

Create an alert box that says "Payment failed. Please try again." Give it a light red background and dark red text, written in HSL. Then add a second box with a semi-transparent black background on top of a page with a colorful body background.

Hint: try hsl(0 70% 95%) for the light red and hsl(0 70% 30%) for the dark red text. For the second box, use rgb(0 0 0 / 0.6) and white text.

Frequently asked questions

Which CSS color format should I use?

Any of them is fine. HEX is very common because design tools use it. HSL is easiest when you need lighter and darker shades. Pick one and be consistent.

What is the difference between rgb() and rgba()?

Today, nothing important. rgb() accepts an alpha value too, like rgb(0 0 0 / 0.5). rgba() is the older name and still works.

Are CSS color names case-sensitive?

No. Red, RED and red all work, and so do #FF0000 and #ff0000. Most developers write them in lowercase.

Key takeaways

  • color sets text color; background-color sets the color behind an element.
  • Colors can be named, HEX (#ff8000), RGB (rgb(255 128 0)) or HSL (hsl(30 100% 50%)).
  • Add alpha for transparency: rgb(0 0 0 / 0.5).
  • opacity fades the whole element, including text.
  • Always check that text has enough contrast with its background.

Open this lesson on its own page ↗

Module 4 of 17

Beginner: The CSS Box Model

Lesson 4.1 5 min read

The CSS Box Model Explained: Content, Padding, Border and Margin

The CSS box model says every element is a box made of content, padding, border and margin. Learn each layer, the shorthand, and how to center a block with margin auto.

What is the CSS box model?

In CSS, every element is a rectangle, even if it doesn't look like one. Each box has four layers, from the inside out:

+---------------------------------------------+
|                   MARGIN                    |
|   +-------------------------------------+   |
|   |               BORDER                |   |
|   |   +-----------------------------+   |   |
|   |   |           PADDING           |   |   |
|   |   |   +---------------------+   |   |   |
|   |   |   |       CONTENT       |   |   |   |
|   |   |   |   text, images...   |   |   |   |
|   |   |   +---------------------+   |   |   |
|   |   +-----------------------------+   |   |
|   +-------------------------------------+   |
+---------------------------------------------+

Think of a framed picture on a wall. The content is the picture. The padding is the white space (the mat) between the picture and the frame. The border is the frame. The margin is the empty wall space between this frame and the next one.

Content, padding, border and margin

  • Content is where your text and images live. Its size can be set with width and height.
  • Padding is space inside the border. It takes the element's background color.
  • Border is a line around the padding. It needs a width, a style and a color, like border: 2px solid #333;. Common styles are solid, dashed and dotted.
  • Margin is space outside the border. It is always transparent and pushes other elements away.
<div class="note">Remember to drink water today!</div>
<div class="note">Take a short walk after lunch.</div>
.note {
  background-color: #e3f2fd;
  padding: 20px;
  border: 3px solid #1e88e5;
  margin: 16px;
}

You will see two light blue boxes with a blue frame. The text has 20 pixels of breathing room inside the frame (padding), and the boxes have space between them and the page edges (margin).

Tip: Right-click a box and choose Inspect. In DevTools, open the Computed tab (or the "Layout" view) to see a colored box model diagram with the exact padding, border and margin numbers.

What is the difference between padding and margin?

This is the most common beginner question. A simple test: does the space need the background color?

  • Padding is inside the box, so the background color fills it. Use it to give content room to breathe.
  • Margin is outside the box and always transparent. Use it to separate one box from another.

Padding and margin shorthand (1 to 4 values)

You can set each side separately with padding-top, padding-right, padding-bottom and padding-left (the same for margin). But the shorthand is faster. It accepts 1 to 4 values, always going clockwise from the top, like a clock.

CodeMeaning
padding: 10px;All four sides 10px
padding: 10px 20px;Top and bottom 10px, left and right 20px
padding: 10px 20px 30px;Top 10px, left and right 20px, bottom 30px
padding: 10px 20px 30px 40px;Top 10px, right 20px, bottom 30px, left 40px

To remember the four-value order, think "TRouBLe": Top, Right, Bottom, Left.

Margin collapse and how to center a block with margin: 0 auto

Here's a surprise. When two block boxes are stacked on top of each other, their vertical margins don't add up. The bigger margin wins. This is called margin collapse.

.first  { margin-bottom: 30px; }
.second { margin-top: 20px; }

You might expect 50px between them, but you get 30px. It only happens with top and bottom margins, never left and right. A simple habit avoids confusion: add vertical space in one direction only, for example always use margin-bottom.

To center a box horizontally, give it a width (or max-width) and set the left and right margins to auto. The browser shares the leftover space equally on both sides.

<main class="container">
  <h1>About Us</h1>
  <p>We are a small team that builds friendly websites for local shops.</p>
</main>
.container {
  max-width: 600px;
  margin: 0 auto;
  padding: 24px;
  background-color: #fafafa;
  border: 1px solid #ddd;
}

The box is at most 600px wide and sits in the middle of the page, with no extra space above or below. On a small phone screen it becomes narrower than 600px, so it still fits.

Common mistake: Using margin: 0 auto on a box without a width. A block element is already full width, so there is no leftover space to share, and nothing seems to happen.

Practice exercise

Create three "testimonial" boxes with a quote and a name in each. Give them padding, a left border only (border-left: 5px solid teal;), a light background, and a margin-bottom of 20px. Put all three in a container that is centered with max-width: 700px and margin: 0 auto.

Hint: use DevTools to check the box model of one testimonial and confirm the numbers match your CSS.

Frequently asked questions

Why is there space around my page even with no CSS?

Browsers add default styles. The <body> has a margin of about 8px, and headings and paragraphs have top and bottom margins. You can remove them with margin: 0;.

Can padding or margin be negative?

Margin can be negative, which pulls elements closer or makes them overlap. Padding cannot be negative.

Does the border add to the width of an element?

By default, yes. Padding and border are added on top of the width. The next lesson shows how box-sizing: border-box fixes this.

Key takeaways

  • Every element is a box with four layers: content, padding, border and margin.
  • Padding is inside the border and shows the background; margin is outside and transparent.
  • Shorthand values go clockwise: top, right, bottom, left.
  • Vertical margins between stacked blocks collapse: the bigger one wins.
  • Center a block with a max-width plus margin: 0 auto.

Open this lesson on its own page ↗

Module 5 of 17

Beginner: Styling Common Elements

Lesson 5.1 5 min read

Styling Lists and Links with CSS

Learn how to style lists and links with CSS: change or remove bullets, build a simple nav menu, add custom markers and style every link state.

How do you change list bullets with list-style-type?

Every <ul> and <ol> comes with a marker: the bullet or number in front of each item. The list-style-type property changes what that marker looks like.

<h3>Shopping list</h3>
<ul class="shopping">
  <li>Bread</li>
  <li>Milk</li>
  <li>Apples</li>
</ul>

<h3>Steps to make tea</h3>
<ol class="steps">
  <li>Boil the water</li>
  <li>Add the tea bag</li>
  <li>Wait three minutes</li>
</ol>
.shopping {
  list-style-type: square;
}

.steps {
  list-style-type: upper-roman;
}

The shopping list now shows small squares instead of round dots, and the tea steps are numbered I, II, III. Common values are disc (the default dot), circle, square, decimal (1, 2, 3), lower-alpha (a, b, c), upper-roman and none.

Two more list properties are useful:

  • list-style-position: inside puts the marker inside the item's box, so wrapped lines start under the marker. The default, outside, hangs the marker in the left padding.
  • list-style-image: url("check.svg") uses a small image as the marker. It is hard to size and position, so most people now use ::marker or ::before instead (see below).

You can also write all three in one shorthand: list-style: square inside;.

Removing bullets to build a navigation menu

A website menu is really a list of links, so we write it as a <ul> inside a <nav>. But menus don't need bullets. To remove them, set list-style: none and also remove the default padding and margin that browsers add to lists.

<nav class="main-nav">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/courses">Courses</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>
.main-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.main-nav li {
  display: inline-block;
  margin-right: 16px;
}

.main-nav a {
  color: #1e3a8a;
  text-decoration: none;
  font-weight: bold;
}

You will see the four links in one row with space between them and no bullets. display: inline-block puts the items side by side. In the Flexbox module you'll learn an even better way to lay out menus.

Common mistake: Removing only the bullets with list-style: none and wondering why the menu is still pushed to the right. Lists have a default padding-left of about 40px. Set padding: 0 too.

Custom markers with ::marker

The ::marker pseudo-element selects the bullet or number itself. You can change its color, size and even its content, without touching the text.

<ul class="features">
  <li>Free lessons</li>
  <li>Practice exercises</li>
  <li>Learn at your own speed</li>
</ul>
.features li::marker {
  content: "✔  ";
  color: #16a34a;
  font-weight: bold;
}

Each item now starts with a green tick instead of a black dot. ::marker only accepts a few properties (like color, content and font properties), but that is enough for most designs.

Tip: Want a red number but black text in an ordered list? Use ol li::marker { color: red; }. Before ::marker existed, people had to hide the number and rebuild it by hand.

A link can be in different states, and you can style each one with a pseudo-class:

  • :link – a link the user has not visited yet.
  • :visited – a link the user has already visited.
  • :hover – the mouse is over the link.
  • :focus-visible – the link was reached with the keyboard (Tab key).
  • :active – the link is being clicked right now.

Write them in this order: Link, Visited, Hover, (focus), Active. Remember it as "LoVe HAte". All these selectors have the same specificity, so the last matching rule wins. If you put :hover before :visited, visited links would never change color on hover.

a:link {
  color: #2563eb;
}

a:visited {
  color: #7c3aed;
}

a:hover {
  color: #1d4ed8;
  text-decoration-thickness: 2px;
}

a:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 2px;
}

a:active {
  color: #dc2626;
}

New links are blue, links you have already opened are purple, the underline gets thicker on hover, keyboard users see an orange ring, and the link flashes red while it is pressed.

Sometimes a link should look like a button, for example a "Start learning" link on a home page. Keep it a real <a> (because it goes to another page) and just style it.

<a class="button-link" href="/signup">Start learning</a>
.button-link {
  display: inline-block;
  padding: 12px 24px;
  background: #2563eb;
  color: white;
  text-decoration: none;
  font-weight: bold;
  border-radius: 8px;
}

.button-link:hover {
  background: #1d4ed8;
}

.button-link:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

.button-link:active {
  background: #1e40af;
}

You will see a blue, rounded "button" that gets darker on hover and darker still when pressed. display: inline-block is important: without it, the top and bottom padding would overlap the lines around the link.

Note: Use <a> when the user goes somewhere (a new page) and <button> when something happens on the same page (open a menu, submit a form). The look can be the same; the meaning is different, and screen readers announce them differently.

You can find all the list values on MDN's list-style-type page.

Practice exercise

Make a "Footer links" section with a heading and a list of five links (Privacy, Terms, Help, Blog, Careers). Remove the bullets, show the links in one row, remove the underline, and bring the underline back only on hover. Then add one button-like link that says "Contact us".

Hint: use text-decoration: none on the links and text-decoration: underline inside a:hover. Don't forget padding: 0 on the <ul>.

Key takeaways

  • list-style-type, list-style-position and the list-style shorthand control list markers.
  • For menus, use list-style: none plus margin: 0 and padding: 0.
  • ::marker lets you color or replace bullets and numbers.
  • Style link states in LVHA order: :link, :visited, :hover, :focus-visible, :active.
  • A link can look like a button with padding, a background and display: inline-block.

Open this lesson on its own page ↗

Module 6 of 17

Intermediate: The Cascade and Advanced Selectors

Lesson 6.1 5 min read

CSS Specificity, Cascade and Inheritance Explained

CSS specificity, the cascade and inheritance are the three rules the browser uses to decide which style wins when two rules target the same element. Learn them and "why is my CSS not working?" becomes easy to answer.

What is the cascade in CSS?

Sometimes two or more rules try to set the same property on the same element. For example, one rule says a heading is blue and another says it is red. The browser cannot use both, so it needs a way to pick a winner. That process is called the cascade (the "C" in CSS stands for "Cascading").

The browser checks these things, in this order:

  1. Origin and importance – styles you write beat the browser's default styles. A declaration marked !important jumps ahead of normal ones.
  2. Specificity – the more specific selector wins. An ID is more specific than a class, and a class is more specific than a tag name.
  3. Source order – if specificity is equal, the rule that comes last in the code wins.

Think of it like a talent show with three rounds. If one contestant wins a round, the show stops. Only a tie moves the contest to the next round.

How is CSS specificity calculated?

Specificity is a score with three columns, often written like 0-1-0. Each column counts a different kind of selector. A higher number in a left column always beats any number in a column to its right, so 1-0-0 beats 0-15-0.

SelectorExampleScore (ID-Class-Type)
Type (tag) or pseudo-elementp, h1, ::before0-0-1
Class, attribute or pseudo-class.card, [type="email"], :hover0-1-0
ID#main1-0-0
Two typesnav a0-0-2
Class + typep.intro0-1-1
ID + class + type#main .card h21-1-1
Universal selector*0-0-0
Inline stylestyle="color: red"Beats all selectors

Combinators like >, + and the space do not add any points. Let's see specificity in action:

<article id="post" class="card">
  <h2 class="title">Learning CSS</h2>
</article>
h2 {
  color: black;       /* 0-0-1 */
}

.card h2 {
  color: green;       /* 0-1-1 */
}

.title {
  color: orange;      /* 0-1-0 */
}

#post .title {
  color: purple;      /* 1-1-0 – this wins */
}

The heading is purple. #post .title has an ID, so its score 1-1-0 beats every other rule. Notice that .card h2 (0-1-1) beats .title (0-1-0) even though .title comes later, because specificity is checked before source order.

Tip: Keep specificity low and flat. If most of your selectors are a single class like .card or .button, they are easy to override later, and you will rarely fight the cascade.

When does source order decide?

When two selectors have the same score, the one written later wins. This is why you often put general styles at the top of a file and special cases below.

.button {
  background: #2563eb;
  color: white;
}

.button {
  background: #16a34a;
}

Both selectors score 0-1-0, so the second rule wins and the button is green. The color: white from the first rule still applies, because the second rule does not set color. The cascade works property by property, not rule by rule.

How does inheritance work in CSS?

Inheritance means some properties pass down from a parent element to its children, like eye color passing from parents to kids. Most text properties inherit: color, font-family, font-size, line-height and text-align. Most box properties do not: margin, padding, border, width and background.

body {
  font-family: system-ui, sans-serif;
  color: #1f2937;
}

Every paragraph, list and heading inside body now uses this font and color, without you writing a rule for each one. An inherited value is always weaker than any rule that targets the element directly, even a plain p selector.

You can control inheritance with four special keywords that work on any property:

  • inherit – take the parent's value, even for properties that normally don't inherit.
  • initial – reset to the property's default value from the CSS specification.
  • unset – act like inherit for inherited properties and like initial for the rest.
  • revert – go back to the browser's default style for that element.
.card {
  border: 2px solid #e5e7eb;
}

.card .inner {
  border: inherit;    /* copies the parent's border */
}

a {
  color: inherit;     /* links match the text around them */
}

The a { color: inherit; } trick is very common. Links stop being default blue and take the color of the paragraph or menu they sit in.

Why you should avoid !important

Adding !important after a value makes that declaration beat normal declarations, no matter the specificity:

.alert {
  color: red !important;
}

It looks like an easy fix, but it creates a new problem. The only way to override an !important rule is with another !important rule, and soon your stylesheet becomes a battle that nobody can follow.

Common mistake: Using !important to "fix" a style that won't apply. Instead, open your browser's developer tools (right-click, then Inspect). The Styles panel shows every rule for the element, with the losing values crossed out. This tells you exactly which rule is winning and why.

There are a few fair uses, such as small utility classes like .hidden { display: none !important; } that must always work. Use it on purpose, never as a habit. You can read more on MDN's specificity page.

Practice exercise

Create a <ul class="menu"> with three <li class="item"> elements. Write three rules that set the color of the items: one using li, one using .item, and one using .menu li. Predict which color wins before you check in the browser. Then add a fourth rule that beats all three without using an ID or !important.

Hint: .menu li scores 0-1-1. A selector like .menu .item scores 0-2-0, which is higher because the class column counts first.

Key takeaways

  • The cascade picks a winner by checking importance, then specificity, then source order.
  • Specificity is scored in three columns: IDs, then classes/attributes/pseudo-classes, then types.
  • When specificity is equal, the rule written last wins.
  • Text properties like color and font-family inherit; box properties like margin do not.
  • Avoid !important; use developer tools to find out why a rule is losing.

Frequently asked questions

Does a class always beat an element selector?

Yes. One class (0-1-0) beats any number of type selectors (0-0-1, 0-0-2 and so on), because the class column is further left.

Do inline styles beat IDs?

Yes. A style attribute in the HTML beats any selector in your stylesheet. Only an !important declaration in the stylesheet can override it, which is one more reason to avoid inline styles.

What does :where() do to specificity?

The :where() pseudo-class always has a specificity of zero, so anything inside it is very easy to override. It is useful for writing default styles in a reset or a library.

Open this lesson on its own page ↗

Module 7 of 17

Intermediate: Positioning and Layering

Lesson 7.1 5 min read

CSS Position: static, relative, absolute, fixed and sticky

The CSS position property controls where an element sits on the page. Learn static, relative, absolute, fixed and sticky with real examples: a card badge, a sticky header and a back-to-top button.

What does the position property do?

By default, the browser places elements in the normal flow: block elements stack from top to bottom, and inline elements sit side by side like words in a sentence. The position property lets you take an element and move it somewhere else, or pin it in place.

position has five values. After you set one, you move the element with the offset properties: top, right, bottom and left. The shorthand inset sets all four at once, just like margin: inset: 0 means top: 0; right: 0; bottom: 0; left: 0.

ValueStays in normal flow?Positioned relative to...
staticYesNot positioned (offsets do nothing)
relativeYesIts own normal position
absoluteNoThe nearest positioned ancestor
fixedNoThe browser window (viewport)
stickyYesIts scroll container, once it reaches the offset

static and relative positioning

position: static is the default for every element. The element sits in the normal flow, and top, left and the other offsets are ignored.

position: relative keeps the element in the normal flow, but lets you nudge it away from where it would normally be. The space it originally took stays reserved, so other elements don't move.

.price-tag {
  position: relative;
  top: -4px;
  left: 10px;
}

The element moves 4 pixels up and 10 pixels to the right, but the elements around it act as if it never moved. Nudging is rarely needed, though. The most important job of position: relative is to become the anchor for absolutely positioned children, as you will see next.

How to use position: absolute (badge on a card)

position: absolute removes the element from the normal flow. Other elements behave as if it does not exist, and it can sit on top of them. Its offsets are measured from its containing block: the nearest ancestor that has a position other than static. If there is none, it uses the whole page.

Think of a sticker on a photo frame. The frame (position: relative) is the anchor, and the sticker (position: absolute) is placed in a corner of that frame.

<div class="product-card">
  <span class="badge">Sale</span>
  <img src="https://picsum.photos/400/250" alt="Running shoes">
  <h3>Running Shoes</h3>
  <p>$59.00</p>
</div>
.product-card {
  position: relative;
  width: 280px;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 12px;
  font-family: system-ui, sans-serif;
}

.product-card img {
  width: 100%;
  border-radius: 8px;
  display: block;
}

.badge {
  position: absolute;
  top: 20px;
  right: 20px;
  background: #dc2626;
  color: white;
  font-size: 0.8rem;
  font-weight: bold;
  padding: 4px 10px;
  border-radius: 999px;
}

A red "Sale" badge appears over the top-right corner of the product image. Because the card has position: relative, the badge's top: 20px; right: 20px is measured from the card's corner, not the page's corner.

Common mistake: Forgetting position: relative on the parent. The absolute element then looks for the next positioned ancestor, often finds none, and jumps to the corner of the whole page. If your badge "flies away", check the parent first.

position: fixed and position: sticky

position: fixed also removes the element from the flow, but it is positioned relative to the viewport (the visible part of the browser window). It stays in the same spot on the screen while the page scrolls. A back-to-top button is a perfect example.

position: sticky is a mix. The element scrolls normally until it reaches the offset you set, like top: 0, and then it sticks there while its parent is still on screen. A sticky header is the classic use.

<header class="site-header">CodeCraft</header>
<main class="content">
  <p>Scroll down to see the header stick and the button stay in place.</p>
  <!-- Add a lot of content here so the page can scroll -->
</main>
<a href="#" class="back-to-top" aria-label="Back to top">↑</a>
.site-header {
  position: sticky;
  top: 0;
  background: #1f2937;
  color: white;
  padding: 16px 24px;
  font-weight: bold;
}

.content {
  min-height: 200vh;
  padding: 24px;
}

.back-to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  width: 48px;
  height: 48px;
  line-height: 48px;
  text-align: center;
  background: #2563eb;
  color: white;
  border-radius: 50%;
  text-decoration: none;
  font-size: 1.4rem;
}

When you scroll down, the dark header moves up until it touches the top of the window, then it stays there. The round blue button sits in the bottom-right corner the whole time. Clicking it jumps back to the top, because href="#" links to the top of the page.

Note: position: sticky needs an offset like top: 0, or it behaves like relative. It also stops working if a parent element has overflow: hidden or overflow: auto, because then that parent becomes the scroll container.

Tip: A fixed or sticky header can cover content when you jump to an anchor link. Add scroll-margin-top: 80px; to your section headings so they land below the header.

Practice exercise

Build a profile card with a round avatar image. Use position: absolute to place a small green "online" dot on the bottom-right edge of the avatar. Then add a fixed "Chat with us" button to the bottom-left corner of the page.

Hint: wrap the avatar <img> in a <div> with position: relative and display: inline-block, then place the dot inside that <div>.

Key takeaways

  • static is the default; offsets like top and left have no effect on it.
  • relative nudges an element and, more importantly, becomes the anchor for absolute children.
  • absolute leaves the normal flow and is positioned from its nearest positioned ancestor.
  • fixed stays in one spot on the screen; sticky scrolls until it hits its offset, then sticks.
  • inset is a shorthand for top, right, bottom and left.

Frequently asked questions

What is the difference between absolute and relative position?

A relative element stays in the normal flow and moves from its own normal spot. An absolute element leaves the flow completely and is placed from the corner of its nearest positioned ancestor.

Why is my position: sticky not working?

The three usual reasons are: you forgot an offset like top: 0, a parent has overflow set to hidden, auto or scroll, or the parent is the same height as the sticky element, so there is no room to stick.

How do I center an absolutely positioned element?

Give it inset: 0 and margin: auto with a set width and height. The element is then centered inside its positioned parent.

Open this lesson on its own page ↗

Module 8 of 17

Intermediate: Flexbox Layout

Lesson 8.1 5 min read

CSS Flexbox Tutorial: The Basics of display: flex

CSS Flexbox is a layout tool for arranging items in a row or a column. In this flexbox tutorial you learn display: flex, the main and cross axis, flex-direction and gap.

What is CSS Flexbox?

Flexbox (short for "Flexible Box Layout") is a way to lay out items in one direction: either a row or a column. It makes common jobs easy that used to be hard: putting items side by side, spacing them evenly, making them the same height, and centering things.

Flexbox always works with two roles:

  • The flex container – the parent element. You turn it on with display: flex.
  • The flex items – the direct children of that parent. They are arranged automatically.

Think of a bookshelf. The shelf is the container, and the books are the items. You decide how the shelf arranges its books; you usually don't move each book by hand.

Note: Only direct children become flex items. Grandchildren (elements inside the items) are not affected, unless you also make their parent a flex container.

How to use display: flex

Let's start with three feature boxes. Without flexbox, <div> elements stack on top of each other, because they are block elements.

<section class="features">
  <div class="feature">Fast</div>
  <div class="feature">Free</div>
  <div class="feature">Friendly</div>
</section>
.features {
  display: flex;
  gap: 16px;
  padding: 16px;
  background: #f3f4f6;
}

.feature {
  background: white;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  padding: 20px;
  font-family: system-ui, sans-serif;
}

With one line, display: flex, the three boxes now sit side by side in a row. Each box is only as wide as its content, and all boxes have the same height. The gap property adds 16 pixels of space between the items, but not on the outside edges.

You can also use display: inline-flex. It works the same inside, but the container itself acts like an inline element, so it only takes the width it needs.

Main axis vs cross axis

This is the most important idea in flexbox. A flex container has two axes (directions):

  • The main axis is the direction the items flow in. By default it goes left to right, as a row.
  • The cross axis runs at a right angle to the main axis. In a row, it goes top to bottom.
flex-directionMain axisCross axis
row (default)Left to rightTop to bottom
row-reverseRight to leftTop to bottom
columnTop to bottomLeft to right
column-reverseBottom to topLeft to right

Why does this matter? Almost every flexbox property works along one of these axes. justify-content works on the main axis, and align-items works on the cross axis. When you switch from row to column, the axes switch too. You will learn alignment in detail in the next lesson.

Note: In right-to-left languages like Arabic, row flows from right to left. Flexbox follows the writing direction of the page, which is why it talks about "start" and "end" rather than "left" and "right".

Changing direction with flex-direction

flex-direction sets which way the main axis goes. Here is a simple sidebar menu that stacks its links in a column:

<nav class="sidebar">
  <a href="#">Dashboard</a>
  <a href="#">My courses</a>
  <a href="#">Certificates</a>
  <a href="#">Settings</a>
</nav>
.sidebar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 200px;
  padding: 12px;
  background: #1f2937;
  border-radius: 12px;
}

.sidebar a {
  color: #e5e7eb;
  text-decoration: none;
  padding: 10px 14px;
  border-radius: 8px;
}

.sidebar a:hover {
  background: #374151;
}

The links are inline elements, but as flex items they each become a block-like box. With flex-direction: column they stack from top to bottom, each one as wide as the sidebar, with 4 pixels between them. Each link gets a soft highlight on hover.

Common mistake: Using margins on every item to create space, then fighting the extra margin on the last item. Use gap on the container instead. It only adds space between items. You can also set two values: gap: 20px 12px; means 20px between rows and 12px between columns.

Tip: Your browser's developer tools have a flexbox inspector. Click the small "flex" badge next to the container in the Elements panel, and the browser draws the items and gaps on the page.

Practice exercise

Build a "pricing" row with three plan cards (Free, Pro, Team) side by side, using display: flex and gap. Then change the container to flex-direction: column and see how the axes switch.

Hint: change row to column in your CSS and reload the page. Notice that the cards now stretch to the full width, because the cross axis is now horizontal.

Key takeaways

  • display: flex turns a parent into a flex container; its direct children become flex items.
  • Flexbox lays out items in one direction: a row or a column.
  • The main axis follows flex-direction; the cross axis runs across it.
  • flex-direction can be row, row-reverse, column or column-reverse.
  • Use gap for space between items instead of margins.

Frequently asked questions

Is Flexbox better than CSS Grid?

Neither is better; they do different jobs. Flexbox is great for one direction, like a navbar or a row of buttons. Grid is great for two directions, rows and columns at once, like a full page layout. Most sites use both.

Does gap work with flexbox in all browsers?

Yes. The gap property has worked with flexbox in all modern browsers for several years, so you can use it safely.

Why are my flex items all the same height?

That is the default behaviour. Flex items stretch to fill the cross axis, because align-items starts as stretch. You can change it with align-items: flex-start.

Open this lesson on its own page ↗

Module 9 of 17

Intermediate: CSS Grid Layout

Lesson 9.1 5 min read

CSS Grid Tutorial: Rows, Columns and the fr Unit

This CSS Grid tutorial shows you how to build layouts with rows and columns using grid-template-columns, the fr unit, repeat(), gap, and how to place items with grid-column and span.

What is CSS Grid?

CSS Grid is a layout system for two directions at once: rows and columns. Flexbox arranges items in one line; Grid arranges them in a table-like structure, where you decide the size of every column and row.

Think of a chocolate bar or a sheet of graph paper. You draw the lines first, then you put items into the squares.

Like Flexbox, Grid has a parent and children:

  • The grid container – the parent, with display: grid.
  • The grid items – its direct children.

A few new words:

  • Grid lines – the invisible lines between columns and rows. They are numbered from 1.
  • Grid track – one column or one row.
  • Grid cell – one square where a row and a column meet.

How to create columns and rows with grid-template-columns

grid-template-columns sets how many columns you want and how wide each one is. Each value is one column. grid-template-rows does the same for rows, but you often skip it and let rows be as tall as their content.

<section class="dashboard">
  <div class="stat">Students: 1,240</div>
  <div class="stat">Courses: 18</div>
  <div class="stat">Lessons: 312</div>
  <div class="stat">Projects: 45</div>
  <div class="stat">Quizzes: 96</div>
  <div class="stat">Hours: 870</div>
</section>
.dashboard {
  display: grid;
  grid-template-columns: 200px 200px 200px;
  gap: 16px;
  padding: 16px;
}

.stat {
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 10px;
  padding: 20px;
  font-family: system-ui, sans-serif;
  font-weight: 600;
}

You see three columns, each 200 pixels wide, and the six stats fill them row by row: three on the first row and three on the second. The browser creates the second row automatically. gap adds space between rows and columns, just like in Flexbox.

What is the fr unit in CSS Grid?

Fixed pixel widths don't adapt to the screen. The fr unit (short for "fraction") means "a share of the free space". It is like cutting a cake: 1fr 1fr 1fr gives three equal slices, and 2fr 1fr gives one slice twice as big as the other.

  • grid-template-columns: 1fr 1fr 1fr; – three equal columns.
  • grid-template-columns: 2fr 1fr; – the first column is twice as wide as the second.
  • grid-template-columns: 250px 1fr; – a fixed 250px sidebar, and the rest for the content.

Writing 1fr many times gets boring, so CSS has repeat(). repeat(4, 1fr) is the same as 1fr 1fr 1fr 1fr. You can mix it too: repeat(2, 1fr) 300px.

Tip: fr shares the space that is left after the gaps and fixed sizes. So with gap, you never need to do maths like calc(33.33% - 16px). Grid does it for you.

Common mistake: Using percentages like 33.33% 33.33% 33.33% together with gap. The gaps are added on top, so the grid becomes wider than its container and overflows. Use 1fr instead.

Placing items with grid-column, grid-row and span

By default, items fill the cells one by one. You can also tell an item exactly where to go, using the numbered grid lines. A grid with three columns has four vertical lines: 1, 2, 3 and 4.

  • grid-column: 1 / 3; – start at line 1 and end at line 3, so the item covers two columns.
  • grid-column: span 2; – cover two columns, starting wherever the item would be.
  • grid-column: 1 / -1; – from the first line to the last line, the full width. Negative numbers count from the end.
  • grid-row: span 2; – cover two rows.

Here is a magazine-style news layout:

<section class="news">
  <article class="story big">Main story: New CSS features in 2026</article>
  <article class="story">Grid tips</article>
  <article class="story">Flexbox tricks</article>
  <article class="story wide">Weekly coding challenge: build a photo gallery</article>
  <article class="story">Q and A</article>
</section>
.news {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 140px;
  gap: 16px;
  padding: 16px;
}

.story {
  background: #f3f4f6;
  border-radius: 10px;
  padding: 16px;
  font-family: system-ui, sans-serif;
}

.big {
  grid-column: span 2;
  grid-row: span 2;
  background: #fde68a;
  font-size: 1.5rem;
  font-weight: bold;
}

.wide {
  grid-column: 1 / -1;
  background: #dbeafe;
}

The main story is a big yellow block that covers two columns and two rows. The two small stories stack on the right side of it. The coding challenge stretches across the full width below, and the "Q and A" story starts a new row. grid-auto-rows: 140px makes every automatically created row 140 pixels tall.

Note: grid-auto-rows sets the height of rows you didn't define yourself. minmax(140px, auto) is a safer value, because it lets a row grow taller if the content needs more room.

Practice exercise

Build a photo grid with four equal columns. Make the first photo span two columns and two rows, and make one photo near the end span the full width as a banner.

Hint: use grid-template-columns: repeat(4, 1fr);, then grid-column: span 2; grid-row: span 2; for the first photo and grid-column: 1 / -1; for the banner. Add width: 100%; height: 100%; object-fit: cover; to the images so they fill their cells.

Key takeaways

  • display: grid creates a grid container; its direct children become grid items.
  • grid-template-columns and grid-template-rows set the size of each track.
  • The fr unit shares the free space; repeat(3, 1fr) makes three equal columns.
  • gap adds space between rows and columns without any maths.
  • Use grid-column and grid-row with line numbers or span to place and size items.

Open this lesson on its own page ↗

Module 10 of 17

Intermediate: Responsive Web Design

Lesson 10.1 5 min read

Responsive Web Design Basics and Mobile-First CSS

Responsive web design means your website adapts to every screen, from phones to big monitors. Learn the viewport meta tag, fluid layouts, flexible images and mobile-first CSS.

What is responsive web design?

People visit websites on phones, tablets, laptops and huge TVs. Responsive web design means building one website that adapts its layout to any screen size, instead of building a separate "mobile site". Think of water: pour it into a glass, a cup or a bottle, and it takes the shape of each one.

Responsive design rests on four ideas:

  1. The viewport meta tag, so phones show your page at the right size.
  2. Fluid layouts that use flexible sizes instead of fixed pixel widths.
  3. Flexible images that never get wider than their container.
  4. Media queries, to change the layout at certain screen sizes (the next lesson).

Why you need the viewport meta tag

Phones have a trick from the early days of the mobile web. When a page has no viewport tag, the phone pretends its screen is about 980 pixels wide, draws the page at that size, then shrinks everything to fit. The text becomes tiny, and your media queries don't work as expected.

The viewport is the visible area of the page in the browser. This one line in the <head> tells the phone to use its real width:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My Responsive Page</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Hello, every screen!</h1>
</body>
</html>

width=device-width makes the page as wide as the device's screen, and initial-scale=1 starts at normal zoom, with no zooming in or out.

Common mistake: Adding maximum-scale=1 or user-scalable=no to stop users from zooming. This is bad for accessibility, because people with low vision need to zoom. Keep the viewport tag exactly as shown above.

Fluid layouts and flexible images

A fluid layout uses sizes that can stretch and shrink, like percentages, fr, flex and viewport units, instead of fixed pixel widths. The most useful pattern combines width and max-width:

* {
  box-sizing: border-box;
}

.container {
  width: 100%;
  max-width: 1100px;
  margin-inline: auto;
  padding-inline: 16px;
}

img,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

h1 {
  font-size: clamp(1.75rem, 4vw + 1rem, 3rem);
}

The .container takes the full width on small screens, but never gets wider than 1100 pixels on big screens, where it is centered by margin-inline: auto. The 16px padding stops text from touching the screen edges on phones.

The image rule is one every website should have. max-width: 100% means an image can be smaller than its container, but never bigger, so a large photo never breaks your layout on a phone. height: auto keeps its proportions.

The heading uses clamp(min, preferred, max): its size grows with the screen width (4vw means 4% of the viewport width), but it never gets smaller than 1.75rem or bigger than 3rem. This is called fluid typography.

Tip: Use rem for font sizes, not px. If a user sets a bigger default font size in their browser, rem sizes respect that choice.

What is mobile-first CSS?

Mobile-first means you write your basic CSS for small screens first, then add styles for bigger screens on top. It is like packing for a trip: start with the things you really need, then add extras if there is room.

Why mobile-first?

  • Small screens usually need simpler layouts: everything stacks in one column, which is mostly the normal flow. So your base CSS is short.
  • Phones load less CSS that they don't need.
  • It forces you to focus on the most important content first.

Here is a product section written mobile-first:

<section class="product">
  <img src="https://picsum.photos/id/26/800/600" alt="Leather backpack">
  <div class="product-info">
    <h2>City Backpack</h2>
    <p>A light, water-resistant backpack with room for a 15-inch laptop.</p>
    <button>Add to cart – $79</button>
  </div>
</section>
/* Base styles: phones first */
.product {
  display: grid;
  gap: 16px;
  padding: 16px;
  font-family: system-ui, sans-serif;
}

.product img {
  max-width: 100%;
  height: auto;
  border-radius: 12px;
}

.product button {
  width: 100%;
  padding: 14px;
  font-size: 1rem;
}

/* Bigger screens: add a second column */
@media (width >= 768px) {
  .product {
    grid-template-columns: 1fr 1fr;
    align-items: center;
    gap: 40px;
  }

  .product button {
    width: auto;
  }
}

On a phone, the image sits on top, the text below it, and the button stretches across the full width so it's easy to tap. On a screen 768 pixels or wider, the image and text sit side by side in two equal columns, and the button returns to its normal size. The @media rule only adds changes; it never undoes a complicated desktop layout.

Note: You can test responsive pages in your browser without a phone. Open developer tools and click the "device toolbar" icon (it looks like a phone and tablet). You can then pick a device or drag the screen width.

Practice exercise

Take any simple page you have built before. Add the viewport meta tag, wrap the content in a .container with a max-width and side padding, and add the img { max-width: 100%; height: auto; } rule. Then test it at 375px, 768px and 1280px wide in the device toolbar.

Hint: look for anything that causes a sideways scrollbar on the small size. Fixed width values in pixels are usually the cause; try changing them to max-width.

Key takeaways

  • Responsive web design makes one site adapt to every screen size.
  • Always add <meta name="viewport" content="width=device-width, initial-scale=1">.
  • Use width: 100% with max-width for fluid containers, and max-width: 100% for images.
  • clamp() creates font sizes that grow with the screen but stay within limits.
  • Mobile-first means base styles for phones, then min-width media queries for bigger screens.

Open this lesson on its own page ↗

Module 11 of 17

Intermediate: Floats and Visual Effects

Lesson 11.1 5 min read

CSS Float and Clear: The Old Layout Method Explained

CSS float lets text wrap around an image, like in a magazine. Learn how float and clear work, how to fix collapsed parents, and why layouts now use Flexbox and Grid.

What was CSS float made for?

The float property pushes an element to the left or right side of its container, and lets the text that follows flow around it. Think of a photo in a newspaper article: the photo sits on one side, and the words wrap around it. That is exactly what float was designed for.

<article class="post">
  <img class="post-photo" src="https://picsum.photos/200/150" alt="A mountain lake at sunrise">
  <p>Last summer we walked for three days in the mountains. Every morning the lake was calm and silver, and the air was cold and fresh. We cooked breakfast on a small stove and watched the sun climb over the hills. It was the most peaceful trip of our lives, and we can't wait to go back next year with more friends.</p>
</article>
.post {
  max-width: 600px;
  font-family: Georgia, serif;
  line-height: 1.6;
}

.post-photo {
  float: left;
  margin: 0 16px 8px 0;
  border-radius: 8px;
}

The photo moves to the left, and the paragraph wraps around its right side and then continues underneath it. The margin keeps the text from touching the image. Use float: right to put the image on the other side, and float: none to cancel a float.

How does clear work?

Sometimes you want an element to start below a floated element, not next to it. The clear property does that. clear: left means "don't sit next to anything floated left", clear: right does the same for the right side, and clear: both clears both.

.post h2 {
  clear: both;
}

Now each new heading in the article starts below the photo, even if the paragraph above was too short to get past it.

The collapsed parent problem: clearfix and display: flow-root

Floated elements are taken partly out of the normal flow. Their parent no longer "sees" their height. So if a box contains only a floated image and a short text, the box can end up shorter than the image, and its border or background stops halfway through the picture. This is called a collapsed parent.

<div class="card">
  <img class="card-img" src="https://picsum.photos/120/120" alt="Profile photo of Sara">
  <p>Sara is a web designer from Lisbon.</p>
</div>
.card {
  border: 2px solid #2563eb;
  padding: 12px;
  display: flow-root;
}

.card-img {
  float: left;
  margin-right: 12px;
}

Without display: flow-root, the blue border wraps only the short text, and the image sticks out below it. With it, the card grows to contain the whole image. flow-root creates a new block formatting context, which is a fancy way of saying "this box contains its floats completely".

Before flow-root existed, developers used the clearfix trick. You will still see it in older code:

.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

It adds an invisible element at the end of the box that clears the floats, which forces the box to stretch around them. Today, display: flow-root does the same job in one line.

Note: Setting overflow: hidden on the parent also contains floats, and you will see it in old tutorials. But it can cut off shadows and dropdowns, so prefer display: flow-root.

Why we now use Flexbox and Grid for layout

For about ten years, developers built whole page layouts with floats: columns, sidebars, card grids. It worked, but it was fragile. Columns had different heights, you needed clearfixes everywhere, and vertical centering was almost impossible. Float was never meant for this.

Flexbox and Grid were made for layout. They handle equal heights, spacing with gap, alignment and reordering easily. So the modern rule is simple:

  • Use float for wrapping text around an image or a pull quote.
  • Use Flexbox for rows or columns of items, like a navbar.
  • Use Grid for two-dimensional layouts, like a full page or a gallery.

Common mistake: Using float: left on cards to put them in a row. Floats and Flexbox don't mix: on a flex item, float is simply ignored. Use display: flex or display: grid on the parent instead.

Wrapping text around shapes with shape-outside

By default, text wraps around a float's rectangle. shape-outside lets text follow a different shape, like a circle. It only works on floated elements.

.round-photo {
  float: left;
  width: 150px;
  height: 150px;
  border-radius: 50%;
  shape-outside: circle(50%);
  margin-right: 16px;
}

The photo looks round because of border-radius, and now the text wraps along the curve too, instead of leaving square empty corners. It is a small detail that makes articles feel designed. More shapes are explained on MDN's shape-outside page.

Practice exercise

Write a short recipe with a floated food photo on the right, two paragraphs, and an "Ingredients" heading that always starts below the photo. Put the whole recipe in a box with a border and make sure the border contains the image.

Hint: use float: right on the image, clear: both on the heading, and display: flow-root on the box.

Key takeaways

  • float was made to let text wrap around images and quotes.
  • clear makes an element start below floated elements.
  • Use display: flow-root to stop a parent from collapsing; the clearfix is the old way.
  • Use Flexbox and Grid for layout, not floats.
  • shape-outside lets text wrap around circles and other shapes.

Open this lesson on its own page ↗

Module 12 of 17

Intermediate: CSS Components and UI Patterns

Lesson 12.1 6 min read

CSS Buttons: Styles, Sizes, Colors and Hover Effects

CSS buttons guide: reset the default look, add sizes, colors, outline and pill styles, icon buttons, button groups, hover and focus states, and a loading state.

How to reset the default button style

You met a simple button with hover, focus and disabled states in the Styling Forms and Buttons lesson. In this lesson we go further and build a small button system: one base class plus extra classes for size, color and shape. It works like LEGO: one base brick, and you snap extra pieces on top.

Every browser draws <button> a little differently. So we start with a base class that removes the browser look and sets the parts every button shares.

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding: 0.6em 1.2em;
  border: 2px solid transparent;
  border-radius: 8px;
  background: #2563eb;
  color: white;
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
}
<button class="btn" type="button">Save changes</button>

You see a blue button with white bold text and soft corners. display: inline-flex lets us put an icon and text side by side later, and gap puts space between them. The transparent border is a small trick: outline buttons will have a visible border, and because every button already has a 2px border, all buttons stay exactly the same height.

Tip: Use em units for padding. Then the padding grows and shrinks with the font size, so a size class only needs to change font-size.

Button sizes, colors and shapes

Now we add modifier classes. A modifier is a small extra class that changes one thing, like size or color. You write two classes in the HTML: class="btn btn-lg".

/* Sizes */
.btn-sm { font-size: 0.875rem; }
.btn-lg { font-size: 1.25rem; }
.btn-block { display: flex; width: 100%; }

/* Colors */
.btn-success { background: #16a34a; }
.btn-danger  { background: #dc2626; }
.btn-dark    { background: #1f2937; }

/* Outline and ghost */
.btn-outline {
  background: transparent;
  border-color: #2563eb;
  color: #2563eb;
}
.btn-ghost {
  background: transparent;
  color: #2563eb;
}

/* Shapes */
.btn-pill { border-radius: 999px; }
.btn-square { border-radius: 0; }
<button class="btn btn-sm" type="button">Small</button>
<button class="btn" type="button">Normal</button>
<button class="btn btn-lg btn-pill" type="button">Large pill</button>
<button class="btn btn-danger" type="button">Delete</button>
<button class="btn btn-outline" type="button">Cancel</button>
<button class="btn btn-ghost" type="button">Skip</button>

You see a row of buttons. The small and large ones keep the same shape because the padding is in em. The pill button has fully round ends, because a huge border-radius like 999px always makes a perfect pill. The outline button has a colored border and no fill, and the ghost button has no border and no fill at all, only colored text. Outline and ghost buttons are great for less important actions, like "Cancel", next to a strong main button.

ClassWhat it changesGood for
.btn-sm / .btn-lgFont size, so the whole button scalesTables, hero sections
.btn-blockFull widthMobile forms
.btn-success, .btn-dangerBackground colorConfirm, delete
.btn-outlineBorder only, no fillSecond action
.btn-ghostText onlyThird action, toolbars
.btn-pillFully rounded endsFriendly, modern look

Hover, active, focus-visible and disabled states

A button talks to the user through its states. Hover says "you can click me", active says "you are pressing me", focus says "the keyboard is here", and disabled says "not right now".

.btn:hover { background: #1d4ed8; }
.btn-success:hover { background: #15803d; }
.btn-danger:hover { background: #b91c1c; }
.btn-outline:hover,
.btn-ghost:hover { background: #eff6ff; }

.btn:active { transform: translateY(1px) scale(0.98); }

.btn:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

Hover each button: the colored ones get darker, the outline and ghost ones get a light blue background. When you press, the button shrinks a tiny bit, like a real button pushed into the page. Press Tab and an orange ring appears only for keyboard users, thanks to :focus-visible. Disabled buttons fade out and stop reacting.

Common mistake: Giving .btn:hover a color but forgetting each color variant. Then a red Delete button turns blue on hover. Give every color class its own hover color, or use a CSS variable for the color (you will learn variables in the Advanced module).

Icon buttons, button groups and a loading state

An icon button shows only an icon, like a close "X" or a heart. Because there is no visible text, you must add aria-label so screen readers can say what the button does.

<button class="btn btn-icon" type="button" aria-label="Close">
  <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
    <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
  </svg>
</button>

<div class="btn-group" role="group" aria-label="Text alignment">
  <button class="btn btn-outline" type="button">Left</button>
  <button class="btn btn-outline" type="button">Center</button>
  <button class="btn btn-outline" type="button">Right</button>
</div>

<button class="btn is-loading" type="button" aria-busy="true">Sending</button>
.btn-icon {
  padding: 0.5em;
  border-radius: 50%;
}

.btn-group {
  display: inline-flex;
}
.btn-group .btn {
  border-radius: 0;
}
.btn-group .btn + .btn {
  margin-left: -2px; /* borders overlap instead of doubling */
}
.btn-group .btn:first-child { border-radius: 8px 0 0 8px; }
.btn-group .btn:last-child  { border-radius: 0 8px 8px 0; }

.is-loading {
  pointer-events: none;
  opacity: 0.8;
}
.is-loading::after {
  content: "";
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

The icon button is a blue circle with a white X. The SVG uses currentColor, so the icon always matches the text color. The button group shows three joined buttons with one shared border and rounded corners only at the outer ends. The loading button shows "Sending" with a small spinning circle next to it. The circle is a border with one side transparent, rotated forever. You will learn @keyframes properly in the Animations lesson.

Sometimes a link should look like a button, for example a "Sign up" link in a hero section. Our .btn class already has text-decoration: none and display: inline-flex, so it works on <a> too.

<a class="btn btn-lg btn-pill" href="/signup">Start free trial</a>
<button class="btn btn-outline btn-lg btn-pill" type="submit">Send form</button>

Both look the same: large pill buttons. The rule for choosing the tag is simple. If it goes to another page, use <a href>. If it does something on this page (submit, open, delete), use <button>. Screen readers and keyboards treat them differently, so the right tag matters even when the look is the same.

Note: A link has no disabled attribute. That is why our disabled rule also matches [aria-disabled="true"]. For a real "not available" link, it is often better to remove the href or not show the link at all.

Practice exercise

Build a small "pricing card" footer with three buttons: a large green pill "Buy now" button, an outline "Compare plans" link styled as a button, and a round icon button with a heart. Then add a "Processing" loading version of the Buy button.

Hint: start from the .btn base class, then combine modifiers like class="btn btn-lg btn-pill btn-success". Remember aria-label="Add to favorites" on the heart button.

Key takeaways

  • Build buttons from one .btn base class plus small modifier classes for size, color and shape.
  • Use em padding so size classes only need to change font-size, and a transparent border so all buttons share the same height.
  • Style every state: :hover, :active, :focus-visible and :disabled (or aria-disabled).
  • Icon-only buttons need aria-label; loading buttons can show a CSS spinner with ::after.
  • Use <a> to go somewhere and <button> to do something, even when they look the same.

Open this lesson on its own page ↗

Module 13 of 17

Advanced: CSS Variables and Modern Functions

Lesson 13.1 5 min read

CSS Variables (Custom Properties) Explained

CSS variables (custom properties) let you store a value like a color once and reuse it everywhere with var(). Change it in one place and your whole site updates.

What are CSS variables?

A CSS variable is a named box that holds a value. You give the box a name, put a value inside, and then use that name wherever you need the value. The official name is custom property, but almost everyone says "CSS variable".

Think of it like a contact in your phone. You do not type your friend's number every time. You save it once as "Sam" and tap "Sam". If Sam gets a new number, you update one contact, not every message.

A variable name always starts with two dashes: --brand-color. You read the value with the var() function.

:root {
  --brand-color: #4f46e5;
  --radius: 8px;
}

.button {
  background: var(--brand-color);
  border-radius: var(--radius);
  color: white;
  padding: 0.75rem 1.25rem;
  border: none;
}

.card {
  border: 2px solid var(--brand-color);
  border-radius: var(--radius);
  padding: 1rem;
}

Here the button and the card both use the same purple and the same rounded corners. If you change --brand-color to green, both turn green at once.

The :root selector matches the <html> element, the top of the page. Variables defined there are available to every element, so :root is the usual place for global variables.

Note: Variable names are case-sensitive. --Brand and --brand are two different variables.

How do you use var() with a fallback value?

The var() function can take a second value, called a fallback. The browser uses it when the variable is not defined.

.alert {
  background: var(--alert-bg, #fef3c7);
  color: var(--alert-text, #92400e);
  padding: 1rem;
}

If nobody has set --alert-bg, the alert still gets a soft yellow background. This is useful for components that you share between projects, where you cannot be sure every variable exists.

Common mistake: Writing var(brand-color) or var(-brand-color). You always need both dashes inside var(): var(--brand-color).

Scoping and overriding variables in components

Variables follow the normal cascade and inheritance rules you already know. A variable set on an element is available to that element and all its children. A closer definition wins over one further up the tree.

This means you can give a component its own "settings" and change them for one version only. Here is a button with a small variant system:

<button class="btn">Save</button>
<button class="btn btn--danger">Delete</button>
<button class="btn btn--ghost">Cancel</button>
.btn {
  --btn-bg: #4f46e5;
  --btn-text: #ffffff;
  background: var(--btn-bg);
  color: var(--btn-text);
  border: 2px solid var(--btn-bg);
  padding: 0.6rem 1.2rem;
  border-radius: 6px;
  font: inherit;
}

.btn--danger {
  --btn-bg: #dc2626;
}

.btn--ghost {
  --btn-bg: transparent;
  --btn-text: #4f46e5;
  border-color: #4f46e5;
}

The .btn rule does all the real styling. The variant classes only change variables. You will see a purple button, a red button and an outlined button, but the main rule is written only once.

You can even change variables inside a media query. Set --gap: 1rem on :root, then set --gap: 2rem inside @media (min-width: 800px), and every element that uses var(--gap) grows on wide screens.

Design tokens: building a small design system

Design tokens are the basic design decisions of a project, saved as named values: colors, font sizes, spacing steps, shadows and corner radius. Using variables for tokens keeps a site consistent, because everyone picks from the same short list.

:root {
  /* Colors */
  --color-primary: #4f46e5;
  --color-text: #1f2937;
  --color-muted: #6b7280;
  --color-surface: #ffffff;

  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 2rem;

  /* Type and shape */
  --font-body: system-ui, sans-serif;
  --radius-md: 8px;
  --shadow-sm: 0 1px 3px rgb(0 0 0 / 0.12);
}

body {
  font-family: var(--font-body);
  color: var(--color-text);
  background: var(--color-surface);
}

.profile-card {
  padding: var(--space-4);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

Now nobody writes random values like 13px or #333. They choose a token. This makes big changes, like a rebrand or dark mode, much easier.

Tip: Give tokens names that describe their job, like --color-text, not their look, like --dark-gray. The job stays the same even if the color changes.

You can read more on MDN: Using CSS custom properties.

Practice exercise

Create a .tag component (small rounded labels like "New" or "Sale"). Give it two variables, --tag-bg and --tag-text, with default values. Then add .tag--success and .tag--warning classes that only change the variables.

Hint: Put the defaults inside .tag itself, like the .btn example, and use var() for background and color.

Key takeaways

  • A CSS variable starts with -- and is read with var(--name).
  • var(--name, fallback) uses the fallback if the variable is missing.
  • Define global variables on :root; override them on any element or inside media queries.
  • Variants can change only variables, so the main component rule stays simple.
  • Design tokens are your project's shared values, stored as variables.

Frequently asked questions

What is the difference between CSS variables and Sass variables?

Sass variables are replaced when the code is compiled, so the browser never sees them. CSS variables live in the browser. They follow the cascade and can change at runtime, for example inside a media query or for dark mode.

Can I use CSS variables inside media query conditions?

No. You cannot write @media (min-width: var(--bp)). You can change a variable's value inside a media query, but not use one in the condition itself.

Do CSS variables work in all browsers?

Yes. Every modern browser has supported custom properties for many years, so you can use them safely.

Open this lesson on its own page ↗

Module 14 of 17

Advanced: Transitions, Transforms and Animations

Lesson 14.1 4 min read

CSS Transitions: Smooth Hover Effects

CSS transitions make a style change happen smoothly over time instead of instantly. They are the easiest way to add polished hover effects to buttons and cards.

What is a CSS transition?

Normally, when a style changes, for example on :hover, the change is instant. The button jumps from blue to dark blue in zero seconds. A transition tells the browser to slide between the old value and the new value over a short time.

Think of a light switch versus a dimmer. Without a transition, the light snaps on. With a transition, it fades up.

A transition has four parts:

PropertyWhat it controlsExample
transition-propertyWhich property animatesbackground-color
transition-durationHow long it takes0.3s or 300ms
transition-timing-functionThe speed curveease, linear, ease-in-out
transition-delayWait time before it starts0.1s

Most of the time you use the transition shorthand, in this order: property, duration, timing function, delay.

.link {
  color: #4f46e5;
  transition: color 0.3s ease;
}

.link:hover {
  color: #db2777;
}

When you hover the link, its color fades from purple to pink over 0.3 seconds. When you move the mouse away, it fades back.

Common mistake: Putting transition only on the :hover rule. Then the effect is smooth on the way in but snaps back instantly on the way out. Put the transition on the normal state, so it works in both directions.

Timing functions and multiple transitions

The timing function controls the feel of the movement:

  • ease (the default): starts slowly, speeds up, slows down at the end. Feels natural.
  • linear: the same speed the whole time. Good for colors, but can look robotic for movement.
  • ease-out: starts fast and slows down. Great for things appearing.
  • ease-in: starts slowly and ends fast. Good for things leaving.
  • cubic-bezier(...): your own custom curve. DevTools has an editor to help you draw one.

You can transition several properties at once with commas, each with its own timing:

.box {
  transition: background-color 0.2s ease, transform 0.3s ease-out 0.05s;
}

Here the background changes in 0.2 seconds, and the movement takes 0.3 seconds, starting 0.05 seconds later.

Tip: Avoid transition: all. It animates every property that changes, including ones you did not expect, and it can hurt performance. List the exact properties you want.

Which CSS properties animate well?

Only properties with values "in between" can transition. A color can be halfway between blue and red. A width can be halfway between 100px and 200px. But display: none to display: block has no middle, so it simply switches.

Some properties are also much cheaper for the browser:

  • Best: transform and opacity. The browser can animate these without recalculating the layout, so they stay smooth even on slow phones.
  • OK: color, background-color, box-shadow, border-color. They repaint the element but do not move other things.
  • Avoid for animation: width, height, top, left, margin. Changing these forces the browser to recalculate the layout of the page on every frame.

Button and card hover effects examples

Here is a button that gently lifts and darkens when you hover it:

<button class="cta">Start learning</button>
.cta {
  background: #4f46e5;
  color: white;
  border: none;
  padding: 0.8rem 1.6rem;
  border-radius: 8px;
  font: inherit;
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}

.cta:hover,
.cta:focus-visible {
  background: #4338ca;
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgb(79 70 229 / 0.35);
}

.cta:active {
  transform: translateY(0);
}

The button moves up 2 pixels, gets darker and shows a soft shadow. When you click, it presses back down. We also style :focus-visible, so keyboard users see the same effect.

Now a product card that lifts and zooms its image:

<article class="product-card">
  <div class="product-card__img">
    <img src="https://picsum.photos/400/300" alt="Leather backpack">
  </div>
  <h3>Leather backpack</h3>
  <p>$89.00</p>
</article>
.product-card {
  max-width: 280px;
  background: white;
  border-radius: 12px;
  padding: 1rem;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.product-card__img {
  overflow: hidden;
  border-radius: 8px;
}

.product-card img {
  display: block;
  width: 100%;
  transition: transform 0.4s ease;
}

.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgb(0 0 0 / 0.15);
}

.product-card:hover img {
  transform: scale(1.05);
}

The whole card rises and its shadow grows. At the same time, the image zooms in slightly. Because the image wrapper has overflow: hidden, the zoomed image stays inside its rounded box.

Practice exercise

Create a navigation bar with three links. On hover, each link should get an underline that changes color smoothly, and its background should fade to a light gray. Use a different duration for each property.

Hint: Use text-decoration-color for the underline color, and remember to put transition on the normal a rule, not on a:hover.

Key takeaways

  • A transition animates the change between two values of a property.
  • The shorthand order is: property, duration, timing function, delay.
  • Put transition on the normal state so it works in and out.
  • transform and opacity animate most smoothly; avoid animating layout properties like width or top.
  • List exact properties instead of using transition: all.

Open this lesson on its own page ↗

Module 15 of 17

Advanced: Modern CSS Features

Lesson 15.1 4 min read

Modern CSS Selectors: :has(), :is() and :where()

Modern CSS selectors :has(), :is() and :where() let you style a parent based on its children, shorten long selector lists and write resets that are easy to override.

What does the :has() selector do?

For years, CSS could only look down the tree. You could style a child based on its parent, but not a parent based on its child. :has() changes that. People often call it the "parent selector".

.card:has(img) means "a .card that contains an <img>". The style goes on the card, not on the image.

<article class="card">
  <img src="https://picsum.photos/600/300" alt="Mountain lake">
  <h3>Trip to the lake</h3>
  <p>A short weekend story.</p>
</article>

<article class="card">
  <h3>Quick update</h3>
  <p>No photo this time, just text.</p>
</article>
.card {
  padding: 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  max-width: 320px;
}

.card:has(img) {
  padding: 0 0 1rem;
  border-color: #4f46e5;
}

.card:has(img) h3,
.card:has(img) p {
  padding-inline: 1rem;
}

.card img {
  width: 100%;
  border-radius: 10px 10px 0 0;
}

The card with a photo loses its top and side padding, so the image touches the edges, and gets a purple border. The text-only card keeps normal padding. You did not need an extra class like .card--with-image.

:has() works great with forms too. This highlights a whole field when its input is invalid:

.field:has(input:user-invalid) {
  border-left: 4px solid #dc2626;
  background: #fef2f2;
}

.field:has(input:user-invalid) .error-text {
  display: block;
}

:user-invalid only matches after the user has interacted with the field, so the error does not appear before they type anything.

Tip: :has() can also look at siblings. h2:has(+ p) selects an <h2> that is directly followed by a <p>.

How do :is() and :where() shorten selectors?

Sometimes you repeat the same parts again and again:

article h1 a:hover,
article h2 a:hover,
article h3 a:hover {
  color: #db2777;
}

:is() takes a list and matches any item in it. This does exactly the same thing:

article :is(h1, h2, h3) a:hover {
  color: #db2777;
}

:where() works the same way: article :where(h1, h2, h3). The only difference is specificity, which you will see in the next section.

Both are forgiving selector lists. In a normal comma list, if one selector is invalid, the browser throws away the whole rule. Inside :is() and :where(), an invalid item is simply ignored, and the rest still work.

Why use :where() for resets? (zero specificity)

Remember specificity: the score that decides which rule wins.

  • :is() takes the specificity of its most specific item. :is(#main, p) counts as an ID.
  • :where() always has zero specificity, no matter what is inside.

That makes :where() perfect for base styles and resets that should be easy to override later.

/* Base list reset: zero specificity */
:where(ul, ol)[role="list"] {
  list-style: none;
  padding: 0;
  margin: 0;
}

:where(a) {
  color: #4f46e5;
  text-underline-offset: 3px;
}

/* A normal class easily wins */
.nav-link {
  color: #1f2937;
}

The :where(a) rule gives links a default color, but it has no weight. Any class, like .nav-link, overrides it without you needing a longer selector or !important. Note that [role="list"] sits outside :where(), so it still adds a little specificity.

Common mistake: Using :is() with an ID inside it, like :is(#sidebar, .panel) a. The whole selector now has ID-level specificity, even when it matches .panel. This can make rules surprisingly hard to override.

SelectorMatchesSpecificity
:has(x)Elements that contain xMost specific item in the list
:is(a, b)Elements that match a or bMost specific item in the list
:where(a, b)Elements that match a or bAlways zero

Practice exercise

Build a simple FAQ list where each item is a <details> element. Using :has(), give the whole <details> box a colored background when it is open. Then use :is() to style both h2 and h3 headings inside main with one rule.

Hint: An open <details> element has the open attribute. Try .faq-item:has([open]) on a wrapper, or simply details[open] first to compare.

Key takeaways

  • :has() selects an element based on what it contains, so you can style parents.
  • :is() and :where() shorten long selector lists.
  • Both are forgiving: one invalid item does not break the whole rule.
  • :is() takes the highest specificity in its list; :where() is always zero.
  • Use :where() for resets and defaults that should be easy to override.

Open this lesson on its own page ↗

Module 16 of 17

Advanced: Cutting-Edge CSS

Lesson 16.1 4 min read

Feature Queries with @supports and Progressive Enhancement

CSS @supports lets you check if a browser understands a feature before you use it. Learn its syntax, how to write fallbacks first, and the idea of progressive enhancement.

What is @supports in CSS?

@supports is a feature query. It asks the browser a question: "Do you understand this property and value?" If the answer is yes, the CSS inside the block is used. If not, the browser skips the whole block. It works like a media query, but instead of checking the screen size, it checks the browser's abilities.

@supports (display: grid) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
  }
}

The gallery becomes a grid only in browsers that support Grid (today, all of them). The test inside the brackets must be a full declaration: a property, a colon and a value.

Note: Browsers don't need @supports to stay safe. If a browser doesn't understand a declaration, it simply ignores that one line. @supports is useful when several rules only make sense together, or when the fallback needs different styles.

Combining tests with not, and, or

You can build more complex questions with three keywords:

  • not – the feature is not supported. Great for writing a fix only for older browsers.
  • and – both features must be supported.
  • or – at least one feature is supported.
@supports not (aspect-ratio: 1 / 1) {
  .video-box {
    height: 0;
    padding-top: 56.25%;
  }
}

@supports (position: sticky) and (backdrop-filter: blur(4px)) {
  .site-header {
    position: sticky;
    top: 0;
    background: rgb(255 255 255 / 0.7);
    backdrop-filter: blur(4px);
  }
}

@supports (text-wrap: balance) or (text-wrap: pretty) {
  .title {
    max-width: none;
  }
}

The first block uses the old "padding hack" only in browsers without aspect-ratio. The second makes a see-through, blurred sticky header only when both features work, because a see-through header without blur would be hard to read. The third removes a manual width limit when the browser can balance lines by itself. When you mix and and or, always use brackets to group them.

Testing selectors with selector()

Sometimes you need to know whether a selector works, not a property. selector() does that. This is very useful for newer selectors like :has().

.card {
  border: 1px solid #e5e7eb;
}

@supports selector(:has(img)) {
  .card:has(img) {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
  }
}

Cards that contain an image get a two-column layout in browsers that support :has(). In other browsers, the cards just stay simple boxes with a border, which is perfectly fine. Without the test, an unsupported selector would make the browser drop the whole rule anyway, but the feature query makes your intention clear and lets you group several related rules.

Progressive enhancement: write the fallback first

Progressive enhancement means: build a version that works everywhere first, then add extras for browsers that can do more. Think of a house: first the walls and the roof (everyone needs those), then the nice paint and the garden lights. If a browser is old, users still get a working house.

In CSS, this means you write the simple styles at the top, and the modern styles after them or inside @supports. The cascade does the rest: later rules win, and unknown rules are ignored.

.button {
  background: #2563eb;
  background: oklch(55% 0.2 260);
}

.hero-title {
  font-size: 2.5rem;
  font-size: clamp(2rem, 5vw, 4rem);
}

Each property is written twice. Every browser understands the first line. Modern browsers also understand the second line, and because it comes later, it wins. An old browser doesn't understand oklch() or clamp(), ignores that line, and keeps the first value. Nobody gets a broken page.

Here is a bigger real-world example: a layout that uses modern container queries but still looks good without them.

.product {
  display: block;
}

.product img {
  width: 100%;
}

@supports (container-type: inline-size) {
  .product-list {
    container-type: inline-size;
  }

  @container (min-width: 500px) {
    .product {
      display: flex;
      gap: 16px;
    }

    .product img {
      width: 200px;
    }
  }
}

Every browser shows the products stacked, with full-width images. Browsers that support container queries switch to a side-by-side layout when there is enough room.

Common mistake: Writing the modern version first and the fallback after it. The fallback comes later, so it wins in every browser, and nobody sees your modern styles. Fallback first, enhancement second.

Tip: @supports checks whether the browser parses the feature, not whether it is fully bug-free. Always test your page in at least two different browsers. Check browser support on MDN's @supports page.

Practice exercise

Style a "testimonials" section. First, give the quote cards a simple stacked layout and a solid background. Then, inside @supports (display: grid) and (gap: 1rem), turn them into a three-column grid. Finally, add a block with @supports not (backdrop-filter: blur(1px)) that makes a frosted header fully solid.

Hint: open DevTools and change a property name in the test (for example to displayy) to see the fallback version.

Key takeaways

  • @supports (property: value) applies CSS only when the browser understands it.
  • Combine tests with not, and and or, and use brackets to group them.
  • selector() tests whether a selector such as :has() is supported.
  • Progressive enhancement: a working base for everyone, extras for modern browsers.
  • Always write the fallback before the modern version.

Open this lesson on its own page ↗

Module 17 of 17

Advanced: Professional CSS: Architecture, Performance and Debugging

Lesson 17.1 4 min read

Organizing CSS: BEM Naming, File Structure and Best Practices

Organizing CSS well means clear class names like BEM, a simple file structure and a modern reset, so your styles stay easy to read as a project grows.

Why does CSS organization matter?

A small page with 50 lines of CSS is easy to manage. A real website can have thousands of lines written by several people. Without a plan, you get problems: you are afraid to delete a rule because something might break, two classes have the same name, and you need !important to win every fight.

Good organization is like labeling the boxes when you move house. It takes a few minutes at the start, but it saves hours later.

What is BEM naming in CSS?

BEM stands for Block, Element, Modifier. It is a naming pattern that tells you what a class does just by reading it.

  • Block: a standalone component, like card, navbar or signup-form.
  • Element: a part of a block, joined with two underscores: card__title, card__image.
  • Modifier: a different version or state, joined with two dashes: card--featured, button--large.
<article class="card card--featured">
  <img class="card__image" src="https://picsum.photos/400/250" alt="Laptop on a desk">
  <div class="card__body">
    <h3 class="card__title">Learn CSS Grid</h3>
    <p class="card__text">Build real layouts in one afternoon.</p>
    <a class="card__button" href="#">Read more</a>
  </div>
</article>
.card {
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  overflow: hidden;
  background: white;
}

.card__image {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

.card__body {
  padding: 1rem;
}

.card__title {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
}

.card__button {
  display: inline-block;
  padding: 0.5rem 1rem;
  background: #4f46e5;
  color: white;
  border-radius: 6px;
  text-decoration: none;
}

.card--featured {
  border: 2px solid #4f46e5;
  box-shadow: 0 10px 25px rgb(79 70 229 / 0.2);
}

Every selector is a single class, so they all have the same low specificity. You never need long selectors like .sidebar .card div h3. When you see card__title in the HTML, you know exactly where its styles live.

Common mistake: Making "grandchild" names like card__body__title. BEM elements belong to the block, not to other elements. Use card__title even if the title sits inside card__body.

Utility classes: small single-purpose helpers

A utility class does one small job, like .text-center or .mt-0. Frameworks like Tailwind CSS are built entirely from utilities. Many teams mix the two ideas: BEM for components, plus a few utilities for small tweaks.

.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

The .visually-hidden utility hides text on screen but keeps it readable for screen readers. It is useful for labels like "Search" on an icon-only button.

A modern CSS reset

Browsers add their own default styles, and they are not always helpful. A reset is a short set of rules at the start of your CSS that gives you a clean, predictable starting point.

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

* {
  margin: 0;
}

body {
  min-height: 100vh;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

This makes sizes include padding and border, removes surprise margins, stops images from overflowing their containers, and makes form fields use your page font instead of a tiny default.

Tip: Notice the comment header above the reset. Big, clear section comments make long files easy to scan. Many editors also show them in the code outline.

How to structure CSS files in a project

For a small site, one file with clear sections is fine. For bigger projects, split the CSS into files by purpose. Here is a common folder structure:

css/
  main.css            imports everything in the right order
  base/
    reset.css
    tokens.css        variables: colors, spacing, fonts
    typography.css
  layout/
    header.css
    footer.css
    grid.css
  components/
    button.css
    card.css
    form.css
  utilities/
    utilities.css

The main.css file loads the others in order, and you can put each group in a cascade layer:

@layer reset, base, layout, components, utilities;

@import url("base/reset.css") layer(reset);
@import url("base/tokens.css") layer(base);
@import url("base/typography.css") layer(base);
@import url("layout/header.css") layer(layout);
@import url("components/card.css") layer(components);
@import url("utilities/utilities.css") layer(utilities);

Note: Each @import is a separate file download. On a real site, a build tool usually joins them into one file. While you learn, this structure still teaches good habits.

Some other best practices:

  • One component per file, named like the block: card.css holds .card, .card__title and so on.
  • Use variables (design tokens) instead of repeating raw colors and sizes.
  • Keep specificity low: prefer single classes over IDs and long chains.
  • Delete unused CSS when you remove a feature.

Practice exercise

Take a signup form you built before and rename all its classes with BEM. The block is signup. Create elements like signup__field, signup__label and signup__button, and a modifier signup__button--disabled. Add a comment header above the section.

Hint: Every rule should use only one class. If you write .signup label, change it to .signup__label.

Key takeaways

  • BEM means Block (card), Element (card__title) and Modifier (card--featured).
  • Single-class selectors keep specificity low and predictable.
  • Utility classes do one small job and work well alongside components.
  • A modern reset gives every project the same clean starting point.
  • Split CSS by purpose (base, layout, components, utilities) and use clear comment headers.

Open this lesson on its own page ↗

Code copied to clipboard!