コンテンツにスキップ
Webフロントエンドエンジニアのための React Native 実践入門
Esc
navigateopen⌘Jpreview
このページの内容

第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-exporeact-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>
  );
}

ポイントはtransitionPropertytransitionDurationの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には「連続的に補間できないプロパティ」(displayalignItemsflexDirectionなど)があります。デフォルトのtransitionBehaviornormalで、これらは無視されます。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と同じく「往復」させるオプションです。pulsefromtoを行ったり来たりすることで、心臓の拍動のような表現になります。

アニメーションを並べる・重ねる

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-colorbackgroundColortransition-propertytransitionPropertyになります。第7章のStyleSheet APIと同じ規則です。

ふたつめは、transformが配列である点です。translateXscaleのような変換は、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のようにpxemは出てきません。時間はms / sどちらでも書けます。

発展: 命令的APIとジェスチャ

ここから先は発展的な内容です。ここまでのCSS APIだけでも、stateの変化やループするアニメーションの多くはカバーできます。「まず動かす」段階ではこの章はここで一区切りにして実践編へ進み、ジェスチャ追従や物理ベースの動き、リストの出入りアニメーションが必要になったタイミングで戻ってきても構いません。

CSS API vs 命令的API: いつどちらを使うか

Reanimatedの「もうひとつの顔」は命令的APIです。useSharedValueでアニメーション可能な値を持ち、useAnimatedStyleでスタイルとして接続し、withTimingwithSpringで滑らかに動かす、という構成です。

ReanimatedのCSS APIと命令的APIの使い分け

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-handlerReanimatedSwipeableは、まさにこの組み合わせをラップしたコンポーネントで、自前でジェスチャを書かずにスワイプ操作を実現できます。

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>
  );
}

enteringexitingに登録したプリセット(FadeInSlideInLeftなど)が、要素のマウント・アンマウント時に自動で再生されます。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アプリを最後まで作り上げます。

最終更新 2026年7月24日