概要
-
Reactアプリケーションで、カードUIのホバーや選択時のアニメーション挙動を簡潔に実装するためのuseCardAnimationカスタムフックを紹介 - 再利用可能な構造にすることで、複数のカードUIに共通のアニメーションを適用可能
- Netflix や Spotify のような UIに近づけたい場合に活用でき、コードの見通し・保守性も高まります
実施条件
- React + TypeScript プロジェクトが構築済みであること
- カードUIを複数作成し、動きを付けたい要件があること
- 状態管理・イベントハンドリングに対してReact Hooksの基礎理解があること
環境
| ツール | バージョン | 目的 |
|---|---|---|
| Node.js | 22.5.1 | Reactアプリ実行環境 |
| React | 19.1.0 | UI構築 |
| TypeScript | 4.9 | 型定義による安全な開発 |
| CSS-in-JS | inline style使用 | アニメーション表現用 |
型定義セクション
// 型定義セクション
export interface CardAnimationState {
isHovered: boolean;
isSelected: boolean;
isAnimating: boolean;
}
export interface CardAnimationHandlers {
onMouseEnter: () => void;
onMouseLeave: () => void;
onClick: () => void;
}
export interface UseCardAnimationReturn {
state: CardAnimationState;
handlers: CardAnimationHandlers;
style: React.CSSProperties;
className: string;
}
useCardAnimationカスタムフックの基本構造
- importセクション
- 型定義セクション
- カスタムフック定義セクション
3.1 内部状態管理セクション
3.2 アニメーションセクション
3.3 イベントハンドラーセクション
3.4 副作用処理セクション
3.5 返り値構築・ロジックセクション
基本構文: useCardAnimation.ts
// 1. importセクション
import { useState, useRef, useCallback, useEffect } from 'react';
// 2. 型定義セクション
// (上記参照)
// 3. カスタムフック定義セクション
export const useCardAnimation = (): UseCardAnimationReturn => {
// 3.1 内部状態管理セクション
const [isHovered, setIsHovered] = useState(false);
const [isSelected, setIsSelected] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// 3.2 アニメーションセクション
const triggerAnimation = useCallback(() => {
setIsAnimating(true);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setIsAnimating(false), 300);
}, []);
// 3.3 イベントハンドラーセクション
const onMouseEnter = useCallback(() => {
setIsHovered(true);
triggerAnimation();
}, [triggerAnimation]);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
triggerAnimation();
}, [triggerAnimation]);
const onClick = useCallback(() => {
setIsSelected((prev) => !prev);
triggerAnimation();
}, [triggerAnimation]);
// 3.4 副作用処理セクション
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
// 3.5 返り値構築・ロジックセクション
const scale = isSelected ? 1.1 : isHovered ? 1.05 : 1;
const zIndex = isSelected ? 10 : isHovered ? 2 : 1;
const style: React.CSSProperties = {
transform: `scale(${scale})`,
zIndex,
transition: 'transform 300ms ease, z-index 300ms ease',
cursor: 'pointer',
transformOrigin: 'center',
};
const className = [
'card',
isHovered && 'hovered',
isSelected && 'selected',
isAnimating && 'animating',
].filter(Boolean).join(' ');
return {
state: { isHovered, isSelected, isAnimating },
handlers: { onMouseEnter, onMouseLeave, onClick },
style,
className,
};
};
活用例
1. 基本カード:選択状態を切り替えるだけのカードUI
// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';
// 2. 型定義セクション(省略)
// 3. 関数定義セクション
const CardBasic = () => {
// 3.1 状態管理セクション
const { handlers, style, className, state } = useCardAnimation();
// 3.2 イベントハンドラーセクション(hook内)
// 3.3 JSX構築セクション
return (
<div
{...handlers}
style={{ ...style, border: '1px solid gray', margin: '1rem', padding: '1rem' }}
className={className}
>
<p>{state.isSelected ? '選択済みカード' : '未選択カード'}</p>
</div>
);
};
export default CardBasic;
2. 画像ギャラリー:複数の画像カードにホバー拡大アニメーションを適用
// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';
// 2. 型定義セクション
interface ImageCardProps {
src: string;
}
// 3. 関数定義セクション
const ImageCard = ({ src }: ImageCardProps) => {
// 3.1 状態管理セクション
const { handlers, style, className } = useCardAnimation();
// 3.3 JSX構築セクション
return (
<div
{...handlers}
style={{ ...style, borderRadius: '8px', overflow: 'hidden', margin: '1rem' }}
className={className}
>
<img src={src} alt="card" style={{ width: '200px', height: 'auto', display: 'block' }} />
</div>
);
};
const ImageCardGallery = () => {
// 3.1 状態管理セクション
const images = ['/img1.jpg', '/img2.jpg', '/img3.jpg'];
// 3.3 JSX構築セクション
return (
<div style={{ display: 'flex' }}>
{images.map((src, i) => <ImageCard key={i} src={src} />)}
</div>
);
};
export default ImageCardGallery;
3. ダッシュボード:各カードがホバー・選択可能なダッシュボードUI
// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';
// 2. 型定義セクション
interface DashboardCardProps {
title: string;
icon: string;
}
// 3. 関数定義セクション
const DashboardCard = ({ title, icon }: DashboardCardProps) => {
// 3.1 状態管理セクション
const { handlers, style, state } = useCardAnimation();
// 3.3 JSX構築セクション
return (
<div
{...handlers}
style={{
...style,
padding: '1.5rem',
backgroundColor: state.isSelected ? '#333' : '#444',
color: '#fff',
borderRadius: '10px',
width: '150px',
textAlign: 'center',
margin: '1rem',
}}
>
<div style={{ fontSize: '2rem' }}>{icon}</div>
<p>{title}</p>
</div>
);
};
const Dashboard = () => {
// 3.1 状態管理セクション
const items = [
{ title: 'Portfolio', icon: '📊' },
{ title: 'Trade', icon: '💹' },
{ title: 'Settings', icon: '⚙️' },
];
// 3.3 JSX構築セクション
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
{items.map((item, i) => <DashboardCard key={i} {...item} />)}
</div>
);
};
export default Dashboard;
カードアニメーションの処理の流れ
- ユーザー操作(hover/click)でイベント発火
- カスタムフック内で
setStateにより状態を更新 - アニメーション用の
transformやzIndexを変更 - 変更された
style/classNameを通じてUIに反映