Project: Responsive Layout

🏗️ Chapter 12 — Project: Fully Responsive Multi-Column Layout

This is the capstone project for the CSS Intermediate course. We'll build a magazine-style blog layout from scratch, combining every technique from the previous eleven chapters: design tokens, Flexbox headers, Grid post grids, fluid typography, transforms on hover, keyframe animations, and BEM naming. Work through each step in order — by Step 9 you'll have a production-quality responsive layout.

What we're building: A blog with a sticky header, a hero section, a three-column featured posts grid, a two-column content area (articles + sidebar), and a responsive footer. It collapses cleanly from 1100px down to 360px mobile without a single media query hack.
┌──────────────────────────────────────────────────┐ HEADER — sticky, Flexbox, logo + nav ├──────────────────────────────────────────────────┤ HERO — fluid clamp() heading + action buttons ├──────────────────────────────────────────────────┤ FEATURED GRID — auto-fit minmax() cards ┌──────────┐ ┌──────────┐ ┌──────────┐ │ card │ │ card │ │ card │ └──────────┘ └──────────┘ └──────────┘ ├──────────────────────────────────────────────────┤ CONTENT AREA — Grid 1fr + 280px ┌───────────────────────┐ ┌──────────┐ │ MAIN (articles) │ │ SIDEBAR │ └───────────────────────┘ └──────────┘ ├──────────────────────────────────────────────────┤ FOOTER — 4-col → 2-col → 1-col Grid └──────────────────────────────────────────────────┘

Step 1 — Design Tokens

Chapter 9 skill: CSS custom properties

Everything else references these tokens. Define them first and nothing downstream ever needs a raw value — dark mode, rebranding, or spacing tweaks become single-line changes.

🎨 CSS — design tokens in :root
:root { /* ── Colors ── */ --clr-bg: #0d0f18; --clr-surface: #1a1d27; --clr-surface-2: #12141e; --clr-border: #2a2d3a; --clr-primary: #4f8ef7; --clr-primary-dk: #3a7ae5; --clr-accent: #c9a8ff; --clr-text: #e2e4ec; --clr-text-muted: #7a7f96; /* ── Fluid spacing scale ── */ --sp-1: clamp(0.5rem, 1.5vw, 0.875rem); --sp-2: clamp(1rem, 2.5vw, 1.5rem); --sp-3: clamp(1.5rem, 4vw, 2.5rem); --sp-4: clamp(2rem, 6vw, 4rem); --gap: clamp(1rem, 2.5vw, 1.75rem); /* ── Fluid type scale ── */ --text-sm: clamp(0.75rem, 1.5vw, 0.875rem); --text-base: clamp(0.9rem, 2vw, 1rem); --text-lg: clamp(1.1rem, 2.5vw, 1.25rem); --text-xl: clamp(1.4rem, 4vw, 2rem); --text-hero: clamp(1.8rem, 6vw, 3.5rem); /* ── Layout ── */ --max-w: 1100px; --header-h: 60px; --sidebar-w: 280px; }

Step 2 — Base Reset

Foundation
🎨 CSS — base reset
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; } body { background: var(--clr-bg); color: var(--clr-text); font-family: system-ui, -apple-system, sans-serif; font-size: var(--text-base); line-height: 1.6; } img { display: block; max-width: 100%; } a { color: inherit; text-decoration: none; } ul { list-style: none; } /* Shared max-width container */ .container { max-width: var(--max-w); margin: 0 auto; padding: 0 var(--sp-2); } /* Section spacing */ .section { padding: var(--sp-3) 0; } .section__heading { font-size: var(--text-xl); font-weight: 700; margin-bottom:var(--sp-2); padding-bottom:var(--sp-1); border-bottom:2px solid var(--clr-primary); }

Step 3 — Sticky Header

Flexbox · position: sticky

The header uses Flexbox with margin-left: auto on the nav to push it to the right — the simplest push technique. position: sticky; top: 0 keeps it visible without JavaScript.

🎨 CSS — .site-header
.site-header { position: sticky; top: 0; z-index: 100; height: var(--header-h); background: var(--clr-surface-2); border-bottom:1px solid var(--clr-border); display: flex; align-items: center; } .site-header__inner { display: flex; align-items: center; gap: var(--sp-2); width: 100%; } .site-header__logo { font-size: var(--text-lg); font-weight: 800; color: var(--clr-primary); } .site-header__nav { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; /* pushes nav to right edge */ } .site-header__link { color: var(--clr-text-muted); font-size: var(--text-sm); transition: color 0.2s; } .site-header__link:hover{ color: var(--clr-text); } .site-header__link--cta{ background: var(--clr-primary); color: #fff; padding: 6px 16px; border-radius:4px; font-weight: 600; transition: background 0.2s; } .site-header__link--cta:hover { background: var(--clr-primary-dk); } @media (max-width: 640px) { .site-header__nav { display: none; } /* add hamburger menu for full build */ }

Step 4 — Hero Section

clamp() · Flexbox buttons
🎨 CSS — .site-hero
.site-hero { background: linear-gradient(135deg, var(--clr-bg) 0%, #1a2a4a 100%); padding: var(--sp-4) var(--sp-2); text-align: center; } .site-hero__inner { max-width: 680px; margin: 0 auto; } .site-hero__label { font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.15em; color: var(--clr-primary); margin-bottom: var(--sp-1); } .site-hero__heading { font-size: var(--text-hero); /* clamp(1.8rem, 6vw, 3.5rem) */ font-weight: 800; color: #fff; line-height: 1.15; margin-bottom:var(--sp-1); } .site-hero__sub { font-size: var(--text-base); color: var(--clr-text-muted); margin-bottom:var(--sp-2); } .site-hero__actions { display: flex; gap: var(--sp-1); justify-content: center; flex-wrap: wrap; /* stack on very narrow screens */ } /* Shared button styles (BEM block) */ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 22px; border-radius:5px; font-weight: 600; font-size: var(--text-sm); border: 2px solid transparent; cursor: pointer; transition: background 0.2s, transform 0.15s; } .btn:hover { transform: translateY(-2px); } .btn:active { transform: translateY(0); } .btn--primary { background: var(--clr-primary); color: #fff; } .btn--primary:hover{ background: var(--clr-primary-dk); } .btn--ghost { border-color: var(--clr-border); color: var(--clr-text-muted); } .btn--ghost:hover { border-color: var(--clr-primary); color: var(--clr-text); }

Step 5 — Featured Posts Grid

Grid · auto-fit · minmax()

The key trick: minmax(min(280px, 100%), 1fr). The inner min() clamps the minimum column width to 100% when the viewport is narrower than 280px — preventing overflow on tiny screens without a media query.

🎨 CSS — .posts-grid + .post-card (BEM)
.posts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: var(--gap); } /* BEM block */ .post-card { background: var(--clr-surface); border: 1px solid var(--clr-border); border-radius: 8px; overflow: hidden; display: flex; flex-direction:column; /* column so body can grow */ transition: transform 0.25s, box-shadow 0.25s; } .post-card:hover { transform: translateY(-5px); box-shadow: 0 12px 32px rgba(0,0,0,.3); } .post-card__image { aspect-ratio: 16 / 9; overflow: hidden; } .post-card__image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s; } .post-card:hover .post-card__image img { transform: scale(1.05); /* subtle zoom on hover */ } .post-card__body { padding: var(--sp-2); flex: 1; } /* flex:1 pushes footer down */ .post-card__tag { display: inline-block; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--clr-primary); margin-bottom: 6px; } .post-card__title { font-size: var(--text-lg); font-weight: 700; margin-bottom:6px; line-height:1.3; } .post-card__excerpt{ font-size: var(--text-sm); color: var(--clr-text-muted); line-height:1.55; } .post-card__footer { padding: var(--sp-1) var(--sp-2); border-top: 1px solid var(--clr-border); display: flex; justify-content:space-between; align-items: center; font-size: var(--text-sm); color: var(--clr-text-muted); }

Step 6 — Content Area: Main + Sidebar

Grid · align-items: start

align-items: start prevents the sidebar from stretching to match the main content height — it stays at its own natural height. Without it, the sidebar would fill the entire grid row.

🎨 CSS — .content-area
.content-area { display: grid; grid-template-columns: 1fr var(--sidebar-w); /* main grows, sidebar fixed */ gap: var(--gap); align-items: start; /* sidebar stays own height */ } /* Sidebar is sticky so it stays visible while scrolling main */ .content-area__sidebar { position: sticky; top: calc(var(--header-h) + var(--sp-2)); /* below sticky header */ } @media (max-width: 768px) { .content-area { grid-template-columns: 1fr; /* one column on mobile */ } .content-area__sidebar { position: static; /* un-sticky on small screens */ } }

Step 7 — Sidebar Widgets

BEM · reusable component
🎨 CSS — .widget (BEM block)
.widget { background: var(--clr-surface); border: 1px solid var(--clr-border); border-radius: 8px; overflow: hidden; margin-bottom: var(--sp-2); } .widget__heading { font-size: var(--text-sm); font-weight: 700; text-transform:uppercase; letter-spacing:0.07em; padding: var(--sp-1) var(--sp-2); border-bottom:1px solid var(--clr-border); color: var(--clr-text); } .widget__body { padding: var(--sp-1) var(--sp-2); } .widget__item { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--clr-border); font-size: var(--text-sm); color: var(--clr-text-muted); } .widget__item:last-child { border-bottom: none; } .widget__count { background: var(--clr-surface-2); color: var(--clr-accent); padding: 1px 7px; border-radius: 10px; font-size: 0.75em; font-weight: 600; } /* Tag cloud widget variant */ .widget--tags .widget__body { display: flex; flex-wrap: wrap; gap: 6px; } .widget__tag { background: var(--clr-surface-2); color: var(--clr-accent); padding: 3px 10px; border-radius: 12px; font-size: var(--text-sm); transition: background 0.2s; } .widget__tag:hover { background: var(--clr-primary); color: #fff; }

Step 8 — Responsive Footer

Grid · multi-column → single
🎨 CSS — .site-footer
.site-footer { background: var(--clr-surface-2); border-top: 1px solid var(--clr-border); padding: var(--sp-3) var(--sp-2); } .footer__grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; /* brand col is wider */ gap: var(--gap); } /* Collapses to 2 columns on tablet */ @media (max-width: 768px) { .footer__grid { grid-template-columns: 1fr 1fr; } } /* Single column on mobile */ @media (max-width: 480px) { .footer__grid { grid-template-columns: 1fr; } } /* Footer bottom bar */ .footer__bottom { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--clr-border); font-size: var(--text-sm); color: var(--clr-text-muted); }

Step 9 — Responsive Strategy Summary

BreakpointWhat changesTechnique
≥ 769px (Desktop)Full layout: header nav visible, 3-col grid, 2-col content, 4-col footerDefault styles
≤ 768px (Tablet)Content area collapses to 1 col; sidebar un-stickies; footer becomes 2-col@media (max-width: 768px)
≤ 640pxHeader nav hides (hamburger shown); hero buttons stack@media (max-width: 640px)
≤ 480pxFooter becomes 1 column@media (max-width: 480px)
Any widthPost card grid reflows automaticallyauto-fit minmax(min(280px,100%), 1fr) — no breakpoint needed
Any widthType and spacing scale fluidlyclamp() on all spacing + font-size tokens
Key insight: Most of the responsiveness comes from intrinsic sizingauto-fit, clamp(), flex-wrap — not from media queries. Only structural changes (collapsing the sidebar, hiding the nav) need breakpoints. Favour intrinsic techniques; add breakpoints only when structure must change.

Live Preview — Resize the Layout

Viewport Simulator

CSS Intermediate · Chapter 12

Fully Responsive
Multi-Column Layout

Grid, Flexbox, fluid type, and BEM — all working together.

Featured Articles

CSS Grid

auto-fit vs auto-fill Explained

Master the subtle but crucial difference with live examples.

Flexbox

The Power of align-items: start

Why your sidebar shouldn't stretch to fill its grid cell.

CSS Functions

clamp() for Fluid Typography

One property replaces a dozen breakpoints.

Latest Articles

BEM Naming: Why Architecture Saves Teams

Jane Smith · 5 min · Jun 14

Container Queries: The Next Responsive Revolution

Tom Lee · 7 min · Jun 13

CSS Custom Properties as a Design Token System

Amy Chen · 6 min · Jun 12

When to Use Grid vs Flexbox

Jane Smith · 4 min · Jun 11

The preview uses CSS container queries (container-type: inline-size) on the frame div — the layout responds to the frame's width, not the viewport width. This is exactly how container queries will work in your real layouts (Chapter 2 of CSS Advanced).

Key Design Decisions

SectionTool chosenWhy not the alternative
Header layoutFlexboxOne-dimensional row — Grid would work but adds no value here
Hero buttonsFlexbox + flex-wrapInline-block would need media queries to stack; flex-wrap handles it automatically
Featured cardsGrid auto-fitFlexbox can't guarantee equal-width columns without media queries; auto-fit does it in one line
Main + SidebarGrid (explicit tracks)Flexbox can't hold the sidebar at a fixed 280px while the main grows — Grid 1fr var(--sidebar-w) does exactly this
Cards (internal)Flexbox columnOne-dimensional vertical stack; flex: 1 on body pushes footer to bottom — Grid overkill here
FooterGrid (explicit tracks)Four columns with unequal widths (brand 2fr, rest 1fr) — Grid template columns are cleaner than flex
Spacingclamp() tokensFixed values need breakpoints for every size; clamp scales continuously
Typographyclamp() tokensSame — no extra media queries needed for text sizing

🚀 Go Further — Optional Challenges

  • Hamburger menu: Add a hamburger button that appears at ≤ 640px and toggles the nav with a CSS class + max-height transition.
  • Skeleton loading: Add a .is-loading modifier to cards that shows a shimmer animation (from Ch 10 Exercise 1) while content loads.
  • Dark / light toggle: Add a toggle button that switches a data-theme="light" attribute on <html>, and remap the color tokens for light mode.
  • Scroll reveal: Use IntersectionObserver to add a .revealed class to cards as they enter the viewport, triggering a translateY(0) + opacity: 1 animation.
  • Reading progress bar: A fixed 1px bar at the top of the header that grows with scaleX() as the user scrolls, driven by scroll event and transform.

✏️ Exercises

Exercise 1
Implement the sticky sidebar with a calc() top offset. The sidebar should stick below the header without overlapping it. The header is 60px tall and you want 16px of breathing room. Write the CSS, and explain why top: calc(var(--header-h) + 16px) is better than top: 76px.
Hint: When the header height changes (e.g. in a media query you set --header-h: 48px), the calc value auto-updates. A hard-coded 76px would need a second override in that media query.
CSS
:root { --header-h: 60px; --sidebar-gap: 16px; } .content-area__sidebar { position: sticky; top: calc(var(--header-h) + var(--sidebar-gap)); /* = 76px by default, but auto-updates if --header-h changes */ } @media (max-width: 640px) { :root { --header-h: 48px; } /* sidebar top now becomes 64px — no second calc() needed */ } @media (max-width: 768px) { .content-area__sidebar { position: static; } /* un-stick when layout collapses to 1 column */ }
Exercise 2
Add a .post-card--featured modifier that makes one card span all columns in the grid and display horizontally (image left, content right) rather than the default vertical stack. Use grid-column: 1 / -1 to span all columns, and switch the card to a row-direction Flexbox layout internally.
Hint: The card itself uses flex-direction: column. Override it to row in the modifier. You'll also need to constrain the image width (flex: 0 0 40%) so it doesn't take all available space.
CSS
.post-card--featured { grid-column: 1 / -1; /* span all columns */ flex-direction: row; } .post-card--featured .post-card__image { flex: 0 0 40%; /* fixed width, no shrink/grow */ aspect-ratio:auto; /* remove the 16/9 ratio — let height fill */ min-height: 220px; } .post-card--featured .post-card__body { padding: var(--sp-3); } /* On mobile, revert to stacked layout */ @media (max-width: 480px) { .post-card--featured { flex-direction: column; } .post-card--featured .post-card__image { flex: none; aspect-ratio:16 / 9; } }
Exercise 3
Add a scroll-reveal animation to the post cards. When a card enters the viewport, it should animate from opacity: 0; transform: translateY(24px) to its natural position. Use IntersectionObserver in JavaScript to add a .revealed class when each card becomes visible. Include a prefers-reduced-motion CSS override that skips the animation entirely.
Hint: Set initial styles on .post-card (opacity 0, translateY), then style .post-card.revealed to restore them. The IntersectionObserver callback adds .revealed when the threshold is crossed. Use observer.unobserve(entry.target) so the animation only fires once.
CSS
.post-card { opacity: 0; transform: translateY(24px); transition:opacity 0.4s ease, transform 0.4s ease; } .post-card.revealed { opacity: 1; transform: translateY(0); } @media (prefers-reduced-motion: reduce) { .post-card { opacity: 1; transform: none; transition: none; } .post-card.revealed{ transform: none; } }
JS
const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('revealed'); observer.unobserve(entry.target); // fire once only } }); }, { threshold: 0.15 }); document.querySelectorAll('.post-card') .forEach((card) => observer.observe(card));
Exercise 4 — Capstone Extension
Add a light/dark theme toggle to the layout. Store the palette in two sets of :root custom properties — one for dark (default) and one for [data-theme="light"] on <html>. The toggle button switches the attribute. Requirements: (a) only the semantic tokens change between themes (--clr-bg, --clr-surface, --clr-text, etc.) — the primitive palette (raw hex values) stays unchanged; (b) all transitions in the layout should run smoothly when the theme switches; (c) the chosen theme should persist in localStorage and be restored on page load.
Hint: Add transition: background 0.3s, color 0.3s, border-color 0.3s to *, *::before, *::after in the base reset (or on the key layout elements). Use localStorage.setItem('theme', 'light') and document.documentElement.dataset.theme.
CSS
/* Semantic tokens — dark mode (default) */ :root { --clr-bg: #0d0f18; --clr-surface: #1a1d27; --clr-surface-2: #12141e; --clr-border: #2a2d3a; --clr-text: #e2e4ec; --clr-text-muted: #7a7f96; } /* Light theme — re-map semantics only */ [data-theme="light"] { --clr-bg: #f8f9fc; --clr-surface: #ffffff; --clr-surface-2: #f0f1f5; --clr-border: #dde0ea; --clr-text: #111827; --clr-text-muted: #6b7280; } /* Smooth transition on theme switch */ *, *::before, *::after { transition: background-color 0.3s, color 0.3s, border-color 0.3s; } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none; } }
JS
// Restore on load const saved = localStorage.getItem('theme'); if (saved) document.documentElement.dataset.theme = saved; // Toggle button click handler document.getElementById('themeToggle') .addEventListener('click', () => { const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light'; document.documentElement.dataset.theme = next; localStorage.setItem('theme', next); });
Course complete! You've finished the CSS Intermediate course — 12 chapters covering Flexbox, Grid, cascade, media queries, CSS functions, transforms, animations, architecture, and a full project build. The CSS Advanced course picks up from here with @layer, container queries, subgrid, :has(), scroll-driven animations, and building a complete design system.