La section hero est l'espace le plus stratégique de votre landing page. Les visiteurs se forgent une impression en 50 millisecondes, donc votre hero doit communiquer sa valeur immédiatement. Ce guide vous accompagne pas à pas dans la construction d'une section hero production-ready en React avec Tailwind CSS v4 et Framer Motion — la même approche utilisée sur des dizaines de landing pages SaaS à fort taux de conversion.
Ce qui fait une bonne section hero
Avant d'écrire la moindre ligne de JSX, clarifiez ce que le composant doit accomplir :
- Titre principal — une proposition de valeur claire, 6 à 10 mots maximum
- Sous-titre — 1 à 2 phrases qui développent le titre et adressent le problème du lecteur
- CTA principal — un seul bouton, avec un libellé orienté action ("Commencer à construire", pas "Valider")
- CTA secondaire — optionnel, engagement plus faible ("Voir les exemples", "Regarder la démo")
- Preuve sociale — avatars, note en étoiles, ou un chiffre unique ("Utilisé par 4 200+ équipes")
- Badge — pill d'annonce optionnel au-dessus du titre ("Nouveau : support du mode sombre")
Structure du composant
Commencez par l'interface TypeScript. Des props typées évitent les erreurs au runtime et rendent le composant auto-documenté :
// 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;
};
}
La directive "use client" est nécessaire car Framer Motion utilise des API navigateur. Dans Next.js 15, tous les composants sont des Server Components par défaut, donc on opte explicitement pour le rendu côté client.
Layout et thématisation
Plutôt que de coder les couleurs en dur comme bg-white ou text-gray-900, utilisez des propriétés CSS personnalisées. Le composant devient ainsi conscient du thème — changez un attribut data-theme sur un élément parent et toute la section se repeint instantanément :
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>
);
}
Le token CSS --section-padding-y-lg vaut 8rem par défaut. --container-max-width fait 1280px. Ces deux valeurs sont définies globalement et héritées automatiquement.
Animations d'entrée Framer Motion avec stagger
Un stagger à l'entrée donne au hero un aspect soigné sans être distrayant. Chaque élément entre 60 à 80 ms après le précédent :
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>
Le clamp() sur le font-size gère le responsive sans media queries — la taille croît linéairement de 2.5rem sur mobile jusqu'à 4.5rem sur grand écran.
Traitements de fond
Trois techniques fonctionnent particulièrement bien pour les arrière-plans de hero :
1. Halo de couleur accent
Un cercle flou et semi-transparent dans la couleur d'accent crée de la profondeur sans distraire du contenu :
<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",
}}
/>
L'attribut aria-hidden masque l'élément décoratif aux lecteurs d'écran. Une opacité à 0.06 est suffisamment subtile pour fonctionner sur n'importe quel thème.
2. Grille de points
Une grille SVG de points répétée ajoute de la 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)",
}}
/>
Le maskImage estompe la grille sur les bords pour qu'elle ne rentre pas en conflit avec la section suivante.
3. Bruit subtil
Une texture de bruit en data URI SVG 200×200px ajoute une touche premium :
background-image: url("data:image/svg+xml,...");
opacity: 0.03;
Bouton CTA
Le bouton principal utilise la couleur d'accent avec une transition de survol fluide :
<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>
Checklist accessibilité
Avant de mettre en production, vérifiez :
<section>a un nom accessible viaaria-labelou un<h1>visible à l'intérieur- Les liens CTA ont un texte descriptif (pas "Cliquez ici")
- Les éléments décoratifs sont
aria-hidden - Le ratio de contraste respecte WCAG AA (4,5:1 pour le texte normal, 3:1 pour le grand texte)
- Le composant respecte
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 }}
Variantes hero prêtes à l'emploi
Construire un hero de zéro prend du temps, et l'optimisation du taux de conversion est un travail continu. Plutôt que de partir de rien, parcourez le catalogue hero d'Incubator — il comprend des heroes centrés, des heroes en split-layout, des backgrounds vidéo plein écran, des heroes avec texte animé, et bien d'autres, tous construits avec le même système de tokens CSS et les patterns Framer Motion décrits ci-dessus. Chaque variante est livrée avec des props TypeScript et des données mock, prête à intégrer dans votre application Next.js.