Project: Responsive Landing Page
Time to put everything together. In this project you will build a complete, responsive landing page for Dabba Express, a small business in Pune that delivers home-style lunch boxes to offices. The page has a sticky header with a working mobile menu, a hero section, feature cards, pricing plans, an FAQ accordion, a sign-up form and a footer. It works on phones and desktops, supports dark mode, and is usable with a keyboard and a screen reader.
You will build it step by step, mobile first, and every step uses something from earlier lessons. At the end you will see the complete page, run it, and learn how to turn the prototype into a production build.
Step 1: plan the page and the design tokens
Before writing classes, decide three things.
The sections, from top to bottom: header, hero, "why us" cards, pricing plans, questions, sign-up band, footer.
The layout on each screen size. On phones everything is one column. From sm some things sit side by side, such as the hero buttons. From md the full navigation appears. From lg the hero becomes two columns and the plans sit in a row of three.
The design tokens. A food brand suits warm colours, so you will add a brand palette in a saffron-brown hue and a serif display font for headings. Choose the shades with contrast in mind: white text needs a dark shade, so buttons use brand-700 and brand-800, while light tints like brand-50 are only for backgrounds. Here is the start of the page with the theme:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dabba Express: home-style lunch in Pune</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--color-brand-50: oklch(97% 0.03 75);
--color-brand-100: oklch(93% 0.06 75);
--color-brand-300: oklch(83% 0.12 70);
--color-brand-700: oklch(52% 0.15 45);
--color-brand-800: oklch(45% 0.13 42);
--color-brand-900: oklch(35% 0.09 40);
--font-display: Georgia, "Times New Roman", serif;
}
</style>
</head>Only the shades the design needs are defined. The --font-display stack uses fonts already on every computer, so nothing has to download. (See Customising with @theme.)
Step 2: a sticky header with a mobile menu
The header must stay visible while scrolling, show all links on wide screens, and collapse into a menu button on phones. Run this step on its own and make the result window narrow, then wide.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Step 2: header</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--color-brand-50: oklch(97% 0.03 75);
--color-brand-100: oklch(93% 0.06 75);
--color-brand-300: oklch(83% 0.12 70);
--color-brand-700: oklch(52% 0.15 45);
--color-brand-800: oklch(45% 0.13 42);
--color-brand-900: oklch(35% 0.09 40);
--font-display: Georgia, "Times New Roman", serif;
}
</style>
</head>
<body class="bg-white text-slate-700 scheme-light dark:bg-slate-950 dark:text-slate-300 dark:scheme-dark">
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-950/90">
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
<a href="#" class="font-display text-xl font-bold text-brand-800 dark:text-brand-300">Dabba Express</a>
<nav aria-label="Main" class="hidden gap-6 md:flex">
<a href="#menu" class="hover:text-brand-700 dark:hover:text-brand-300">Menu</a>
<a href="#plans" class="hover:text-brand-700 dark:hover:text-brand-300">Plans</a>
<a href="#faq" class="hover:text-brand-700 dark:hover:text-brand-300">FAQ</a>
</nav>
<a href="#signup" class="hidden rounded-lg bg-brand-700 px-4 py-2 font-semibold text-white hover:bg-brand-800 md:inline-block">Start a trial</a>
<button id="menu-toggle" aria-expanded="false" aria-controls="mobile-menu" class="group rounded-md p-2 hover:bg-slate-100 md:hidden dark:hover:bg-slate-800">
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-6 fill-none stroke-current stroke-2 group-aria-expanded:hidden"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
<svg aria-hidden="true" viewBox="0 0 24 24" class="hidden size-6 fill-none stroke-current stroke-2 group-aria-expanded:block"><path d="M6 6l12 12M18 6 6 18"/></svg>
<span class="sr-only">Menu</span>
</button>
</div>
<nav id="mobile-menu" aria-label="Mobile" hidden class="border-t border-slate-200 px-4 py-3 md:hidden dark:border-slate-800">
<a href="#menu" class="block py-2">Menu</a>
<a href="#plans" class="block py-2">Plans</a>
<a href="#faq" class="block py-2">FAQ</a>
<a href="#signup" class="mt-2 block rounded-lg bg-brand-700 py-2 text-center font-semibold text-white">Start a trial</a>
</nav>
</header>
<main id="main" class="mx-auto max-w-6xl px-4 py-10">
<p class="max-w-prose">Scroll down: the header stays at the top. Make the window narrow to see the menu button.</p>
<div class="mt-6 h-[150vh] rounded-xl bg-brand-50 dark:bg-slate-900"></div>
</main>
<script>
const toggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
toggle.addEventListener('click', () => {
const open = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!open));
mobileMenu.hidden = open;
});
</script>
</body>
</html>How it works:
sticky top-0 z-40keeps the header on top of the content while you scroll.bg-white/90 backdrop-blur-smmakes it slightly see-through with a frosted look.- Inside,
mx-auto max-w-6xl px-4is the content column used by every section on the page, so all sections line up. - The desktop
<nav>and "Start a trial" button arehidden md:flexandhidden md:inline-block. The menu button is the opposite:md:hidden. - The button carries the menu's state in
aria-expanded. Two icons inside it swap withgroup-aria-expanded:hiddenandgroup-aria-expanded:block, so the icon always matches what screen readers announce. - The script only flips
aria-expandedand thehiddenattribute of the mobile menu. All the visual changes come from classes.
Step 3: the hero section
The hero is the first thing visitors see, so it needs a clear headline and one main action.
<section class="bg-brand-50 dark:bg-slate-900">
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 py-16 lg:grid-cols-2 lg:py-24">
<div>
<p class="text-sm font-semibold tracking-wider text-brand-700 uppercase dark:text-brand-300">Pune · Monday to Saturday</p>
<h1 class="mt-3 font-display text-4xl font-bold text-balance text-slate-900 sm:text-5xl dark:text-white">Home-style lunch, delivered hot to your desk</h1>
<div class="mt-8 flex flex-col gap-3 sm:flex-row">...two buttons...</div>
</div>
<img src="..." alt="A steel lunch box with rice, dal and two sabzis" class="aspect-video w-full rounded-2xl object-cover shadow-xl">
</div>
</section>The section's background stretches the full width, while the inner <div> keeps the content in the centred column. grid lg:grid-cols-2 stacks text and image on phones and puts them side by side on large screens. text-balance avoids a single lonely word on the last line of the headline, and the heading grows from text-4xl to sm:text-5xl. The two buttons stack full width on phones (flex-col) and sit in a row from sm, which gives phone users large, easy targets.
Step 4: feature cards
Three cards explain why customers choose the service:
<div class="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<article class="rounded-2xl border border-slate-200 p-6 dark:border-slate-800 dark:bg-slate-900">...</article>
<article class="rounded-2xl border border-slate-200 p-6 dark:border-slate-800 dark:bg-slate-900">...</article>
<article class="rounded-2xl border border-slate-200 p-6 sm:col-span-2 lg:col-span-1 dark:border-slate-800 dark:bg-slate-900">...</article>
</div>One column on phones, two from sm, three from lg. With two columns, three cards would leave a gap, so the third card uses sm:col-span-2 to fill the whole row, and lg:col-span-1 resets it when there are three columns. Every border has an explicit colour, because v4 borders otherwise use the text colour.
Step 5: pricing plans
Pricing cards have different amounts of text, but their links should line up at the bottom. Each card is a flex column, and the link area has mt-auto to take the spare space. The recommended plan gets a stronger ring and a badge across its top edge:
<article class="relative flex flex-col rounded-2xl bg-white p-6 ring-2 ring-brand-700 dark:bg-slate-950">
<span class="absolute -top-3 left-6 rounded-full bg-brand-700 px-3 py-0.5 text-xs font-semibold text-white">Most popular</span>
<h3 class="font-semibold">Full thali</h3>
<p class="mt-2 text-3xl font-bold text-slate-900 dark:text-white">₹3,200<span class="text-base font-normal text-slate-500">/month</span></p>
<p class="mt-2">4 rotis, rice, dal, two sabzis, salad.</p>
<div class="mt-auto pt-6">
<a href="#signup" class="block rounded-lg bg-brand-700 py-2.5 text-center font-semibold text-white hover:bg-brand-800">Choose Full thali</a>
</div>
</article>The other cards use a thin ring-1 ring-slate-200 instead of a border, so all three cards have exactly the same size whether they are highlighted or not; rings never change the layout. The pt-6 wrapper keeps space above the button on phones, where the cards are not stretched and mt-auto adds nothing.
Step 6: questions with an accordion
The FAQ uses the HTML <details> element, which opens and closes without JavaScript. Giving every <details> the same name makes it an exclusive accordion: opening one closes the others.
<details name="faq" class="group rounded-xl border border-slate-200 p-4 open:bg-brand-50 dark:border-slate-800 dark:open:bg-slate-900">
<summary class="flex cursor-pointer list-none items-center justify-between font-semibold [&::-webkit-details-marker]:hidden">
Which areas do you deliver to?
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 shrink-0 fill-none stroke-current stroke-2 motion-safe:transition-transform group-open:rotate-180">...</svg>
</summary>
<p class="mt-3">Kothrud, Karve Nagar, Aundh, Baner and Hinjewadi Phase 1.</p>
</details>open: styles the open item, and group-open:rotate-180 turns the arrow. list-none hides the default triangle, and the arbitrary variant [&::-webkit-details-marker]:hidden hides it in Safari too. The arrow only animates with motion-safe:.
Step 7: the sign-up band
The last section asks for a mobile number. The input and button stack on phones and sit side by side from sm:
<form id="trial" class="mt-8 flex w-full max-w-md flex-col gap-3 sm:flex-row lg:mt-0">
<label for="phone" class="sr-only">Mobile number</label>
<input id="phone" type="tel" required pattern="[6-9][0-9]{9}" inputmode="numeric" placeholder="10-digit mobile number"
class="min-w-0 flex-1 rounded-lg bg-white px-4 py-3 text-slate-900 placeholder:text-slate-500 focus:outline-2 focus:outline-brand-300 user-invalid:outline-2 user-invalid:outline-red-400">
<button class="rounded-lg bg-brand-300 px-6 py-3 font-semibold text-brand-900 hover:bg-white">Call me</button>
</form>
<p id="status" role="status" class="... empty:hidden"></p>The label is sr-only because the design has no visible label, but screen readers still announce "Mobile number". The browser checks the pattern before the form can submit, and user-invalid: shows a red outline only after the visitor has typed. The status paragraph is hidden while empty (empty:hidden), and role="status" makes screen readers read the thank-you message when the script fills it in.
Step 8: dark mode and the accessibility pass
With the structure done, go through the page once more:
- Dark mode: every background and text colour gets a
dark:partner, such asdark:bg-slate-950anddark:text-slate-300. Accent text switches to the lighterbrand-300, which reads better on dark backgrounds.scheme-light dark:scheme-darkon the<body>makes form controls match. - Skip link: the first element in the body is
sr-only focus:not-sr-onlywithfocus:padding. - Anchors under a sticky header: clicking "Plans" would scroll the heading underneath the header.
scroll-mt-20on each section leaves room above it. - Focus: the main buttons have
focus-visible:outline-*styles; links keep the browser's default focus outline, which Tailwind does not remove. - Images and icons: the photo has a real
alttext; decorative SVGs havearia-hidden="true".
The complete page
Here is the whole page. Run it, resize the result window, open the mobile menu, open the questions, and try the form with a wrong and a right number. Switch your system to dark mode to see the dark version.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dabba Express: home-style lunch in Pune</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--color-brand-50: oklch(97% 0.03 75);
--color-brand-100: oklch(93% 0.06 75);
--color-brand-300: oklch(83% 0.12 70);
--color-brand-700: oklch(52% 0.15 45);
--color-brand-800: oklch(45% 0.13 42);
--color-brand-900: oklch(35% 0.09 40);
--font-display: Georgia, "Times New Roman", serif;
}
</style>
</head>
<body class="bg-white text-slate-700 scheme-light dark:bg-slate-950 dark:text-slate-300 dark:scheme-dark">
<a href="#main" class="sr-only rounded-md bg-brand-700 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-50 focus:px-4 focus:py-2">Skip to content</a>
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-950/90">
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
<a href="#" class="font-display text-xl font-bold text-brand-800 dark:text-brand-300">Dabba Express</a>
<nav aria-label="Main" class="hidden gap-6 md:flex">
<a href="#menu" class="hover:text-brand-700 dark:hover:text-brand-300">Menu</a>
<a href="#plans" class="hover:text-brand-700 dark:hover:text-brand-300">Plans</a>
<a href="#faq" class="hover:text-brand-700 dark:hover:text-brand-300">FAQ</a>
</nav>
<a href="#signup" class="hidden rounded-lg bg-brand-700 px-4 py-2 font-semibold text-white hover:bg-brand-800 md:inline-block">Start a trial</a>
<button id="menu-toggle" aria-expanded="false" aria-controls="mobile-menu" class="group rounded-md p-2 hover:bg-slate-100 md:hidden dark:hover:bg-slate-800">
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-6 fill-none stroke-current stroke-2 group-aria-expanded:hidden"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
<svg aria-hidden="true" viewBox="0 0 24 24" class="hidden size-6 fill-none stroke-current stroke-2 group-aria-expanded:block"><path d="M6 6l12 12M18 6 6 18"/></svg>
<span class="sr-only">Menu</span>
</button>
</div>
<nav id="mobile-menu" aria-label="Mobile" hidden class="border-t border-slate-200 px-4 py-3 md:hidden dark:border-slate-800">
<a href="#menu" class="block py-2">Menu</a>
<a href="#plans" class="block py-2">Plans</a>
<a href="#faq" class="block py-2">FAQ</a>
<a href="#signup" class="mt-2 block rounded-lg bg-brand-700 py-2 text-center font-semibold text-white">Start a trial</a>
</nav>
</header>
<main id="main">
<section class="bg-brand-50 dark:bg-slate-900">
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 py-16 lg:grid-cols-2 lg:py-24">
<div>
<p class="text-sm font-semibold tracking-wider text-brand-700 uppercase dark:text-brand-300">Pune · Monday to Saturday</p>
<h1 class="mt-3 font-display text-4xl font-bold text-balance text-slate-900 sm:text-5xl dark:text-white">Home-style lunch, delivered hot to your desk</h1>
<p class="mt-4 max-w-prose text-lg text-pretty">Fresh dal, sabzi, rotis and rice cooked every morning in our Kothrud kitchen, packed in steel dabbas and delivered before 1 pm.</p>
<div class="mt-8 flex flex-col gap-3 sm:flex-row">
<a href="#signup" class="rounded-lg bg-brand-700 px-6 py-3 text-center font-semibold text-white shadow-sm hover:bg-brand-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-700">Start a 3-day trial</a>
<a href="#menu" class="rounded-lg px-6 py-3 text-center font-semibold text-brand-800 ring-1 ring-brand-300 hover:bg-white dark:text-brand-300 dark:hover:bg-slate-800">See this week's menu</a>
</div>
</div>
<img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="A steel lunch box with rice, dal and two sabzis" class="aspect-video w-full rounded-2xl object-cover shadow-xl">
</div>
</section>
<section id="menu" class="mx-auto max-w-6xl scroll-mt-20 px-4 py-16">
<h2 class="font-display text-3xl font-bold text-slate-900 dark:text-white">Why office teams choose us</h2>
<div class="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<article class="rounded-2xl border border-slate-200 p-6 dark:border-slate-800 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Cooked this morning</h3>
<p class="mt-2">No frozen food. The menu changes every day and is posted each Sunday.</p>
</article>
<article class="rounded-2xl border border-slate-200 p-6 dark:border-slate-800 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Less oil, real ghee</h3>
<p class="mt-2">Simple home recipes, with Jain and diabetic-friendly options on request.</p>
</article>
<article class="rounded-2xl border border-slate-200 p-6 sm:col-span-2 lg:col-span-1 dark:border-slate-800 dark:bg-slate-900">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">No plastic</h3>
<p class="mt-2">Food travels in steel dabbas, which we collect the next day.</p>
</article>
</div>
</section>
<section id="plans" class="scroll-mt-20 bg-slate-50 dark:bg-slate-900">
<div class="mx-auto max-w-6xl px-4 py-16">
<h2 class="font-display text-3xl font-bold text-slate-900 dark:text-white">Simple monthly plans</h2>
<div class="mt-8 grid gap-6 lg:grid-cols-3">
<article class="flex flex-col rounded-2xl bg-white p-6 ring-1 ring-slate-200 dark:bg-slate-950 dark:ring-slate-800">
<h3 class="font-semibold">Mini</h3>
<p class="mt-2 text-3xl font-bold text-slate-900 dark:text-white">₹2,400<span class="text-base font-normal text-slate-500">/month</span></p>
<p class="mt-2">4 rotis, dal, one sabzi.</p>
<a href="#signup" class="mt-auto pt-6 font-semibold text-brand-700 hover:underline dark:text-brand-300">Choose Mini</a>
</article>
<article class="relative flex flex-col rounded-2xl bg-white p-6 ring-2 ring-brand-700 dark:bg-slate-950">
<span class="absolute -top-3 left-6 rounded-full bg-brand-700 px-3 py-0.5 text-xs font-semibold text-white">Most popular</span>
<h3 class="font-semibold">Full thali</h3>
<p class="mt-2 text-3xl font-bold text-slate-900 dark:text-white">₹3,200<span class="text-base font-normal text-slate-500">/month</span></p>
<p class="mt-2">4 rotis, rice, dal, two sabzis, salad.</p>
<div class="mt-auto pt-6">
<a href="#signup" class="block rounded-lg bg-brand-700 py-2.5 text-center font-semibold text-white hover:bg-brand-800">Choose Full thali</a>
</div>
</article>
<article class="flex flex-col rounded-2xl bg-white p-6 ring-1 ring-slate-200 dark:bg-slate-950 dark:ring-slate-800">
<h3 class="font-semibold">Team of 10+</h3>
<p class="mt-2 text-3xl font-bold text-slate-900 dark:text-white">₹2,900<span class="text-base font-normal text-slate-500">/person</span></p>
<p class="mt-2">Full thali for offices, one bill each month.</p>
<a href="#signup" class="mt-auto pt-6 font-semibold text-brand-700 hover:underline dark:text-brand-300">Talk to us</a>
</article>
</div>
</div>
</section>
<section id="faq" class="mx-auto max-w-3xl scroll-mt-20 px-4 py-16">
<h2 class="font-display text-3xl font-bold text-slate-900 dark:text-white">Questions</h2>
<div class="mt-6 flex flex-col gap-3">
<details name="faq" class="group rounded-xl border border-slate-200 p-4 open:bg-brand-50 dark:border-slate-800 dark:open:bg-slate-900">
<summary class="flex cursor-pointer list-none items-center justify-between font-semibold text-slate-900 dark:text-white [&::-webkit-details-marker]:hidden">
Which areas do you deliver to?
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 shrink-0 fill-none stroke-current stroke-2 motion-safe:transition-transform group-open:rotate-180"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<p class="mt-3">Kothrud, Karve Nagar, Aundh, Baner and Hinjewadi Phase 1.</p>
</details>
<details name="faq" class="group rounded-xl border border-slate-200 p-4 open:bg-brand-50 dark:border-slate-800 dark:open:bg-slate-900">
<summary class="flex cursor-pointer list-none items-center justify-between font-semibold text-slate-900 dark:text-white [&::-webkit-details-marker]:hidden">
Can I pause on holidays?
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 shrink-0 fill-none stroke-current stroke-2 motion-safe:transition-transform group-open:rotate-180"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<p class="mt-3">Yes. Tell us by 8 pm the day before and that day is added to the end of your month.</p>
</details>
</div>
</section>
<section id="signup" class="scroll-mt-20 bg-brand-900 text-brand-50">
<div class="mx-auto max-w-6xl px-4 py-16 lg:flex lg:items-center lg:justify-between lg:gap-10">
<div>
<h2 class="font-display text-3xl font-bold text-white">Try three lunches for ₹300</h2>
<p class="mt-2 text-brand-100">We will call you to confirm your address and start date.</p>
</div>
<form id="trial" class="mt-8 flex w-full max-w-md flex-col gap-3 sm:flex-row lg:mt-0">
<label for="phone" class="sr-only">Mobile number</label>
<input id="phone" name="phone" type="tel" required pattern="[6-9][0-9]{9}" inputmode="numeric" placeholder="10-digit mobile number" class="min-w-0 flex-1 rounded-lg bg-white px-4 py-3 text-slate-900 placeholder:text-slate-500 focus:outline-2 focus:outline-brand-300 user-invalid:outline-2 user-invalid:outline-red-400">
<button class="rounded-lg bg-brand-300 px-6 py-3 font-semibold text-brand-900 hover:bg-white">Call me</button>
</form>
</div>
<p id="status" role="status" class="mx-auto max-w-6xl px-4 pb-8 text-brand-100 empty:hidden"></p>
</section>
</main>
<footer class="border-t border-slate-200 dark:border-slate-800">
<div class="mx-auto flex max-w-6xl flex-col gap-2 px-4 py-8 text-sm sm:flex-row sm:justify-between">
<p>Dabba Express, Kothrud, Pune</p>
<p>Kitchen open 7 am to 1 pm, Monday to Saturday</p>
</div>
</footer>
<script>
const toggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
toggle.addEventListener('click', () => {
const open = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!open));
mobileMenu.hidden = open;
});
document.getElementById('trial').addEventListener('submit', (event) => {
event.preventDefault();
document.getElementById('status').textContent = 'Thank you! We will call you within one working day.';
});
</script>
</body>
</html>Step 9: from prototype to production
The browser build is perfect for building the page, but the live site needs a real CSS file. With Vite (see Installing Tailwind CSS v4):
- Create a Vite project, install
tailwindcssand@tailwindcss/vite, and add the plugin tovite.config.js. - Move the contents of the
<style type="text/tailwindcss">element intosrc/style.css, below the import. - Delete the browser
<script>tag and link the stylesheet instead. - Run
npm run buildand upload thedistfolder.
/* src/style.css */
@import "tailwindcss";
@theme {
--color-brand-50: oklch(97% 0.03 75);
--color-brand-700: oklch(52% 0.15 45);
/* ...the other brand shades and the display font... */
}npm run buildNothing in the HTML changes: the same classes now come from a small, minified CSS file. If any style is missing after the build, check the list in Performance & Production Builds.
Ideas to extend the project
- Add a testimonials section whose cards use container queries (
@containerand@md:flex-row), so the same card works in the main column and in a sidebar. - Add a manual dark-mode toggle with
@custom-variant dark (&:where(.dark, .dark *));and a remembered choice, as in Dark Mode. - Turn the repeated feature cards and plans into components, either a JavaScript template function or Blade components, as in Reusable Components.
- Add a weekly menu table that scrolls sideways on phones with
overflow-x-auto.
Key points
- Plan sections, breakpoints and design tokens before writing classes, and build mobile first.
- One content column (
mx-auto max-w-6xl px-4) inside full-width sections keeps the whole page aligned. - Store UI state in attributes (
aria-expanded,open) and let variants such asgroup-aria-expanded:andgroup-open:do the visual work. gridwith breakpoint column counts andcol-span-*handles cards;flex flex-colwithmt-autoaligns buttons in uneven cards.- Finish with a dark mode and accessibility pass:
dark:colours, a skip link, focus styles,scroll-mt-*for anchors, labels and alt text. - For the live site, move the theme into a CSS file and build with Vite or the CLI instead of the browser build.
Exercise
Add a "What our customers say" section between the plans and the questions. It should contain two testimonial cards, each with an avatar ({{samples}}/avatar.svg), a short quote, and the person's name and company. Make the section's cards one column on phones and two from md. Each card should place the avatar above the text by default and beside it when the card's container is at least @sm wide, and the section must look right in dark mode.
Show solution
<section class="mx-auto max-w-6xl px-4 py-16">
<h2 class="font-display text-3xl font-bold text-slate-900 dark:text-white">What our customers say</h2>
<div class="mt-8 grid gap-6 md:grid-cols-2">
<div class="@container">
<figure class="flex flex-col gap-4 rounded-2xl border border-slate-200 p-6 @sm:flex-row dark:border-slate-800 dark:bg-slate-900">
<img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" alt="" class="size-14 shrink-0 rounded-full">
<div>
<blockquote class="text-pretty">"Our whole team of twelve eats Dabba Express every day. The dal tastes like home."</blockquote>
<figcaption class="mt-3 text-sm font-semibold text-slate-900 dark:text-white">Ananya Kulkarni, design studio in Baner</figcaption>
</div>
</figure>
</div>
<div class="@container">
<figure class="flex flex-col gap-4 rounded-2xl border border-slate-200 p-6 @sm:flex-row dark:border-slate-800 dark:bg-slate-900">
<img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" alt="" class="size-14 shrink-0 rounded-full">
<div>
<blockquote class="text-pretty">"Always on time, even in the monsoon. The steel dabbas are a nice touch."</blockquote>
<figcaption class="mt-3 text-sm font-semibold text-slate-900 dark:text-white">Rohan Deshpande, accounts office in Kothrud</figcaption>
</div>
</figure>
</div>
</div>
</section>The grid gives one column on phones and two from md. Each card sits in its own @container, so @sm:flex-row reacts to the card's actual width rather than the screen. The avatar is decorative here because the name is written next to it, so its alt is empty.
Quick Quiz
1. Why does each section use a full-width outer element and an inner mx-auto max-w-6xl px-4 element?
The outer element carries the background colour; the inner column keeps every section's content lined up.
2. Why does each section have scroll-mt-20?
Scroll margin leaves room above the target, so the sticky header does not hide it.
3. How does the mobile menu button switch between the "open" and "close" icons?
The script only flips aria-expanded; the variants read it, so the icon always matches the announced state.
4. What must you change in the HTML when you move from the browser build to a Vite production build?
The classes are identical; only how the CSS is generated and loaded changes.
Learn Tailwind CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
