freeNew127 lines · framer-motion
Hero Gradient Beams
Slow light beams, zero assets to load. A centred hero with 2–5 rotating conic-gradient beams layered behind the headline, built from pure CSS and framer-motion with no canvas or WebGL.
- Use it for
- Launch pages that want depth and motion on the first screen but need to stay light — no shader, no particles, just colour and rotation.
- Themes
- Light + dark
- License
- Commercial use
Live
New · AI workspace
Light, focused on one idea
Slow rotating beams give the first screen depth without a single asset to load.
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;
}
/**
* Centred hero with rotating conic-gradient light beams radiating from behind
* the headline. Pure CSS/SVG + framer-motion, no canvas or WebGL — the
* cheapest background in the set, built for the free tier.
*/
export interface HeroGradientBeamsProps {
eyebrow?: string;
headline?: string;
subcopy?: string;
primaryCta?: string;
secondaryCta?: string;
/** Number of beam layers (2–5). */
beamCount?: number;
/** Rotation speed multiplier. */
speed?: number;
className?: string;
}
const BEAM_DEFS = [
{ colorVar: "--signal", size: 150, opacity: 0.4, dur: 34, dir: 1 },
{ colorVar: "--info", size: 120, opacity: 0.3, dur: 26, dir: -1 },
{ colorVar: "--lime", size: 95, opacity: 0.22, dur: 20, dir: 1 },
{ colorVar: "--ok", size: 70, opacity: 0.18, dur: 16, dir: -1 },
{ colorVar: "--signal", size: 50, opacity: 0.16, dur: 12, dir: 1 },
];
export function HeroGradientBeams({
eyebrow = "New · AI workspace",
headline = "Light, focused on one idea",
subcopy = "Slow rotating beams give the first screen depth without a single asset to load.",
primaryCta = "Get started",
secondaryCta = "Explore features",
beamCount = 3,
speed = 1,
className,
}: HeroGradientBeamsProps) {
const reduce = useReducedMotionSafe();
const beams = BEAM_DEFS.slice(0, Math.max(2, Math.min(5, beamCount)));
return (
<section className={cn("relative isolate overflow-hidden bg-background", className)}>
<div className="absolute inset-0 flex items-center justify-center" aria-hidden>
<div className="relative size-[60rem] max-w-none">
{beams.map((b, i) => (
<motion.div
key={i}
className="absolute inset-0 rounded-full"
style={{
background: `conic-gradient(from 0deg, transparent, color-mix(in srgb, var(${b.colorVar}) ${Math.round(b.opacity * 100)}%, transparent) ${b.size / 4}deg, transparent ${b.size / 2}deg, transparent)`,
filter: "blur(18px)",
}}
animate={reduce ? undefined : { rotate: b.dir * 360 }}
transition={reduce ? undefined : { duration: b.dur / Math.max(0.2, speed), repeat: Infinity, ease: "linear" }}
/>
))}
<div className="absolute inset-[30%] rounded-full bg-[radial-gradient(circle,var(--signal),transparent_70%)] opacity-30 blur-3xl" />
</div>
</div>
<div
className="absolute inset-0 [background-image:radial-gradient(circle_at_1px_1px,var(--border)_1px,transparent_1px)] [background-size:28px_28px] opacity-[0.25] [mask-image:radial-gradient(ellipse_70%_55%_at_50%_35%,black,transparent_75%)]"
aria-hidden
/>
<div className="absolute inset-0 bg-gradient-to-b from-background/0 via-background/10 to-background" aria-hidden />
<div className="relative mx-auto flex min-h-[30rem] max-w-4xl flex-col items-center px-4 pb-20 pt-20 text-center sm:min-h-[34rem] sm:px-6 sm:pt-28">
<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/80 px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground backdrop-blur"
>
<span className="size-1.5 rounded-full bg-signal" aria-hidden />
{eyebrow}
</motion.span>
<motion.h1
initial={reduce ? false : { opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: 0.1 }}
className="mt-6 font-display text-[clamp(2.4rem,6.6vw,5rem)] font-extrabold leading-[0.96] tracking-[-0.04em]"
>
{headline}
</motion.h1>
<motion.p
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.28 }}
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.38 }}
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>
</div>
</section>
);
}
export default HeroGradientBeams;
How it behaves
- →Each beam is a blurred conic-gradient circle rotating at its own speed and direction via framer-motion's animate, layered from largest/dimmest to smallest/brightest.
- →beamCount (2–5) controls how many layers render; speed scales every beam's rotation duration together so they stay in proportion.
- →A soft radial glow sits at the centre, and a dot-grid overlay fades out toward the edges to keep the composition anchored behind the headline.
- →prefers-reduced-motion removes the animate/transition props entirely, leaving the beams static instead of looping.
Accessibility
- ✓All gradient and grid layers are aria-hidden; only the eyebrow, heading, copy and two real <a> CTAs are in the accessibility tree.
- ✓Headline is a single <h1>; CTAs keep the design system's visible focus ring.
- ✓No canvas/WebGL dependency, so the effect degrades to a plain rotation with CSS support alone.
- ✓Honours prefers-reduced-motion.