Modern Hero
A centred landing hero on a soft aurora background, with a badge, gradient headline, dual CTAs and avatar social proof.
Live preview and source code for Modern Hero
"use client"
import React from "react";
import { motion } from "framer-motion";
import { ArrowRight, Play, Star } from "lucide-react";
import Badge from "../../components/badge/component";
import Button from "../../components/button/component";
import { AvatarGroup } from "../../components/avatar/component";
export interface ModernHeroProps {
/** Small pill above the headline. Pass `null` to hide it */
eyebrow?: string | null;
/** Main headline. The words in `highlight` are tinted */
headline?: string;
/** Words inside the headline to accent with the gradient */
highlight?: string;
/** Supporting paragraph under the headline */
subline?: string;
/** Label of the primary call to action */
primaryCta?: string;
/** Label of the secondary call to action. Pass `null` to hide it */
secondaryCta?: string | null;
/** Social-proof line under the buttons. Pass `false` to hide it */
socialProof?: boolean;
}
const SOCIAL_PROOF_PEOPLE = [
{ name: "Ana Ruiz" },
{ name: "Dev Mehta" },
{ name: "Kai Fischer" },
{ name: "Lena Novak" },
{ name: "Omar Haddad" },
{ name: "Priya Nair" },
];
const fadeUp = {
hidden: { opacity: 0, y: 16, filter: "blur(6px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
};
/**
* ModernHero — a centred landing hero on a soft aurora background,
* with a badge, gradient headline, dual CTAs and avatar social proof.
* Renders standalone with zero props.
*/
export default function ModernHero({
eyebrow = "New — v2.0 is live",
headline = "Ship interfaces your users actually remember",
highlight = "actually remember",
subline = "A component collection built for teams who care about the last five percent. Copy, paste, and make it yours — no config, no lock-in.",
primaryCta = "Start building",
secondaryCta = "Watch the demo",
socialProof = true,
}: ModernHeroProps) {
// Split the headline so the highlighted phrase can carry its own gradient.
// `at` may legitimately be 0, and the tail may legitimately be "", so key
// the decision off the index rather than off the emptiness of either part.
const at = highlight ? headline.indexOf(highlight) : -1;
const hasHighlight = at !== -1;
const before = hasHighlight ? headline.slice(0, at) : headline;
const after = hasHighlight ? headline.slice(at + highlight!.length) : "";
return (
<section className="mh-root">
{/* Aurora background */}
<div className="mh-aurora" aria-hidden="true">
<span className="mh-blob mh-blob-a" />
<span className="mh-blob mh-blob-b" />
<span className="mh-blob mh-blob-c" />
</div>
<div className="mh-grid" aria-hidden="true" />
<motion.div
className="mh-inner"
initial="hidden"
animate="visible"
transition={{ staggerChildren: 0.09 }}
>
{eyebrow && (
<motion.div variants={fadeUp} transition={{ duration: 0.5, ease: "easeOut" }}>
<Badge label={eyebrow} variant="info" size="lg" dot icon={Star} />
</motion.div>
)}
<motion.h1 className="mh-headline" variants={fadeUp} transition={{ duration: 0.55, ease: "easeOut" }}>
{before}
{hasHighlight && <span className="mh-highlight">{highlight}</span>}
{after}
</motion.h1>
<motion.p className="mh-subline" variants={fadeUp} transition={{ duration: 0.55, ease: "easeOut" }}>
{subline}
</motion.p>
<motion.div className="mh-actions" variants={fadeUp} transition={{ duration: 0.55, ease: "easeOut" }}>
<Button label={primaryCta} icon={ArrowRight} iconPosition="right" size="lg" blobColor="#c4b5fd" />
{secondaryCta && (
<button type="button" className="mh-ghost">
<span className="mh-ghost-icon">
<Play size={12} fill="currentColor" strokeWidth={0} />
</span>
{secondaryCta}
</button>
)}
</motion.div>
{socialProof && (
<motion.div className="mh-proof" variants={fadeUp} transition={{ duration: 0.55, ease: "easeOut" }}>
<AvatarGroup people={SOCIAL_PROOF_PEOPLE} max={4} size="md" />
<span className="mh-proof-text">
Trusted by <strong>2,400+</strong> product teams
</span>
</motion.div>
)}
</motion.div>
<style>{`
.mh-root {
position: relative;
width: 100%;
overflow: hidden;
padding: 96px 24px 104px;
background: #fbfbfd;
font-family: inherit;
color: #18181b;
isolation: isolate;
}
.mh-aurora {
position: absolute;
inset: 0;
z-index: -2;
filter: blur(64px);
opacity: 0.75;
}
.mh-blob {
position: absolute;
border-radius: 9999px;
}
.mh-blob-a {
top: -12%; left: 8%;
width: 40%; padding-bottom: 40%;
background: #c4b5fd;
}
.mh-blob-b {
top: 4%; right: 6%;
width: 34%; padding-bottom: 34%;
background: #7dd3fc;
}
.mh-blob-c {
bottom: -20%; left: 34%;
width: 38%; padding-bottom: 38%;
background: #fbcfe8;
}
/* Fading dot grid for a bit of texture under the copy */
.mh-grid {
position: absolute;
inset: 0;
z-index: -1;
background-image: radial-gradient(circle, rgba(24,24,27,0.13) 1px, transparent 1px);
background-size: 22px 22px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, #000 30%, transparent 75%);
-webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, #000 30%, transparent 75%);
}
.mh-inner {
position: relative;
max-width: 760px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 22px;
}
.mh-headline {
margin: 0;
font-size: clamp(34px, 6.2vw, 62px);
line-height: 1.04;
letter-spacing: -0.035em;
font-weight: 600;
text-wrap: balance;
}
.mh-highlight {
background: linear-gradient(100deg, #7c3aed 0%, #2563eb 48%, #db2777 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.mh-subline {
margin: 0;
max-width: 540px;
font-size: clamp(15px, 1.6vw, 17px);
line-height: 1.6;
color: #52525b;
text-wrap: balance;
}
.mh-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 6px;
}
.mh-ghost {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 14px 26px;
font-family: inherit;
font-size: 17px;
font-weight: 600;
color: #3f3f46;
background: transparent;
border: 1px solid rgba(24,24,27,0.14);
border-radius: 14px;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.mh-ghost:hover {
background: rgba(255,255,255,0.75);
border-color: rgba(24,24,27,0.26);
transform: translateY(-1px);
}
.mh-ghost-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 9999px;
background: rgba(24,24,27,0.08);
}
.mh-proof {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
flex-wrap: wrap;
justify-content: center;
}
.mh-proof-text {
font-size: 14px;
color: #52525b;
}
.mh-proof-text strong {
color: #18181b;
font-weight: 600;
}
@media (max-width: 640px) {
.mh-root { padding: 64px 18px 72px; }
.mh-ghost { padding: 12px 20px; font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
.mh-ghost { transition: none; }
}
`}</style>
</section>
);
}
Ship interfaces your users actually remember
A component collection built for teams who care about the last five percent. Copy, paste, and make it yours — no config, no lock-in.
A centred landing page hero section: an eyebrow pill, a large headline with one phrase picked out in a gradient, a supporting line, two calls to action and a row of overlapping avatars as social proof. The aurora background and dot grid are drawn in CSS rather than shipped as an image, so there is nothing to optimise and the whole section scales with the viewport. Every element fades and unblurs upward in a stagger on mount.
Installation
Copy the source from the Code tab above into your project as ModernHero.tsx.
Its styles are a <style> block of mh- prefixed classes that travels with the
file, so there is no stylesheet to import and no Tailwind config to touch.
It imports framer-motion for the entrance stagger and lucide-react for the
arrow, play and star icons:
npm install framer-motion lucide-reactIt also renders three items from this collection, so copy those files too and fix the import paths at the top to match where you put them: Avatar, Badge and Button.
Usage
Every prop is optional and the defaults are a complete hero, so start by rendering it and replacing the copy one prop at a time:
import ModernHero from "./components/ModernHero";
export default function Example() {
return (
<ModernHero
headline="Ship interfaces your users actually remember"
highlight="actually remember"
primaryCta="Start building"
/>
);
}highlight is the substring of headline that gets the gradient. The match is
exact and case sensitive and only the first occurrence is tinted; if it is not
found, the headline still renders, just without an accent.
eyebrow and secondaryCta each accept null to remove that element, and
socialProof={false} removes the avatar row.
When to reach for it
The top of a product or marketing page where the job is one message, one action, and enough visual interest that the fold does not look like a document.
A launch or changelog page, using eyebrow for the version pill. It renders a
Badge with a dot and a star icon.
An internal tool's landing screen, where the aurora and the avatar stack do the work a designer would otherwise have to spend a day on.
When not to
If the page already has an <h1>. This renders one, and two on a page is a
structural problem rather than a styling one.
If the hero needs to capture anything. There is no children slot and no form, so an email capture, a search field or a video embed means editing the copied file rather than passing a prop.
If your brand is dark. The palette is hard-coded light: a near-white background, near-black text and three pastel blobs. That is four values to change in the copied file, but no prop reaches them.
Accessibility
The aurora blobs and the dot grid are aria-hidden, so nothing decorative
reaches the accessibility tree. The headline is a real <h1> and the
highlighted phrase is a <span> inside it, so the gradient does not split the
heading text.
The entrance animation is not gated on prefers-reduced-motion. Content starts
at reduced opacity and blurred and animates in, which means removing the
initial and animate values in the copied file if you honour that setting.
The ghost button's hover transition is gated, but that is the only part that is.
Notes
The social proof line is placeholder copy. The six names and the "Trusted
by 2,400+ product teams" string are constants in the source, not props. Replace
them with something true or set socialProof={false}, because shipping an
invented number is worse than shipping no number.
Neither call to action has a click handler. The primary one is a
Button and the secondary is a plain <button> styled
in the block's own CSS, and both do nothing until you wire them up or swap them
for links.
The mh- class names are global once the section renders. Two heroes on one
page share one set of rules, which is fine, but a class of your own named
mh-something would collide.