交叉淡化
两个元素交叉切换:一个淡出的同时另一个淡入,实现平滑的内容过渡。
交互预览
动效定义与代码
{
"slug": "crossfade",
"name": "交叉淡化",
"nameEn": "Crossfade",
"description": "两个元素交叉切换:一个淡出的同时另一个淡入,实现平滑的内容过渡。",
"descriptionEn": "Two elements cross-dissolve: one fades out while the other fades in, creating a smooth content transition.",
"category": "transition",
"tags": [
"fade",
"crossfade",
"transition",
"swap"
],
"trigger": "manual",
"difficulty": "intermediate",
"duration": "500ms",
"easing": "ease-in-out",
"cssProperties": [
"opacity"
],
"isGPUAccelerated": true,
"previewBg": "light",
"keywords": [
"crossfade",
"dissolve",
"swap",
"transition",
"toggle",
"tab switch"
],
"useCases": [
"Tab content switching",
"Image carousel transition",
"Content toggle animation",
"Step wizard transition"
],
"relatedAnimations": [
"slide-swap",
"blur-in",
"morph-transition"
],
"recommendedStyles": [
"minimalist-flat",
"glassmorphism",
"soft-ui"
],
"codeSnippets": [
{
"label": "CSS Keyframes",
"language": "css",
"code": "@keyframes crossfade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes crossfade-out {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n.crossfade-container {\n position: relative;\n}\n\n.crossfade-enter {\n --sk-duration: 500ms;\n --sk-ease: ease-in-out;\n animation: crossfade-in var(--sk-duration) var(--sk-ease) both;\n}\n\n.crossfade-exit {\n --sk-duration: 500ms;\n --sk-ease: ease-in-out;\n position: absolute;\n inset: 0;\n animation: crossfade-out var(--sk-duration) var(--sk-ease) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .crossfade-enter,\n .crossfade-exit {\n animation: none;\n }\n}"
},
{
"label": "Tailwind CSS",
"language": "css",
"code": "@keyframes crossfade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes crossfade-out {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@utility animate-crossfade-in {\n --sk-duration: 500ms;\n --sk-ease: ease-in-out;\n animation: crossfade-in var(--sk-duration) var(--sk-ease) both;\n}\n\n@utility animate-crossfade-out {\n --sk-duration: 500ms;\n --sk-ease: ease-in-out;\n position: absolute;\n inset: 0;\n animation: crossfade-out var(--sk-duration) var(--sk-ease) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .crossfade-enter,\n .crossfade-exit {\n animation: none;\n }\n}"
},
{
"label": "AnimeJS",
"language": "tsx",
"code": "\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\ntype AnimeAnimation = {\n cancel(): unknown;\n};\n\nfunction prefersReducedMotion() {\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\nexport function CrossfadeTabs({ panels }: { panels: ReactNode[] }) {\n const [active, setActive] = useState(0);\n const [exiting, setExiting] = useState<number | null>(null);\n const activeRef = useRef<HTMLDivElement>(null);\n const exitingRef = useRef<HTMLDivElement>(null);\n const activeAnimationRef = useRef<AnimeAnimation | null>(null);\n const exitingAnimationRef = useRef<AnimeAnimation | null>(null);\n\n useEffect(() => {\n return () => {\n activeAnimationRef.current?.cancel();\n exitingAnimationRef.current?.cancel();\n };\n }, []);\n\n useEffect(() => {\n const activeElement = activeRef.current;\n if (!activeElement || prefersReducedMotion()) return;\n\n let cancelled = false;\n\n async function run() {\n const { animate } = await import(\"animejs\");\n if (cancelled || activeRef.current !== activeElement) return;\n\n activeAnimationRef.current?.cancel();\n activeAnimationRef.current = animate(activeElement, {\n opacity: [0, 1],\n duration: 500,\n ease: \"inOut(2)\",\n });\n\n const exitingElement = exitingRef.current;\n if (!exitingElement) return;\n\n exitingAnimationRef.current?.cancel();\n const animation = animate(exitingElement, {\n opacity: [1, 0],\n duration: 500,\n ease: \"inOut(2)\",\n onComplete: () => setExiting(null),\n });\n exitingAnimationRef.current = animation;\n }\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [active, exiting]);\n\n function select(nextActive: number) {\n if (nextActive === active) return;\n if (prefersReducedMotion()) {\n setExiting(null);\n setActive(nextActive);\n return;\n }\n if (exiting !== null) return;\n setExiting(active);\n setActive(nextActive);\n }\n\n return (\n <div>\n {panels.map((_, index) => (\n <button key={index} type=\"button\" onClick={() => select(index)}>\n Tab {index + 1}\n </button>\n ))}\n <div className=\"relative overflow-hidden\">\n {exiting !== null && (\n <div ref={exitingRef} className=\"absolute inset-0\">\n {panels[exiting]}\n </div>\n )}\n <div ref={activeRef}>{panels[active]}</div>\n </div>\n </div>\n );\n}"
},
{
"label": "Framer Motion",
"language": "tsx",
"code": "import { motion, AnimatePresence } from \"framer-motion\";\n\n<AnimatePresence mode=\"wait\">\n <motion.div\n key={activeTab}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.5, ease: \"easeInOut\" }}\n >\n {content}\n </motion.div>\n</AnimatePresence>"
}
]
}