React + TypeScript: Framer MotionのvariantsでDOMツリーに調和したアニメーションを加える

[Framer Motion](https://www.framer.com/motion/)は、本番環境に対応したReact用のモーションライブラリです。構文は宣言的で、複雑なアニメーションのコードも簡潔に書けます。つまり、コードベースが読みやすく、保守しやすいということです。

ライブラリの基本的な使い方については「[React + TypeScript: Framer Motionでアニメーションを加える](https://qiita.com/FumioNonaka/items/ccadd4261522aafe51f5)」をお読みください。本稿は、公式サイト「Animation」の「[Variants](https://www.framer.com/motion/animation/#variants)」について解説します。つぎのサンプル001が、公式作例を参考にしたプルダウンメニューです。

**サンプル001■React + TypeScript: Framer Motion Variants 01**
https://codesandbox.io/s/react-typescript-framer-motion-variants-01-862dkh

コンポーネントごとのアニメーションは、`animate`プロパティにオブジェクトで与えられます。けれど、DOMツリー全体に調和したアニメーションを加えたいことがあるでしょう。そのときに用いるのが[`variants`](https://www.framer.com/motion/animation/#variants)プロパティです。

# ボタンのアニメーション
`variants`には、アニメーションをあらかじめターゲットごとに、オブジェクト(`menuButtonVariants`)で定めてください。アニメーションとして参照されるのは、ターゲットのラベル(`open`と`closed`)です。

```react:src/Button.tsx
const menuButtonVariants: Variants = {
	open: { rotate: 180 },
	closed: { rotate: 0 }
};
const ArrowSign: FC = () => {
	return (
		<motion.div
			variants={menuButtonVariants}
			transition={{ duration: 0.2 }}
		>
			<svg />
		</motion.div>
	);
};
```

`variants`によるアニメーションは、DOMツリーの親からコントロールできます。`animate`プロパティの値に与えるのは、`variants`に定めたアニメーションのラベルです。つぎのルートコンポーネントは、状態変数値(`isOpen`)に応じて`variants`のアニメーションを切り替えます。

```react:src/App.tsx
export default function App() {
	const [isOpen, setIsOpen] = useState(false);
	const handleClick = () => setIsOpen(!isOpen);
	return (
		<motion.nav
			initial={false}
			animate={isOpen ? 'open' : 'closed'}
		>
			<Button onClick={handleClick} />
		</motion.nav>
	);
}
```

ボタンをクリックするたびに、ボタン上に表示された矢印(`svg`)が180度回転するようになりました(前掲サンプル001)。


# メニューのアニメーション
メニュー項目を表示するのが`<MenuItems>`コンポーネントです。

```react:src/App.tsx
export default function App() {
	const [isOpen, setIsOpen] = useState(false);

	return (
		<motion.nav
			initial={false}
			animate={isOpen ? 'open' : 'closed'}
		>

			<MenuItems isOpen={isOpen} />
		</motion.nav>
	);
}
```

`variants`を定める基本は変わりません。変数`menuVariants`のラベル`open`と`closed`に、それぞれアニメーションのオブジェクトを与えました。なお、[`clipPath`](https://www.framer.com/motion/animation/##supported-values)は、CSSの[`clip-path`](https://developer.mozilla.org/docs/Web/CSS/clip-path)に準じて要素の表示範囲を決めるプロパティです。

```react:src/MenuItems.
const menuVariants: Variants = {
	open: {
		clipPath: 'inset(0% 0% 0% 0% round 10px)',
		transition: {
			type: 'spring',
			bounce: 0,
			duration: 0.7,
			delayChildren: 0.3,
			staggerChildren: 0.05
		}
	},
	closed: {
		clipPath: 'inset(10% 50% 90% 50% round 10px)',
		transition: {
			type: 'spring',
			bounce: 0,
			duration: 0.3
		}
	}
};
export const MenuItems: FC<Props> = ({ isOpen }) => {
	return (
		<motion.ul
			variants={menuVariants}
		>
			{Array.from(new Array(5), (_, index) => (
				<motion.li key={index}>
					Item {String(index + 1).padStart(2, '0')}
				</motion.li>
			))}
		</motion.ul>
	);
};
```

メニューボタンクリックで、メニューが開いたり閉じたりするようになりました(前掲サンプル001)。

メニュー項目にも、`variants`で簡単なアニメーションを加えましょう(`itemVariants`)。垂直方向の移動と透明度の変化です(前掲サンプル001)。これで、プルダウンメニューの作例ができ上がりました。

```react:src/
const itemVariants: Variants = {
	open: {
		opacity: 1,
		y: 0,
		transition: { type: 'spring', stiffness: 300, damping: 24 }
	},
	closed: { opacity: 0, y: 20, transition: { duration: 0.2 } }
};

export const MenuItems: FC<Props> = ({ isOpen }) => {
	return (
		<motion.ul
			variants={menuVariants}
		>
			{Array.from(new Array(5), (_, index) => (
				// <motion.li key={index}>
				<motion.li variants={itemVariants} key={index}>
					Item {String(index + 1).padStart(2, '0')}
				</motion.li>
			))}
		</motion.ul>
	);
};
```

今回ご紹介したのは、`variants`はコンポーネントごとに定め、`animate`プロパティに与えるラベルを親のルートコンポーネント(`App`)で切り替えるという仕組みでした。`variants`にはほかにも応用できる手法があります。詳しくは、公式サイトの「[Variants](https://www.framer.com/motion/animation/#variants)」をご参照ください。