freeNew125 lines · framer-motion
Hero Centered Badge
One promise, two buttons, no noise. A minimal centred SaaS hero: a badge pill, a bold headline, two CTAs and a row of generic trust chips, over a single slow-breathing ambient glow — the calmest hero in the set.
- Use it for
- Pages that want confidence over spectacle: a single clear value proposition with no chart, canvas or 3D scene competing for attention.
- Themes
- Light + dark
- License
- Commercial use
Live
New · Now with MCP
The calm way to ship AI products
A centred hero for teams who want confidence, not confetti: one clear promise, two clear actions.
No credit cardCancel anytimeSOC2-ready infra
tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
/**
* Minimal centred SaaS hero: a badge pill, a bold headline, two CTAs and a
* row of generic trust chips, over a single slow-breathing ambient glow.
* The calmest hero in the set — no canvas, no WebGL, no chart.
*/
export interface HeroCenteredBadgeProps {
badge?: string;
headline?: string;
subcopy?: string;
primaryCta?: string;
secondaryCta?: string;
/** Ambient glow strength (0.4–1.6). */
glow?: number;
/** Show the trust-chip row under the CTAs. */
showChips?: boolean;
className?: string;
}
const CHIPS = ["No credit card", "Cancel anytime", "SOC2-ready infra"];
export function HeroCenteredBadge({
badge = "New · Now with MCP",
headline = "The calm way to ship AI products",
subcopy = "A centred hero for teams who want confidence, not confetti: one clear promise, two clear actions.",
primaryCta = "Start free",
secondaryCta = "Talk to sales",
glow = 1,
showChips = true,
className,
}: HeroCenteredBadgeProps) {
const reduce = useReducedMotionSafe();
return (
<section className={cn("relative isolate overflow-hidden bg-background", className)}>
<div
className="absolute inset-0 [background-image:radial-gradient(circle_at_1px_1px,var(--border)_1px,transparent_1px)] [background-size:32px_32px] opacity-[0.3] [mask-image:radial-gradient(ellipse_65%_55%_at_50%_30%,black,transparent_75%)]"
aria-hidden
/>
<motion.div
className="absolute left-1/2 top-[22%] size-[26rem] -translate-x-1/2 rounded-full bg-[radial-gradient(circle,var(--signal),transparent_70%)] blur-3xl"
style={{ opacity: 0.22 * glow }}
animate={reduce ? undefined : { scale: [1, 1.12, 1], opacity: [0.18 * glow, 0.28 * glow, 0.18 * glow] }}
transition={reduce ? undefined : { duration: 6, repeat: Infinity, ease: "easeInOut" }}
aria-hidden
/>
<div className="relative mx-auto flex min-h-[28rem] max-w-3xl flex-col items-center px-4 pb-16 pt-20 text-center sm:px-6 sm:pt-24">
<motion.span
initial={reduce ? false : { opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground"
>
<span className="size-1.5 rounded-full bg-signal" aria-hidden />
{badge}
</motion.span>
<motion.h1
initial={reduce ? false : { opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: 0.1 }}
className="mt-6 font-display text-[clamp(2.6rem,7vw,5.25rem)] font-extrabold leading-[0.96] tracking-[-0.045em]"
>
{headline}
</motion.h1>
<motion.p
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.25 }}
className="mt-5 max-w-lg text-lg leading-relaxed text-muted-foreground"
>
{subcopy}
</motion.p>
<motion.div
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.35 }}
className="mt-8 flex flex-wrap items-center justify-center gap-3"
>
<a href="#" className="inline-flex h-12 items-center rounded-full bg-primary px-6 text-[15px] font-semibold text-primary-foreground transition hover:opacity-90">
{primaryCta} →
</a>
<a href="#" className="inline-flex h-12 items-center rounded-full border-2 border-foreground px-6 text-[15px] font-semibold transition hover:bg-foreground hover:text-background">
{secondaryCta}
</a>
</motion.div>
{showChips && (
<motion.div
initial={reduce ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="mt-12 flex flex-wrap items-center justify-center gap-2.5"
>
{CHIPS.map((c) => (
<span key={c} className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 font-mono text-[11px] text-muted-foreground">
<span className="size-1 rounded-full bg-ok" aria-hidden />
{c}
</span>
))}
</motion.div>
)}
</div>
</section>
);
}
export default HeroCenteredBadge;
How it behaves
- →A single radial-gradient glow sits behind the badge and headline, scaling and fading on a slow 6s breathing loop via framer-motion.
- →A dot-grid texture fades out toward the edges (mask-image) to keep the composition anchored behind the text column.
- →glow scales the ambient glow's base and peak opacity; showChips toggles a row of generic trust chips (no credit card, cancel anytime, SOC2-ready infra) below the CTAs.
- →prefers-reduced-motion removes the breathing animation, leaving the glow static at its resting opacity.
Accessibility
- ✓The glow and dot-grid are aria-hidden; only the badge, heading, copy, two real <a> CTAs and the trust chips are in the accessibility tree.
- ✓Headline is a single <h1>; CTAs keep the design system's visible focus ring.
- ✓No canvas/WebGL dependency at all — the lightest hero to render in the whole set.
- ✓Honours prefers-reduced-motion.