anima.js
ギャラリー
背景

AuroraBackground

CSS

ゆっくり漂うオーロラ / メッシュグラデーションの背景 — ぼかした光のブロブをスクリーン合成、フィルムグレインと周辺フェード付き。ヒーローセクションの中身を上に重ねられる純粋な CSS。

v2.0 をリリースしました

Build at the speed of thought

思考のスピードで、プロダクトを形に。アイデアから本番まで、ひとつのワークフローで。

AI に貼るだけで導入Claude Code / Cursor / ChatGPT などにそのまま貼り付けてください。ソースコード一式と現在の設定、組み込み手順が含まれています(約23 KB)。
# AuroraBackground をこのプロジェクトに追加してください

このプロンプトは自己完結しています。コンポーネントのソースコード一式が下に含まれているので、外部サイトへのアクセスや shadcn CLI は不要です。手順どおりにファイルを作成し、依存パッケージを追加し、使用例を配置して、そのまま動作する状態にしてください。

## コンポーネントについて
- 名前: AuroraBackground
- 説明: ゆっくり漂うオーロラ / メッシュグラデーションの背景 — ぼかした光のブロブをスクリーン合成、フィルムグレインと周辺フェード付き。ヒーローセクションの中身を上に重ねられる純粋な CSS。
- 構成ファイル: `components/aurora-background/index.tsx`, `components/aurora-background/AuroraBackground.module.css`

## 前提と注意
- React 19 / TypeScript / Next.js(App Router)のプロジェクトを想定しています。Next.js 以外の React プロジェクト(Vite など)でもそのまま動きます。
- スタイルは同梱の CSS Modules(ある場合)で自己完結しており、Tailwind CSS の有無や設定には依存しません。Tailwind の設定変更やグローバル CSS の追加は、この依頼文に明記された分だけにしてください。
- ブラウザ API やインタラクションを使うクライアントコンポーネントです。ファイル先頭の "use client" を維持してください。
- コンポーネント自体の背景は透過です。配置先ページの背景がそのまま透けて見える前提で作られているので、白背景や単色の `div` などで囲わず、コンポーネントだけをそのまま配置してください。
- ソースコードの内容は変更しないでください。例外は、`@/` パスエイリアスが無いプロジェクトで import パスを相対パスに直す場合だけです。

## 手順
### 1. 依存パッケージを追加する
追加の npm パッケージは不要です(React のみ)。

### 2. ファイルを作成する
`@/components` が指すディレクトリ(多くは `src/components/` か `components/`。shadcn の `components.json` があれば `aliases.components` の指す場所)に、以下の 2 ファイルを**一字一句そのまま**作成してください。

#### `components/aurora-background/index.tsx`
```tsx
"use client";

import {
  useEffect,
  useRef,
  type CSSProperties,
  type ReactNode,
} from "react";
import styles from "./AuroraBackground.module.css";

/** Lets CSS custom properties pass the CSSProperties type check. */
type CSSVars = CSSProperties & Record<`--${string}`, string | number>;

export type AuroraBackgroundProps = {
  /**
   * 3–5 color stops, one per drifting blob. Fewer than 3 are padded with
   * the defaults (two blobs leave visible holes); extras beyond 5 are
   * ignored. Bright, saturated colors work best — they're screen-blended
   * over a near-black base.
   */
  colors?: string[];
  /** Drift speed multiplier. 1 = default pace, 0 = frozen composition. */
  speed?: number;
  /** Blur applied over the whole blob layer, in px. */
  blur?: number;
  /** Opacity of the aurora layer over the base color (0–1). */
  intensity?: number;
  /** Static film-grain overlay — hides gradient banding on 8-bit displays. */
  grain?: boolean;
  /** Opacity of the grain overlay (0–1). */
  grainOpacity?: number;
  /** Radial mask so the aurora fades into the page at the edges. */
  vignette?: boolean;
  className?: string;
  /** Hero content rendered above the aurora. */
  children?: ReactNode;
  /** Root element. "section" when the aurora wraps a landmark hero. */
  as?: "div" | "section";
};

export const DEFAULT_AURORA_COLORS = ["#6d4aff", "#1fb6ff", "#ff4d9d", "#2ee6a8"];
const FALLBACK_FIFTH = "#8b5cf6";
const DEFAULT_SPEED = 1;
const DEFAULT_BLUR = 80;
const DEFAULT_INTENSITY = 0.8;
const DEFAULT_GRAIN_OPACITY = 0.12;
const MIN_BLOBS = 3;
const MAX_BLOBS = 5;

/** The stylesheet's own defaults, per blob slot (see --ab-c0…c4). */
const CSS_DEFAULT_COLORS = [...DEFAULT_AURORA_COLORS, FALLBACK_FIFTH];

/**
 * Slowly drifting aurora / mesh-gradient backdrop. Pure CSS: a few large
 * radial blobs drift on transform-only keyframes at co-prime durations so
 * the composition never visibly repeats, screen-blended and blurred into
 * one field. Optional static grain and an edge mask let it melt into the
 * page. Fills its parent; children render on top.
 */
export function AuroraBackground({
  colors = DEFAULT_AURORA_COLORS,
  speed = DEFAULT_SPEED,
  blur = DEFAULT_BLUR,
  intensity = DEFAULT_INTENSITY,
  grain = true,
  grainOpacity = DEFAULT_GRAIN_OPACITY,
  vignette = true,
  className,
  children,
  as = "div",
}: AuroraBackgroundProps) {
  // Typed as "div" for the ref: the only thing read through it is
  // `dataset`, which <section> shares, so the narrower type is harmless.
  const Tag = as as "div";
  const rootRef = useRef<HTMLDivElement>(null);

  // Blurred, full-bleed layers are the most expensive thing on a landing
  // page; stop compositing their animation while the hero is scrolled away.
  // Written straight to a data attribute — no re-render per visibility flip.
  useEffect(() => {
    const el = rootRef.current;
    if (!el || typeof IntersectionObserver === "undefined") return;
    const io = new IntersectionObserver(([entry]) => {
      el.dataset.offscreen = entry.isIntersecting ? "false" : "true";
    });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const stops = colors.slice(0, MAX_BLOBS);
  for (let i = stops.length; i < MIN_BLOBS; i++) stops.push(CSS_DEFAULT_COLORS[i]);

  // Only non-default values go inline, so the default render is exactly
  // the stylesheet and generated markup stays clean.
  const vars: CSSVars = {};
  stops.forEach((color, i) => {
    if (color !== CSS_DEFAULT_COLORS[i]) vars[`--ab-c${i}`] = color;
  });
  const frozen = !(speed > 0);
  if (!frozen && speed !== DEFAULT_SPEED) vars["--ab-speed"] = speed;
  if (blur !== DEFAULT_BLUR) vars["--ab-blur"] = `${Math.max(0, blur)}px`;
  if (intensity !== DEFAULT_INTENSITY) {
    vars["--ab-intensity"] = Math.min(1, Math.max(0, intensity));
  }
  if (grainOpacity !== DEFAULT_GRAIN_OPACITY) {
    vars["--ab-grain-opacity"] = Math.min(1, Math.max(0, grainOpacity));
  }

  return (
    <Tag
      ref={rootRef}
      className={className ? `${styles.root} ${className}` : styles.root}
      style={vars}
      data-frozen={frozen ? "true" : undefined}
      data-vignette={vignette ? "true" : undefined}
    >
      <div className={styles.aurora} aria-hidden="true">
        {stops.map((_, i) => (
          <span key={i} className={styles.blob} />
        ))}
      </div>
      {grain ? <div className={styles.grain} aria-hidden="true" /> : null}
      <div className={styles.content}>{children}</div>
    </Tag>
  );
}

export default AuroraBackground;
```

#### `components/aurora-background/AuroraBackground.module.css`
```css
/* ==========================================================================
   Aurora background — drifting mesh gradient for dark hero sections

   Layers (back → front): base color → blob field (screen-blended, blurred,
   optionally edge-masked) → static film grain → content. Every animated
   property is a transform, so the drift stays on the compositor.
   ========================================================================== */

.root {
  /* Spec values. The component only sets these inline for non-default
     values, so the default render is exactly this stylesheet. */
  --ab-c0: #6d4aff; /* violet */
  --ab-c1: #1fb6ff; /* sky */
  --ab-c2: #ff4d9d; /* pink */
  --ab-c3: #2ee6a8; /* aurora green */
  --ab-c4: #8b5cf6; /* only used when a 5th color is passed */
  --ab-speed: 1;
  --ab-blur: 80px;
  --ab-intensity: 0.8;
  --ab-grain-opacity: 0.12;
  /* Not a prop: override via className to match a non-#0a0a0a page. */
  --ab-base: #08080c;

  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* Keeps the blend modes inside the component instead of blending with
     whatever sits behind it on the page. */
  isolation: isolate;
  background: var(--ab-base);
  color: #fff;
}

/* --- Blob field ---------------------------------------------------------
   A size container so blobs can be sized in cqmax — relative to the
   hero's longer side, so the composition reads the same in a wide desktop
   hero and a tall phone screen. Size containment lives on this absolute
   layer, never on .root, or an auto-height hero would collapse to 0. */
.aurora {
  position: absolute;
  inset: 0;
  z-index: 0;
  container-type: size;
  background: var(--ab-base);
  opacity: var(--ab-intensity);
  filter: blur(var(--ab-blur));
  /* Pre-blur bleed: without it the blur pulls the base color in from the
     edges and the field looks inset. */
  transform: scale(1.15);
  pointer-events: none;
}

/* Fade the aurora into the page: strongest a little above center (behind
   the headline), gone at the edges and the bottom. Unprefixed only — the
   build adds -webkit-, same as backdrop-filter in the glass components. */
.root[data-vignette="true"] .aurora {
  mask-image: radial-gradient(
    ellipse 70% 72% at 50% 38%,
    #000 30%,
    rgba(0, 0, 0, 0.55) 62%,
    transparent 100%
  );
}

.blob {
  position: absolute;
  border-radius: 50%;
  /* closest-side + transparent tail = a soft ellipse with no hard rim
     even before the layer blur. */
  background: radial-gradient(
    closest-side,
    var(--ab-color),
    transparent 100%
  );
  mix-blend-mode: screen;
  will-change: transform;
  animation-name: ab-drift-a;
  animation-duration: calc(var(--ab-dur) / var(--ab-speed));
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  /* Alternate: each path eases back the way it came, so there's never a
     visible jump at the loop seam. */
  animation-direction: alternate;
}

/* Slots are ordered so any first three already cover the frame:
   top-left, right, bottom; then top-center and lower-left. Durations are
   roughly co-prime, and negative delays start each blob mid-path so the
   first paint is already an organic composition. */
.blob:nth-child(1) {
  --ab-color: var(--ab-c0);
  --ab-dur: 23s;
  left: -12%;
  top: -22%;
  width: 72cqmax;
  height: 56cqmax;
  animation-delay: -6s;
}

.blob:nth-child(2) {
  --ab-color: var(--ab-c1);
  --ab-dur: 29s;
  left: 52%;
  top: 0%;
  width: 62cqmax;
  height: 62cqmax;
  animation-name: ab-drift-b;
  animation-delay: -11s;
}

.blob:nth-child(3) {
  --ab-color: var(--ab-c2);
  --ab-dur: 37s;
  left: 12%;
  top: 52%;
  width: 76cqmax;
  height: 46cqmax;
  animation-name: ab-drift-c;
  animation-delay: -17s;
}

.blob:nth-child(4) {
  --ab-color: var(--ab-c3);
  --ab-dur: 31s;
  left: 34%;
  top: -18%;
  width: 46cqmax;
  height: 40cqmax;
  animation-name: ab-drift-b;
  animation-direction: alternate-reverse;
  animation-delay: -4s;
}

.blob:nth-child(5) {
  --ab-color: var(--ab-c4);
  --ab-dur: 41s;
  left: -18%;
  top: 44%;
  width: 52cqmax;
  height: 52cqmax;
  animation-name: ab-drift-c;
  animation-direction: alternate-reverse;
  animation-delay: -21s;
}

/* Translate % is relative to the blob itself, so travel scales with it. */
@keyframes ab-drift-a {
  0% {
    transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
  }
  50% {
    transform: translate3d(22%, 14%, 0) rotate(35deg) scale(1.12);
  }
  100% {
    transform: translate3d(-8%, 26%, 0) rotate(-15deg) scale(0.94);
  }
}

@keyframes ab-drift-b {
  0% {
    transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
  }
  50% {
    transform: translate3d(-26%, 18%, 0) rotate(-40deg) scale(0.9);
  }
  100% {
    transform: translate3d(-10%, -12%, 0) rotate(20deg) scale(1.1);
  }
}

@keyframes ab-drift-c {
  0% {
    transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
  }
  50% {
    transform: translate3d(18%, -22%, 0) rotate(25deg) scale(1.08);
  }
  100% {
    transform: translate3d(-16%, -8%, 0) rotate(-30deg) scale(0.96);
  }
}

/* speed = 0, or the hero is scrolled out of view: hold the current pose
   rather than snapping back to frame 0. */
.root[data-frozen="true"] .blob,
.root[data-offscreen="true"] .blob {
  animation-play-state: paused;
}

/* --- Film grain ---------------------------------------------------------
   Static grayscale fractal noise as an inline SVG, overlay-blended. Hides
   gradient banding on 8-bit panels and gives the "printed" texture current
   marketing pages use. Not animated: per-frame noise costs a full repaint. */
.grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--ab-grain-opacity);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

/* --- Content slot ------------------------------------------------------- */
.content {
  position: relative;
  z-index: 2;
  height: 100%;
}

/* Reduced motion: the same composition, simply held still. The negative
   delays above mean the frozen pose is mid-drift, not the stiff start. */
@media (prefers-reduced-motion: reduce) {
  .blob {
    animation-play-state: paused;
  }
}
```

代替手段: `components.json` がありシェルを実行できる環境なら、shadcn CLI でも同じファイルと依存パッケージが入ります。
```bash
npx shadcn@latest add https://anima-js.vercel.app/r/aurora-background.json
```

### 3. 使用例を配置する
以下は配信元のプレイグラウンドで設定されていた値をそのまま反映した使用例です。これを基に、適切なページ・レイアウトへ配置してください。コード中の TODO コメントは、対応するか、何をすべきかを説明してください。
```tsx
import { AuroraBackground } from "@/components/aurora-background";

// TODO: ヒーローの中身(見出し・ボタンなど)を children として渡してください: <AuroraBackground …>…</AuroraBackground>。親要素に高さ(例: min-height: 100svh)を与えると、その全面を埋めます
<AuroraBackground
  blur={80}
  intensity={0.8}
  vignette={true}
  grain={true}
  grainOpacity={0.12}
  speed={1}
  colors={["#6d4aff", "#1fb6ff", "#ff4d9d", "#2ee6a8"]}
/>
```

### 4. 組み込み手順
1. ヒーローの中身(見出し・バッジ・ボタン)を `children` として渡す: `<AuroraBackground colors={[…]}>…</AuroraBackground>`。ランドマークにしたい場合は `as="section"` と `aria-label` 付きの見出しを中に置く。
2. ルートは `width: 100%; height: 100%` で**親を埋める**。親に高さを与える(例: ラッパーに `min-height: 100svh`、または `className` で `min-height` を指定)。高さの無い親の中では中身の高さだけになる。
3. `colors` は 3〜5 色。ほぼ黒の下地にスクリーン合成するので、**明るく彩度の高い色**ほど映える。暗い色やグレーは沈んで見えない。2 色以下は既定色で補われ、6 色目以降は無視される。
4. 下地色はページ背景に合わせる。既定は `#08080c`。変えるときは `className` で CSS 変数 `--ab-base` を上書きする(例: `.hero { --ab-base: #0a0a0a; }`)。`vignette` のフェード先もこの色になる。
5. 中身の文字はオーロラの上に直接乗る。読みにくい場合は `intensity` を下げるか、文字色に白 .65 程度のコントラストを確保する。
6. `"use client"` コンポーネント(画面外で一時停止するための IntersectionObserver を使う)。Server Component から `children` 付きでそのまま使える。
7. `npm run build` が通ることを確認し、実機で「スクロールしてもカクつかない」「OS の視差効果を減らす設定で静止する」を確認する。

#### 触ってはいけないところ
| 症状 | 原因 | 対処 |
| --- | --- | --- |
| ヒーローの高さが 0 になり何も見えない | `container-type: size` をルート(`.root`)に移した。size コンテナは中身から高さを取らない | size コンテナは**絶対配置のブロブ層(`.aurora`)だけ**に置く。ルートには付けない |
| Chrome で周辺フェードが効かない / 消える | `mask-image` と `-webkit-mask-image` を併記すると Next.js(Lightning CSS)が unprefixed を削ることがある | **unprefixed だけ書く**。プレフィックスはビルド時に自動付与される |
| 端に下地色の縁が出て、オーロラが内側に縮んで見える | ブロブ層の `filter: blur()` が端で下地色を引き込む | `.aurora` の `transform: scale(1.15)` を外さない(ぼかし分のはみ出し) |
| ページ全体の色が変わる / 下のセクションと混ざる | ルートの `isolation: isolate` を消すと `mix-blend-mode: screen` がページ背景と合成される | `isolation: isolate` を外さない |
| スクロールが重い・バッテリーを食う | `left` / `top` / `background-position` をアニメーションさせた、またはグレインを毎フレーム動かした | アニメーションは `transform` のみ。グレインは静止画のまま |
| ループの継ぎ目でブロブが跳ぶ | キーフレームの 0% と 100% が違うのに `animation-direction: normal` にした | `alternate` を維持する(往復するので継ぎ目が無い) |

### 5. 完了条件
- 型チェックとビルド(`npm run build` 相当)が通る
- 使用例を置いたページでコンポーネントが表示され、操作に反応する
- "use client" が維持され、不透明な背景のラッパーが追加されていない
- 使用例の TODO コメントが解消されている(または対応方法が説明されている)
- 「組み込み手順」にある作業がすべて済んでいる

## 付録: 見た目と挙動の仕様(レビュー用)
正となるのは上のソースコードです。以下は、実装後に見た目と挙動がギャラリーと一致しているかを確認するための仕様です。ソースを使えない事情がある場合は、この仕様を満たすように同じコンポーネントを実装してください。
### 前提
- Next.js(App Router)、React 19、TypeScript、CSS Modules。追加の npm 依存なし(純粋な CSS アニメーション)
- ファイル: `components/aurora-background/AuroraBackground.tsx`(`"use client"`)+ `AuroraBackground.module.css`
- Props: `colors?: string[]`(3〜5 色、既定 `["#6d4aff", "#1fb6ff", "#ff4d9d", "#2ee6a8"]`)、`speed?: number`(倍率、既定 1、0 で静止)、`blur?: number`(px、既定 80)、`intensity?: number`(0〜1、既定 0.8)、`grain?: boolean`(既定 true)、`grainOpacity?: number`(既定 0.12)、`vignette?: boolean`(既定 true)、`className`、`children`、`as?: "div" | "section"`
- 既定値は CSS 変数(`--ab-c0〜c4`、`--ab-speed`、`--ab-blur`、`--ab-intensity`、`--ab-grain-opacity`、`--ab-base`)としてルートクラスに持ち、既定と異なる値だけ inline style で上書きする

### 見た目
層構成は「下地 → ブロブ層 → グレイン → 中身」。

- **ルート(.root)**: `position: relative; width: 100%; height: 100%; overflow: hidden; isolation: isolate`、背景 `var(--ab-base)`(`#08080c`)、文字色 白
- **ブロブ層(.aurora)**: `position: absolute; inset: 0; container-type: size`、背景 `var(--ab-base)`、`opacity: var(--ab-intensity)`、`filter: blur(var(--ab-blur))`、`transform: scale(1.15)`(ぼかしで端が痩せないように)、`aria-hidden`
  - `vignette` 時は `mask-image: radial-gradient(ellipse 70% 72% at 50% 38%, #000 30%, rgba(0,0,0,.55) 62%, transparent 100%)` — 見出しの背後が最も明るく、端と下側がページへ溶ける
- **ブロブ(.blob、色ごとに 1 つ)**: `border-radius: 50%`、`background: radial-gradient(closest-side, 色, transparent)`、`mix-blend-mode: screen`。サイズはコンテナの長辺基準(`cqmax`)で、横長でも縦長でも同じ構図になる
  1. 左上: `left -12% / top -22%`、72×56cqmax、23s
  2. 右: `left 52% / top 0`、62×62cqmax、29s
  3. 下: `left 12% / top 52%`、76×46cqmax、37s
  4. 上中央: `left 34% / top -18%`、46×40cqmax、31s(逆方向)
  5. 左下: `left -18% / top 44%`、52×52cqmax、41s(逆方向)
  - 3 色でも画面が埋まるよう、先頭 3 つで四隅をカバーする順序にする
- **グレイン(.grain)**: inline SVG(`feTurbulence type="fractalNoise" baseFrequency=".85" numOctaves="3"` + `feColorMatrix type="saturate" values="0"`)を 180px でタイル、`mix-blend-mode: overlay`、`opacity: var(--ab-grain-opacity)`。静止
- **中身(.content)**: `position: relative; z-index: 2; height: 100%`

### モーション
- 各ブロブは `transform`(translate3d / rotate / scale)だけのキーフレーム 3 種を使い分け、`ease-in-out`・`infinite`・`alternate`
  - 例: `0% → translate(0,0) rotate(0) scale(1)`、`50% → translate(22%,14%) rotate(35deg) scale(1.12)`、`100% → translate(-8%,26%) rotate(-15deg) scale(.94)`(% はブロブ自身のサイズ基準)
- 周期は互いに素に近い値(23 / 29 / 31 / 37 / 41s)にして、構図が繰り返して見えないようにする。`animation-duration: calc(周期 / var(--ab-speed))`
- 負の `animation-delay` で各ブロブを軌道の途中から開始し、初回描画から有機的な構図にする
- `speed = 0` と画面外(IntersectionObserver → `data-offscreen`)では `animation-play-state: paused`(フレーム 0 に戻さず、その場で止める)
- `@media (prefers-reduced-motion: reduce)` でも paused。負の delay のおかげで静止状態も途中の自然な構図になる

### アクセシビリティ
- 装飾レイヤー(ブロブ層・グレイン)はすべて `aria-hidden="true"`、`pointer-events: none`
- `as="section"` で使う場合は、中に見出しを置くか `aria-labelledby` を付ける
- 中身の文字のコントラストを確保する(本文は白 .65 以上を目安に)。動きは常にゆっくりで、点滅しない

### 受け入れ条件
- 親を全面で埋め、4 色のブロブが重なり合って溶けたメッシュグラデーションになる。輪郭や継ぎ目が見えない
- ゆっくり漂い、ループの継ぎ目で跳ばない。`speed` で速さが変わり、0 で静止する
- `vignette` で端と下側がページ背景へ自然にフェードする。`grain` でごく薄いノイズが乗り、バンディングが目立たない
- OS の「視差効果を減らす」で静止しても、構図は魅力的なまま
- スクロールで画面外に出るとアニメーションが止まる
インストール
npx shadcn@latest add https://anima-js.vercel.app/r/aurora-background.json
生成コード
import { AuroraBackground } from "@/components/aurora-background";

// TODO: ヒーローの中身(見出し・ボタンなど)を children として渡してください: <AuroraBackground …>…</AuroraBackground>。親要素に高さ(例: min-height: 100svh)を与えると、その全面を埋めます
<AuroraBackground
  blur={80}
  intensity={0.8}
  vignette={true}
  grain={true}
  grainOpacity={0.12}
  speed={1}
  colors={["#6d4aff", "#1fb6ff", "#ff4d9d", "#2ee6a8"]}
/>