expo-animation について
expo-animationは、ExpoでReact Nativeアニメーションを実装するための構築スキルです。モバイル開発者が動きの必要性を判断し、UIランタイムで応答性を保つ方法を選べるよう、視差効果の軽減と実機での検証も扱います。
主な機能
- 01アニメーションの必要性と目的の判断
- 02UIランタイムでのReanimated worklets
- 03ジェスチャーに対応するアニメーション手法
- 04時間、スプリング、プロパティの指針
- 05視差効果の軽減とデバイステスト
活用シーン
- 01タップやジェスチャーのフィードバック実装
- 02シート、モーダル、画面遷移の構築
- 03低速デバイスでのアニメーションの遅延回避
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ expo/ skills --skill expo-animationインストール後、Claude Code で/expo-animationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSERECIPES.mdagents/openai.yaml詳細
Expo でアニメーションを構築する
このスキルは Emil Kowalski と共同で作成され、ほかの便利なアニメーションスキルとともに、emilkowalski/skills リポジトリでも公開されています。
React Native 向けの構築スキルです。モーションの要望を、シミュレーターでも開発モードの最新機種でもなく、実機での厳しいレビューに耐えられる実装へと変換します。
モバイルではアニメーションに関する要素が3つ変わり、このスキルの内容はすべてそれに基づいています。
- ホバーはありません。 Web でホバーに割り当てられているあらゆる操作上の手がかりは、押下、位置、または何もない状態で表現する必要があります。
- ランタイムは2つあります。 Worklet(Reanimated 4)では、この違いが明確になります。React Native ランタイムでは React がレンダリングを行い、アプリのロジックが実行されます。一方、UI ランタイムでは Worklet が毎フレーム実行されます(バックグラウンド処理用に、オプションでワーカーランタイムを追加することもできます)。RN ランタイムに触れるアニメーションは、アプリがほかの処理を行った途端にカクつきます。モーションを UI ランタイム上で動かし続けることが、技術の要です。
- ユーザーの指が要素に触れています。 ジェスチャーが主要な入力方法なので、途中で中断できることや速度を引き継ぐことは、仕上げではなく基本要件です。
開発姿勢
あなたはアニメーションを自ら構築する上級モバイルエンジニアです。判断を下し、その理由を一行で述べ、コードを書いてください。モーションの選択肢をメニューのように提示してはいけません。
失敗のパターンは2つあります。より深刻なのは1つ目です。
- アニメーションさせるべきでないものを動かすこと。 以下の判定基準は、コードが一行も不要な場合があることを前提としています。
- 適切な対象を誤ったスレッドで動かすこと — フレームごとの
setState、PanResponder、アニメーションするheightなどです。開発中に自分のスマートフォンで見ると問題なくても、3年前の Android では20fpsまで低下します。
厳守事項
- 順番どおりに進めてください。 ステップ1と2が、以降すべての前提です。
- コアの
Animatedではなく Reanimated を使ってください。 コアのAnimatedはブリッジをまたがずにジェスチャーで動かすことができず、useNativeDriverが扱えるのも transform と opacity だけです。Reanimated の Worklet は UI スレッドで実行され、JS が処理中でも動き続けます。 - 値を概算しないでください。 カーブとスプリング設定には、以下の表の値を使用してください。
- アニメーションの提供時点で、モーション軽減にも対応してください。 後回しにしてはいけません。
- 実際の使用感は、サポート対象の中で最も遅いデバイスのリリースビルドで評価してください。 それ以外は検証済みとはみなせません。
構築手順
1. そもそもアニメーションさせるべきか?
| 頻度 | 判断 |
|---|---|
| 1日に100回以上 — タブ切り替え、キーボードの開閉、スクロール、設定の切り替え | アニメーションなし。 プラットフォームのデフォルト、または何もしません。ここで終了します。 |
| 1日に数十回 — 押下フィードバック、リストの移動、行の選択 | ほとんど気づかれない程度に限ります。150ms未満、または何もしません |
| たまに — シート、モーダル、トースト、オンボーディングのステップ | 標準的なアニメーション |
| まれ/初回のみ — 成功状態、空状態のイラスト、祝福の演出 | 喜びを生み出す演出の予算はここで使います |
タブの切り替えでスライドさせてはいけません。 タブは同列の項目であり、階層関係はありません。スライドは実際にはない奥行きを示唆し、ユーザーは1回のセッションで何十回もその負担を負うことになります。animation: 'none'。
この基準を満たさない要望には、その旨を伝えてコードを書かないでください。
2. 目的は何か?
先に一語で目的を名付けてから続けてください。フィードバック、空間的一貫性、状態の表示、唐突な変化の防止、説明、または喜びの演出(まれなケースのみ)です。
目的を名付けられない場合は、構築しないでください。
3. ツールを選ぶ — 動作する中で最も低コストなもの
順にたどり、最初に当てはまったものを使います。
| 要件 | ツール |
|---|---|
| ジェスチャーを伴わない状態駆動の変化 — 押下、トグル、色、値の切り替え | Reanimated CSS transition(スタイル内の transitionProperty) |
| ループ、多段階、または状態変化なしでマウント時に再生 | Reanimated CSS animation(animationName キーフレーム) |
| 要素のマウント・アンマウント、またはリストの再配置 | Layout animations(entering / exiting / itemLayoutAnimation) |
| 指が触れるものすべて、またはスクロールから導出されるものすべて | useSharedValue + Gesture + useAnimatedStyle |
| 画面から画面へ | Expo Router のネイティブスタックオプション。 これを自作しないでください |
| それ自体が 1 つの画面であるボトムシート | presentation: 'formSheet' — 本物の UISheetPresentationController であり、無料で正確です |
| タブバー | NativeTabs(expo-router/unstable-native-tabs から)— プラットフォームの本物のタブバーで、その挙動とトランジションも含まれます |
| コンテキストメニュー、長押しプレビュー | Link.Menu / Link.Preview(Expo Router、iOS 専用)— ネイティブのメニューとピークであり、JS で作り直さないでください |
| ラージタイトルに折りたたまれるヘッダー | ネイティブスタックの headerLargeTitleEnabled(iOS 専用、headerLargeTitle は非推奨)— スクロール worklet ではありません |
| 引っ張って更新 | RefreshControl — シグネチャーとなるインタラクションの場合のみ自作します(しきい値のレシピを参照) |
| キーボードを追従する UI | react-native-keyboard-controller — キーボードの実際の位置を、フレームごとに、UI スレッド上で |
| ベクターイラスト、お祝い、空状態 | Lottie — イラスト専用で、UI の状態には決して使わないでください |
| 巨大なアニメーションシーン、自由な描画 | @shopify/react-native-skia — ビュー階層自体がボトルネックになる場合のためのキャンバス |
共有値に手を伸ばすのは、その値が連続的または中断可能な場合だけにしてください。押下時のスケールは CSS transition であり、ドラッグは共有値です。2 状態のトグルに worklet を使うのは、フェードのためにモーションライブラリをインストールするモバイル版に相当します。
依存関係。 npx expo install <package> でインストールしてください — プロジェクトの SDK に一致するバージョンを解決しますが、素の npm install ではそれができません。
| 要件 | パッケージ |
|---|---|
| アニメーション | react-native-reanimated + react-native-worklets |
| ジェスチャー | react-native-gesture-handler |
| ナビゲーション、シート、ネイティブタブ、メニュー | expo-router |
| ハプティクス | expo-haptics |
| キーボード追従 UI | react-native-keyboard-controller(ルートに KeyboardProvider が必要 — キーボードのレシピを参照) |
| イラスト、お祝い | lottie-react-native |
| 非常に大きなアニメーションシーン、カスタム描画 | @shopify/react-native-skia |
4. プロパティを選ぶ
transformとopacityは無料です。 それ以外はすべてレイアウトパスです。width、height、margin、padding、flex、top、left、gapは、そのノードとその兄弟に対して毎フレーム Yoga を再実行します。- 唯一の例外: 子を持たない絶対配置の要素 — タブのピル、プログレスバーのフィル。これはフローから外れているため他は再レイアウトされず、
widthをアニメーションさせればscaleXが歪めてしまう角丸を保てます。 scale(0)は決して使わないでください。scale(0.9〜0.97)+opacity: 0から始めます。現実世界で何かが無から現れることはありません。transformは配列であり、順序が重要です —[{ translateY }, { scale }]は移動後にスケールします。逆にすると、translate もスケールされます。乗算を望まない限り translate を先にしてください。- Android の影は
elevationであり、elevation をアニメーションさせると毎フレーム影が再レンダリングされます。 代わりに、あらかじめ影を付けたレイヤーの opacity をアニメーションさせてください。 BlurViewの intensity は決してアニメーションさせないでください。 Android では毎フレームブラーが再レンダリングされます。代わりに、静的なBlurViewの opacity をクロスフェードさせてください。translateではパーセンテージが使えます そして要素自身のサイズに対する相対値です —translateY('100%')は、内容にかかわらずシートを自身の高さだけ移動させます。
5. タイミングかスプリングか
指を使った場合は、スプリングを使います。 スプリングは中断時にも速度を引き継ぎますが、タイミングカーブは最初からやり直します。それ以外はすべてタイミングを使います。
Reanimated のスプリングには、Apple のデザイナー向けパラメーターをそのまま指定します。mass/stiffness/damping ではなく、次の形式を使ってください。
| インタラクション | 設定 |
|---|---|
| デフォルトの収束、オーバーシュートなし | { duration: 400, dampingRatio: 1 } |
| ドラッグ後の位置変更/スナップバック | { duration: 400, dampingRatio: 0.8, velocity } |
| シート、ドロワー | { duration: 300, dampingRatio: 0.8, velocity } |
| 硬い境界を越えてはならない場合 | overshootClamping: true を追加 |
ジェスチャーに勢いがあった場合に限り、バウンスさせます。 フェードインしたメニューがオーバーシュートすると違和感がありますが、フリックしたカードなら自然です。
イージングは、指を使わない動きすべてに使います。
| 状況 | イージング |
|---|---|
| 入場または退場 | ease-out |
| 画面上での移動/変形 | ease-in-out |
| 一定の動き(進行状況、マーキー) | linear |
| デフォルト | ease-out |
UI では ease-in を決して使わないでください。 動き出しが遅く、ユーザーが注目している瞬間を遅らせます。Reanimated の組み込みイージングは CSS と同じく弱いため、次を使ってください。
import { cubicBezier, Easing } from 'react-native-reanimated';
// transitionTimingFunction / animationTimingFunction
const CSS_EASE_OUT = cubicBezier(0.23, 1, 0.32, 1);
// withTiming / .easing(...)
const EASE_OUT = Easing.bezier(0.23, 1, 0.32, 1); // strong ease-out for UI
const EASE_IN_OUT = Easing.bezier(0.77, 0, 0.175, 1); // on-screen movement
const EASE_SHEET = Easing.bezier(0.32, 0.72, 0, 1); // iOS sheet curve
Reanimated 4.1.1 と 4.5.1 では、生の 'cubic-bezier(...)' 文字列は受け付けられません。CSS のトランジションとアニメーションでは cubicBezier(...) を、withTiming と .easing(...) では Easing.bezier(...) を使います。
時間:
| 要素 | 時間 |
|---|---|
| 押下時のフィードバック | 100–150ms |
| トグル、チップ、小さな状態変化 | 150–200ms |
| シート、モーダル、ドロワー | スプリング、体感で約300ms |
| 画面遷移 | プラットフォームのデフォルト。上書きしないでください |
モバイル UI のアニメーションは、ウェブと同様に300ms未満にします。プラットフォーム独自のトランジションはより長く(iOS のプッシュ遷移は350ms)、ナビゲーションではプラットフォームに合わせ、それ以外では常にそれを上回る速さにします。
6. JS スレッドから切り離す
これはモバイル特有の技であり、React Native のモーションが最もよく機能しなくなる箇所です。
- ジェスチャーハンドラーやスクロールハンドラーから
setStateを決して呼び出さないでください。 フレームごとに React のレンダーが1回発生することが、RN アプリでカクつきが起きる最大の原因です。共有値からuseAnimatedStyleを使えば、React の再レンダーは一切発生しません。 onUpdateやスクロールハンドラー内で RN ランタイムへの呼び出しを予約しないでください。react-native-workletsのscheduleOnRN(fn, ...args)は、非推奨のrunOnJS(fn)(...args)に代わる Reanimated 4 の機能です。RN ランタイムへの呼び出しをキューに追加しますが、onUpdate内では毎秒60~120回も呼び出されます。これはonEnd内、または値がしきい値を超えたときに実行されるuseAnimatedReaction内で使ってください。- レンダー中に共有値を読み取らないでください(JSX 内の
translateY.get()など)。これは更新されないスナップショットであり、気づかないうちに同期がずれます。レンダー中に書き込むことも決してしないでください。 再調整の途中で実行され、意図しない再レンダーによって書き込みが再実行されます。共有値に触れるのは、ワークレット、ハンドラー、エフェクトの中だけにしてください。 .valueではなく.get()/.set()を使ってください。 API は同じですが、.valueに直接アクセスする形式は React Compiler が解析できません。Reanimated のドキュメントではget/setがコンパイラーセーフな方法とされています。また、setでは関数による更新も使えます。sv.set((v) => v + 1)。- ワークレットから呼び出す関数では、先頭行に
'worklet'が必要です。 ない場合、デバッガーでは正常に動作しても、デバイス上では実行時にエラーが発生します。
7. ホバーではなく、押下
Web のホバー操作はすべて、移植するのではなく再設計する必要があります。
- 押し始めでフィードバックし、押し終わりで確定します。 タップが完了するまで何も表示されないと、反応がないように感じます。これが、ユーザーが実際に感じる遅延です。
- ボタンのように押せる要素には、
Pressableと CSS トランジションを使って、**100~150msでscale: 0.97**にします。scaleはラベルやアイコンも一緒に動かすため、物理的な感触が生まれます。全幅のリスト行は例外で、背景色をハイライトします。行全体を拡大縮小すると、画面全体が縮むように感じられます。 - 最小タッチ領域は44×44pt(Androidでは48dp)にします。見た目がそれより小さい場合は、見た目を大きくせず、
hitSlopを追加します。 - **
pressRetentionOffset**を設定し、指が数ピクセルずれても、意図した押下がキャンセルされないようにします。 - Android のリップルは、Material スタイルのアプリでのみ使います。 独自デザインのアプリでは、一方のプラットフォームだけにリップルを使うより、両方のプラットフォームで同じ拡大縮小を使うほうが一貫性があります。
8. ハプティクス
モバイルには、Webにはない感覚があります。控えめに使えばアプリに高級感を与えますが、どこでも使うとユーザーはオフにしてしまいます。
| 場面 | 呼び出し |
|---|---|
| 値が刻み幅を通過する — ピッカー、スライダーのデテント、セグメントコントロール | Haptics.selectionAsync() |
| 何かが元の位置にスナップする、シートのデテントに引っかかる、ドラッグが確定する | Haptics.impactAsync(ImpactFeedbackStyle.Light) |
| 重いオブジェクトが着地する、破壊的な操作が実行される | Haptics.impactAsync(ImpactFeedbackStyle.Medium) |
| 操作が成功または失敗する | Haptics.notificationAsync(NotificationFeedbackType.Success / Error) |
守るべきルールは3つで、例外はありません。
- 視覚的な変化と同じフレームで発生させます。 アニメーションより遅れるハプティクスは、フィードバックではなく不具合のように感じられます。アニメーションが終わったときではなく、デテントに引っかかる瞬間など、原因となるタイミングで発生させます。
- ユーザー操作1回につき1回です。 スクロール中やフレームごとには発生させず、ユーザーが起こしていない開始アニメーションでも発生させません。
- ハプティクスだけに頼ってはいけません。 多くのユーザーはシステム全体でハプティクスをオフにしており、Android の多くのデバイスでは音も鳴りません。視覚的な表示だけでも意味が伝わる必要があります。
worklet からハプティクスを呼び出す場合は、RN ランタイムにスケジュールする必要があります:scheduleOnRN(Haptics.selectionAsync)。
9. モーション軽減とアクセシビリティ
import { useReducedMotion, ReduceMotion, withSpring } from 'react-native-reanimated';
const reduced = useReducedMotion();
const y = useSharedValue(reduced ? 0 : SHEET_HEIGHT);
// or let each animation decide
withSpring(0, { duration: 300, dampingRatio: 0.8, reduceMotion: ReduceMotion.System });
モーション軽減は、ゼロにするのではなく、より少なく、より穏やかにすることです。状態変化を説明する不透明度や色の変化は残し、移動、拡大縮小、視差、オーバーシュートは省きます。画面遷移はanimation: 'fade'にします。
テキストは拡大されます。 allowFontScalingはデフォルトで有効なため、デフォルトの文字サイズで測定した高さは、200%では正しくありません。ハードコードした高さにアニメーションさせてはいけません。onLayoutで測定するか、代わりにトランスフォームをアニメーションさせます。
モーションを気づかないうちに壊す設定
「アニメーションがまったく動かない」場合は、まず次の項目を確認してください。
- SDK とバージョンを一致させるため、Expo 経由でインストールします:
npx expo install react-native-reanimated react-native-worklets。Expo プロジェクトでは、babel-preset-expoが worklets Babel プラグインを自動的に設定するため、babel.config.jsでの手順は不要です。このプラグインを手動で追加するのは、そのプリセットを使わない bare RN プロジェクトのみで、その場合はリストの最後に配置する必要があります。プラグインがない、または配置が誤っている場合、現在はサイレントにフォールバックせず、実行時にFailed to create a workletがスローされます。 - アプリを
GestureHandlerRootViewで囲む必要があります。囲まれていない場合、エラーなしでジェスチャーが何も機能しません。 - Reanimated 4 には New Architecture が必要です。
- Expo Go はパフォーマンスを評価する環境ではありません。 リリースビルドで操作感を評価してください。開発ビルドでは JS スレッドが遅く、確認したい問題が隠れてしまいます。
120fps
ProMotion 対応の iPhone では、CADisableMinimumFrameDurationOnPhoneを設定しない限り、サードパーティ製のアニメーションは60fpsに制限されます。最近の Expo SDK ではデフォルトで設定されています。設定されていることを確認し、ない場合は追加してください。
{ "expo": { "ios": { "infoPlist": { "CADisableMinimumFrameDurationOnPhone": true } } } }
これにより、フレームの予算は16msではなく8msになります。このため、WebよりもモバイルでUIスレッド上のアニメーションが重要になります。
レシピ
すぐに実装できるパターン(押下フィードバック、ドラッグで閉じるシート、スワイプで削除、折りたたみヘッダー、リストの表示アニメーション、キーボードに同期する UI、タブインジケーター、画面遷移)については、RECIPES.md を参照してください。リクエストがいずれかに該当する場合は、該当するレシピを読み込み、白紙のファイルからではなくレシピをベースにしてください。
リリース禁止
| 禁止事項 | 代わりに使用 |
|---|---|
PanResponder | gesture-handler の Gesture.Pan() |
ジェスチャーやスクロールのハンドラー内での setState | shared value + useAnimatedStyle |
runOnJS(Reanimated 4 では非推奨) | react-native-worklets の scheduleOnRN |
フレームごとの scheduleOnRN | onEnd、またはしきい値での useAnimatedReaction |
| レンダリング中の shared value の読み取りや書き込み | worklet、ハンドラー、エフェクト内での .get() / .set() |
指で操作する要素に対する Core Animated | Reanimated |
height / width / margin / flex / top のアニメーション | transform + opacity(絶対配置で子要素のない要素は除く) |
BlurView の強度や Android の elevation のアニメーション | 静的なレイヤーをクロスフェード |
仮想化リストの行への entering | コンテナーをアニメーションするか、itemLayoutAnimation を使用 |
| JS で再構築した画面遷移 | ネイティブスタックの animation |
| タブ間のスライド | animation: 'none' |
UI 要素での Easing.in(...) | Easing.bezier(0.23, 1, 0.32, 1) |
Reanimated の CSS スタイル内での 'cubic-bezier(...)' | react-native-reanimated の cubicBezier(...) |
scale(0) による表示アニメーション | scale(0.95) + opacity: 0 |
| 距離だけで決まる閉じるしきい値 | 速度 または 距離 — フリックだけで十分です |
| 境界での急停止 | ラバーバンド抵抗 |
| フレームごとのハプティクス、またはハプティクスだけのフィードバック | コミットごとに 1 回、必ず視覚的なフィードバックと併用 |
| Expo Go やシミュレーターでの操作感の評価 | リリースビルド、サポート対象の中で最も遅いデバイス |
出力
コードを書いてください。その後、数行以内で次の内容を示してください。
- ゲートの判定 — 頻度の分類と目的の名称。却下した内容とその理由も述べてください。
- 構成要素 — ツール、プロパティ、スプリングまたはカーブ + 時間、スレッド。
- デバイスで確認する操作感 — ジェスチャー、速度の受け渡し、ハプティクスのタイミングはコードだけでは判断できません。試す操作を挙げてください。フリックする、動作中に割り込む、反転させる、手元で最も遅い Android で実行する、などです。
成果物はコードです。報告書のように冗長にしないでください。
トーン
意見を明確にし、簡潔にしてください。正直な答えが「これはアニメーションさせるべきではありません」や「適切かどうかを判断するには実機が必要です」であれば、そのまま伝えてください。
フィードバックの送信
このスキルにエラー、誤解を招く情報、または古くなった情報があれば、Expo の改善に役立てるため報告してください。
npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-animation" "<actionable feedback>"
具体的で実行可能な報告内容がある場合にのみ送信してください。関連する背景情報をできるだけ含めてください。
エージェントが Expo のタスクを何度も失敗した場合、またはユーザーが介入する必要があった場合は、タスクの概要、期待される結果、問題点を簡潔にまとめ、--category evals と関連する Expo のパッケージまたはコマンドを --subject として報告してください。