Next.js 時代のアニメーション設計ベストプラクティス

Next.js がフロントエンド開発の主流となった今、アニメーション設計の重要性は以前にも増して高まっています。しかし、多くの開発者が「アニメーションを実装したいけど、Next.js ではどうすればいいの?」という疑問を抱えているのではないでしょうか。
実際、Next.js の SSR(サーバーサイドレンダリング)や SSG(静的サイト生成)の特性は、従来の SPA とは異なるアニメーション設計を要求します。この記事では、Next.js の特性を活かしながら、パフォーマンスとユーザー体験を両立するアニメーション設計のベストプラクティスを実践的に解説していきます。
あなたが今抱えているアニメーション実装の悩みが、この記事を読むことで解決されることを願っています。
Next.js アニメーションの基礎知識
Next.js のレンダリング方式とアニメーションの関係
Next.js には複数のレンダリング方式があり、それぞれがアニメーション実装に異なる影響を与えます。
SSR(サーバーサイドレンダリング) サーバーで HTML を生成するため、初期表示は高速ですが、クライアントサイドでのハイドレーションが必要です。
typescript// pages/index.tsx
import { useEffect, useState } from 'react';
export default function HomePage() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
// クライアントサイドでのみアニメーションを実行
if (!isClient) {
return <div>Loading...</div>;
}
return <AnimatedComponent />;
}
SSG(静的サイト生成) ビルド時に HTML を生成するため、最も高速ですが、動的なアニメーションには制限があります。
typescript// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
// ビルド時にデータを取得
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 60, // ISR(Incremental Static Regeneration)
};
}
CSR(クライアントサイドレンダリング) 従来の SPA と同様の動作ですが、SEO に不利な場合があります。
SSR/SSG でのアニメーション実装の注意点
SSR/SSG 環境では、サーバーとクライアントで異なる HTML が生成される可能性があります。これが「ハイドレーションエラー」の原因となります。
typescript// ❌ よくあるエラー例
// Warning: Text content did not match. Server: "Hello" Client: "Hello World"
const [count, setCount] = useState(0);
useEffect(() => {
// サーバーとクライアントで異なる値になる可能性
setCount(Math.random());
}, []);
解決策:クライアントサイドでのみ実行
typescript// ✅ 正しい実装例
const [mounted, setMounted] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {
setMounted(true);
setCount(Math.random());
}, []);
if (!mounted) {
return <div>Loading...</div>;
}
クライアントサイドハイドレーションとの連携
ハイドレーションは、サーバーで生成された HTML に JavaScript の機能を追加するプロセスです。アニメーションはこのプロセス完了後に実行する必要があります。
typescript// components/AnimatedCounter.tsx
import { motion, AnimatePresence } from 'framer-motion';
import { useEffect, useState } from 'react';
export default function AnimatedCounter() {
const [isHydrated, setIsHydrated] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {
setIsHydrated(true);
}, []);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<motion.button
onClick={handleClick}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
disabled={!isHydrated}
>
Increment
</motion.button>
<AnimatePresence mode='wait'>
{isHydrated && (
<motion.div
key={count}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
Count: {count}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
パフォーマンス最適化のベストプラクティス
アニメーション用ライブラリの選定基準
Next.js プロジェクトでは、バンドルサイズとパフォーマンスが重要な選定基準となります。
主要ライブラリの比較
ライブラリ | バンドルサイズ | SSR 対応 | 学習コスト | 用途 |
---|---|---|---|---|
Framer Motion | ~40KB | ✅ | 中 | 汎用アニメーション |
React Spring | ~30KB | ✅ | 高 | 物理ベース |
Lottie | ~50KB | ❌ | 低 | 複雑なアニメーション |
CSS Transitions | ~0KB | ✅ | 低 | シンプルなアニメーション |
推奨ライブラリ:Framer Motion
bashyarn add framer-motion
typescript// lib/animation.ts
import { Variants } from 'framer-motion';
// 再利用可能なアニメーション変数
export const fadeInUp: Variants = {
initial: { opacity: 0, y: 60 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -60 },
};
export const staggerContainer: Variants = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
};
バンドルサイズの最適化手法
動的インポートの活用
typescript// components/HeavyAnimation.tsx
import dynamic from 'next/dynamic';
// 重いアニメーションコンポーネントを動的インポート
const HeavyAnimation = dynamic(
() => import('./HeavyAnimation'),
{
loading: () => <div>Loading animation...</div>,
ssr: false, // クライアントサイドでのみ実行
}
);
Tree Shaking の活用
typescript// ❌ 全機能をインポート(バンドルサイズ大)
import * as FramerMotion from 'framer-motion';
// ✅ 必要な機能のみインポート(バンドルサイズ小)
import { motion, AnimatePresence } from 'framer-motion';
レンダリングパフォーマンスの向上テクニック
React.memo の活用
typescript// components/AnimatedCard.tsx
import { motion } from 'framer-motion';
import { memo } from 'react';
interface AnimatedCardProps {
title: string;
description: string;
onClick: () => void;
}
const AnimatedCard = memo(
({ title, description, onClick }: AnimatedCardProps) => {
return (
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onClick}
className='p-4 border rounded-lg cursor-pointer'
>
<h3 className='text-lg font-bold'>{title}</h3>
<p className='text-gray-600'>{description}</p>
</motion.div>
);
}
);
AnimatedCard.displayName = 'AnimatedCard';
export default AnimatedCard;
useCallback と useMemo の活用
typescript// hooks/useAnimationConfig.ts
import { useMemo, useCallback } from 'react';
export function useAnimationConfig(duration: number = 0.3) {
const config = useMemo(
() => ({
duration,
ease: [0.4, 0, 0.2, 1], // easeOutQuart
}),
[duration]
);
const handleAnimationComplete = useCallback(() => {
console.log('Animation completed');
}, []);
return { config, handleAnimationComplete };
}
実装パターン別ベストプラクティス
ページ遷移アニメーションの実装
Next.js の App Router では、ページ遷移アニメーションの実装方法が大きく変わりました。
App Router での実装
typescript// app/layout.tsx
import { AnimatePresence } from 'framer-motion';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='ja'>
<body>
<AnimatePresence mode='wait'>
{children}
</AnimatePresence>
</body>
</html>
);
}
typescript// app/page.tsx
'use client';
import { motion } from 'framer-motion';
export default function HomePage() {
return (
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.5 }}
>
<h1>Welcome to Next.js</h1>
</motion.div>
);
}
Pages Router での実装
typescript// pages/_app.tsx
import { AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/router';
export default function MyApp({ Component, pageProps }) {
const router = useRouter();
return (
<AnimatePresence mode='wait' initial={false}>
<Component {...pageProps} key={router.route} />
</AnimatePresence>
);
}
コンポーネントマウント/アンマウントアニメーション
リストアイテムのアニメーション
typescript// components/AnimatedList.tsx
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
interface Item {
id: number;
title: string;
}
export default function AnimatedList() {
const [items, setItems] = useState<Item[]>([
{ id: 1, title: 'Item 1' },
{ id: 2, title: 'Item 2' },
{ id: 3, title: 'Item 3' },
]);
const addItem = () => {
const newItem = {
id: Date.now(),
title: `Item ${items.length + 1}`,
};
setItems([...items, newItem]);
};
const removeItem = (id: number) => {
setItems(items.filter((item) => item.id !== id));
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<AnimatePresence>
{items.map((item) => (
<motion.div
key={item.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className='p-4 border-b'
>
<span>{item.title}</span>
<button onClick={() => removeItem(item.id)}>
Remove
</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}
スクロールベースアニメーション
Intersection Observer API の活用
typescript// hooks/useScrollAnimation.ts
import { useEffect, useRef, useState } from 'react';
export function useScrollAnimation() {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, []);
return { ref, isVisible };
}
typescript// components/ScrollAnimatedSection.tsx
import { motion } from 'framer-motion';
import { useScrollAnimation } from '../hooks/useScrollAnimation';
export default function ScrollAnimatedSection() {
const { ref, isVisible } = useScrollAnimation();
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 100 }}
animate={
isVisible
? { opacity: 1, y: 0 }
: { opacity: 0, y: 100 }
}
transition={{ duration: 0.6 }}
className='min-h-screen flex items-center justify-center'
>
<h2 className='text-4xl font-bold'>
Scroll Animation
</h2>
</motion.div>
);
}
インタラクティブアニメーション
ドラッグ&ドロップアニメーション
typescript// components/DraggableCard.tsx
import {
motion,
useMotionValue,
useTransform,
} from 'framer-motion';
import { useState } from 'react';
export default function DraggableCard() {
const [isDragging, setIsDragging] = useState(false);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(y, [-100, 100], [30, -30]);
const rotateY = useTransform(x, [-100, 100], [-30, 30]);
const handleDragStart = () => setIsDragging(true);
const handleDragEnd = () => setIsDragging(false);
return (
<motion.div
style={{
x,
y,
rotateX,
rotateY,
z: isDragging ? 1000 : 0,
}}
drag
dragElastic={0.1}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className='w-64 h-40 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg cursor-grab active:cursor-grabbing'
>
<div className='p-6 text-white'>
<h3 className='text-xl font-bold'>
Draggable Card
</h3>
<p>Drag me around!</p>
</div>
</motion.div>
);
}
アクセシビリティとユーザビリティ
アニメーション無効設定への対応
ユーザーの中には、アニメーションが苦手な方や、パフォーマンスを重視する方がいます。これらの設定を尊重することは、包括的デザインの基本です。
prefers-reduced-motion の検出
typescript// hooks/useReducedMotion.ts
import { useEffect, useState } from 'react';
export function useReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] =
useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
setPrefersReducedMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setPrefersReducedMotion(event.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () =>
mediaQuery.removeEventListener(
'change',
handleChange
);
}, []);
return prefersReducedMotion;
}
アニメーション無効時の代替実装
typescript// components/AccessibleAnimatedButton.tsx
import { motion } from 'framer-motion';
import { useReducedMotion } from '../hooks/useReducedMotion';
interface AccessibleAnimatedButtonProps {
children: React.ReactNode;
onClick: () => void;
disabled?: boolean;
}
export default function AccessibleAnimatedButton({
children,
onClick,
disabled = false,
}: AccessibleAnimatedButtonProps) {
const prefersReducedMotion = useReducedMotion();
const buttonVariants = prefersReducedMotion
? {
// アニメーション無効時は即座に状態変化
hover: {},
tap: {},
}
: {
// 通常時はアニメーション付き
hover: { scale: 1.05 },
tap: { scale: 0.95 },
};
return (
<motion.button
variants={buttonVariants}
whileHover='hover'
whileTap='tap'
onClick={onClick}
disabled={disabled}
className='px-6 py-3 bg-blue-500 text-white rounded-lg disabled:opacity-50'
>
{children}
</motion.button>
);
}
モーションセーフの実装
CSS での実装
css/* styles/globals.css */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
JavaScript での実装
typescript// utils/animationUtils.ts
export function getSafeAnimationDuration(
duration: number
): number {
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
return mediaQuery.matches ? 0 : duration;
}
return duration;
}
export function getSafeAnimationConfig(baseConfig: any) {
const duration = getSafeAnimationDuration(
baseConfig.duration || 0.3
);
return {
...baseConfig,
duration,
// アニメーション無効時は即座に完了
...(duration === 0 && {
type: 'tween',
ease: 'linear',
}),
};
}
ユーザー設定の尊重
設定の永続化
typescript// hooks/useAnimationSettings.ts
import { useState, useEffect } from 'react';
interface AnimationSettings {
enabled: boolean;
duration: number;
intensity: 'low' | 'medium' | 'high';
}
export function useAnimationSettings() {
const [settings, setSettings] =
useState<AnimationSettings>({
enabled: true,
duration: 0.3,
intensity: 'medium',
});
useEffect(() => {
// ローカルストレージから設定を読み込み
const saved = localStorage.getItem('animationSettings');
if (saved) {
setSettings(JSON.parse(saved));
}
}, []);
const updateSettings = (
newSettings: Partial<AnimationSettings>
) => {
const updated = { ...settings, ...newSettings };
setSettings(updated);
localStorage.setItem(
'animationSettings',
JSON.stringify(updated)
);
};
return { settings, updateSettings };
}
実装例とコードサンプル
Framer Motion を使った実装例
モーダルアニメーション
typescript// components/AnimatedModal.tsx
import { motion, AnimatePresence } from 'framer-motion';
import { useReducedMotion } from '../hooks/useReducedMotion';
interface AnimatedModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export default function AnimatedModal({
isOpen,
onClose,
children,
}: AnimatedModalProps) {
const prefersReducedMotion = useReducedMotion();
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
const modalVariants = prefersReducedMotion
? {
hidden: { opacity: 0 },
visible: { opacity: 1 },
}
: {
hidden: { opacity: 0, scale: 0.8, y: 50 },
visible: { opacity: 1, scale: 1, y: 0 },
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'
variants={backdropVariants}
initial='hidden'
animate='visible'
exit='hidden'
onClick={onClose}
>
<motion.div
className='bg-white rounded-lg p-6 max-w-md w-full mx-4'
variants={modalVariants}
initial='hidden'
animate='visible'
exit='hidden'
onClick={(e) => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
CSS-in-JS でのアニメーション実装
styled-components での実装
typescript// components/StyledAnimatedCard.tsx
import styled, { keyframes } from 'styled-components';
import { useState } from 'react';
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const StyledCard = styled.div<{ isVisible: boolean }>`
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
animation: ${({ isVisible }) =>
isVisible ? fadeIn : 'none'} 0.6s ease-out;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
@media (prefers-reduced-motion: reduce) {
animation: none;
transition: none;
&:hover {
transform: none;
}
}
`;
export default function StyledAnimatedCard() {
const [isVisible, setIsVisible] = useState(false);
return (
<StyledCard
isVisible={isVisible}
onMouseEnter={() => setIsVisible(true)}
>
<h3 className='text-lg font-bold mb-2'>
Styled Card
</h3>
<p className='text-gray-600'>
This card uses styled-components for animations.
</p>
</StyledCard>
);
}
カスタムフックの活用
アニメーション状態管理フック
typescript// hooks/useAnimationState.ts
import { useState, useCallback } from 'react';
type AnimationState =
| 'idle'
| 'loading'
| 'success'
| 'error';
export function useAnimationState() {
const [state, setState] =
useState<AnimationState>('idle');
const [isAnimating, setIsAnimating] = useState(false);
const startAnimation = useCallback(() => {
setIsAnimating(true);
setState('loading');
}, []);
const completeAnimation = useCallback(
(newState: 'success' | 'error') => {
setState(newState);
setIsAnimating(false);
// 3秒後にidle状態に戻る
setTimeout(() => {
setState('idle');
}, 3000);
},
[]
);
return {
state,
isAnimating,
startAnimation,
completeAnimation,
};
}
使用例
typescript// components/AnimatedButton.tsx
import { motion } from 'framer-motion';
import { useAnimationState } from '../hooks/useAnimationState';
export default function AnimatedButton() {
const {
state,
isAnimating,
startAnimation,
completeAnimation,
} = useAnimationState();
const handleClick = async () => {
startAnimation();
try {
// 非同期処理をシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 2000)
);
completeAnimation('success');
} catch (error) {
completeAnimation('error');
}
};
const buttonVariants = {
idle: { scale: 1 },
loading: { scale: 0.95 },
success: { scale: 1.1, backgroundColor: '#10b981' },
error: { scale: 1.1, backgroundColor: '#ef4444' },
};
return (
<motion.button
variants={buttonVariants}
animate={state}
onClick={handleClick}
disabled={isAnimating}
className='px-6 py-3 bg-blue-500 text-white rounded-lg disabled:opacity-50'
>
{state === 'loading' && 'Loading...'}
{state === 'success' && 'Success!'}
{state === 'error' && 'Error!'}
{state === 'idle' && 'Click me'}
</motion.button>
);
}
デバッグとトラブルシューティング
よくある問題と解決策
1. ハイドレーションエラー
typescript// ❌ エラーの原因
// Warning: Expected server HTML to contain a matching <div> in <div>
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(true);
}, []);
return <div>{isVisible && <AnimatedComponent />}</div>;
typescript// ✅ 解決策
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div>Loading...</div>;
}
return (
<div>
<AnimatedComponent />
</div>
);
2. メモリリーク
typescript// ❌ メモリリークの原因
useEffect(() => {
const interval = setInterval(() => {
// アニメーション更新処理
}, 16);
// クリーンアップ関数がない
}, []);
typescript// ✅ 解決策
useEffect(() => {
const interval = setInterval(() => {
// アニメーション更新処理
}, 16);
return () => {
clearInterval(interval);
};
}, []);
3. パフォーマンス問題
typescript// ❌ パフォーマンス問題の原因
const [items, setItems] = useState([]);
// 毎回新しいオブジェクトを作成
const animationConfig = {
duration: 0.3,
ease: [0.4, 0, 0.2, 1],
};
return (
<div>
{items.map((item) => (
<motion.div
key={item.id}
animate={{ opacity: 1 }}
transition={animationConfig} // 毎回新しいオブジェクト
>
{item.name}
</motion.div>
))}
</div>
);
typescript// ✅ 解決策
const [items, setItems] = useState([]);
// メモ化された設定
const animationConfig = useMemo(
() => ({
duration: 0.3,
ease: [0.4, 0, 0.2, 1],
}),
[]
);
return (
<div>
{items.map((item) => (
<motion.div
key={item.id}
animate={{ opacity: 1 }}
transition={animationConfig}
>
{item.name}
</motion.div>
))}
</div>
);
パフォーマンス監視ツールの活用
React DevTools Profiler
typescript// components/PerformanceMonitor.tsx
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: string,
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
});
}
export default function PerformanceMonitor({
children,
}: {
children: React.ReactNode;
}) {
return (
<Profiler id='AnimationApp' onRender={onRenderCallback}>
{children}
</Profiler>
);
}
カスタムパフォーマンスフック
typescript// hooks/usePerformanceMonitor.ts
import { useEffect, useRef } from 'react';
export function usePerformanceMonitor(
componentName: string
) {
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const currentTime = performance.now();
const timeSinceLastRender =
currentTime - lastRenderTime.current;
console.log(
`${componentName} render #${
renderCount.current
}: ${timeSinceLastRender.toFixed(2)}ms`
);
lastRenderTime.current = currentTime;
});
return {
renderCount: renderCount.current,
};
}
ブラウザ互換性の確保
Feature Detection
typescript// utils/browserSupport.ts
export function checkBrowserSupport() {
const support = {
webAnimations: 'animate' in Element.prototype,
intersectionObserver: 'IntersectionObserver' in window,
requestAnimationFrame:
'requestAnimationFrame' in window,
cssTransforms:
'transform' in document.documentElement.style,
};
return support;
}
export function getFallbackAnimation() {
const support = checkBrowserSupport();
if (!support.webAnimations) {
// CSS Transitionsを使用
return 'css-transitions';
}
if (!support.intersectionObserver) {
// Scrollイベントを使用
return 'scroll-events';
}
return 'modern';
}
Polyfill の活用
typescript// pages/_app.tsx
import { useEffect } from 'react';
export default function MyApp({ Component, pageProps }) {
useEffect(() => {
// Intersection Observer Polyfill
if (!('IntersectionObserver' in window)) {
import('intersection-observer').then(() => {
console.log(
'Intersection Observer polyfill loaded'
);
});
}
}, []);
return <Component {...pageProps} />;
}
まとめ
Next.js 時代のアニメーション設計は、従来の SPA とは異なるアプローチが必要です。SSR/SSG の特性を理解し、ハイドレーションエラーを回避しながら、パフォーマンスとユーザー体験を両立することが重要です。
この記事で紹介したベストプラクティスを実践することで、以下のような効果が期待できます:
- パフォーマンスの向上: バンドルサイズの最適化とレンダリング効率の改善
- ユーザビリティの向上: アクセシビリティ対応とユーザー設定の尊重
- 保守性の向上: 再利用可能なコンポーネントとカスタムフックの活用
- デバッグ効率の向上: 適切なエラーハンドリングとパフォーマンス監視
アニメーションは、ユーザー体験を劇的に向上させる強力なツールです。しかし、その実装には慎重な設計と配慮が必要です。Next.js の特性を活かしながら、ユーザーにとって心地よいアニメーションを実装していきましょう。
最後に、アニメーション実装において最も重要なのは「ユーザーを第一に考える」ことです。美しいアニメーションも、ユーザーが不快に感じるものであれば意味がありません。常にユーザーの立場に立ち、適切なアニメーション設計を心がけてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来