A dark hero with warm shafts of light falling from the top edge, like sun through high windows. Each beam is a blurred gradient that fades in, leans a degree or two and fades out again on its own timing, so the light never repeats in an obvious way. The demo puts a concert hall headline, two buttons and a row of three figures on top. On phones the buttons and figures stack and the beams keep their size.
// Adapted from Magic UI (https://magicui.design/docs/components/light-rays)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import type { CSSProperties } from "react";
/** Colour of the light, as "r, g, b". */
const RAY_COLOR = "255, 206, 138";
/** Seconds for one slow fade in and out. Each ray varies around this. */
const CYCLE = 12;
const RAY_COUNT = 9;
const stats = [
{ value: "4,200", label: "seats across three halls" },
{ value: "38", label: "concerts this season" },
{ value: "1911", label: "the year the doors opened" },
];
/** Small repeatable random, so the server and the browser draw the same rays. */
function seeded(seed: number) {
let s = seed;
return () => {
s = (s * 16807) % 2147483647;
return (s - 1) / 2147483646;
};
}
const rand = seeded(41);
const rays = Array.from({ length: RAY_COUNT }, (_, i) => {
const duration = CYCLE * (0.75 + rand() * 0.5);
return {
id: i,
left: 6 + (i / (RAY_COUNT - 1)) * 88 + (rand() - 0.5) * 6,
rotate: -26 + rand() * 52,
width: 90 + rand() * 150,
swing: 0.8 + rand() * 1.8,
intensity: 0.55 + rand() * 0.45,
duration,
// A negative delay starts each ray part of the way through, so the first frame is already lit.
delay: -rand() * duration,
};
});
export default function LightRaysBackgroundWithSwayingBeams() {
return (
<section className="relative isolate overflow-hidden bg-[#0b0906] px-4 py-24 sm:px-6 sm:py-32">
<style>{`
@keyframes light-rays-background-with-swaying-beams-sway {
0%, 100% { opacity: 0; transform: translateX(-50%) rotate(calc(var(--ray-rotate) - var(--ray-swing))); }
50% { opacity: var(--ray-intensity); transform: translateX(-50%) rotate(calc(var(--ray-rotate) + var(--ray-swing))); }
}
.light-rays-background-with-swaying-beams-ray {
transform: translateX(-50%) rotate(var(--ray-rotate));
opacity: 0;
animation: light-rays-background-with-swaying-beams-sway var(--ray-duration) ease-in-out var(--ray-delay) infinite;
will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) {
.light-rays-background-with-swaying-beams-ray { animation: none; opacity: calc(var(--ray-intensity) * 0.6); }
}
`}</style>
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
<div className="absolute inset-0 opacity-70" style={{ background: `radial-gradient(circle at 22% 0%, rgba(${RAY_COLOR}, 0.22), transparent 62%)` }} />
<div className="absolute inset-0 opacity-70" style={{ background: `radial-gradient(circle at 78% -6%, rgba(${RAY_COLOR}, 0.16), transparent 66%)` }} />
{rays.map((r) => (
<div
key={r.id}
className="light-rays-background-with-swaying-beams-ray absolute -top-[14%] h-[105%] origin-top rounded-full mix-blend-screen blur-xl"
style={
{
left: `${r.left}%`,
width: r.width,
background: `linear-gradient(to bottom, rgba(${RAY_COLOR}, 0.6), rgba(${RAY_COLOR}, 0.14) 55%, transparent)`,
"--ray-rotate": `${r.rotate}deg`,
"--ray-swing": `${r.swing}deg`,
"--ray-intensity": r.intensity,
"--ray-duration": `${r.duration}s`,
"--ray-delay": `${r.delay}s`,
} as CSSProperties
}
/>
))}
{/* Floor shadow so the text at the bottom keeps its contrast. */}
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-[#0b0906] via-[#0b0906]/70 to-transparent" />
</div>
<div className="mx-auto max-w-4xl text-center">
<p className="inline-flex items-center gap-2 rounded-full border border-amber-200/20 bg-amber-200/5 px-4 py-1.5 text-sm font-medium text-amber-100">
<span className="h-1.5 w-1.5 rounded-full bg-amber-300" aria-hidden="true" />
Autumn season on sale Friday
</p>
<h1 className="mt-8 font-serif text-5xl font-medium leading-[1.05] tracking-tight text-amber-50 sm:text-7xl">
An evening under <em className="text-amber-200">the old lights</em>
</h1>
<p className="mx-auto mt-6 max-w-xl text-lg leading-relaxed text-stone-300">
Orchestras, quartets and late-night jazz in the restored Calloway Hall. Pick a seat, bring someone, and stay for the last note.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a href="#" className="inline-flex h-12 items-center rounded-full bg-amber-200 px-7 text-sm font-semibold text-stone-950 transition-colors hover:bg-amber-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-200 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-950">
See what is on
</a>
<a href="#" className="inline-flex h-12 items-center rounded-full border border-stone-500 px-7 text-sm font-semibold text-amber-50 transition-colors hover:border-amber-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-200">
Become a member
</a>
</div>
<dl className="mx-auto mt-20 grid max-w-3xl gap-8 border-t border-amber-200/15 pt-10 sm:grid-cols-3">
{stats.map((s) => (
<div key={s.label} className="flex flex-col-reverse gap-1">
<dt className="text-sm text-stone-300">{s.label}</dt>
<dd className="font-serif text-4xl text-amber-100">{s.value}</dd>
</div>
))}
</dl>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/light-rays-background-with-swaying-beams.json
Each ray is a tall `div` with a top-to-bottom gradient, a heavy blur and `mix-blend-screen`, pinned by its top edge. A keyframe animation changes its opacity and rotates it by a couple of degrees around that top edge.
Every ray gets a negative `animation-delay`, which starts it part of the way through its cycle. Without that, all of the rays would begin at zero opacity and the hero would look empty for the first few seconds.
No. The positions come from a small seeded generator, so the server and the browser produce the same layout and there is no flicker when the page loads. Change the seed to change the layout.
It animates only `opacity` and `transform` on nine elements, which browsers handle on the compositor. Large blurs do use memory, so keep the count under about a dozen.
Screen blending adds light, so it disappears on white. On a light page, remove `mix-blend-screen`, lower the gradient opacity and use a colour darker than the page.
Adapted from Light Rays in Magic UI, used under its open-source licence. Licences and credits
Related: Aurora Background with Drifting Colour Bands, Dark Hero with Spotlight Sweep and Line Reveal, Starfield Background with Depth and Shooting Stars.
More animated backgrounds or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.