Motion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
Motion(旧 Framer Motion)を使ったアニメーション実装は、React 開発において非常に人気の高い選択肢です。しかし、TypeScript と組み合わせた際に Variant の型定義や Props の推論が弱くなり、型安全性が損なわれる場面に遭遇したことはありませんか。
本記事では、Motion と TypeScript を組み合わせる際の Variant 型定義の強化方法と、Props 推論を最大限活用するための実践的な設定レシピをご紹介します。これらのテクニックを習得することで、より安全で保守性の高いアニメーションコードを書けるようになるでしょう。
背景
Motion(旧 Framer Motion)とは
Motion は、React アプリケーションにおいて宣言的なアニメーションを実装するためのライブラリです。以前は Framer Motion という名称でしたが、2024 年にリブランディングされ、現在は Motion として提供されています。
このライブラリの最大の特徴は、variants という仕組みを使って複雑なアニメーションを宣言的に記述できる点にあります。しかし、この便利な variants を TypeScript で扱う際には、型定義が緩くなりがちで、開発体験が損なわれるケースが多く見られます。
以下の図は、Motion における基本的なアニメーションフローを示しています。
mermaidflowchart LR
developer["開発者"] -->|定義| variants["Variants オブジェクト"]
variants -->|props 経由| motion["motion コンポーネント"]
motion -->|アニメーション実行| dom["DOM 要素"]
state["状態管理"] -->|variant 名| motion
TypeScript との組み合わせで生じる課題
Motion を TypeScript プロジェクトで使用する際、以下のような課題に直面します。
型推論の弱さ: variants オブジェクトを定義しても、そのキー名が自動で推論されないため、variant 名を文字列リテラルで指定する際にタイポが発生しやすくなります。
Props の型安全性: initial、animate、exit などの props に渡す値が、定義した variants のキー名と一致しているかをコンパイル時にチェックできません。
拡張性の問題: カスタムの motion コンポーネントを作成する際に、variants の型情報を適切に伝播させるのが困難です。
課題
Variant 型定義の問題点
Motion の公式ドキュメントで紹介されている基本的な variants の記述方法は、以下のようなものです。
typescriptconst variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
このコードは動作しますが、TypeScript の恩恵をほとんど受けられません。具体的には以下の課題があります。
文字列リテラルの自動補完が効かない: animate="visible" と書く際に、IDE が "visible" という選択肢を提示してくれません。
タイポの検出ができない: animate="visble" のようなタイポがあっても、コンパイルエラーにならず、実行時まで気づけないでしょう。
リファクタリングの困難さ: variant 名を変更する際、すべての使用箇所を手動で修正する必要があり、変更漏れのリスクが高まります。
Props 推論の限界
Motion コンポーネントの props は、以下のように多様な値を受け入れます。
typescript<motion.div
initial='hidden'
animate='visible'
exit='hidden'
variants={variants}
/>
しかし、initial、animate、exit に渡せる文字列が、variants で定義したキー名に限定されていることを TypeScript が理解できません。このため、存在しない variant 名を指定してもエラーにならず、バグの温床となります。
下図は、型安全性が弱い場合に発生する問題フローを示しています。
mermaidflowchart TD
code["コード記述"] -->|タイポあり| compile["コンパイル成功"]
compile -->|実行| runtime["ランタイムエラー"]
runtime -->|デバッグ| debug["原因特定に時間"]
debug -->|修正| code
code2["型安全なコード"] -->|型チェック| error["コンパイルエラー"]
error -->|即座に修正| code2
解決策
Variant 型を厳密に定義する
Motion と TypeScript の型安全性を強化するには、variants オブジェクトの型を明示的に定義し、その型情報を活用する必要があります。
ステップ 1:型定義のためのユーティリティ型を作成
まず、variants の型情報を抽出するためのユーティリティ型を定義します。
typescriptimport { Variants } from 'motion/react';
// Variants のキー名を Union 型として抽出
type VariantKeys<T extends Variants> = keyof T & string;
この VariantKeys 型は、variants オブジェクトからキー名だけを Union 型として抽出します。& string を付けることで、symbol や number 型のキーを除外し、文字列リテラルの Union 型だけを得られます。
ステップ 2:型安全な variants を定義
次に、実際の variants オブジェクトを as const アサーションと組み合わせて定義しましょう。
typescriptconst boxVariants = {
hidden: {
opacity: 0,
scale: 0.8,
},
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.3 },
},
exit: {
opacity: 0,
scale: 0.8,
transition: { duration: 0.2 },
},
} as const satisfies Variants;
as const を使うことで、オブジェクトのプロパティがリテラル型として推論されます。さらに satisfies Variants を付けることで、Motion の Variants 型に準拠していることを保証できます。
ステップ 3:型抽出とエクスポート
定義した variants から型を抽出し、他のファイルでも使えるようにします。
typescript// variants のキー名を型として抽出
type BoxVariantKeys = VariantKeys<typeof boxVariants>;
// 結果: "hidden" | "visible" | "exit"
export { boxVariants };
export type { BoxVariantKeys };
これで、BoxVariantKeys 型は "hidden" | "visible" | "exit" という文字列リテラルの Union 型になります。
Props の型推論を強化する
variants の型定義ができたら、次は motion コンポーネントの props で型推論が効くようにします。
カスタムコンポーネントの型定義
再利用可能なアニメーションコンポーネントを作る際、props の型を厳密に定義することで型安全性が向上します。
typescriptimport { motion, MotionProps } from 'motion/react';
import { ComponentProps } from 'react';
type AnimatedBoxProps = {
variant: BoxVariantKeys;
children?: React.ReactNode;
} & Omit<MotionProps, 'variants' | 'initial' | 'animate'>;
この型定義では、variant という props を追加し、その型を先ほど定義した BoxVariantKeys に限定しています。これにより、存在しない variant 名を指定しようとするとコンパイルエラーが発生します。
コンポーネントの実装
型定義をもとに、実際のコンポーネントを実装しましょう。
typescriptexport const AnimatedBox = ({
variant,
children,
...motionProps
}: AnimatedBoxProps) => {
return (
<motion.div
variants={boxVariants}
initial='hidden'
animate={variant}
exit='exit'
{...motionProps}
>
{children}
</motion.div>
);
};
この実装により、<AnimatedBox variant="visible" /> のように使う際、IDE が自動補完で "hidden" | "visible" | "exit" を提示してくれるようになります。
ジェネリクスを活用した汎用的な解決策
特定の variants に依存しない、より汎用的なアプローチも可能です。
ジェネリック型の定義
typescripttype StrictMotionProps<V extends Variants> = {
variants: V;
initial?: VariantKeys<V> | boolean;
animate?: VariantKeys<V>;
exit?: VariantKeys<V>;
whileHover?: VariantKeys<V>;
whileTap?: VariantKeys<V>;
whileFocus?: VariantKeys<V>;
} & Omit<
MotionProps,
| 'variants'
| 'initial'
| 'animate'
| 'exit'
| 'whileHover'
| 'whileTap'
| 'whileFocus'
>;
この StrictMotionProps 型は、variants の型をジェネリクスで受け取り、各アニメーション props の型を variants のキー名に限定します。
汎用コンポーネントの実装
typescriptfunction createStrictMotionComponent<V extends Variants>(
component: keyof typeof motion
) {
return function StrictMotion(
props: StrictMotionProps<V>
) {
const Component = motion[component] as any;
return <Component {...props} />;
};
}
このファクトリー関数を使うことで、任意の motion コンポーネントを型安全にラップできます。
下図は、型安全なコンポーネント設計のフローを示しています。
mermaidflowchart TD
define["Variants 定義<br/>(as const satisfies)"] -->|型抽出| keys["VariantKeys 型"]
keys -->|適用| props["Props 型定義"]
props -->|実装| component["型安全な<br/>コンポーネント"]
component -->|使用時| autocomplete["IDE 自動補完"]
component -->|誤使用時| error["コンパイルエラー"]
具体例
実践例:モーダルアニメーション
実際のプロジェクトで使える、モーダルコンポーネントの実装例を見ていきましょう。
Variants の定義
typescriptconst modalVariants = {
hidden: {
opacity: 0,
scale: 0.95,
y: -20,
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 300,
damping: 25,
},
},
exit: {
opacity: 0,
scale: 0.95,
y: 20,
transition: {
duration: 0.2,
},
},
} as const satisfies Variants;
type ModalVariantKeys = VariantKeys<typeof modalVariants>;
この variants 定義では、モーダルの開閉時のアニメーションを表現しています。hidden から visible へ、そして exit へと状態が遷移します。
背景オーバーレイの Variants
typescriptconst overlayVariants = {
hidden: {
opacity: 0,
},
visible: {
opacity: 1,
transition: { duration: 0.2 },
},
exit: {
opacity: 0,
transition: { duration: 0.15 },
},
} as const satisfies Variants;
type OverlayVariantKeys = VariantKeys<
typeof overlayVariants
>;
オーバーレイは、モーダルの背景として表示される半透明の黒い層です。モーダル本体とは異なる variants を持たせることで、独立したアニメーションを実現できます。
型安全なモーダルコンポーネント
typescriptimport { AnimatePresence } from 'motion/react';
type ModalProps = {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
};
export const Modal = ({
isOpen,
onClose,
children,
className,
}: ModalProps) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景オーバーレイ */}
<motion.div
variants={overlayVariants}
initial='hidden'
animate='visible'
exit='exit'
onClick={onClose}
className='modal-overlay'
/>
{/* モーダル本体 */}
<motion.div
variants={modalVariants}
initial='hidden'
animate='visible'
exit='exit'
className={`modal-content ${className || ''}`}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
};
この実装では、AnimatePresence を使って、コンポーネントのマウント・アンマウント時にアニメーションを適用しています。initial、animate、exit に指定する文字列は、variants で定義したキー名に限定されます。
実践例:リストアニメーション
複数の要素に連続的なアニメーションを適用する、ステージング効果の実装例です。
親子関係を持つ Variants
typescriptconst containerVariants = {
hidden: {
opacity: 0,
},
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
} as const satisfies Variants;
const itemVariants = {
hidden: {
opacity: 0,
x: -20,
},
visible: {
opacity: 1,
x: 0,
transition: {
type: 'spring',
stiffness: 300,
damping: 25,
},
},
} as const satisfies Variants;
type ContainerVariantKeys = VariantKeys<
typeof containerVariants
>;
type ItemVariantKeys = VariantKeys<typeof itemVariants>;
staggerChildren を使うことで、子要素のアニメーションを順次実行できます。delayChildren は、最初の子要素のアニメーション開始を遅らせる設定です。
リストコンポーネントの実装
typescripttype AnimatedListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
className?: string;
};
export function AnimatedList<T>({
items,
renderItem,
className,
}: AnimatedListProps<T>) {
return (
<motion.ul
variants={containerVariants}
initial='hidden'
animate='visible'
className={className}
>
{items.map((item, index) => (
<motion.li key={index} variants={itemVariants}>
{renderItem(item, index)}
</motion.li>
))}
</motion.ul>
);
}
このコンポーネントは、ジェネリクス <T> を使って任意の型のリストをサポートしています。親要素の containerVariants が "visible" 状態になると、子要素の itemVariants が順次 "visible" に遷移します。
使用例
typescripttype Task = {
id: string;
title: string;
completed: boolean;
};
const tasks: Task[] = [
{
id: '1',
title: 'TypeScript の型定義を学ぶ',
completed: true,
},
{
id: '2',
title: 'Motion のアニメーションを実装',
completed: false,
},
{ id: '3', title: '記事を執筆する', completed: false },
];
function TaskList() {
return (
<AnimatedList
items={tasks}
renderItem={(task) => (
<div className='task-item'>
<input
type='checkbox'
checked={task.completed}
readOnly
/>
<span>{task.title}</span>
</div>
)}
/>
);
}
この例では、タスクリストの各項目が 0.1 秒ずつ遅れてアニメーションします。型安全性が保たれているため、renderItem 関数内で task オブジェクトのプロパティに安全にアクセスできます。
実践例:条件付き Variants
状態に応じて異なるアニメーションを適用するパターンも、型安全に実装できます。
多状態 Variants の定義
typescriptconst buttonVariants = {
idle: {
scale: 1,
backgroundColor: '#3b82f6',
},
hover: {
scale: 1.05,
backgroundColor: '#2563eb',
transition: { duration: 0.2 },
},
pressed: {
scale: 0.95,
backgroundColor: '#1d4ed8',
},
loading: {
scale: 1,
backgroundColor: '#93c5fd',
transition: {
repeat: Infinity,
duration: 1,
},
},
disabled: {
scale: 1,
backgroundColor: '#9ca3af',
cursor: 'not-allowed',
},
} as const satisfies Variants;
type ButtonVariantKeys = VariantKeys<typeof buttonVariants>;
このボタンは、通常・ホバー・押下・ローディング・無効化の 5 つの状態を持ちます。
状態管理と連携したコンポーネント
typescripttype AnimatedButtonProps = {
children: React.ReactNode;
onClick?: () => void;
isLoading?: boolean;
isDisabled?: boolean;
className?: string;
};
export const AnimatedButton = ({
children,
onClick,
isLoading = false,
isDisabled = false,
className,
}: AnimatedButtonProps) => {
// 現在の状態を判定
const getCurrentVariant = (): ButtonVariantKeys => {
if (isDisabled) return 'disabled';
if (isLoading) return 'loading';
return 'idle';
};
return (
<motion.button
variants={buttonVariants}
initial='idle'
animate={getCurrentVariant()}
whileHover={
!isDisabled && !isLoading ? 'hover' : undefined
}
whileTap={
!isDisabled && !isLoading ? 'pressed' : undefined
}
onClick={
!isDisabled && !isLoading ? onClick : undefined
}
disabled={isDisabled || isLoading}
className={className}
>
{isLoading ? 'Loading...' : children}
</motion.button>
);
};
getCurrentVariant 関数の戻り値の型が ButtonVariantKeys に限定されているため、存在しない variant 名を返すことができません。これにより、実装ミスを防げます。
下図は、条件分岐を含むアニメーション状態遷移を示しています。
mermaidstateDiagram-v2
[*] --> idle: 初期化
idle --> hover: マウスオーバー
hover --> idle: マウスアウト
hover --> pressed: クリック
pressed --> idle: リリース
idle --> loading: 処理開始
loading --> idle: 処理完了
idle --> disabled: 無効化
disabled --> idle: 有効化
図で理解できる要点:
- ボタンは 5 つの状態(idle, hover, pressed, loading, disabled)を持つ
- ユーザー操作や内部状態によって状態が遷移する
- 各状態遷移は型安全に管理される
まとめ
本記事では、Motion(旧 Framer Motion)と TypeScript を組み合わせる際の Variant 型定義と Props 推論の強化方法をご紹介しました。
主なポイントは以下の通りです。
| # | 項目 | 内容 |
|---|---|---|
| 1 | Variants の型定義 | as const satisfies Variants を使い、リテラル型として定義する |
| 2 | 型抽出 | VariantKeys<T> ユーティリティ型でキー名を Union 型として抽出 |
| 3 | Props の型安全化 | カスタムコンポーネントで variant 名を型として限定する |
| 4 | ジェネリクスの活用 | 汎用的な型定義で、複数の variants に対応できる |
| 5 | 実践的な実装 | モーダル、リスト、ボタンなど実用的なパターンを適用 |
これらのテクニックを活用することで、以下のメリットが得られます。
開発体験の向上: IDE の自動補完やリファクタリング機能が最大限に活かせるようになり、コーディング効率が大幅に改善されます。
バグの早期発見: 存在しない variant 名の指定やタイポをコンパイル時に検出できるため、実行時エラーのリスクが減少します。
保守性の向上: 型定義によってコードの意図が明確になり、チーム開発やコードレビューがスムーズになるでしょう。
Motion のアニメーション機能は非常に強力ですが、TypeScript の型システムと適切に組み合わせることで、さらに安全で保守性の高いコードベースを構築できます。ぜひこれらのパターンをプロジェクトに取り入れて、型安全なアニメーション実装を体験してみてください。
関連リンク
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
articleMotion(旧 Framer Motion)× GSAP 併用/置換の判断基準:大規模アニメの最適解を探る
articleMotion(旧 Framer Motion)で exit が発火しない/遅延する問題の原因切り分けガイド
articleMotion(旧 Framer Motion)で学ぶ物理ベースアニメ:バネ定数・減衰・質量の直感入門
articleMotion(旧 Framer Motion)デザインレビュー運用:Figma パラメータ同期と差分共有のワークフロー
articleMotion(旧 Framer Motion)アニメオーケストレーション設計:timeline・遅延・相互依存の整理術
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来