Lesson 49 • Advanced Track
Building Full Responsive Pages Without Frameworks
By the end of this lesson you'll be able to build a complete, production-quality responsive page with pure modern CSS — fluid grids, fluid type, and a token system — without ever loading Bootstrap or Tailwind.
What You'll Learn
var(--name) and :root tokens feel new, review CSS Custom Properties first.💡 Think of It Like This
A CSS framework is like flat-pack furniture; building without one is custom carpentry. Flat-pack is quick, but every shelf is a fixed size and you live with the gaps. Custom carpentry means you measure each space and cut to fit.
Modern CSS hands you professional power tools so the custom approach is fast, not painful. A fluid auto-fit grid is a shelf that quietly adds or removes columns to fit the wall; clamp() is a leg that extends and retracts so the table is always the right height. You stop fighting fixed sizes and let the layout measure the room for you.
1. Fluid Grids with auto-fit & minmax()
A responsive grid is one that changes how many columns it shows based on the space available — not on a list of fixed breakpoints. The trick is one line: grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));. Read it as: "make as many columns as fit, each at least 250px wide, sharing the leftover space equally."
minmax(250px, 1fr) sets a floor of 250px and a ceiling of 1fr (one share of free space). auto-fit packs in as many of those tracks as fit, then stretches the ones that exist to fill the row. Wrapping min(100%, 250px) as the floor stops the column overflowing on screens narrower than 250px. The result reflows from four columns to one as the window shrinks — with zero media queries.
Worked example: a grid with no breakpoints
auto-fit + minmax() reflows columns as the preview resizes
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- This meta tag is REQUIRED for any responsive page to work on phones. -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fluid grid</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e5e7eb; padding: 24px; }
.grid {
display: grid;
/* "As many columns as fit, each >= 250
...2. Flexible Bars with Flexbox
Grid is for two-dimensional layouts; flexbox shines for one-dimensional rows like nav bars, toolbars, and button groups. display: flex lays children in a row; justify-content: space-between pushes the first and last to the edges; align-items: center lines them up vertically.
The one property that makes a flex row responsive is flex-wrap: wrap. Without it, a row of items squashes and overflows on small screens. With it, items that no longer fit drop onto the next line on their own — a button group that becomes two rows on a phone, no media query required.
Worked example: a nav bar that wraps
flex + space-between + flex-wrap for a one-dimensional bar
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flexbox bar</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; padding: 24px; }
.nav {
display: flex; /* lay the children out in a row */
justify-content: space-between; /* brand on the left, links on the right */
...3. Fluid Type & Spacing with clamp()
clamp(min, preferred, max) gives you a value that scales with the viewport but is locked between a floor and a ceiling. The browser uses the preferred value — usually something with a vw (viewport-width) unit so it grows with the screen — but never lets it drop below min or rise above max.
So font-size: clamp(1.5rem, 5vw, 3rem) is a heading that is 1.5rem on a tiny phone, grows smoothly as the screen widens, and stops at 3rem so it never becomes comically large. Use the same idea for padding, gaps, and margins to get spacing that breathes — without a single breakpoint. This is the "fluid" half of responsive design; the grid handles the structural half.
Worked example: everything scales with clamp()
Fluid type and spacing, no media queries
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>clamp() fluid sizing</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body {
font-family: system-ui, sans-serif; background: #0f172a; color: #e5e7eb;
/* Padding is tight on phones (16px), spacious on desktop (64px). */
padding: clamp(16px, 5vw, 64px);
}
/* Floor 1.5rem, scales with 6vw, ce
...4. Media Queries vs Container Queries
When the layout must change shape — not just size — you need a query. A media query reacts to the viewport (the whole window): @media (min-width: 600px) { … } applies its rules only when the window is at least 600px wide. That is how a stacked layout becomes a sidebar layout on bigger screens.
A container query reacts to the size of a component's own container, not the window. Mark an ancestor with container-type: inline-size, then @container (min-width: 400px) { … } styles the child based on how much room it has. The same card can stack in a narrow sidebar and go side-by-side in a wide main column — on the same page. Use media queries for page structure, container queries for reusable components.
Worked example: a container query card
The card adapts to its column, not the window
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Container queries</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e5e7eb; padding: 24px; }
/* The two columns are different widths on purpose. */
.layout { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; }
/* Each colum
...5. Mobile-First: A Complete Page
Mobile-first means your base CSS (everything outside a media query) describes the small-screen layout, and you add complexity for bigger screens with min-width queries. The base is the simplest case and always applies, so a phone gets a working single-column layout before any query runs. You layer on multi-column grids and a sidebar as the screen grows — never the other way round.
Here is the capstone: a full page that pulls every piece together — a flexbox nav, a hero with clamp() type, an auto-fit feature grid, :root design tokens, and one mobile-first media query for the sidebar. No Bootstrap, no Tailwind — just the platform.
Worked example: complete responsive page
Tokens + flex nav + clamp hero + auto-fit grid + one media query
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Framework-free page</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
/* DESIGN TOKENS: one source of truth for the whole page. */
:root {
--primary: #1976D2; --primary-dark: #1565C0;
--text: #1a1a1a; --muted: #666;
--bg: #fafafa; --surface: #fff; --border: #e0e0e0;
--space: clamp(16px,
...🎯 Your Turn #1 — A breakpoint-free card grid
This grid is fixed at three columns, so it overflows on a phone. Replace the fixed columns with an auto-fit / minmax() track so it reflows on its own. Fill in the blanks marked ___, then run it and check the comment.
Your Turn #1: make the grid responsive
Swap fixed columns for auto-fit + minmax()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Your Turn 1</title>
<style>
/* 🎯 YOUR TURN — fill in the blanks marked ___ */
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
.grid {
display: grid;
gap: 16px;
/* This is fixed at 3 columns and overflows on sm
...🎯 Your Turn #2 — Fluid type + a mobile-first sidebar
Make the heading fluid with clamp(), then add one mobile-first media query that turns the single-column layout into a two-column sidebar layout on wide screens. Fill in the blanks and verify against the comment.
Your Turn #2: clamp() heading + min-width sidebar
Fluid type plus a mobile-first layout change
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Your Turn 2</title>
<style>
/* 🎯 YOUR TURN — fill in the blanks marked ___ */
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
/* 1) Make this heading fluid: min 1.4rem, scale at 5vw, max 2.8rem. */
h1 { color:#60a5fa; margi
...🧩 Mini-Challenge — A responsive pricing section
Support is faded now — only an outline is given. Build a small pricing section from scratch using the techniques from this lesson. Lean on the worked examples in sections 1, 3, and 5 if you get stuck.
Mini-Challenge: pricing section from scratch
Tokens + auto-fit grid + clamp(), no frameworks
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mini-Challenge</title>
<style>
/* 🧩 MINI-CHALLENGE: a responsive pricing section, no frameworks
1. Reset: *, *::before, *::after { box-sizing: border-box; margin: 0; }
2. In :root, declare tokens: --primary, --surface, --border,
--radius, and a fluid --space using clamp(16px, 4vw, 40px)
3. Give the <h1> a fluid fo
...⚠️ Common Errors (and the fix)
- Fixed widths instead of fluid tracks.
grid-template-columns: 300px 300px 300px;overflows the moment the screen is narrower than 900px. Fix: userepeat(auto-fit, minmax(min(100%, 300px), 1fr))so tracks shrink and reflow. - px everywhere for type and spacing. Hard-coding
font-size: 48pxmeans tiny phones get the same giant heading as desktops. Fix: useclamp(min, vw, max)so sizing scales between limits. - Desktop-first, then patching for mobile. Starting with a complex desktop layout and tearing it down with
max-widthqueries is fragile. Fix: write the simple mobile layout as the base, then add withmin-widthqueries. - Missing the viewport meta tag. Without
<meta name="viewport" content="width=device-width, initial-scale=1">, phones render at ~980px and shrink the page, so nothing looks right. Fix: add it to every page's<head>. - Forgetting box-sizing: border-box. Padding then adds to the declared width and blows out your grid math. Fix: start every stylesheet with
*, *::before, *::after { box-sizing: border-box; }.
📋 Quick Reference
| Goal | Use this |
|---|---|
| Responsive grid, no breakpoints | repeat(auto-fit, minmax(min(100%, 250px), 1fr)) |
| One-dimensional bar that wraps | display:flex; flex-wrap:wrap; justify-content:space-between |
| Fluid font / spacing | clamp(1.5rem, 5vw, 3rem) |
| Layout change by window size | @media (min-width: 900px) { … } |
| Layout change by component size | container-type:inline-size; @container (min-width:400px) |
| Centre a page container | max-width: 1100px; margin: 0 auto; |
| Make a page work on phones | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| Stop padding breaking widths | *, *::before, *::after { box-sizing: border-box; } |
❓ Frequently Asked Questions
Do I still need media queries if I use clamp() and auto-fit grids?
Far fewer, but not zero. clamp() and auto-fit/minmax() handle continuous sizing — text and columns that grow smoothly as the viewport changes — so you no longer need a breakpoint just to bump a font up a few pixels. You still reach for a media query (or a container query) when the layout itself must change shape: a sidebar that should stack under the content on small screens, a nav that collapses to a hamburger, or a two-column grid that becomes one column. Rule of thumb: use clamp/auto-fit for sizing, media queries for structural rearrangement.
What is the difference between auto-fit and auto-fill in repeat()?
Both let the grid create as many columns as fit. The difference shows up when there are fewer items than would fill the row. auto-fill keeps the empty column tracks, so your items can leave gaps on the right. auto-fit collapses those empty tracks to zero width, so the existing items stretch to fill the whole row. For most responsive card layouts you want auto-fit, because a lone card expanding to full width usually looks better than one card sitting next to three empty slots.
Why is my mobile layout zoomed out and tiny?
You are almost certainly missing the viewport meta tag. Without <meta name="viewport" content="width=device-width, initial-scale=1">, mobile browsers assume the page was designed for desktop and render it at roughly 980px wide, then shrink the whole thing to fit the screen — so your media queries never fire and everything looks miniature. Add that one line to the <head> and the browser maps CSS pixels to the device width, which is what every other responsive technique assumes.
What is mobile-first and why does it use min-width?
Mobile-first means your base CSS — the rules outside any media query — describes the small-screen layout, and you layer on complexity for bigger screens using min-width queries. It is preferred over desktop-first (max-width) because the base styles are the simplest case and always apply, so a phone gets a working layout even before any media query is evaluated. You add columns, sidebars, and wider spacing as the screen grows, rather than building a complex desktop layout and trying to tear it back down for phones.
When should I use a container query instead of a media query?
Use a media query when the decision depends on the viewport — the whole browser window. Use a container query when the decision depends on the space available to a specific component, regardless of the window size. A card component that should switch from stacked to side-by-side whenever its own column is wide enough is the classic case: the same card might be narrow in a sidebar and wide in the main area on the same page, and only a container query can adapt to both. Container queries are supported in all current major browsers.
Do I really not need Bootstrap or Tailwind for this?
For most landing pages, marketing sites, portfolios, and content sites — correct, you do not. CSS Grid, flexbox, clamp(), and custom properties cover responsive layout, fluid sizing, and theming natively, with no bundle to download and no build step. Frameworks still earn their place on large teams that want a shared utility vocabulary, or when you need a big component library fast. The point of this lesson is that you should use a framework because you chose to, not because you assumed plain CSS could not do the job.
🎉 Lesson Complete
You can now build a fully responsive page with pure modern CSS. The essentials:
- ✅
repeat(auto-fit, minmax(…, 1fr))makes grids reflow with no breakpoints - ✅
display:flex+flex-wrap:wrapbuilds bars and button groups that wrap - ✅
clamp(min, vw, max)gives fluid type and spacing between safe limits - ✅ Media queries react to the viewport; container queries react to a component's own width
- ✅ Go mobile-first: a simple base layout, enhanced with
min-widthqueries - ✅ Always include the viewport meta tag and a
box-sizingreset
Next up: the Final Project, where you'll combine everything in this course into one complete site.
Sign up for free to track which lessons you've completed and get learning reminders.