Créer une section CTA sombre avec accent lumineux en React
Une section CTA React sombre centre un bloc de texte sur un fond profond, utilise un div radial flouté à 6% d'opacité pour émettre un glow d'accent doux, et anime l'ensemble à l'entrée dans le viewport avec une transition whileInView Framer Motion. Un mot mis en valeur dans le titre est coloré via une custom property CSS pour rester cohérent avec le thème du design system.
- Stack : React + Framer Motion + Lucide React + Tailwind v4, ~110 lignes, zéro dépendance supplémentaire.
- Animation : un seul bloc whileInView (opacity 0→1, y 24→0, durée 0,6s, ease custom [0,16, 1, 0,3, 1]).
- Thème : entièrement basé sur des tokens CSS, couleurs de fond, d'accent et de bordure viennent de custom properties, pas de valeurs codées en dur.
- Accessible : balises <h2> et <a> sémantiques, aria-hidden sur le div glow décoratif.
- Responsive : échelle typographique fluide via les breakpoints Tailwind (text-3xl / md:text-4xl / lg:text-5xl), fonctionne sur tous les écrans.
CtaDark est une section call-to-action centrée conçue pour les landing pages à thème sombre. Elle associe un fond profond à un glow d'accent radial subtil, assez visible pour attirer l'oeil sans distraire. Une seule animation d'entrée Framer Motion donne au bloc un aspect intentionnel plutôt que statique. C'est la section qu'on place en fin de page quand on a besoin d'une action unique, bien contrastée.
Anatomie
La section comporte trois couches empilées dans un conteneur relative. Au fond, la couleur de fond (--color-background-dark). La couche du milieu est un div centré en absolu de 600x400px, flouté à 120px, coloré avec --color-accent à 6% d'opacité. Il est aria-hidden et pointer-events:none. Au-dessus, un motion.div centré en max-w-2xl contient le titre, un paragraphe et une rangée de boutons. Le bouton principal est une pilule remplie avec fond --color-accent ; un bouton secondaire optionnel est une pilule en outline avec --color-border-dark.
Comment ça marche
L'animation utilise un seul motion.div Framer Motion avec whileInView, donc le bloc s'anime uniquement la première fois qu'il entre dans le viewport (viewport: { once: true }). L'état initial est opacity:0 et y:24 ; l'état animé est opacity:1 et y:0. La transition dure 0,6s avec un cubic-bezier custom [0,16, 1, 0,3, 1], un ease-out fort qui décélère vite en fin de course, pour un effet pop-up percutant mais fluide. Le mot accent du titre est calculé par un simple split sur titleAccent, puis enveloppé dans un span coloré avec var(--color-accent). Pas de canvas, pas de SVG, pas de hooks supplémentaires.
Comment le coder en React
Pose le shell de section sombre
Crée une section relative/overflow-hidden avec background-color:var(--color-background-dark) et un padding vertical généreux. C'est le canvas sur lequel tout repose. Utiliser un token CSS plutôt qu'un hex codé en dur fait fonctionner les sept thèmes de la bibliothèque sans retouche.
<section className="relative overflow-hidden" style={{ backgroundColor: "var(--color-background-dark)", paddingTop: "var(--section-padding-y-lg)", paddingBottom: "var(--section-padding-y-lg)", }} >Ajoute le glow d'accent radial
Place un div centré en absolu de 600x400px, flouté à 120px avec blur-[120px], et mets son backgroundColor à var(--color-accent) à 0,06 d'opacité. Garde-le aria-hidden et pointer-events-none pour qu'il soit invisible aux technologies d'assistance et ne puisse pas intercepter les clics.
<div aria-hidden className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] rounded-full blur-[120px] pointer-events-none" style={{ backgroundColor: "var(--color-accent)", opacity: 0.06 }} />Anime le contenu avec whileInView
Enveloppe le titre, la description et les boutons dans un seul motion.div avec initial={{ opacity:0, y:24 }} et whileInView={{ opacity:1, y:0 }}, viewport={{ once:true }}. Passe une transition de 0,6s avec un ease custom pour un reveal au scroll percutant qui se déclenche une fois et reste.
<motion.div initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }} className="text-center max-w-2xl mx-auto" >Mets en valeur le mot accent dans le titre
Coupe la chaîne du titre sur le mot titleAccent, puis rends les parties avec un span entre elles coloré par var(--color-accent). L'accent reste ainsi lié au token du design system, et changer de thème met à jour le highlight automatiquement sans toucher au composant.
const titleParts = titleAccent ? title.split(titleAccent) : [title]; // In JSX: {titleAccent ? ( <> {titleParts[0]} <span style={{ color: "var(--color-accent)" }}>{titleAccent}</span> {titleParts[1]} </> ) : title}
Quand l'utiliser
Utilise CtaDark en bas d'une page SaaS ou agence à thème sombre, après les testimonials ou une section de features, quand tu veux une action bien contrastée avant le footer. Le guide de composition dans la meta l'appelle explicitement en position 'late'. À éviter en milieu de page ou directement après un autre CTA ; le fond sombre a besoin d'espace pour lire comme intentionnel. Sur les pages à thème clair, préfère une variante claire avec la même structure plutôt que de combattre le contraste.
Utilisé par
- Stripe, Utilise des sections CTA sur fond sombre en bas de ses pages produit avec une seule action principale bien contrastée.
- Vercel, Des blocs CTA sombres avec glow en dégradé radial apparaissent en fin de pages de features et d'études de cas.
- Linear, Des panneaux CTA sombres centrés avec highlights de titre en couleur d'accent closent la landing page principale.
FAQ
Comment changer la couleur du glow d'accent ?
La couleur du glow vient de var(--color-accent), donc elle suit le preset de thème actif sur la page. Change l'attribut data-theme sur l'élément parent en l'un des sept presets de la bibliothèque et le glow se met à jour automatiquement.
Peut-on ajouter un deuxième bouton ?
Passe une valeur à la prop ctaSecondaryLabel et le bouton secondaire en outline apparaît automatiquement à côté du principal. Les deux partagent la prop ctaUrl ; si tu as besoin d'URLs séparées, fork le composant et ajoute une prop ctaSecondaryUrl.
L'animation au scroll se rejoue-t-elle à chaque visite ?
Non. viewport: { once: true } dans la config whileInView signifie que l'animation se déclenche une fois par chargement de page quand la section entre dans le viewport, puis reste dans son état animé. Supprime ce flag si tu veux qu'elle se réinitialise à chaque sortie et rentrée de la section.
Que se passe-t-il si titleAccent n'est pas trouvé dans la chaîne du titre ?
Le split retourne un tableau avec le titre complet comme seul élément, et la logique titleParts revient au rendu du titre brut sans span mis en valeur. Pas d'erreur levée ; le titre s'affiche simplement sans coloration d'accent.