レシピ · 6手 · 1プロンプト実装

モーショングラフィックス層を追加する

既存イベント app に @event/motion を配線し、<Reveal> 入場・<CountUp> 数値・[data-scroll-fade](ネイティブ scroll-driven)・kinetic-title を reduced-motion 全停止つきで使える状態にする

前提: new-event解説 ch.47module: モーショングラフィックス

このレシピは @event/motion(実装済み・zero-dep / React のみ peer)をイベント app に配線する完全手順。 対象 app を apps/<app> とする(starter には deps + transpilePackages が既に入っている)。上から順に実行すれば完了する。

この機能は feature flag 不要(全ページ共通の基盤層。event.config.ts の features には何も足さない。 nav も route も増えず、layout に <MotionStyles /> を 1 回置くだけで各所の JSX から使えるようになる)。

仕組みの要点(設計判断の詳細は第47章):

[data-scroll-fade] 属性を置くだけで JS メインスレッドを使わずに動く。 未対応環境(Firefox 既定)は @supports静的表示に自然降格(分岐コードを書かない)

実 export(packages/motion/src/index.ts): MOTION_CSS / MotionStyles / Reveal(+ RevealProps)/ CountUp(+ CountUpProps)。

1. 依存を追加

apps/<app>/package.json の dependencies に追加:

"@event/motion": "*"

apps/<app>/next.config.tstranspilePackages 配列に "@event/motion" を足す。

monorepo root で:

yarn install

2. layout に <MotionStyles /> を1回

apps/<app>/src/app/layout.tsx<head> に置く(server component で描画可 — MOTION_CSS 文字列を <style> に注入するだけの純粋描画。add-design の tokensToCssVars 注入と同居できる):

import { MotionStyles } from "@event/motion";

// <head> 内に1行:
<head>
  <MotionStyles />
</head>

これでトークン(--ease-out / --ease-spring / --dur-1..3)・[data-reveal] / [data-scroll-fade] / .kinetic-title のスタイル・reduced-motion kill-switch が全ページに入る。

3. <Reveal> — 入場アニメと stagger

Reveal"use client" 済みなので Server Component の page からそのまま使える。 ビューポート入場(IntersectionObserver・threshold 0.12)で .is-in が付き、 opacity 0 → 1 + translateY(14px) → 0var(--dur-2)(480ms)var(--ease-out) で走る:

import { Reveal } from "@event/motion";

// 単発(既定 dir="up")
<Reveal>
  <h1>イベント名</h1>
</Reveal>

// 方向と遅延
<Reveal dir="left" delay={120}>
  <p>横から入る要素</p>
</Reveal>

// stagger — 親側で index * 60ms を渡すだけ
{items.map((item, i) => (
  <Reveal key={item.id} delay={i * 60}>
    <article>{item.title}</article>
  </Reveal>
))}

props(実装 packages/motion/src/Reveal.tsx の転写):

prop型 / 既定意味
dir`"up" \"left" \"right" \"scale"(既定 "up"`)入場方向(left/right は ±18px、scale は 0.96 から)
delaynumber(ms・既定 0)--reveal-delay に写像(transition-delay)。stagger は親が index で計算
onceboolean(既定 true一度入場したら以後監視しない。false で出入りのたびにトグル
classNamestring?ラッパー div に付与

IntersectionObserver が無い環境では即 .is-in(常に見える)。CSS 未注入(no-JS で <MotionStyles /> も無い等) でも [data-reveal] スタイルが無ければ素で見える — fail-visible の設計。

4. <CountUp> — 数値カウントアップ

ビューポート入場(threshold 0.4)で 0 → to へ easeOutCubic のカウントアップ。 SSR は最終値を出す(no-JS でも数字が見える)。reduced-motion では即座に最終値:

import { CountUp } from "@event/motion";

<CountUp to={46} />
<CountUp to={1331} duration={1200} suffix=" 票" />
<CountUp to={20} suffix=" パッケージ" />

> **RSC 注意**: `format` は関数 prop のため **Server Component から渡すと実行時エラー**
> (Functions cannot be passed directly to Client Components)。Server Component の page では
> 直列化可能な `prefix` / `suffix` を使い、`format` は client component 内でのみ使う。

props(実装 packages/motion/src/CountUp.tsx の転写): to: number(最終値)/ format?: (n: number) => string(表示整形・既定そのまま)/ duration?: number(ms・既定 900)/ className?

5. [data-scroll-fade] — ネイティブ scroll-driven

component 不要・JS 不要。属性を置くだけ:

<section data-scroll-fade>
  <h2>スクロールで浮かび上がるセクション</h2>
</section>

MOTION_CSS 側の実装(転写):

@supports (animation-timeline: view()) {
  @keyframes motionViewFade {
    from { opacity: 0; transform: translateY(22px); }
    to { opacity: 1; transform: none; }
  }
  [data-scroll-fade] {
    animation: motionViewFade linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 68%;
  }
}

(linear なのは進行がスクロール位置そのものだから — easing はスクロール操作が担う)

「JS で IntersectionObserver に降格」のような分岐は書かない(それは <Reveal> の役割)

6. kinetic-title — ロード時のクリップ立ち上がり

見出し用。overflow: hidden の行から文字ブロックがせり上がる。--k-i で 70ms ずつの stagger:

<h1 className="kinetic-title">
  <span>EVENT 2026</span>
</h1>

// 複数語 / 複数行は --k-i を増やす(span ごとに 70ms 遅延)
<h1 className="kinetic-title">
  <span style={{ "--k-i": 0 } as React.CSSProperties}>統合</span>{" "}
  <span style={{ "--k-i": 1 } as React.CSSProperties}>イベント</span>{" "}
  <span style={{ "--k-i": 2 } as React.CSSProperties}>基盤</span>
</h1>

.kinetic-title > spanmotionRiseIn(0.6em 下から・0.7s・--ease-out)で立ち上がる。 Reveal と違いロード時に即走る — ファーストビューの見出し向け。スクロール到達で出すなら Reveal を使う。)

補助 keyframes として motionPulseX(左→右へ流れて消える)/ motionPopIn(scale 0.6 → 1)も MOTION_CSS に入っており、図解の「流れ」表現などに animation: motionPulseX 2s var(--ease-out) infinite の形で使える。

7. 検証

yarn workspace <app> typecheck   # 型が通ること
yarn workspace <app> dev
  1. トップページで <Reveal> 要素が下から 14px 立ち上がる。list の stagger が delay={i * 60} の順に走る
  2. <CountUp to={46} /> が視界に入った時点で 0 → 46 へ約 0.9s でカウントアップ(初速が速い easeOutCubic)
  3. Chrome / Edge / Safari 26: [data-scroll-fade] セクションがスクロール進行に同期してフェードイン。

スクロールを戻すと逆再生される(scroll-driven はスクロール位置への写像)。 Firefox(既定): 同じセクションが最初から静的に見えている(欠落ではなく降格)

  1. OS の「視覚効果を減らす」を ON(Windows: 設定 > アクセシビリティ > 視覚効果 > アニメーション効果 OFF)

→ リロードで 全モーションが停止: Reveal は即表示・CountUp は即最終値・scroll-fade / kinetic-title も静止

完了条件: typecheck が通り、dev で入場アニメとカウントアップが動き、reduced-motion で全停止する。