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…

import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Keyframes ship inside the component via React 19 hoisted <style> —
 * no tailwind config edits, and duplicates dedupe by href.
 * The breath animates opacity only, so each beam keeps its own inline
 * skew transform.
 */
const KEYFRAMES = `@keyframes zy-beam-breathe{0%,100%{opacity:0.35}50%{opacity:1}}`

/** color-mix alpha tier — the single knob for how bright the shafts burn. */
const ALPHA = {
  subtle: "20%",

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/light-beams.json

Prompt

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 transform slot 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 abstinencescreen and multiply each only work on one background lightness; plain color-mix alpha 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.

On This Page