# WavesBackground

> A calm sea along the bottom edge under an open sky: four wave layers scroll at different speeds and directions and swell gently, with foam glints on the near crests. The rest of the box is clear sky, so it suits footers, heroes and sign-in pages. CSS keyframes and SVG strips that loop seamlessly.

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

## Usage

```tsx
import { WavesBackground } from '@/components/backgrounds/waves-background';

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

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<WavesTheme>` | — | 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` | `#d7ecf7` |
| `skyBottom` | `--sky-bottom` | `#f6fbfd` |
| `wave1` | `--wave-1` | `#a9d4ea` |
| `wave2` | `--wave-2` | `#7fbad9` |
| `wave3` | `--wave-3` | `#4f97bf` |
| `wave4` | `--wave-4` | `#2f7aa6` |
| `foam` | `--foam` | `#ffffff` |

## Source

`components/backgrounds/waves-background.tsx`

```tsx
// <WavesBackground>: a calm sea rolling along the bottom edge under an open sky: four wave layers
// scroll at different speeds, with foam glints on the crests. Good behind footers and heroes. Animated.

import { BackgroundFrame, type BackgroundProps } from './background';
import styles from './waves-background.module.css';

export const wavesTheme = {
  skyTop: '#d7ecf7',
  skyBottom: '#f6fbfd',
  wave1: '#a9d4ea',
  wave2: '#7fbad9',
  wave3: '#4f97bf',
  wave4: '#2f7aa6',
  foam: '#ffffff',
};
export type WavesTheme = typeof wavesTheme;

/**
 * A wave strip 800 units wide (two 400-unit periods) in a 100-unit-tall box, filled to the bottom.
 * The strip is 200% of the layer's width, so sliding it by half loops seamlessly.
 */
const wave = (crest: number, amp: number, len: number, phase = 0) => {
  let d = `M0 100 L0 ${crest}`;
  for (let x = 0; x <= 800; x += 10) {
    d += ` L${x} ${(crest - amp * Math.sin((x / len) * Math.PI * 2 + phase)).toFixed(1)}`;
  }
  return `${d} L800 100 Z`;
};

const LAYERS = [
  { height: 34, d: wave(30, 8, 200), fill: 'var(--wave-1)', time: 22, reverse: true },
  { height: 26, d: wave(30, 10, 400 / 3, 1), fill: 'var(--wave-2)', time: 16, reverse: false },
  { height: 19, d: wave(30, 12, 200, 2), fill: 'var(--wave-3)', time: 12, reverse: true },
  { height: 12, d: wave(30, 14, 400, 0.5), fill: 'var(--wave-4)', time: 9, reverse: false },
];

/** A calm sea rolling along the bottom edge under an open sky. */
export function WavesBackground(props: BackgroundProps<WavesTheme>) {
  return (
    <BackgroundFrame
      {...props}
      defaultTheme={wavesTheme}
      layer={
        <>
          <div className={styles.sky} />
          {LAYERS.map((l, i) => (
            <div key={i} className={styles.band} style={{ height: `${l.height}%` }}>
              <div className={styles.bob} style={{ animationDelay: `${-i * 0.9}s` }}>
                <svg
                  className={`${styles.strip} ${l.reverse ? styles.reverse : ''}`}
                  style={{ animationDuration: `${l.time}s` }}
                  viewBox="0 0 800 100"
                  preserveAspectRatio="none"
                >
                  <path d={l.d} fill={l.fill} />
                </svg>
              </div>
            </div>
          ))}
          <div className={styles.glints}>
            {[12, 34, 58, 81].map((x, i) => (
              <span key={x} className={styles.glint} style={{ left: `${x}%`, animationDelay: `${-i * 0.7}s` }} />
            ))}
          </div>
        </>
      }
    />
  );
}
```

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

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

/* Each layer is a band anchored to the bottom; nearer bands are shorter, so they sit in front. */
.band {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
}

.bob {
  position: absolute;
  inset: 0;
  animation: bob 4.5s ease-in-out infinite alternate;
}

/* Twice the layer's width; sliding by half a strip is one full period. */
.strip {
  display: block;
  width: 200%;
  height: 100%;
  animation: roll linear infinite;
}
.reverse { animation-direction: reverse; }

.glints {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8%;
  height: 4px;
}

.glint {
  position: absolute;
  width: 26px;
  height: 2px;
  border-radius: 1px;
  background: var(--foam);
  opacity: 0;
  animation: glint 3s ease-in-out infinite;
}

@keyframes roll { to { transform: translateX(-50%); } }
@keyframes bob {
  from { transform: translateY(-3px); }
  to   { transform: translateY(3px); }
}
@keyframes glint {
  0%, 100% { opacity: 0; transform: scaleX(0.4); }
  50%      { opacity: 0.8; transform: scaleX(1); }
}
```
