css-animation-creator
プロフェッショナルなCSSアニメーション、トランジション、マイクロインタラクション、複雑なモーションデザインを作成します。アニメーションの追加、ホバーエフェクト、ローディング状態、ページ遷移、スクロールアニメーションなど、あらゆるモーションデザインの実装が必要な際に使用してください。
description の原文を見る
Create professional CSS animations, transitions, micro-interactions, and complex motion design. Use when adding animations, hover effects, loading states, page transitions, scroll animations, or any motion design work.
SKILL.md 本文
CSS Animation Creator
説明
アニメーションを作成する際:
- 目的を理解する - フィードバック、喜び、ガイダンス、またはストーリーテリング
- 適切な手法を選択する - CSS transitions、keyframes、または JS ライブラリ
- パフォーマンスを最適化する - GPU加速プロパティのみを使用
- アクセシビリティを尊重する - prefers-reduced-motion を遵守
- タイミングを自然に保つ - 適切なイージングと継続時間を使用
アニメーション原則
UI に適用された 12 の原則 (Disney)
| 原則 | UI 応用 |
|---|---|
| Squash & Stretch | ボタン押下、エラスティック効果 |
| Anticipation | ホバー状態と先行する動作 |
| Staging | 重要な要素に注目を集める |
| Follow Through | 過度な動きから落ち着き |
| Ease In/Out | 自然な加速・減速 |
| Arcs | 曲線的な動きのパス |
| Secondary Action | 補足的なアニメーション |
| Timing | 継続時間は重さ・重要性を表現 |
| Exaggeration | 明確化のための誇張 |
| Appeal | 心地よく洗練された動き |
タイミングガイドライン
| アニメーションタイプ | 継続時間 | イージング |
|---|---|---|
| Micro-interaction | 100-200ms | ease-out |
| ボタン/ホバー | 150-250ms | ease |
| モーダル オープン | 200-300ms | ease-out |
| モーダル クローズ | 150-200ms | ease-in |
| ページ遷移 | 300-500ms | ease-in-out |
| ローディング ループ | 1000-2000ms | linear/ease-in-out |
| 注目集中 | 500-1000ms | elastic |
CSS Transitions
基本構文
.element {
/* 単一プロパティ */
transition: opacity 0.3s ease;
/* 複数プロパティ */
transition:
transform 0.3s ease,
opacity 0.3s ease,
background-color 0.2s ease;
/* 短縮形: プロパティ 継続時間 タイミング関数 遅延 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
イージング関数
/* 組み込み */
transition-timing-function: linear;
transition-timing-function: ease; /* デフォルト - スロースタート、速い中間、スロー終了 */
transition-timing-function: ease-in; /* スロースタート */
transition-timing-function: ease-out; /* スロー終了 */
transition-timing-function: ease-in-out; /* スロースタートと終了 */
/* カスタム cubic-bezier */
/* Material Design 標準 */
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
/* Decelerate (entering) */
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
/* Accelerate (exiting) */
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
/* バウンス効果 */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Elastic */
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
Tailwind Transitions
// 継続時間
<div className="transition duration-150" /> // 150ms
<div className="transition duration-300" /> // 300ms
<div className="transition duration-500" /> // 500ms
// タイミング関数
<div className="transition ease-linear" />
<div className="transition ease-in" />
<div className="transition ease-out" />
<div className="transition ease-in-out" />
// 特定プロパティ (パフォーマンス向上)
<div className="transition-opacity" />
<div className="transition-transform" />
<div className="transition-colors" />
<div className="transition-shadow" />
<div className="transition-all" />
// 組み合わせ
<button className="transition-all duration-200 ease-out hover:scale-105 hover:shadow-lg">
Hover me
</button>
キーフレーム アニメーション
基本構文
@keyframes animationName {
0% { /* 開始状態 */ }
50% { /* 中間地点の状態 */ }
100% { /* 終了状態 */ }
}
.element {
animation: animationName 1s ease-in-out infinite;
/* 名前 | 継続時間 | タイミング | 反復回数 */
/* 完全な構文 */
animation-name: animationName;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite; /* または数字 */
animation-direction: normal; /* reverse, alternate, alternate-reverse */
animation-fill-mode: forwards; /* none, forwards, backwards, both */
animation-play-state: running; /* paused */
}
必須アニメーション ライブラリ
フェードアニメーション
/* Fade In */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Fade In Up */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fade In Down */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fade In Left */
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Fade In Right */
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Fade In Scale */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Fade Out */
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
スケール アニメーション
/* Scale In */
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
/* Scale In Bounce */
@keyframes scaleInBounce {
0% { transform: scale(0); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Pop */
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Pulse */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Heartbeat */
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.3); }
28% { transform: scale(1); }
42% { transform: scale(1.3); }
70% { transform: scale(1); }
}
バウンス アニメーション
/* Bounce */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-20px); }
60% { transform: translateY(-10px); }
}
/* Bounce In */
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
/* Bounce In Down */
@keyframes bounceInDown {
0% {
opacity: 0;
transform: translateY(-100px);
}
60% {
opacity: 1;
transform: translateY(20px);
}
80% { transform: translateY(-10px); }
100% { transform: translateY(0); }
}
/* Rubber Band */
@keyframes rubberBand {
0% { transform: scaleX(1); }
30% { transform: scaleX(1.25) scaleY(0.75); }
40% { transform: scaleX(0.75) scaleY(1.25); }
50% { transform: scaleX(1.15) scaleY(0.85); }
65% { transform: scaleX(0.95) scaleY(1.05); }
75% { transform: scaleX(1.05) scaleY(0.95); }
100% { transform: scaleX(1) scaleY(1); }
}
回転アニメーション
/* Spin */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Spin Reverse */
@keyframes spinReverse {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
/* Swing */
@keyframes swing {
20% { transform: rotate(15deg); }
40% { transform: rotate(-10deg); }
60% { transform: rotate(5deg); }
80% { transform: rotate(-5deg); }
100% { transform: rotate(0deg); }
}
/* Wobble */
@keyframes wobble {
0% { transform: translateX(0); }
15% { transform: translateX(-15px) rotate(-5deg); }
30% { transform: translateX(12px) rotate(3deg); }
45% { transform: translateX(-9px) rotate(-3deg); }
60% { transform: translateX(6px) rotate(2deg); }
75% { transform: translateX(-3px) rotate(-1deg); }
100% { transform: translateX(0); }
}
/* Flip */
@keyframes flipX {
0% { transform: perspective(400px) rotateX(90deg); opacity: 0; }
40% { transform: perspective(400px) rotateX(-20deg); }
60% { transform: perspective(400px) rotateX(10deg); opacity: 1; }
80% { transform: perspective(400px) rotateX(-5deg); }
100% { transform: perspective(400px) rotateX(0deg); }
}
スライド アニメーション
/* Slide In Up */
@keyframes slideInUp {
from {
transform: translateY(100%);
visibility: visible;
}
to { transform: translateY(0); }
}
/* Slide In Down */
@keyframes slideInDown {
from {
transform: translateY(-100%);
visibility: visible;
}
to { transform: translateY(0); }
}
/* Slide In Left */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
visibility: visible;
}
to { transform: translateX(0); }
}
/* Slide In Right */
@keyframes slideInRight {
from {
transform: translateX(100%);
visibility: visible;
}
to { transform: translateX(0); }
}
/* Slide Out */
@keyframes slideOutUp {
from { transform: translateY(0); }
to {
transform: translateY(-100%);
visibility: hidden;
}
}
注目アニメーション
/* Shake */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
/* Shake Horizontal (強い) */
@keyframes shakeX {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
/* Jello */
@keyframes jello {
0%, 11.1%, 100% { transform: none; }
22.2% { transform: skewX(-12.5deg) skewY(-12.5deg); }
33.3% { transform: skewX(6.25deg) skewY(6.25deg); }
44.4% { transform: skewX(-3.125deg) skewY(-3.125deg); }
55.5% { transform: skewX(1.5625deg) skewY(1.5625deg); }
66.6% { transform: skewX(-0.78125deg) skewY(-0.78125deg); }
77.7% { transform: skewX(0.390625deg) skewY(0.390625deg); }
88.8% { transform: skewX(-0.1953125deg) skewY(-0.1953125deg); }
}
/* Flash */
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}
/* Tada */
@keyframes tada {
0% { transform: scale(1) rotate(0); }
10%, 20% { transform: scale(0.9) rotate(-3deg); }
30%, 50%, 70%, 90% { transform: scale(1.1) rotate(3deg); }
40%, 60%, 80% { transform: scale(1.1) rotate(-3deg); }
100% { transform: scale(1) rotate(0); }
}
ローディング アニメーション
スピナー
// シンプルなスピナー
<div className="w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin" />
// デュアルリング
<div className="relative w-12 h-12">
<div className="absolute inset-0 border-4 border-blue-200 rounded-full" />
<div className="absolute inset-0 border-4 border-transparent border-t-blue-600 rounded-full animate-spin" />
</div>
// グラデーション スピナー
<div className="w-10 h-10 rounded-full animate-spin"
style={{
background: 'conic-gradient(from 0deg, transparent, #3b82f6)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px))'
}}
/>
/* パルスするリング */
@keyframes pingRing {
0% {
transform: scale(1);
opacity: 1;
}
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
.ping-ring {
position: relative;
}
.ping-ring::before {
content: '';
position: absolute;
inset: 0;
border: 2px solid currentColor;
border-radius: 50%;
animation: pingRing 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
}
ドット ローディング
// バウンスするドット
<div className="flex gap-1">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.3s]" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.15s]" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" />
</div>
// パルスするドット
<div className="flex gap-1">
{[0, 1, 2].map((i) => (
<div
key={i}
className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"
style={{ animationDelay: `${i * 0.15}s` }}
/>
))}
</div>
/* スケールするドット */
@keyframes dotScale {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.dot-loader {
display: flex;
gap: 4px;
}
.dot-loader span {
width: 8px;
height: 8px;
background: currentColor;
border-radius: 50%;
animation: dotScale 1.4s ease-in-out infinite;
}
.dot-loader span:nth-child(1) { animation-delay: -0.32s; }
.dot-loader span:nth-child(2) { animation-delay: -0.16s; }
.dot-loader span:nth-child(3) { animation-delay: 0s; }
スケルトン ローダー
// 基本的なスケルトン
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
// カード スケルトン
<div className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-t-lg" />
<div className="p-4 space-y-3">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
</div>
// シマー効果
<div className="relative overflow-hidden bg-gray-200 rounded">
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/60 to-transparent" />
</div>
/* シマー キーフレーム */
@keyframes shimmer {
100% { transform: translateX(100%); }
}
プログレス バー
// 不確定なプログレス
<div className="h-1 w-full bg-gray-200 rounded overflow-hidden">
<div className="h-full bg-blue-600 w-1/3 animate-[progress_1s_ease-in-out_infinite]" />
</div>
// ストライプ プログレス
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-blue-600 transition-all duration-300"
style={{
width: `${progress}%`,
backgroundImage: 'linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%)',
backgroundSize: '1rem 1rem',
animation: 'progress-stripes 1s linear infinite'
}}
/>
</div>
@keyframes progress {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
@keyframes progress-stripes {
from { background-position: 1rem 0; }
to { background-position: 0 0; }
}
マイクロ インタラクション
ボタン エフェクト
// プレス効果
<button className="transition-transform duration-100 active:scale-95">
Click me
</button>
// リップル効果 (React)
function RippleButton({ children, ...props }) {
const [ripples, setRipples] = useState([]);
const handleClick = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setRipples([...ripples, { x, y, id: Date.now() }]);
setTimeout(() => setRipples(r => r.slice(1)), 600);
};
return (
<button className="relative overflow-hidden" onClick={handleClick} {...props}>
{ripples.map(ripple => (
<span
key={ripple.id}
className="absolute bg-white/30 rounded-full animate-[ripple_0.6s_ease-out]"
style={{
left: ripple.x,
top: ripple.y,
transform: 'translate(-50%, -50%)'
}}
/>
))}
{children}
</button>
);
}
@keyframes ripple {
from {
width: 0;
height: 0;
opacity: 0.5;
}
to {
width: 200px;
height: 200px;
opacity: 0;
}
}
ホバー エフェクト
// リフト効果
<div className="transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
Card content
</div>
// グロー効果
<button className="transition-shadow duration-300 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)]">
Glow button
</button>
// ボーダー アニメーション
<div className="relative group">
<div className="absolute -inset-0.5 bg-gradient-to-r from-pink-600 to-purple-600 rounded-lg blur opacity-0 group-hover:opacity-75 transition duration-300" />
<div className="relative bg-white rounded-lg p-6">Content</div>
</div>
// アンダーライン アニメーション
<a className="relative inline-block after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-current after:transition-all after:duration-300 hover:after:w-full">
Animated link
</a>
// フィル アニメーション
<a className="relative overflow-hidden group">
<span className="relative z-10 transition-colors duration-300 group-hover:text-white">
Hover me
</span>
<span className="absolute inset-0 bg-blue-600 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
</a>
アイコン アニメーション
// ホバー時に回転
<button className="group">
<SettingsIcon className="transition-transform duration-500 group-hover:rotate-180" />
</button>
// ホバー時にバウンス
<button className="group">
<ArrowIcon className="transition-transform group-hover:translate-x-1 group-hover:animate-bounce" />
</button>
// スケール + 回転
<button className="group">
<PlusIcon className="transition-all duration-300 group-hover:scale-110 group-hover:rotate-90" />
</button>
フォーム インタラクション
// 入力フォーカス効果
<div className="relative">
<input
className="peer w-full border-b-2 border-gray-300 focus:border-blue-600 outline-none py-2 transition-colors"
placeholder=" "
/>
<label className="absolute left-0 top-2 text-gray-500 transition-all peer-focus:-top-4 peer-focus:text-sm peer-focus:text-blue-600 peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-sm">
Email
</label>
</div>
// チェックボックス アニメーション
<label className="flex items-center gap-2 cursor-pointer">
<div className="relative">
<input type="checkbox" className="peer sr-only" />
<div className="w-5 h-5 border-2 rounded transition-colors peer-checked:bg-blue-600 peer-checked:border-blue-600" />
<CheckIcon className="absolute inset-0 m-auto w-3 h-3 text-white opacity-0 scale-0 transition-all peer-checked:opacity-100 peer-checked:scale-100" />
</div>
Label text
</label>
// トグル スイッチ
<button
role="switch"
aria-checked={enabled}
onClick={() => setEnabled(!enabled)}
className={cn(
"relative w-11 h-6 rounded-full transition-colors",
enabled ? "bg-blue-600" : "bg-gray-200"
)}
>
<span className={cn(
"absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform",
enabled && "translate-x-5"
)} />
</button>
成功/エラー状態
// 成功チェックマーク
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
<svg className="w-8 h-8 text-green-600" viewBox="0 0 24 24">
<path
className="animate-[draw_0.5s_ease-out_forwards]"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="24"
strokeDashoffset="24"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
// エラー シェーク
<input className="animate-[shake_0.5s_ease-in-out] border-red-500" />
@keyframes draw {
to { stroke-dashoffset: 0; }
}
ページ遷移
CSS のみの遷移
/* View Transitions API (Chrome 111+) */
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: fadeOut 0.3s ease-out;
}
::view-transition-new(root) {
animation: fadeIn 0.3s ease-in;
}
/* 特定要素の遷移 */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 0.5s;
}
Framer Motion
import { motion, AnimatePresence } from 'framer-motion';
// フェード遷移
const pageVariants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
function PageWrapper({ children }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
// スライド遷移
const slideVariants = {
initial: { opacity: 0, x: 20 },
animate: { opacity: 1, x: 0 },
exit: { opacity: 0, x: -20 },
};
// スケール + フェード
const scaleVariants = {
initial: { opacity: 0, scale: 0.95 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 1.05 },
};
// 共有レイアウト アニメーション
function Gallery({ items, selectedId }) {
return (
<>
{items.map(item => (
<motion.div key={item.id} layoutId={item.id}>
<img src={item.src} />
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div layoutId={selectedId} className="modal">
<img src={items.find(i => i.id === selectedId).src} />
</motion.div>
)}
</AnimatePresence>
</>
);
}
スタッガー アニメーション
// Framer Motion スタッガー
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
// CSS スタッガー (カスタムプロパティ使用)
<ul className="stagger-list">
{items.map((item, i) => (
<li
key={item.id}
style={{ '--i': i } as React.CSSProperties}
className="animate-fadeInUp opacity-0"
>
{item.name}
</li>
))}
</ul>
.stagger-list li {
animation: fadeInUp 0.5s ease forwards;
animation-delay: calc(var(--i) * 0.1s);
}
スクロール アニメーション
Intersection Observer
function useInView(options = {}) {
const ref = useRef(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
if (options.once) observer.disconnect();
}
}, { threshold: 0.1, ...options });
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return [ref, isInView];
}
// 使用方法
function AnimatedSection() {
const [ref, isInView] = useInView({ once: true });
return (
<div
ref={ref}
className={cn(
"transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
)}
>
Content
</div>
);
}
Framer Motion でスクロール トリガー
import { motion, useScroll, useTransform } from 'framer-motion';
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
return (
<motion.div ref={ref} style={{ y, opacity }}>
Parallax content
</motion.div>
);
}
// スクロール リンク プログレス
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}
CSS スクロール駆動アニメーション
/* ネイティブ スクロール駆動アニメーション (Chrome 115+) */
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
/* スクロール プログレス インジケータ */
.progress-bar {
transform-origin: left;
animation: grow linear;
animation-timeline: scroll();
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
アクセシビリティ
縮減モーション
/* グローバル縮減モーション */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* 要素ごとの制御 */
.animated-element {
animation: bounce 1s infinite;
}
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none;
}
}
// Tailwind motion-safe/motion-reduce
<div className="motion-safe:animate-bounce motion-reduce:animate-none">
ユーザー設定を尊重
</div>
// React フック
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mediaQuery.matches);
const handler = (e) => setPrefersReducedMotion(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return prefersReducedMotion;
}
// 使用方法
function AnimatedComponent() {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<motion.div
animate={{ x: 100 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.3
}}
/>
);
}
Tailwind アニメーション設定
// tailwind.config.js
module.exports = {
theme: {
extend: {
animation: {
// フェード
'fade-in': 'fadeIn 0.5s ease forwards',
'fade-in-up': 'fadeInUp 0.5s ease forwards',
'fade-in-down': 'fadeInDown 0.5s ease forwards',
'fade-out': 'fadeOut 0.3s ease forwards',
// スライド
'slide-in-left': 'slideInLeft 0.3s ease-out',
'slide-in-right': 'slideInRight 0.3s ease-out',
'slide-in-up': 'slideInUp 0.3s ease-out',
'slide-in-down': 'slideInDown 0.3s ease-out',
// スケール
'scale-in': 'scaleIn 0.2s ease-out',
'pop': 'pop 0.3s ease-out',
// 注目
'shake': 'shake 0.5s ease-in-out',
'wiggle': 'wiggle 1s ease-in-out infinite',
'heartbeat': 'heartbeat 1.5s ease-in-out infinite',
// ローディング
'shimmer': 'shimmer 2s infinite',
'progress': 'progress 1s ease-in-out infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeInDown: {
'0%': { opacity: '0', transform: 'translateY(-20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideInLeft: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(0)' },
},
slideInRight: {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
slideInUp: {
'0%': { transform: 'translateY(100%)' },
'100%': { transform: 'translateY(0)' },
},
slideInDown: {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(0)' },
},
scaleIn: {
'0%': { transform: 'scale(0)' },
'100%': { transform: 'scale(1)' },
},
pop: {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.1)' },
'100%': { transform: 'scale(1)' },
},
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-5px)' },
'20%, 40%, 60%, 80%': { transform: 'translateX(5px)' },
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' },
},
heartbeat: {
'0%, 100%': { transform: 'scale(1)' },
'14%': { transform: 'scale(1.3)' },
'28%': { transform: 'scale(1)' },
'42%': { transform: 'scale(1.3)' },
'70%': { transform: 'scale(1)' },
},
shimmer: {
'100%': { transform: 'translateX(100%)' },
},
progress: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(400%)' },
},
},
},
},
};
パフォーマンス ベストプラクティス
GPU加速プロパティ
/* 良い例 - GPU加速 */
transform: translateX(100px);
transform: scale(1.1);
transform: rotate(45deg);
opacity: 0.5;
/* 悪い例 - レイアウト/ペイントをトリガー */
left: 100px;
top: 50px;
width: 200px;
height: 100px;
margin: 20px;
padding: 10px;
border-width: 2px;
font-size: 16px;
will-change (控えめに使用)
/* 複雑なアニメーションが必要な場合のみ */
.complex-animation {
will-change: transform, opacity;
}
/* アニメーション後に削除 */
.complex-animation.done {
will-change: auto;
}
Contain によるアイソレーション
.animated-section {
contain: layout style paint;
}
アニメーション パフォーマンス チェックリスト
-
transformとopacityのみをアニメーション化 -
will-changeは必要な時のみ使用 - UI フィードバックのアニメーションは 300ms 以下
- 低スペック デバイスでテスト
- 分離されたセクションで
containを使用 - スクロール時のアニメーション削減
- 画面外のアニメーションを一時停止
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- onewave-ai
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/onewave-ai/claude-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。