# SnowfallBackground

> Snow falling at three depths over a pale winter sky: small slow flakes far away, six-armed spinning flakes up close, all swaying as they fall onto soft drifts at the bottom. About 75 flakes, CSS keyframes only, sized to any box with container units.

- Page: https://www.boatui.dev/backgrounds/snowfall
- Source: https://github.com/wi11s/boatui/blob/main/components/backgrounds/snowfall-background.tsx
- Animated. Generate: ~1,028 tokens (source size, single pass). Import: ~21 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 `snowfall-background.tsx` and `snowfall-background.module.css` into `components/backgrounds/`. Shared files are in https://www.boatui.dev/llms-full.txt.

## Usage

```tsx
import { SnowfallBackground } from '@/components/backgrounds/snowfall-background';

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

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<SnowfallTheme>` | — | 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` | `#bcd3ee` |
| `skyBottom` | `--sky-bottom` | `#eef4fb` |
| `flake` | `--flake` | `#ffffff` |
| `drift` | `--drift` | `#ffffff` |
| `driftShadow` | `--drift-shadow` | `#d9e6f4` |

## Source

`components/backgrounds/snowfall-background.tsx`

```tsx
// <SnowfallBackground>: snow falling at three depths onto soft drifts. Animated.
// Far flakes are small and slow, near flakes are six-armed and spin as they sway.

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

export const snowfallTheme = {
  skyTop: '#bcd3ee',
  skyBottom: '#eef4fb',
  flake: '#ffffff',
  drift: '#ffffff',
  driftShadow: '#d9e6f4',
};
export type SnowfallTheme = typeof snowfallTheme;

const LAYERS = [
  { count: 40, size: [2, 3.5], fall: [18, 26], sway: [6, 14], opacity: 0.6 },
  { count: 24, size: [4, 6], fall: [12, 17], sway: [12, 24], opacity: 0.85 },
  { count: 12, size: [12, 18], fall: [8, 12], sway: [18, 34], opacity: 1 },
];

const rand = seeded(41);
const between = ([a, b]: number[]) => a + rand() * (b - a);
const FLAKES = LAYERS.flatMap((layer, depth) =>
  Array.from({ length: layer.count }, () => ({
    depth,
    x: r2(rand() * 100),
    y: r2(rand() * 100),
    size: r2(between(layer.size)),
    fall: r2(between(layer.fall)),
    delay: r2(rand() * 30),
    sway: r2(between(layer.sway)),
    swayTime: r2(2 + rand() * 3),
    spin: r2(6 + rand() * 8),
    opacity: layer.opacity,
  })),
);

// Six arms, each with a small V near the tip.
const ARM = 'M0 0 V-9 M0 -6 L-2.5 -8.5 M0 -6 L2.5 -8.5';
function Flake({ size }: { size: number }) {
  return (
    <svg className={styles.flake} width={size} height={size} viewBox="-10 -10 20 20" fill="none" strokeWidth="1.6" strokeLinecap="round">
      {[0, 60, 120, 180, 240, 300].map(a => <path key={a} d={ARM} transform={`rotate(${a})`} />)}
    </svg>
  );
}

/** Snow falling at three depths onto soft drifts. */
export function SnowfallBackground(props: BackgroundProps<SnowfallTheme>) {
  return (
    <BackgroundFrame
      {...props}
      defaultTheme={snowfallTheme}
      layer={
        <>
          <div className={styles.sky} />
          {FLAKES.map((f, i) => (
            <div
              key={i}
              className={`${base.particle} ${styles.fall}`}
              style={{ left: `${f.x}%`, '--y': `${f.y}%`, animationDuration: `${f.fall}s`, animationDelay: `${-f.delay}s`, opacity: f.opacity } as CSSProperties}
            >
              <div className={styles.sway} style={{ '--sway': `${f.sway}px`, animationDuration: `${f.swayTime}s` } as CSSProperties}>
                {f.depth === 2 ? (
                  <span className={styles.spin} style={{ animationDuration: `${f.spin}s` }}>
                    <Flake size={f.size} />
                  </span>
                ) : (
                  <span className={styles.dot} style={{ width: f.size, height: f.size }} />
                )}
              </div>
            </div>
          ))}
          <svg className={styles.drifts} viewBox="0 0 400 72" preserveAspectRatio="none">
            <path fill="var(--drift-shadow)" d="M0 40 C60 18 110 30 160 34 S260 14 320 28 S380 30 400 24 V72 H0 Z" />
            <path fill="var(--drift)" d="M0 52 C50 36 100 46 150 48 S240 32 300 42 S370 46 400 40 V72 H0 Z" />
          </svg>
        </>
      }
    />
  );
}
```

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

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

.fall {
  animation: fall linear infinite;
  will-change: transform;
}

.sway { animation: sway ease-in-out infinite alternate; }
.spin { animation: spin linear infinite; display: block; }

.dot {
  display: block;
  border-radius: 50%;
  background: var(--flake);
  box-shadow: 0 0 2px rgb(90 120 160 / 0.25);
}

.flake {
  display: block;
  stroke: var(--flake);
  filter: drop-shadow(0 0 1px rgb(90 120 160 / 0.35));
}

.drifts {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 72px;
}

@keyframes fall {
  from { transform: translateY(-24px); }
  to   { transform: translateY(calc(100cqh + 24px)); }
}
@keyframes sway {
  from { transform: translateX(calc(var(--sway) * -1)); }
  to   { transform: translateX(var(--sway)); }
}
@keyframes spin { to { transform: rotate(360deg); } }
```
