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…
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.
- 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.