Building a Landing Page in Tailwind CSS: Section by Section
Build a complete landing page with Tailwind CSS — hero, features, pricing, and footer — section by section with real code you can copy directly into your React project.
Why Tailwind Is Actually Good for Landing Pages
Landing pages are one of those things where the tooling matters more than you'd think. You're not building a complex app — you're building something that needs to look sharp, load fast, and convert. Tailwind CSS, as of v3.4 (and even more so with v4's changes in 2025), is genuinely well-suited to this. Utility-first means you're not context-switching between files, which keeps iteration speed high.
Honestly, the real win is responsiveness. With Tailwind's sm:, md:, lg:, xl: breakpoint prefixes, you can write mobile-first layouts inline. No separate media query file. No weird specificity battles. You write flex flex-col md:flex-row and it just works.
That said, a landing page is only as good as its structure. Most convert well when they follow a predictable pattern: hero → social proof → features → pricing → CTA → footer. The sections below walk through each of those, with code you can actually use.
One more thing — if you want pre-built components that snap into a Tailwind project without fighting design decisions, browse components to see what's ready out of the box. Some of the glassmorphism and neobrutalism components from Empire UI drop right in.
The Hero Section
Your hero has one job: make someone want to keep scrolling. You've got maybe 3 seconds. The layout should be dead simple — centered content, a headline, a subheadline, and one or two buttons. Everything else is distraction.
Here's a clean starting point that works on mobile at 375px and scales up without changes:
export function HeroSection() {
return (
<section className="relative min-h-screen flex items-center justify-center bg-neutral-950 px-6">
{/* Optional background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-violet-950/40 via-transparent to-transparent pointer-events-none" />
<div className="relative z-10 max-w-3xl mx-auto text-center">
<span className="inline-block text-xs font-semibold tracking-widest text-violet-400 uppercase mb-4">
Now in public beta
</span>
<h1 className="text-5xl md:text-7xl font-extrabold text-white leading-tight mb-6">
Ship faster with{" "}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-pink-400">
real components
</span>
</h1>
<p className="text-lg text-neutral-400 mb-10 max-w-xl mx-auto">
Copy-paste UI blocks built in React + Tailwind. No config, no drama.
Your landing page, done in an afternoon.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="#get-started"
className="px-8 py-4 bg-violet-600 hover:bg-violet-500 text-white font-semibold rounded-xl transition-colors"
>
Get started free
</a>
<a
href="#features"
className="px-8 py-4 border border-white/10 hover:border-white/30 text-neutral-300 font-semibold rounded-xl transition-colors"
>
See how it works
</a>
</div>
</div>
</section>
);
}Worth noting: the bg-clip-text trick for gradient text works in all modern browsers as of 2023, but you still need the -webkit- vendor prefix in some Tailwind setups. Add [-webkit-background-clip:text] if the gradient on your h1 isn't showing up.
The pointer-events-none on the background gradient div is easy to forget, and it'll silently block your CTA buttons if you leave it off. Ask me how I know.
Social Proof and Logo Bar
Right below the hero, you want trust signals. Before features, before pricing. A simple logo bar with a line like "Trusted by teams at —" does more for conversion than another paragraph of copy. Keep it unbranded enough that visitors project their own company onto the list.
const logos = [
{ name: "Vercel", src: "/logos/vercel.svg" },
{ name: "Linear", src: "/logos/linear.svg" },
{ name: "Figma", src: "/logos/figma.svg" },
{ name: "Stripe", src: "/logos/stripe.svg" },
{ name: "Loom", src: "/logos/loom.svg" },
];
export function LogoBar() {
return (
<section className="py-16 border-y border-white/5 bg-neutral-950">
<p className="text-center text-xs font-semibold uppercase tracking-widest text-neutral-600 mb-10">
Trusted by teams at
</p>
<div className="flex flex-wrap items-center justify-center gap-10 px-6 opacity-50 hover:opacity-100 transition-opacity duration-500">
{logos.map((logo) => (
<img
key={logo.name}
src={logo.src}
alt={logo.name}
className="h-6 w-auto grayscale"
/>
))}
</div>
</section>
);
}The grayscale class keeps everything visually neutral. The hover:opacity-100 on the container is a nice touch — logos feel 'alive' when you hover over the row but don't fight with the hero above.
Quick aside: if you're not using real client logos, placeholder SVG services like simpleicons.org let you pull recognizable brand shapes. Don't hotlink them in production though.
Features Grid
The features section is where most landing pages die. Too much text, icons that don't mean anything, six columns on desktop that collapse into an unreadable stack on mobile. The fix is simple: three columns max, one clear sentence per feature, and an icon that actually maps to what you're describing.
const features = [
{
icon: "⚡",
title: "Zero config setup",
description: "Drop in a component, it works. No wrappers, no providers, no 40-line setup guides.",
},
{
icon: "🎨",
title: "Dark mode by default",
description: "Every component is built dark-first. Light mode is a class swap, not a rewrite.",
},
{
icon: "📦",
title: "Tailwind + React native",
description: "Pure utility classes. No CSS-in-JS runtime. Ships as small as you want it to.",
},
{
icon: "🔧",
title: "Fully customizable",
description: "Every token, every radius, every shadow is yours to override in tailwind.config.js.",
},
{
icon: "♿",
title: "Accessible out of the box",
description: "Keyboard nav, focus rings, ARIA labels — all baked in, not bolted on afterward.",
},
{
icon: "🚀",
title: "Copy-paste ready",
description: "No npm install dance. Grab the code, paste it, adjust colors, ship it.",
},
];
export function FeaturesGrid() {
return (
<section id="features" className="py-24 px-6 bg-neutral-950">
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl md:text-5xl font-bold text-white text-center mb-4">
Everything you need to ship
</h2>
<p className="text-neutral-400 text-center mb-16 max-w-xl mx-auto">
Built for developers who'd rather be shipping features than fighting CSS.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((f) => (
<div
key={f.title}
className="p-6 rounded-2xl border border-white/5 bg-white/[0.03] hover:bg-white/[0.06] transition-colors"
>
<span className="text-3xl mb-4 block">{f.icon}</span>
<h3 className="text-white font-semibold text-lg mb-2">{f.title}</h3>
<p className="text-neutral-400 text-sm leading-relaxed">{f.description}</p>
</div>
))}
</div>
</div>
</section>
);
}In practice, bg-white/[0.03] with an hover:bg-white/[0.06] transition is one of the cleanest card hover states you can do in Tailwind without reaching for any plugin. The arbitrary value bracket syntax ([0.03]) has been supported since Tailwind v3.0 — you're not hacking anything.
If you want the cards to feel more premium, look at some of the glassmorphism components on Empire UI. The frosted-glass card variant works particularly well in a dark features grid like this one — just adds a backdrop-blur and a slightly higher opacity border.
Pricing Section
Pricing tables are weirdly hard to get right. Not because the logic is complex — it's not — but because the visual hierarchy has to carry all the decision-making weight. Which plan is recommended? What's included vs. not? Can I tell at a glance? If the answer to any of those is 'no', you're losing conversions.
const plans = [
{
name: "Free",
price: "$0",
description: "For side projects and exploration.",
features: ["50 components", "Community support", "MIT license"],
cta: "Get started",
highlight: false,
},
{
name: "Pro",
price: "$19",
description: "For teams shipping products.",
features: ["All 200+ components", "Priority support", "Figma source files", "Early access to new drops"],
cta: "Start free trial",
highlight: true,
},
{
name: "Lifetime",
price: "$149",
description: "Pay once, use forever.",
features: ["Everything in Pro", "All future updates", "Commercial license", "Discord access"],
cta: "Buy now",
highlight: false,
},
];
export function PricingSection() {
return (
<section className="py-24 px-6 bg-neutral-950">
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl md:text-5xl font-bold text-white text-center mb-16">
Simple, transparent pricing
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{plans.map((plan) => (
<div
key={plan.name}
className={`p-8 rounded-2xl border ${
plan.highlight
? "border-violet-500 bg-violet-950/30"
: "border-white/10 bg-white/[0.03]"
}`}
>
{plan.highlight && (
<span className="text-xs font-bold text-violet-400 uppercase tracking-widest mb-4 block">
Most popular
</span>
)}
<h3 className="text-2xl font-bold text-white mb-1">{plan.name}</h3>
<p className="text-neutral-400 text-sm mb-6">{plan.description}</p>
<div className="text-4xl font-extrabold text-white mb-1">{plan.price}</div>
<p className="text-neutral-500 text-xs mb-8">per month</p>
<ul className="space-y-3 mb-8">
{plan.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-sm text-neutral-300">
<span className="text-violet-400">✓</span> {f}
</li>
))}
</ul>
<a
href="#"
className={`block text-center py-3 rounded-xl font-semibold transition-colors ${
plan.highlight
? "bg-violet-600 hover:bg-violet-500 text-white"
: "border border-white/10 hover:border-white/30 text-neutral-300"
}`}
>
{plan.cta}
</a>
</div>
))}
</div>
</div>
</section>
);
}Look, the 'Most popular' label with a highlighted border is the oldest trick in SaaS pricing — it works because it gives people permission to choose the middle option. Make sure the highlight treatment is visually obvious. border-violet-500 at 1px is sometimes too subtle on dark backgrounds, so bump it to border-2 border-violet-500 if it's not reading clearly.
Worth noting: if you're building on Next.js and pulling pricing from Stripe, you'd swap the static plans array for a getStaticProps (or RSC fetch) call. The component structure stays identical — data shape is all that changes.
CTA and Footer
The bottom CTA is your last shot. At this point the visitor has read everything — or skimmed it. Either way, they need one more nudge. Keep it tight: a bold line, one button, maybe a tiny line of trust copy underneath.
export function BottomCTA() {
return (
<section className="py-32 px-6 bg-neutral-950 text-center">
<h2 className="text-4xl md:text-6xl font-extrabold text-white mb-6">
Ready to stop starting from scratch?
</h2>
<p className="text-neutral-400 mb-10 max-w-md mx-auto">
Join 4,000+ developers already using Empire UI to ship faster.
</p>
<a
href="/pricing"
className="inline-block px-10 py-4 bg-violet-600 hover:bg-violet-500 text-white font-bold rounded-xl transition-colors text-lg"
>
Browse components →
</a>
<p className="text-neutral-600 text-xs mt-4">No credit card required. Free forever plan available.</p>
</section>
);
}
export function Footer() {
return (
<footer className="border-t border-white/5 bg-neutral-950 px-6 py-16">
<div className="max-w-5xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-sm">
<div>
<p className="text-white font-bold mb-4">Empire UI</p>
<p className="text-neutral-500 leading-relaxed">
React + Tailwind component library for developers who care about design.
</p>
</div>
{["Product", "Resources", "Company"].map((col) => (
<div key={col}>
<p className="text-white font-semibold mb-4">{col}</p>
<ul className="space-y-2 text-neutral-500">
<li><a href="#" className="hover:text-white transition-colors">Link one</a></li>
<li><a href="#" className="hover:text-white transition-colors">Link two</a></li>
<li><a href="#" className="hover:text-white transition-colors">Link three</a></li>
</ul>
</div>
))}
</div>
<p className="text-center text-neutral-700 text-xs mt-16">
© 2026 Empire UI. All rights reserved.
</p>
</footer>
);
}The footer grid uses grid-cols-2 md:grid-cols-4 — on mobile the two-column layout is enough for four items without them wrapping into a mess. It's a small call but it saves you from a sm:grid-cols-2 lg:grid-cols-4 chain that's easy to forget.
Once you've got all the sections, wire them into a single page component. In Next.js App Router this is just your page.tsx. No routing, no providers needed unless you're adding something like a modal or a theme toggle.
// app/page.tsx
import { HeroSection } from "@/components/HeroSection";
import { LogoBar } from "@/components/LogoBar";
import { FeaturesGrid } from "@/components/FeaturesGrid";
import { PricingSection } from "@/components/PricingSection";
import { BottomCTA } from "@/components/BottomCTA";
import { Footer } from "@/components/Footer";
export default function HomePage() {
return (
<main>
<HeroSection />
<LogoBar />
<FeaturesGrid />
<PricingSection />
<BottomCTA />
<Footer />
</main>
);
}If you want to add animated backgrounds, scrolling effects, or style treatments on top of this structure, tools like the gradient generator and the box shadow generator are useful for generating values you can paste directly into your Tailwind config or inline styles without guessing.
Performance and Polish
A landing page that looks good but loads in 4 seconds is a landing page that doesn't convert. A few things matter here. First, if you're using images, use Next.js <Image> with proper width and height — it handles lazy loading, format conversion, and responsive sizing. Second, keep your Tailwind bundle small by making sure you've configured content paths in tailwind.config.js correctly so PurgeCSS/JIT doesn't include utilities you're not using.
Third — and this is the one people forget — don't ship animations that fight the user's prefers-reduced-motion preference. You can gate Tailwind's transition classes with the motion-safe: variant: motion-safe:transition-colors. Three characters, instant accessibility win.
In practice, the biggest performance mistake on Tailwind landing pages is loading a web font without font-display: swap. You'd be surprised how often this tanks your Largest Contentful Paint score. If you're using Google Fonts via @next/font (which you should be in 2026), swap is already the default. If you're self-hosting, add it manually.
That said, don't over-optimize before you ship. Get the page live, run a Lighthouse audit, fix the two or three issues that actually move the needle. Premature perf work on a landing page that doesn't exist yet is procrastination in a lab coat.
FAQ
No. All the code in this article uses core Tailwind utilities available in v3.x and v4.x. The only thing you need is Tailwind itself configured in your project.
Yes — every Tailwind class here works in plain HTML. Swap JSX syntax for standard HTML attributes and remove the JavaScript logic for the mapped arrays. The visual output is identical.
Add scroll-smooth to your <html> element in your root layout, then use standard anchor links like href="#features". Tailwind's utility handles the rest natively.
A simple hamburger toggle with a full-width slide-down menu works well. Use hidden md:flex on the desktop nav links and a state-toggled block div for mobile — no library required.