本页出处
设计风格资料库
内容来自 luckymaomi 的 UI 风格库(复刻版),以 MIT 许可 提供。本站镜像收录,仅作学习参考;这一页的原始地址是 原站页面。
← 回到导览访问原站仓库
返回动效库
转场进阶

交叉淡化

两个元素交叉切换:一个淡出的同时另一个淡入,实现平滑的内容过渡。

手动触发500ms

交互预览

动效定义与代码

{
  "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>"
    }
  ]
}