A hero section is the most critical piece of real estate on your landing page. Visitors form an impression within 50 milliseconds, so your hero has to communicate value immediately. This guide walks you through building a production-ready hero section in React with Tailwind CSS v4 and Framer Motion — the same approach used across dozens of high-converting SaaS landing pages.
What Makes a Great Hero
Before writing a single line of JSX, understand what the component needs to accomplish:
- Headline — one clear value proposition, 6–10 words maximum
- Subheadline — 1–2 sentences expanding on the headline, addressing the reader's problem
- Primary CTA — one button, action-oriented label ("Start building", not "Submit")
- Secondary CTA — optional, lower commitment ("See examples", "Watch demo")
- Social proof — avatars, star rating, or a single number ("Trusted by 4,200+ teams")
- Badge — optional announcement pill above the headline ("New: dark mode support")
Component Structure
Start with the TypeScript interface. Typed props prevent runtime errors and make the component self-documenting:
// HeroCentered.tsx
"use client";
import { motion } from "framer-motion";
import { ArrowRight } from "lucide-react";
interface HeroCenteredProps {
badge?: string;
title: string;
titleAccent?: string;
description: string;
ctaLabel: string;
ctaUrl: string;
ctaSecondaryLabel?: string;
ctaSecondaryUrl?: string;
socialProof?: {
avatars: string[]; // image URLs
count: number;
label: string;
};
}
The "use client" directive is required because Framer Motion uses browser APIs. In Next.js 15, all components are Server Components by default, so you opt into client rendering explicitly.
Layout and Theming
Instead of hardcoding colors like bg-white or text-gray-900, use CSS custom properties. This makes the component theme-aware — switch a data-theme attribute on a parent element and the whole section repaints:
export default function HeroCentered({ title, titleAccent, description, ctaLabel, ctaUrl, badge }: HeroCenteredProps) {
return (
<section
style={{
position: "relative",
overflow: "hidden",
paddingTop: "var(--section-padding-y-lg)",
paddingBottom: "var(--section-padding-y-lg)",
background: "var(--color-background)",
minHeight: "85vh",
display: "flex",
alignItems: "center",
}}
>
<div
style={{
width: "100%",
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<div style={{ maxWidth: "720px", margin: "0 auto", textAlign: "center" }}>
{/* Content goes here */}
</div>
</div>
</section>
);
}
The CSS token --section-padding-y-lg is 8rem by default. --container-max-width is 1280px. Both are defined globally and inherited automatically.
Framer Motion Entrance Animations with Stagger
A staggered entrance makes the hero feel polished without being distracting. Each element enters 60–80ms after the previous one:
const EASE = [0.16, 1, 0.3, 1] as const; // custom spring-like cubic-bezier
// Badge
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE }}
>
<span style={{
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-accent-border)",
background: "var(--color-accent-subtle)",
color: "var(--color-foreground-muted)",
fontSize: "0.8125rem",
fontWeight: 500,
}}>
{badge}
</span>
</motion.div>
// Headline — starts 60ms after badge
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.06, ease: EASE }}
style={{
fontSize: "clamp(2.5rem, 5vw, 4.5rem)",
fontWeight: 700,
lineHeight: 1.08,
letterSpacing: "-0.03em",
color: "var(--color-foreground)",
}}
>
{title}{" "}
<em style={{
fontStyle: "italic",
fontWeight: 400,
textDecoration: "underline",
textDecorationColor: "var(--color-accent)",
textDecorationThickness: "3px",
textUnderlineOffset: "6px",
}}>
{titleAccent}
</em>
</motion.h1>
// Description — starts 140ms after badge
<motion.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.14, ease: EASE }}
style={{
fontSize: "1.125rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
}}
>
{description}
</motion.p>
The clamp() on font-size handles responsive scaling without media queries — it grows linearly from 2.5rem on mobile to 4.5rem on wide screens.
Background Treatments
Three techniques work particularly well for hero backgrounds:
1. Accent Glow
A blurred, semi-transparent circle in the accent color creates depth without distracting from the copy:
<div
aria-hidden
style={{
position: "absolute",
top: "-20%",
left: "50%",
transform: "translateX(-50%)",
width: "60vw",
height: "60vw",
maxWidth: "800px",
borderRadius: "50%",
background: "var(--color-accent)",
opacity: 0.06,
filter: "blur(100px)",
pointerEvents: "none",
}}
/>
The aria-hidden attribute hides the decorative element from screen readers. Opacity 0.06 is subtle enough to work on any theme.
2. Dot Grid
A repeating SVG dot grid adds texture:
<div
aria-hidden
style={{
position: "absolute",
inset: 0,
backgroundImage: "radial-gradient(circle, var(--color-border) 1px, transparent 1px)",
backgroundSize: "24px 24px",
opacity: 0.5,
maskImage: "radial-gradient(ellipse 70% 60% at 50% 50%, black, transparent)",
}}
/>
The maskImage fades the grid at the edges so it doesn't clash with the bottom section.
3. Subtle Noise
A 200×200px noise texture SVG data URI adds a premium tactile feel:
background-image: url("data:image/svg+xml,...");
opacity: 0.03;
CTA Button
The primary button uses the accent color with a smooth hover transition:
<a
href={ctaUrl}
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "0.875rem 2rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-foreground)",
fontWeight: 600,
fontSize: "0.9375rem",
textDecoration: "none",
transition: "background var(--duration-normal) var(--ease-out)",
}}
onMouseEnter={(e) => {
(e.target as HTMLAnchorElement).style.background = "var(--color-accent-hover)";
}}
onMouseLeave={(e) => {
(e.target as HTMLAnchorElement).style.background = "var(--color-accent)";
}}
>
{ctaLabel}
<ArrowRight style={{ width: 16, height: 16 }} />
</a>
Accessibility Checklist
Before shipping, verify:
<section>has an accessible name viaaria-labelor a visible<h1>inside it- CTA links have descriptive text (not "Click here")
- Decorative elements are
aria-hidden - Contrast ratio meets WCAG AA (4.5:1 for normal text, 3:1 for large)
- The component respects
prefers-reduced-motion:
import { useReducedMotion } from "framer-motion";
const shouldReduceMotion = useReducedMotion();
// Use duration: 0 when shouldReduceMotion is true
transition={{ duration: shouldReduceMotion ? 0 : 0.6, ease: EASE }}
Ready-Made Hero Variants
Building a hero from scratch takes time, and conversion-rate optimization is an ongoing experiment. Instead of starting from zero, browse the Incubator hero catalog — it includes centered heroes, split-layout heroes, fullscreen video backgrounds, animated text heroes, and more, all built with the same CSS token system and Framer Motion patterns described above. Each variant ships with TypeScript props and mock data, ready to drop into your Next.js app.