第12章 アニメーションをCSSライクなAPIで書く
transitionやkeyframesの感覚のまま、Reanimated 4のCSS互換APIでアプリに動きを付けられます。
ボタンを押した時にふわっと動く、画面遷移で要素がスライドする、ローディング中にスピナーが回る。アプリを「触っていて気持ちいい」と感じさせる要素の多くはアニメーションです。Webではtransitionと@keyframesを書けば済んでいた領域ですが、React Nativeでは別の道具立てが必要になります。
本章では、その定番ライブラリであるReact Native Reanimatedを取り上げます。Reanimated 4から導入されたCSS Animations / CSS Transitionsは、Webの感覚に極めて近い書き心地で、命令的なReanimatedの世界に踏み込む前の足がかりとして最適です。
本章の前半はこのCSSライクなAPIが主役で、Web経験者ならほぼそのまま書けます。後半の「発展: 命令的APIとジェスチャ」以降(命令的API、ジェスチャ連動、Layout Animations)は一段踏み込んだ内容なので、まず動かしたい段階では読み飛ばして、必要になったときに戻ってきても構いません。
なぜReanimatedなのか
React Native自体にもAnimated APIという標準のアニメーション機構があります。一方、現在のExpoテンプレートにはreact-native-reanimatedが最初から含まれており、コミュニティの大半のライブラリ(Gesture Handler、Expo Router、Skia連携など)もReanimatedを前提に作られています。新規プロジェクトであれば、迷わずReanimatedを選んで構いません。
Reanimatedの強みはざっくり次の3点です。
- UIスレッド上でアニメーションが走るので、JSスレッドが詰まっていてもカクつかない
- 宣言的なAPIで、Reactのレンダーモデルに自然に乗る
- CSS Animations / CSS Transitions互換のAPIを備えており、Webと同じメンタルモデルで書ける
最後の項目が本章の主役です。Reanimated 3まではShared ValueとuseAnimatedStyleで命令的に書くのが基本でしたが、Reanimated 4以降はCSSと同じ感覚で書ける道が用意されました。「とりあえず動かしたい」局面では、まずCSS APIで書いてみるのがおすすめです。
セットアップ
Expoプロジェクトに導入する手順です。
npx expo install react-native-reanimated react-native-worklets
Reanimated 4から、内部のWorklet実装がreact-native-workletsという別パッケージに切り出されました。両方を入れます。
現行のExpoテンプレートでは、babel-preset-expoがreact-native-workletsの存在を検出してBabelプラグインを自動で組み込むため、Expo管理下のプロジェクトではbabel.config.jsを自分で書き換える必要はありません。本書のサンプルアプリにもbabel.config.js自体が存在しないのはこのためです。
手動設定が要るのはReact Native Community CLI(bare)プロジェクトの場合だけです。該当する場合はbabel.config.jsに次のプラグインを追加してください(Reanimated 3でreact-native-reanimated/pluginを入れていた場合は、react-native-worklets/pluginに置き換えます)。
// Community CLIプロジェクトのみ、手動での追加が必要
module.exports = function (api) {
api.cache(true);
return {
presets: [...],
plugins: ["react-native-worklets/plugin"],
};
};
これでセットアップは完了です。Expo Goでも動きますが、ライブラリのバージョンによってはDevelopment Buildを作り直す必要があります。
最後にひとつだけ前提を確認しておきます。Reanimatedでアニメーションさせるには、Reanimatedが提供するコンポーネントを使う必要があります。
import Animated from "react-native-reanimated";
<Animated.View style={{ width: 100, height: 100, backgroundColor: "violet" }} />;
通常の<View>ではなく<Animated.View>、<Text>ではなく<Animated.Text>、といった具合です。サードパーティのコンポーネントはAnimated.createAnimatedComponent(...)で包めばアニメーション対象にできます。
CSS Transitions: 値の変化をなめらかにする
Webのtransitionプロパティと同じく、「スタイル値が変わった時にその差分を時間をかけて埋める」のがCSS Transitionsです。React側でstateを切り替えると、対応するスタイル値が変わり、Reanimatedがその間を補間してくれます。
最小の例
ボタンを押すたびに横幅が伸び縮みする例を見ます。
import { useState } from "react";
import { Pressable, Text } from "react-native";
import Animated from "react-native-reanimated";
export default function ResizableBox() {
const [expanded, setExpanded] = useState(false);
return (
<Pressable onPress={() => setExpanded((v) => !v)}>
<Animated.View
style={{
width: expanded ? 240 : 120,
height: 80,
backgroundColor: "#7c3aed",
borderRadius: 12,
transitionProperty: "width",
transitionDuration: 300,
}}
>
<Text style={{ color: "white", padding: 16 }}>tap me</Text>
</Animated.View>
</Pressable>
);
}
ポイントはtransitionPropertyとtransitionDurationの2つだけです。widthが変わるたびに、その差分を300msかけて埋めます。Web版CSSとほぼ同じ書き方で、JSのstateと連動します。
複数プロパティを同時に動かす
transitionPropertyは配列も受け取ります。色も一緒にアニメーションさせたい場合は、
transitionProperty: ["width", "backgroundColor"],
transitionDuration: 300,
のように書きます。プロパティごとに別の時間をかけたい場合は、transitionDurationも配列にして揃えます。
transitionProperty: ["width", "transform", "borderRadius"],
transitionDuration: [3000, 100, 500],
配列の順番がtransitionPropertyと対応します。widthは3秒、transformは0.1秒、borderRadiusは0.5秒。Webのtransition: width 3s, transform 0.1s, border-radius 0.5sと同じ意味です。
timing functionと遅延
イージングはtransitionTimingFunctionに渡します。
transitionProperty: "height",
transitionDuration: 300,
transitionTimingFunction: "ease-out",
CSS同様、linear / ease / ease-in / ease-out / ease-in-out / step-start / step-endのキーワードが使えます。さらに、react-native-reanimatedから関数をimportすれば、cubic-bezier()相当の細かい制御も可能です。
import { cubicBezier, linear, steps } from "react-native-reanimated";
transitionTimingFunction: cubicBezier(0.25, 0.1, 0.5, 2),
linear(0, [0.25, "75%"], 1)のようにポイントを並べてカスタムカーブを作ったり、steps(4, "jump-end")のように段階的なステップにしたりもできます。
開始までに待ち時間を入れるにはtransitionDelayを指定します。
transitionProperty: "borderRadius",
transitionDuration: 500,
transitionDelay: "300ms",
300(数値、ms扱い)でも"300ms"でも"0.3s"でも書けます。
transitionBehavior: 離散プロパティの扱い
CSSには「連続的に補間できないプロパティ」(display、alignItems、flexDirectionなど)があります。デフォルトのtransitionBehaviorはnormalで、これらは無視されます。allow-discreteを指定すると、補間できないものはアニメーションの中点でパッと切り替わるようになります。
transitionProperty: "alignItems",
transitionDuration: 500,
transitionBehavior: "allow-discrete",
ただし、見た目が「いきなり飛ぶ」ことに変わりはありません。離散プロパティを滑らかに見せたい場合は、後述するLayout Animationsを使うほうが筋が良いことが多いです。
CSS Animations: keyframesで動きを定義する
「stateの変化で発火する」のがTransitions、「マウントされている間に独立して走らせる」のがAnimationsです。Webの@keyframes + animation: ...に対応します。
keyframesの書き方
animationNameにkeyframesを渡し、animationDurationで長さを決めます。
function App() {
return (
<Animated.View
style={{
width: 80,
height: 80,
backgroundColor: "#fb7185",
animationName: {
"100%": {
transform: [{ translateX: 100 }],
},
},
animationDuration: "300ms",
}}
/>
);
}
100%がkeyframeの位置で、Webと同じくfrom / toという別名でも書けます。
animationName: {
from: { width: 120 },
to: { width: 240 },
},
animationDuration: "1s",
複数の中間点を置きたい場合はパーセンテージで並べます。
animationName: {
"0%": { transform: [{ translateY: 0 }] },
"50%": { transform: [{ translateY: -20 }] },
"100%": { transform: [{ translateY: 0 }] },
},
animationDuration: "600ms",
animationIterationCount: "infinite",
これでぴょこぴょこ跳ねるバウンスのループが書けます。
keyframesを定数として切り出す
毎回インラインで書くと読みにくくなるので、CSSAnimationKeyframes型で外に出すのが定石です。
import Animated, { type CSSAnimationKeyframes } from "react-native-reanimated";
const pulse: CSSAnimationKeyframes = {
from: {
transform: [{ scale: 0.8 }, { rotateZ: "-15deg" }],
},
to: {
transform: [{ scale: 1.2 }, { rotateZ: "15deg" }],
},
};
function HeartIcon() {
return (
<Animated.View
style={{
animationName: pulse,
animationDuration: "600ms",
animationIterationCount: "infinite",
animationDirection: "alternate",
}}
/>
);
}
animationDirection: "alternate"は、Webと同じく「往復」させるオプションです。pulseのfromとtoを行ったり来たりすることで、心臓の拍動のような表現になります。
アニメーションを並べる・重ねる
animationNameに配列を渡すと、複数のアニメーションを順番に流したり並列で重ねたりできます。
animationName: [fadeInOut, moveLeft, bounce],
animationDuration: ["2.5s", "5s", "1s"],
animationDurationも配列にすると、それぞれの長さを個別に指定できます。複雑なシーケンスを宣言的に組めるのが便利です。
主要なプロパティ早見表
CSS Animation系のプロパティはWebとほぼ1対1で対応しています。
| プロパティ | 役割 | デフォルト |
|---|---|---|
animationName |
keyframesオブジェクトまたは配列 | (必須) |
animationDuration |
1サイクルの長さ | 0 |
animationDelay |
開始までの待ち時間 | 0 |
animationTimingFunction |
イージング | ease |
animationDirection |
normal / reverse / alternate / alternate-reverse |
normal |
animationIterationCount |
繰り返し回数("infinite"または数値) |
1 |
animationFillMode |
終了後の状態の保持(forwards / backwardsなど) |
none |
animationPlayState |
running / paused |
running |
animationFillMode: "forwards"は「アニメーションが終わった後も最終状態を保つ」設定で、Webでよく使うのと同じです。animationPlayStateをstateで"paused"に切り替えれば、途中で止めることもできます。
Web版CSSとの差分
ほぼCSSと同じ感覚で書けるとはいえ、いくつかReact Native特有の癖があります。
ひとつめは、プロパティ名がキャメルケースである点です。background-colorはbackgroundColor、transition-propertyはtransitionPropertyになります。第7章のStyleSheet APIと同じ規則です。
ふたつめは、transformが配列である点です。translateXやscaleのような変換は、CSSのようにtransform: "translateX(100px) scale(1.2)"と書く代わりに、
transform: [{ translateX: 100 }, { scale: 1.2 }],
のようにオブジェクトの配列で渡します。keyframesの中でも同じ形です。
3つめは、Webプラットフォームでの一部の制約です。textShadowRadiusのようにモバイルでは単独で動くプロパティが、Web版ではkeyframesにshadow色を含めないと正しく動かない、といったケースがあります。プロダクションでReact Native for Web経由のアプリを動かす場合は、ドキュメントの「Supported Properties」ページを一度通読しておくと安心です。
4つめは、色や数値の単位です。色はRGBやHEXに加えてCSS色名も使えます。長さは原則としてdp(数値)で指定し、Webのようにpxやemは出てきません。時間はms / sどちらでも書けます。
発展: 命令的APIとジェスチャ
ここから先は発展的な内容です。ここまでのCSS APIだけでも、stateの変化やループするアニメーションの多くはカバーできます。「まず動かす」段階ではこの章はここで一区切りにして実践編へ進み、ジェスチャ追従や物理ベースの動き、リストの出入りアニメーションが必要になったタイミングで戻ってきても構いません。
CSS API vs 命令的API: いつどちらを使うか
Reanimatedの「もうひとつの顔」は命令的APIです。useSharedValueでアニメーション可能な値を持ち、useAnimatedStyleでスタイルとして接続し、withTimingやwithSpringで滑らかに動かす、という構成です。

stateの変化やkeyframesで表せる動きはCSS API、ジェスチャに追従する値や物理挙動は命令的APIが向いています。両方を同じ画面で使っても構いません。
import {
useSharedValue,
useAnimatedStyle,
withSpring,
} from "react-native-reanimated";
import Animated from "react-native-reanimated";
import { Pressable } from "react-native";
function DraggyBox() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
return (
<Pressable onPress={() => (offset.value = offset.value === 0 ? 100 : 0)}>
<Animated.View
style={[{ width: 80, height: 80, backgroundColor: "#22d3ee" }, animatedStyle]}
/>
</Pressable>
);
}
CSS APIと命令的APIの使い分けは、ざっくり次のような目安です。
- CSS API: 単純なtransition、繰り返しのkeyframesアニメーション、Reactのstate変化に紐づく動き
- 命令的API: ジェスチャと連動した値の動き、複雑な相互依存があるアニメーション、
withSpringの物理ベースの動きを細かく制御したい場合
両者は同じコンポーネント内で混在させても問題ありません。簡単なものはCSS、複雑なものは命令的、と気軽に使い分けて構いません。
withRepeatでループする
命令的APIにはwithRepeatという便利な関数もあります。
import { withRepeat, withTiming } from "react-native-reanimated";
scale.value = withRepeat(
withTiming(1.2, { duration: 600 }),
-1, // 無限回
true, // reverseで往復させる
);
animationIterationCount: "infinite" + animationDirection: "alternate"に相当する書き方です。CSS APIで足りるならCSS、ジェスチャや他の値と組み合わせる必要があるなら命令的、と覚えておくと迷いません。
ジェスチャとアニメーションの組み合わせ
第8章で「タップ以上の操作はGesture Handlerに任せる」と書きました。React Native Gesture HandlerはReanimatedとセットで使うと真価を発揮します。スワイプで削除する行、ピンチで拡大する画像、ドラッグで並び替えるリスト。これらはどれも、Gesture HandlerがイベントをUIスレッドで拾い、Reanimatedが値を補間する、という分担になっています。
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
useSharedValue,
useAnimatedStyle,
} from "react-native-reanimated";
function DraggableCard() {
const translateX = useSharedValue(0);
const pan = Gesture.Pan().onChange((event) => {
translateX.value += event.changeX;
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[{ width: 200, height: 100, backgroundColor: "#fbbf24" }, animatedStyle]} />
</GestureDetector>
);
}
ここまで来るとCSS APIで書ける範囲を超えますが、「stateで切り替わる動きはCSS、指の動きに追従する動きは命令的」と整理すれば自然に書き分けできます。
第16章で扱うreact-native-gesture-handlerのReanimatedSwipeableは、まさにこの組み合わせをラップしたコンポーネントで、自前でジェスチャを書かずにスワイプ操作を実現できます。
Layout Animations
最後に、CSS APIでも命令的APIでも書きにくい領域として「レイアウト変化のアニメーション」があります。リストに項目が追加された時のフェードイン、削除された時の縮小フェードアウト、並び順変更時の入れ替え。これらはReanimatedのLayout Animationsで扱います。
import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";
function TodoRow({ todo }: { todo: Todo }) {
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
layout={LinearTransition}
style={{ padding: 12 }}
>
{/* ... */}
</Animated.View>
);
}
enteringとexitingに登録したプリセット(FadeIn、SlideInLeftなど)が、要素のマウント・アンマウント時に自動で再生されます。layoutは、要素のサイズや位置が変わった時の遷移を補間してくれます。
リスト系のアプリでは、Layout Animationsを足すだけで体験が一段上がります。プリセットだけでもバリエーションが豊富で、FadeIn.duration(300).delay(100)のようにメソッドチェーンで微調整もできます。
さらに深く学ぶには
本章はReanimatedの入り口だけを扱いました。命令的APIの詳しい使い方、ジェスチャやスクロールとの連動、レイアウトアニメーション、そしてワークレットの仕組みといった先の話は、別に用意しているReact Native Reanimated ガイドブックにまとめてあります。本書と同じくReanimated 4系(New Architecture前提)を対象に、ブラウザ上で動くデモを見ながら読み進められる構成です。
Reanimatedの手が届かない領域 ― 図形やテキストを自前で描く、シェーダをかける、動画やLottieを扱う ― に進みたくなったときは、React Native Skia ガイドブックもあります。第2章で「高度なグラフィックス描画を可能にするレンダリングエンジン」として名前だけ挙げたReact Native Skiaを、Canvasの基本から順に扱っています。
まとめ
- ReanimatedのCSS API(Animations / Transitions)はWebの感覚で書けるので、最初の選択肢として有力
- 単純な状態変化は
transitionProperty+transitionDurationで十分。複雑な動きはanimationName+ keyframes - ジェスチャ追従や物理ベースの動きは
useSharedValue+useAnimatedStyle+withSpringの命令的APIに切り替える - リストの出入りやレイアウト変化はLayout Animationsで宣言的に書ける
アニメーションは「効きすぎ」も「無さすぎ」も体験を損ねます。最初は短めのduration(150〜300ms)、ease-outまたはease-in-outから始めて、画面遷移と要素の出入りに最小限ずつ足していくのが安全です。
次章からは実践編に入ります。ここまでで揃えた基礎を組み合わせて、TODOアプリを最後まで作り上げます。