コンテンツにスキップ

エージェント用スキル

ui-motion-principles は、Claude Code などのコーディングエージェントが twelve-principles を正しく使えるようにするための Agent Skill です。 「このボタンに気持ちいい押し心地を付けて」「保存したらお祝い演出を」と頼むだけで、エージェントが状況に合う原則と API を選び、既定のガードレールを守って実装し、最後にブラウザで動きを数値確認します。

ここで配布しているファイルは、ライブラリのリポジトリにある skills/ui-motion-principles/ をビルド時にそのまま読み込んだものです。ライブラリと常に同じ版になります。

エージェントは Skill を名前ではなく description で選びます。次の説明文に書いた言い回しや作業内容が依頼に含まれると読み込まれます。

SKILL.md › description
UI コンポーネントに「動き」を付けるときに使う Skill。Disney の 12 原則を実装した TypeScript ライブラリ
twelve-principles で、ボタンの押し心地・ホバー・入場/退場・トースト/モーダルの
出し入れ・リストの順次表示・エラーの shake・お祝い演出・派手なアニメーションを実装する。
「アニメーションを付けて」「動きをつけて」「モーション」「気持ちいい押し心地」「ぬるっと出して」「フェードイン」
「バウンス」「派手に」「お祝い演出」「12原則」「twelve-principles を使って」などで発動。
React / 素の DOM / Astro アイランドのどれでも使える。
  1. 上のボタンから ui-motion-principles.zip をダウンロードします。ターミナルからなら次の 1 行です。

    ターミナルウィンドウ
    curl -fsSLO https://twelve-principles.iru-yo.com/skill/ui-motion-principles.zip
  2. スキル置き場に展開します。プロジェクト単位なら .claude/skills/、全プロジェクト共通なら ~/.claude/skills/ です。

    ターミナルウィンドウ
    unzip ui-motion-principles.zip -d .claude/skills/
  3. エージェントを再起動(または新しいセッションを開始)すると、スキル一覧に ui-motion-principles が現れます。

  • ディレクトリui-motion-principles/
    • SKILL.md 手順・決定表・数値の目安・落とし穴
    • ディレクトリreferences/
      • api-cheatsheet.md 主要 API と既定値の早見表
    • ディレクトリtemplates/
      • MotionButton.tsx press + hover + クリックで jump
      • Toast.tsx Presence で出し入れするトースト
      • CascadeList.tsx 順次表示するリスト
      • Celebrate.tsx cartoon personality のお祝い演出
    • ディレクトリscripts/
      • sample-motion.js 再生中の transform/opacity を記録する検証スニペット

SKILL.md がエージェントに渡す本体で、次の流れを指示します。

  1. 依頼を UI の状況に分類し、決定表から API を選ぶ(レシピ → ビヘイビア → 原則関数の順に既存部品を探す)。
  2. アプリ全体の personality を 1 つ決める。派手な演出面だけ bouncy / cartoonguardrails: false)を使う。
  3. templates/ を雛形に実装し、必要なら anticipatefollowThroughexaggerate を重ねる。
  4. scripts/sample-motion.js でブラウザ上の動きを数値で確かめ、reduced motion と静止後のインラインスタイルも確認する。

テンプレートはリポジトリの型チェック(npm run typecheck)の対象なので、ライブラリの API 変更で壊れればビルド前に分かります。

ui-motion-principles/SKILL.md
---
name: ui-motion-principles
description: |
UI コンポーネントに「動き」を付けるときに使う Skill。Disney の 12 原則を実装した TypeScript ライブラリ
twelve-principles で、ボタンの押し心地・ホバー・入場/退場・トースト/モーダルの
出し入れ・リストの順次表示・エラーの shake・お祝い演出・派手なアニメーションを実装する。
「アニメーションを付けて」「動きをつけて」「モーション」「気持ちいい押し心地」「ぬるっと出して」「フェードイン」
「バウンス」「派手に」「お祝い演出」「12原則」「twelve-principles を使って」などで発動。
React / 素の DOM / Astro アイランドのどれでも使える。
---
# ui-motion-principles — 12 原則ライブラリで UI に動きを付ける
## いつ使うか
- Web UI(React / Vanilla TS / Astro island)に、トランジション・マイクロインタラクション・演出を付けるとき。
- 既存の CSS transition / framer-motion 的な実装を「原則に沿った」動きに置き換えるとき。
- 派手な演出(オンボーディング、成功時のお祝い、ゲーム的 UI、LP)を作るとき。
動きの品質レビュー(監査)が目的なら監査用のスキルを、実装は本スキルを使う。
## 前提: ライブラリの導入
- 解説サイト: https://twelve-principles.iru-yo.com (全 API・ライブデモ・原則ごとの解説)。API 全量は `/reference/api/`
- 導入は npm から:
```sh
npm i twelve-principles
```
- パッケージには本スキルも同梱される: `cp -r node_modules/twelve-principles/skills/ui-motion-principles .claude/skills/`
- ローカルのリポジトリを試すときは `npm pack` した tarball を入れる(ディレクトリ直指定の symlink だとライブラリ側の `node_modules/react` を掴み React が二重化する)。
- import: コア `twelve-principles`、React `twelve-principles/react``react >= 18` が peer)。ランタイム依存ゼロ、WAAPI + `commitStyles`(Chrome 84+ / Firefox 75+ / Safari 13.1+)。
## 手順
1. **状況を分類し、API を選ぶ**(下の決定表)。自前でキーフレームを書く前に、レシピ → ビヘイビア → 原則関数の順で既存部品を探す。
2. **personality を 1 つ決める**(原則 12: Appeal)。アプリ全体を `MotionProvider` で包む。
- 通常 UI: `natural`(既定)/ `snappy`(業務系)/ `calm`(落ち着き)/ `playful`(楽しい)— `guardrails: true`
- 派手な面: `bouncy` / `cartoon``guardrails: false`(押下の縮み最大 20%、大きな弾み)。
- ブランド独自: `definePersonality({ ...overrides }, base)`**モジュールスコープの定数**にする。
3. **実装する**(パターンは下記、雛形は `templates/`)。
4. **合成で質を上げる**: `anticipate`(溜め)→ `followThrough`(行き過ぎて戻る)→ `exaggerate`(振れ幅)を必要な分だけ重ねる。
5. **実機で検証する**(必須)。ブラウザでボタンを押し、再生中の `getComputedStyle(el).transform` を rAF でサンプリングして
「溜め → 本動作 → オーバーシュート → 静止」の数値を確認する。スクリプトは `scripts/sample-motion.js`
reduced motion(`reducedMotion="fade"`)と、静止後にインラインスタイルが消えることも確認する。
## 決定表
| 状況 | 使う API | 主な原則 |
| --- | --- | --- |
| ボタン押下の手応え | `<Motion press>` / `usePress` / `pressable` | 1, 5 |
| ホバーで浮く | `<Motion hover>` / `useHover` / `hoverable` | 11, 6 |
| カードが視線に傾く | `<Motion tilt>` / `useTilt` / `tiltable` | 11 |
| マウント時の入場 | `<Motion enter="rise">` / `useEnter` / `play(el, enter(kind))` | 6, 9 |
| トースト・ポップオーバー・モーダルの出し入れ | `<Presence show enter exit>` / `usePresence`(退場完了後にアンマウント) | 6, 2 |
| リスト/グリッドの順次表示 | `useCascade(input, { each, cap, drag, trigger })` / `overlap` | 5, 9 |
| 主役に注目させる(モーダル、選択カード) | `useStage(active)` / `stage(el)` | 3 |
| 入力エラー | `shake()` | 5 |
| 通知・注意喚起 | `jump()` / `heartbeat()` / `tada()` / `accompany(primary, "wiggle")` | 2, 8 |
| 成功・お祝い・オンボーディング | `bounceIn` / `fallIn` / `tada` / `rubberBand` / `flip` + `bouncy`/`cartoon` | 1, 5, 10 |
| A 点から B 点へ移動 | `arc(from, to, { bend, orient })` + `travelDuration(distance)` | 7, 9 |
| トグル状態の往復(開閉など) | `useMotion().animateTo(pose)` / `setLayer`(割り込みでも飛ばない) | 6 |
| 物理シミュレーション(落下・揺れ) | `straightAhead((t, ms) => pose, { duration })` | 4a |
| 独自のキーポーズ | `poseToPose([...frames], { duration })` | 4b |
コンテキストメニューは入場アニメーションを付けない(退場のみ)。進捗バー以外で `linear` を使わない。
## 実装パターン
```tsx
// アプリ全体
<MotionProvider personality="playful">{children}</MotionProvider>
// 宣言的に
<Motion as="button" press hover enter="pop" onClick={save}>保存</Motion>
<Presence show={open} enter="pop" exit="pop" role="status">保存しました</Presence>
// イベント駆動(personality はコールバックで受け取る)
const { ref, play } = useMotion<HTMLDivElement>();
<div ref={ref} onClick={() => play((p) => jump({ personality: p }))} />
// 複数フックの ref を束ねるときは必ず useMemo
const merged = useMemo(() => mergeRefs(ref, pressRef, hoverRef), [ref, pressRef, hoverRef]);
```
```ts
// Vanilla
const off = pressable(button, { personality: "snappy" }); // 解除関数を返す
play(card, followThrough(anticipate(enter("rise")), { bounce: 0.35 }));
```
## 数値の目安(既定値に組み込み済み)
- ユーザー起点の動きは 300ms 以内(`USER_INITIATED_MAX_MS`)。押下 100ms、ホバー 200ms、入場 200ms、退場 150ms。
- スタッガーは 1 項目 50ms 以内(`MAX_STAGGER_MS``cap` で変更可)。派手なカスケードは `cap: 120〜150`
- 押下の縮みは 2〜5%(guardrails 時)。squash/stretch は UI で ±5%、キャラクター的演出で 20% 以上。
- 入場 `easings.out`、退場 `easings.in`、画面内移動 `easings.inOut`、オーバーシュートは bezier ではなく `spring`
## 落とし穴 (Pitfalls)
- **ライブラリが所有するプロパティ**: アニメーション対象要素の `transform` `transform-origin` `opacity` `filter` `box-shadow`
同じ要素に CSS transition やレイアウト用 transform を併用しない。必要なら外側ラッパーでレイアウト、内側で動きを分ける。
`elevation` を含むモーション中は CSS の `box-shadow` が上書きされる。
- **1 要素 1 モーション**: 同じ要素で `play` すると前のモーションは置き換わる。同時に別の動き(例: 本体の jump と
アイコンの wiggle)は別要素に付ける。ホバー + 押下 + 傾きの共存は `setLayer`(ビヘイビアは自動で使う)。
- **`mergeRefs` を毎レンダー作らない**`useMemo`)。作るとアタッチ/デタッチが毎回走る。
- **`Presence` はラッパー要素(`as`、既定 `div`)を描画する**。レイアウトに影響する場合は `as` を合わせる。
- **`useCascade` は直下の子要素だけ**を動かす。再生し直すには `trigger` を変える。
- **`exit(kind)``enter(kind)` の開始姿勢へ戻る**`exit("slideLeft")` は右へ去る)。去る方向を指定したいときは
`play(el, { duration, frames: [{ x: 0, easing: easings.in }, { x: -24, opacity: 0 }] })` のように直接書く。
- **短い区間の `followThrough` は行き過ぎが小さい**。長距離移動で大きく弾ませたいなら `arc(..., { easing: spring({ bounce }) })` のように spring を easing に渡す。
- **SSR/Astro**: `useEnter``Presence` の初回入場はハイドレーション後に再生される。Astro では `client:visible` 島にする。
- **Expressive レシピは 300ms を超える**。ホバーや押下など日常のフィードバックには使わない。無限ループは画面外で止める
`el.getAnimations({ subtree: true }).forEach(a => a.pause())` を IntersectionObserver で)。
- **reduced motion**: 既定 `"auto"` は OS 設定を尊重し空間移動をフェードに置換する。`"full"` は本質的な動きに限る。
- **personality オブジェクトを JSX 内で生成しない**(毎レンダー別物になり、ビヘイビアが付け直される)。
## 運用方針
- 動きは「控えめな UI 既定値」を基本に、演出面(お祝い・オンボーディング等)だけ意図的に派手にする。
- 実装後は必ずブラウザで動作を数値確認してから報告する(ユニットテストだけで済ませない)。
## サポートファイル
- `references/api-cheatsheet.md` — 主要 API のシグネチャと既定値の早見表
- `templates/MotionButton.tsx` — press + hover + クリックで jump するボタン
- `templates/Toast.tsx` — Presence で出し入れするトースト(自動クローズ付き)
- `templates/CascadeList.tsx` — 順次表示するリスト(cap/drag 付き)
- `templates/Celebrate.tsx` — 成功時に cartoon personality で派手に祝うコンポーネント
- `scripts/sample-motion.js` — ブラウザで再生中の transform/opacity をサンプリングする検証スニペット