The plan: anatomy of a landing page
Almost every landing page is the same four stacked regions. We'll build each one, then stack them. Here's the structure we're aiming for:
The semantic skeleton
Start with meaningful HTML landmarks — <header>, <nav>, <main>, <section>, <footer>. Good structure helps screen readers and keeps your CSS sane.
The HTML
<header> <a class="logo">Brand</a> <button class="burger" aria-label="Open menu">…</button> <nav class="drawer">…</nav> </header> <main> <section class="hero">…</section> <section class="cards">…</section> </main> <footer>…</footer>
The base CSS (set once for the whole page)
/* reset every element to a predictable starting point */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, sans-serif; color: #1f2433; line-height: 1.6; } img, video { max-width: 100%; display: block; } media never overflows main { max-width: 1100px; margin: 0 auto; padding: 0 20px; } centered content column
<head>: <meta name="viewport" content="width=device-width, initial-scale=1.0"> — without it, none of the responsive CSS will work on phones.Hero option A — a background image
The simplest, most common hero: a full-width background image, a dark overlay so text stays readable, and a centered heading + call-to-action button.
The HTML
<section class="hero"> <div class="overlay"></div> dark layer for readable text <div class="htext"> <h1>Welcome to BrightCo</h1> <p>Beautiful sites, built fast.</p> <a class="cta" href="#">Get started</a> </div> </section>
The CSS
.hero { background: url('photo.jpg') center/cover; min-height: 60vh; display: grid; place-items: center; position: relative; text-align: center; } .hero .overlay { position: absolute; inset: 0; background: linear-gradient(rgba(15,23,42,.25), rgba(15,23,42,.7)); }
center/cover keeps the image filling the hero without distortion at any screen size. The dark overlay gradient is what guarantees your white text passes contrast over any photo.Hero option B — a background video
Same idea, more motion: a <video> that fills the hero behind the text. It must be muted to autoplay, plus loop and playsinline; a poster shows while it loads.
<section class="hero"> <video class="bg" autoplay muted loop playsinline poster="fallback.jpg"> <source src="clip.mp4" type="video/mp4"> </video> <div class="overlay"></div> <div class="htext">…</div> </section>
.hero video.bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
poster image, and remember some phones won't autoplay — the poster (and your overlay text) must look fine on their own.Hero option C — a slideshow
Show several images (or videos) in rotation. Each slide is stacked in the same spot; only the active one is opaque, and we cross-fade between them. It advances on its own, with dots and arrows to navigate:
The HTML
<div class="slideshow"> <div class="slide on" style="background-image:url('1.jpg')"><span class="cap">Mountains</span></div> <div class="slide" style="background-image:url('2.jpg')"><span class="cap">Waterfall</span></div> <div class="slide" style="background-image:url('3.jpg')"><span class="cap">Forest</span></div> <button class="nav prev">‹</button> <button class="nav next">›</button> <div class="dots"></div> dots added by JS </div>
The CSS
.slide { position: absolute; inset: 0; opacity: 0; transition: opacity .8s; } .slide.on { opacity: 1; } JS moves the .on class every few seconds /* prev / next arrows */ .slideshow .nav { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,.4); color: #fff; border: 0; width: 34px; height: 34px; border-radius: 50%; cursor: pointer; } .slideshow .prev { left: 10px; } .slideshow .next { right: 10px; } /* dots */ .slideshow .dots { position: absolute; bottom: 12px; left: 0; right: 0; display: flex; gap: 7px; justify-content: center; } .slideshow .dots button { width: 10px; height: 10px; border-radius: 50%; border: 0; background: rgba(255,255,255,.5); cursor: pointer; } .slideshow .dots button.on { background: #fff; }
let i = 0; const slides = document.querySelectorAll('.slide'); function show(n) { slides[i].classList.remove('on'); i = (n + slides.length) % slides.length; wrap around slides[i].classList.add('on'); } setInterval(() => show(i + 1), 3500); auto-advance
Three cards, three ways
Cards group related content. Here are three common constructions — pick whichever fits your content. They sit in a responsive grid that wraps on small screens:
Icon / feature card
An icon, heading, and a line of text. Great for listing features or benefits.
Overlay card
Text laid over the image with a gradient for contrast.
The HTML — three card styles in one grid
<section class="cards"> <!-- 1 · image-top card --> <article class="card c1"> <div class="pic"></div> <div class="body"> <h4>Image-top card</h4> <p>A photo, title, text, and a link.</p> <a href="#">Read more →</a> </div> </article> <!-- 2 · icon / feature card --> <article class="card c2"> <div class="ic"><i class="ph ph-rocket-launch"></i></div> <h4>Icon / feature card</h4> <p>An icon, heading, and a line of text.</p> </article> <!-- 3 · overlay card (text over the image) --> <article class="card c3"> <div class="body"> <h4>Overlay card</h4> <p>Text laid over the image.</p> </div> </article> </section>
The CSS — the responsive grid
.cards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; overflow: hidden; } .card.c3 { position: relative; background: url('photo.jpg') center/cover; display: flex; align-items: flex-end; }
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) fits as many cards as will comfortably fit and wraps the rest — three across on desktop, one on a phone, no media query needed. Build more variations in the Cards Lab.Tying responsiveness together
Each piece is already fluid; a few deliberate breakpoints make it shine. The pattern across the whole page:
| Piece | Phone | Desktop |
|---|---|---|
| Nav | Hamburger + right drawer | Inline links (hamburger hidden) |
| Hero | Shorter, smaller type (clamp()) | Tall, large headline |
| Cards | 1 column (auto-fit wraps) | 3 across |
| Footer | Columns stacked | Multi-column grid |
repeat(auto-fit, minmax(…)) grids that wrap on their own, and a couple of @media (min-width: …) queries for the deliberate changes (showing inline nav, growing the hero). Full depth in the Responsive Design lesson.The whole page, assembled
All four pieces stacked into one responsive page — with the right-side hamburger nav working. Tap the menu, scroll the content, see the hero, cards, and footer in place:
Built in one lesson
Get startedFast
Loads in a blink.
Responsive
Fits every screen.
Accessible
Works for everyone.
Landing-page checklist
- Semantic skeleton:
header/nav/main/footer+ the viewport tag - Mobile nav: hamburger right, drawer slides from the right, closes 4 ways
- Hero has a dark overlay so text stays readable over any image/video
- Background video is
muted loop playsinlinewith aposter - Slideshow cross-fades, auto-advances, and has manual controls
- Cards in an
auto-fitgrid; footer columns stack on phones - Tested at phone, tablet & desktop widths