# Light Rays (Moonarc)

Soft beams of light falling from the top edge or a top corner over whatever is behind them: a fan of blurred gradients, each a little wider, brighter or slower than the next, swaying a few degrees on its own clock and blended as light (screen). It fills the positioned box it is placed in and never takes a click; the colour and the strength are the light's, the spread is how wide the fan opens, and a seed draws the same fan on every build. Decoration, hidden from a screen reader. Reduced motion holds the rays still. Zero JavaScript.

- Import: `import LightRays from '@moonarc/core/LightRays'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/light-rays.json`
- Tier A · category background · trigger always
- Readout: `<LightRays from="top-left" rays={6} />`
- Browser support: widely (every browser); elsewhere: nothing to fall back from
- Measured cost: 0 B JS (CSS 4.8 kB raw)
- Page: https://moonarc.dev/components/light-rays/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `from` | `'top' | 'top-left' | 'top-right'` | `'top'` | Where the light comes from. |
| `rays` | `number` | `6` | How many beams, 3 to 9. |
| `colour` | `string` | `'oklch(0.97 0.06 90)'` | The light's colour, any CSS colour. |
| `strength` | `number` | `0.55` | How strong the light is, 0 to 1. |
| `spread` | `number` | `46` | How wide the fan opens, in degrees. |
| `seed` | `string` | `''` | A word the fan is varied by. |
| `still` | `boolean` | `false` | Hold the rays still. |

## Usage

```astro
<div class="hero" style="position: relative">
  <img src="/terrace.webp" alt="The terrace at noon" width="1600" height="900" />
  <LightRays from="top-left" rays={6} strength={0.5} />
</div>
```

## Reduced motion

The rays stand still at their own angles.

## With ClientRouter

No script: it works on any page however it was reached.

## Craft

- Each beam is one span with a gradient that fades down its length and a blur, rotated about its top; its angle, width, strength, duration and phase come from the seed at build.
- The sway is a rotate of a few degrees on alternate, each beam on its own duration and a negative delay, so the fan never moves in step.
- mix-blend-mode: screen lightens what is under the beams and never darkens it, so the rays read as light on any picture.

## Replaces

- Light Rays (React Bits)
- Lamp (Aceternity)
- Light rays (Magic UI)

## Source

```astro
---
/**
 * LightRays — soft beams of light falling from an edge or a corner over whatever is behind them: a fan of blurred
 * gradients, each a little wider, brighter or slower than the next, swaying a few degrees on its own clock. Place it in
 * a positioned box (it fills it, above the box's content and below its words) and give it the colour of the light.
 * The rays are decoration, hidden from a screen reader, and they never take a click. `seed` varies the fan and draws
 * the same one on every build. Reduced motion holds them still. Zero JavaScript.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Where the light comes from. */
  from?: 'top' | 'top-left' | 'top-right';
  /** How many beams, 3 to 9. */
  rays?: number;
  /** The light's colour, any CSS colour. */
  colour?: string;
  /** How strong the light is, 0 to 1. */
  strength?: number;
  /** How wide the fan opens, in degrees. */
  spread?: number;
  /** A word the fan is varied by. */
  seed?: string;
  /** Hold the rays still. */
  still?: boolean;
}

const { from = 'top', rays = 6, colour = 'oklch(0.97 0.06 90)', strength = 0.55, spread = 46, seed = '', still = false, class: className, style, ...rest } = Astro.props;
if (rays < 3 || rays > 9) throw new Error(`LightRays: rays is ${rays}; it draws three to nine.`);
let h = 2166136261;
for (const c of `${from}:${rays}:${seed}`) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
const r = () => ((h = Math.imul(h ^ (h >>> 15), 2246822507) + 0x6d2b79f5) >>> 0) / 4294967296;
const n = (v: number) => +v.toFixed(2);
// a corner's fan opens away from its corner; the top's fans out both ways from straight down
const centre = from === 'top-left' ? -spread / 2 - 8 : from === 'top-right' ? spread / 2 + 8 : 0;
const beams = Array.from({ length: rays }, (_, i) => {
  const t = rays === 1 ? 0.5 : i / (rays - 1);
  return {
    angle: n(centre + (t - 0.5) * spread + (r() - 0.5) * 6),
    // along the edge for the top's fan; all from the corner for a corner's
    x: n(from === 'top' ? 12 + t * 76 + (r() - 0.5) * 6 : from === 'top-left' ? 2 + r() * 6 : 92 + r() * 6),
    width: n(6 + r() * 10),
    alpha: n(0.35 + r() * 0.65),
    duration: n(7 + r() * 7),
    delay: n(-r() * 10),
  };
});
const vars = [`--ma-rays-colour:${colour}`, `--ma-rays-strength:${strength}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<div class:list={['ma-rays', className]} data-from={from} data-still={still ? '' : undefined} aria-hidden="true" style={vars} {...rest}>
  {beams.map((b) => <span class="ma-rays__ray" style={`--a:${b.angle}deg;--x:${b.x}%;--w:${b.width}%;--o:${b.alpha};--d:${b.duration}s;--l:${b.delay}s`} />)}
</div>

<style is:global>
  @layer components {
    :where(.ma-rays) { --ma-rays-colour: oklch(0.97 0.06 90); --ma-rays-strength: 0.55; position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; mix-blend-mode: screen; }
    :where(.ma-rays__ray) {
      position: absolute;
      inset-block-start: -12%;
      left: var(--x);
      inline-size: var(--w);
      block-size: 135%;
      translate: -50% 0;
      rotate: var(--a);
      transform-origin: 50% 0;
      background: linear-gradient(to bottom, color-mix(in oklab, var(--ma-rays-colour) calc(var(--o) * var(--ma-rays-strength) * 100%), transparent), transparent 78%);
      filter: blur(14px);
      animation: ma-rays-sway var(--d) ease-in-out var(--l) infinite alternate;
    }
    :where(.ma-rays[data-still] .ma-rays__ray) { animation: none; }
    @keyframes ma-rays-sway {
      from { rotate: calc(var(--a) - 2.5deg); opacity: 0.75; }
      to { rotate: calc(var(--a) + 2.5deg); opacity: 1; }
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-rays__ray) { animation: none; }
    }
  }
</style>

```
