CSS Project: Build a Responsive Landing Page
Put HTML and CSS together: build a clean, responsive landing page from scratch, covering layout, type, and polish, live in your browser.

You've spent eleven lessons collecting tools: semantic structure, the box model, flexbox, grid, media queries, custom properties, transitions. On their own each one is a party trick. Bolted together they're a real website. So let's build one: a responsive landing page for a made-up product, from an empty file to something you'd actually ship. We'll do it section by section, and every step runs live in the box right next to it.
The product is Brew, a fictional coffee-subscription app. Doesn't matter that it's fake. The structure you'll write here is the structure behind almost every marketing page on the web: a sticky nav, a hero that sells the thing, a grid of features, and a footer. Build this once and you've got a template you can repurpose forever.
What we're building
Here's the finished page. Edit anything in the code (change a colour, a word, a number) and the preview updates. Drag the preview narrower to watch it reflow to mobile. We'll spend the rest of the lesson building up to exactly this, one piece at a time, so don't worry about understanding all of it yet.
Looks like a product, doesn't it. Now let's see how every piece got there. Open a blank index.html and styles.css in your head and follow along. Each section below is a self-contained step you can run on its own.
Set the stage: page shell and custom properties
Every CSS project I start opens the same way: a small reset and a block of custom properties. Properties are your theme. Define a colour or a radius once at the top, reference it everywhere, and changing the whole look later is a one-line edit instead of a find-and-replace nightmare.
:root {
--brand: #7c3aed;
--ink: #1e1b2e;
--muted: #6b6880;
--surface: #f6f4fb;
--radius: 14px;
--max: 1040px;
}
* { box-sizing: border-box; margin: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
color: var(--ink);
line-height: 1.6;
}:root is the document, so properties declared there are global. var(--brand) pulls a value back out wherever you need it. The * { box-sizing: border-box } line is the one reset I never skip: it makes width mean the total width including padding and border, which is how everyone wishes the box model worked by default. We covered why in selectors and the box model. Zeroing margin kills the browser's default spacing so you start from a clean slate.
system-ui tells the browser to use the operating system's own font: San Francisco on a Mac, Segoe on Windows. It's fast (nothing to download), looks native, and is a perfectly good default before you reach for custom fonts. Type and colour basics live in colours, units, and typography if you want the full tour.
The nav bar: flexbox in three lines
The header is the cleanest possible use of flexbox: a logo pinned left, links pinned right, vertically centred. That's the job flexbox was born for.
Three declarations do all the heavy lifting. display: flex lays the children out in a row. align-items: center lines them up on the cross axis so the logo and links sit at the same height. And justify-content: space-between shoves the first child to the far left and the last to the far right, with all the empty space dumped in the middle. That's what splits the logo from the link group with zero manual margins. Inside .nav-links, a second flex container plus gap: 20px spaces the links evenly. No floats, no positioning hacks. This is why flexbox replaced a decade of CSS workarounds.
The hero: type that does the selling
The hero is mostly typography. A big headline, a softer sub-line, one obvious button. The trick that makes it feel responsive without a single media query is clamp().
.hero h1 {
font-size: clamp(2rem, 6vw, 3.5rem);
line-height: 1.1;
letter-spacing: -0.02em;
}clamp(min, preferred, max) gives you a font size that scales with the viewport but never gets too small or too big. Here the headline wants to be 6vw (6% of the viewport width), but it'll never shrink below 2rem on a phone or grow past 3.5rem on a wide monitor. One line replaces three breakpoints. It's part of the modern CSS toolkit and it's genuinely magic for headings.
The rest is restraint: an uppercase "eyebrow" label above the headline, a muted lead paragraph capped at max-width: 540px so lines don't run uncomfortably wide, and the page centred with margin: 0 auto. Tight line lengths are one of the cheapest readability wins there is.
One job for the hero
A hero should answer "what is this and why do I care" in the time it takes to scroll past. One headline, one sentence, one button. When you catch yourself adding a third paragraph or a second button, you're diluting the one thing the hero is for.
The features grid: auto-fit and minmax
This is the section that earns the "responsive" label. Four feature cards that sit in a row on a wide screen, two-up on a tablet, and stack to one column on a phone, and crucially, without you writing a single breakpoint for it. Grid does it with one line.
The whole responsive behaviour is in this one line:
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));Read it inside out. minmax(220px, 1fr) says "each column is at least 220px wide, but if there's spare room, grow to share it equally" (1fr means one fraction of the leftover space). auto-fit says "fit as many of those columns as you can across the row, and stretch them to fill." So on a wide screen four cards fit. Squeeze the window and the grid drops to three, then two, then one, recalculating on its own as the available width changes. Drag the preview above narrower and watch it happen. This pattern is the single most useful thing in CSS grid, and once it clicks you'll reach for it constantly. We dug into it in the responsive card layout lesson.
Quick check
In repeat(auto-fit, minmax(220px, 1fr)), what makes the cards reflow from four columns down to one as the screen narrows?
Polish: hover transitions and the mobile pass
Two finishing touches separate a page that works from a page that feels good. First, motion. A transition on the cards and buttons makes them respond to a cursor instead of sitting there like a screenshot.
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(124, 58, 237, 0.12);
}
.btn {
transition: background 0.2s ease, transform 0.2s ease;
}
.btn:hover {
background: var(--brand-dark);
transform: translateY(-2px);
}The transition line lives on the resting state, not the hover state. It describes how to animate into and out of any change. List the exact properties you're animating (transform, box-shadow) and a duration. On hover, the card lifts 4px and gains a soft brand-tinted shadow, and the lift is small on purpose. A 4px nudge reads as "this is interactive." A 40px launch reads as "something is broken."
Second, the mobile pass. The grid already reflows itself, the hero already uses clamp(), so most of the page is responsive for free. That's the payoff of building mobile-first. A small media query just tightens the padding where a phone is cramped:
@media (max-width: 560px) {
.nav { padding: 12px 16px; }
.nav-links { gap: 12px; }
.hero { padding: 56px 20px; }
}That's it: three rules, only the spacing that actually needs adjusting on a narrow screen. Notice how little there is. When your layout is built on flexbox, grid auto-fit, and clamp(), the media query stops being where you rebuild the page for mobile and becomes where you nudge a few numbers. That's the goal.
Respect reduced motion
Some people get motion sick from animations, and their OS has a setting for it. Wrap non-essential motion in @media (prefers-reduced-motion: reduce) and dial it down. On a real project you'd add a block that sets transition: none for those users. It's a few lines and it's the considerate thing to do.
Put it together and make it yours
Scroll back up to the full page at the top. You now know what every line is doing. The shell and custom properties set the theme. Flexbox builds the nav. clamp() makes the hero type fluid. grid with auto-fit and minmax gives you a features section that reflows on its own. Transitions add the feel, and one tiny media query handles the rest.
The best way to lock this in is to break it. Open that first Playground and try:
- Change
--brandfrom purple to a green or an orange and watch the entire page re-theme from one line. That's the power of custom properties. - Add a fifth
<article class="card">and notice the grid absorb it with no CSS change at all. - Drop the
minmaxminimum from220pxto300pxand see the cards reflow to fewer columns sooner. - Crank the hover
translateYto-20pxto feel why subtle wins.
Where this leaves you, and what's next
You started this series not knowing what a tag was. You can now build a complete, responsive, accessible web page from a blank file: semantic HTML for structure, the box model for sizing, flexbox and grid for layout, media queries and clamp() for responsiveness, custom properties for theming, and transitions for polish. That's the full front-end presentation layer. Every marketing page, dashboard, and blog you'll build leans on exactly these pieces. If you ever need the canonical reference for any property, MDN's Learn CSS is the best free resource on the web, and the broader MDN Web Docs cover every property in depth.
But a landing page is static. It shows the same thing to everyone and doesn't really do anything. The next leap is interfaces that respond to data and clicks: counters that count, forms that validate as you type, lists that update. That's what React for Beginners is for, and it builds directly on the HTML, CSS, and JavaScript you've gathered here. You've got the structure and the style down. Time to make it think.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


