Backgrounds
Light Beams
Angled shafts of light falling from the top edge, each breathing on its own tempo and fading out before the bottom.
Preview in your theme
Loading preview…
- Registry dependencies
- utils
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/light-beams.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "LightBeams" component — angled shafts of
light falling from the top edge, used as a background layer. Its only
dependency is a cn() class merger (clsx + tailwind-merge). No hooks, no events,
no browser APIs, so it must stay a server component — do NOT add "use client".
Contract
- export function LightBeams(props): props extend
Omit<React.ComponentProps<"div">, "children"> (rest props + ref spread onto
the root div) plus:
- beamCount?: number (default 5) — how many shafts to cast. Rounded and
clamped to 1..24; a non-finite value (NaN, Infinity) falls back to 5. Each
beam is a blurred, animated layer, so an unclamped 5000 would mount 5000 of
them and a NaN would render none.
- angle?: number (default 14) — skewX angle in degrees; negative tilts the
other way, 0 is vertical.
- speed?: number (default 8) — seconds for one brighten/dim breath of the
base beam.
- intensity?: "subtle" | "medium" | "bold" (default "medium") — color-mix
alpha tier for the beams (20% / 34% / 48%) plus a much lower paired tier
for the source wash (8% / 12% / 18%).
- colors?: string[] (default ["primary", "chart-1", "chart-3"]) — theme token
names WITHOUT the leading "--", cycled across the beams.
- It renders no children: it is a layer, not a wrapper. The consumer puts it
inside a `relative` ancestor and writes content in a `relative` sibling,
which stacks above it without any z-index bookkeeping.
Behavior
- Root div: pointer-events-none absolute inset-0 overflow-hidden, aria-hidden.
Inline style publishes --zy-beams-speed = `${speed}s`, --zy-beams-alpha and
--zy-beams-wash (the two tier percentages), with the consumer `style` spread
last so all three stay overridable.
- First child is a "source wash": a full-width band across the top 22%,
linear-gradient(to bottom, color-mix(in oklab, var(--<colors[0]>)
var(--zy-beams-wash), transparent), transparent). Without it the beams read
as disconnected stripes instead of light entering from above — but keep it
short and much fainter than the beams, or it swallows them.
- Each beam is one absolutely-positioned div: -top-[15%], h-[130%] (it must
start above the frame and end past it), width 4–11% of the container, an
inline transform: skewX(<angle>deg) with origin-top, and a MODERATE blur
(blur-lg / 16px). Blur is the make-or-break number: past roughly 24px the
shafts smear into a single wash and stop reading as light.
- Fill per beam: linear-gradient(to bottom, color-mix(in oklab, var(--<token>)
var(--zy-beams-alpha), transparent), transparent 92%) — bright where it
enters, gone before the bottom edge.
- Placement must be deterministic, never Math.random(): a small integer hash
of (index, salt) drives the horizontal offset, width, tempo factor
(0.75–1.55) and phase. Beams are laid on an even 100/beamCount step with a
hashed jitter of half a step, so any count fills the width. SSR output stays
stable and re-renders never reshuffle the field.
- The breath animates OPACITY ONLY (0.35 -> 1 -> 0.35), which is what lets each
beam keep its own inline skew transform — an animated transform would fight
it. Duration is calc(var(--zy-beams-speed) * factor) and the delay is
negative, so the beams start mid-breath and never pulse in unison.
- Pass duration and delay as per-beam CSS custom properties and apply them via
one static arbitrary class
[animation:zy-beam-breathe_var(--zy-beam-dur)_ease-in-out_var(--zy-beam-delay)_infinite].
Do NOT put the `animation` shorthand in inline style: it would outrank the
class and defeat the motion-reduce override.
- The @keyframes ship inside the component via a React 19 hoisted
<style href="zyeon-light-beams" precedence="medium"> tag — no Tailwind config
edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: motion-reduce:[animation:none] on every beam. With
the animation off the beams settle at full opacity and stay rendered — the
lighting composition survives, only the breathing stops.
Rendering & styling
- Semantic tokens only: var(--primary) / var(--chart-*) inside
color-mix(in oklab, …, transparent). No hex / rgb() / oklch() anywhere, so
the beams re-skin with the host theme and dark mode.
- Deliberately NO mix-blend-mode: `screen` needs a dark surface and `multiply`
needs a light one, so either would break one of the two color schemes. Plain
alpha over a blur works in both — the beams read as light on dark and as a
soft tinted wash on light.
- Keep the alpha ceiling low enough (48% at "bold") that body text on top of
the beams stays readable in either scheme.
- Merge consumer className via cn() on the root so the call site can retarget
or restack the layer.
Customization levers
- Density and spread: beamCount (3 reads as deliberate, 8+ as stage lighting)
and the 4–11% width range in the hash.
- Softness: the blur utility on each beam — 8–12px for hard theatrical shafts,
16px (default) for volumetric light, 24px+ only if you actually want a wash.
- Direction: angle — negative leans the other way, 0 is a vertical curtain, 30+
is dramatic raking light.
- Pace: speed is seconds per breath; 12–16 s is ambience, 4–5 s is energy. The
0.35 floor in the keyframe controls how far each beam dims.
- Palette: colors takes token names; ["primary"] alone gives a monochrome
spotlight rig, ["chart-2", "chart-4"] a duotone one.
- Light source: drop the top wash for beams that appear to come from nowhere,
or move it to the bottom (and flip the gradients) for uplighting.
- Dark-surface bloom: if the component always sits on a dark panel, adding
mix-blend-screen to the beams makes them glow — just accept that it will
wash out on a light theme.Concepts
- Directional light — every beam starts above the frame and dies before the bottom, which is what separates "light entering a room" from a decorative stripe pattern.
- Deterministic scatter — an integer hash of the index (never
Math.random()) drives offset, width, tempo and phase, so the field is identical on server and client and never reshuffles on re-render. - Opacity-only breath — animating just opacity leaves the
transformslot free for the per-beam skew; separating the two properties is what keeps an inline transform and a shared keyframe from overwriting each other. - Phase-staggered tempo — durations are the base speed times a hashed factor with negative delays, so beams brighten out of sync and the loop never reads as a pulse.
- Blend-mode abstinence —
screenandmultiplyeach only work on one background lightness; plaincolor-mixalpha plus blur is the version that survives both light and dark themes. - Reduced-motion honesty — with the animation off the beams settle fully lit instead of hiding, so the lighting composition (and the contrast it gives the copy) is preserved.