# RainBackground

> Soft rain falling on an 11° slant at two depths (72 streaks, short and faint far away, longer and brighter up close) over a grey-blue sky, with ripples spreading in the puddles along the bottom edge. CSS keyframes and container units, sized to any box.

- Page: https://www.boatui.dev/backgrounds/rain
- Source: https://github.com/wi11s/boatui/blob/main/components/backgrounds/rain-background.tsx
- Animated. Generate: ~904 tokens (source size, single pass). Import: ~19 tokens.

## Install

All backgrounds: `npx degit wi11s/boatui/components/backgrounds components/backgrounds`

Only this one: copy the shared files (`background.tsx`, `background.module.css`) and `rain-background.tsx` and `rain-background.module.css` into `components/backgrounds/`. Shared files are in https://www.boatui.dev/llms-full.txt.

## Usage

```tsx
import { RainBackground } from '@/components/backgrounds/rain-background';

export default function Layout({ children }: { children: React.ReactNode }) {
  return <RainBackground style={{ minHeight: '100vh' }}>{children}</RainBackground>;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<RainTheme>` | — | Colour overrides. Keys listed under theme tokens. |
| `paused` | `boolean` | false | Freezes the animation. |
| `className` | `string` | — | Applied to the root element. Size it like any block element. |
| `style` | `CSSProperties` | — | Merged into the root element style, after theme variables. |
| `children` | `ReactNode` | — | Your content. The background paints behind it. |

## Theme tokens

| Key | CSS variable | Default |
| --- | --- | --- |
| `skyTop` | `--sky-top` | `#9fb3c4` |
| `skyBottom` | `--sky-bottom` | `#dbe4ec` |
| `drop` | `--drop` | `#ffffff` |
| `ground` | `--ground` | `#b7c6d3` |
| `ripple` | `--ripple` | `#ffffff` |

## Source

`components/backgrounds/rain-background.tsx`

```tsx
// <RainBackground>: soft rain falling on a slant at two depths, with ripples spreading in the
// puddles along the bottom. Animated.

import type { CSSProperties } from 'react';
import { BackgroundFrame, backgroundStyles as base, r2, seeded, type BackgroundProps } from './background';
import styles from './rain-background.module.css';

export const rainTheme = {
  skyTop: '#9fb3c4',
  skyBottom: '#dbe4ec',
  drop: '#ffffff',
  ground: '#b7c6d3',
  ripple: '#ffffff',
};
export type RainTheme = typeof rainTheme;

const LAYERS = [
  { count: 46, length: [10, 16], fall: [1.2, 1.6], width: 1, opacity: 0.45 },
  { count: 26, length: [20, 30], fall: [0.7, 0.95], width: 1.6, opacity: 0.75 },
];

const rand = seeded(61);
const between = ([a, b]: number[]) => a + rand() * (b - a);
const DROPS = LAYERS.flatMap(layer =>
  Array.from({ length: layer.count }, () => ({
    // Start further right than the box: the slant carries drops left as they fall.
    x: r2(rand() * 125),
    y: r2(rand() * 100),
    length: r2(between(layer.length)),
    fall: r2(between(layer.fall)),
    delay: r2(rand() * 3),
    width: layer.width,
    opacity: layer.opacity,
  })),
);

const RIPPLES = Array.from({ length: 14 }, () => ({
  x: r2(3 + rand() * 94),
  y: r2(6 + rand() * 30),
  size: r2(14 + rand() * 18),
  time: r2(1.6 + rand() * 1.4),
  delay: r2(rand() * 3),
}));

/** Soft rain falling on a slant, with ripples in the puddles below. */
export function RainBackground(props: BackgroundProps<RainTheme>) {
  return (
    <BackgroundFrame
      {...props}
      defaultTheme={rainTheme}
      layer={
        <>
          <div className={styles.sky} />
          {DROPS.map((d, i) => (
            <div
              key={i}
              className={`${base.particle} ${styles.fall}`}
              style={{ left: `${d.x}%`, '--y': `${d.y}%`, animationDuration: `${d.fall}s`, animationDelay: `${-d.delay}s` } as CSSProperties}
            >
              <span className={styles.streak} style={{ height: d.length, width: d.width, opacity: d.opacity }} />
            </div>
          ))}
          <div className={styles.ground}>
            {RIPPLES.map((r, i) => (
              <span
                key={i}
                className={styles.ripple}
                style={{ left: `${r.x}%`, bottom: `${r.y}%`, width: r.size, height: r.size * 0.32, animationDuration: `${r.time}s`, animationDelay: `${-r.delay}s` }}
              />
            ))}
          </div>
        </>
      }
    />
  );
}
```

`components/backgrounds/rain-background.module.css`

```css
.sky {
  position: absolute;
  inset: 0;
  background: linear-gradient(var(--sky-top), var(--sky-bottom));
}

/* Drops fall the layer's full height plus a little, drifting left by a fifth of that: a slant
   of about 11°, which the streak matches. */
.fall {
  animation: fall linear infinite;
  will-change: transform;
}

.streak {
  display: block;
  border-radius: 1px;
  background: linear-gradient(transparent, var(--drop));
  transform: rotate(11.3deg);
}

.ground {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 56px;
  background: linear-gradient(transparent, var(--ground) 40%);
}

.ripple {
  position: absolute;
  border: 1px solid var(--ripple);
  border-radius: 50%;
  transform: translateX(-50%) scale(0.1);
  opacity: 0;
  animation: ripple ease-out infinite;
}

@keyframes fall {
  from { transform: translate(0, -40px); }
  to   { transform: translate(calc(-0.2 * (100cqh + 80px)), calc(100cqh + 40px)); }
}
@keyframes ripple {
  0%   { transform: translateX(-50%) scale(0.1); opacity: 0; }
  10%  { opacity: 0.9; }
  100% { transform: translateX(-50%) scale(1); opacity: 0; }
}
```
