返回动效库
转场进阶

滑动切换

新旧内容以水平滑动方式交替切换,旧内容滑出左侧的同时新内容从右侧滑入。

手动触发400ms

交互预览

动效定义与代码

{
  "slug": "slide-swap",
  "name": "滑动切换",
  "nameEn": "Slide Swap",
  "description": "新旧内容以水平滑动方式交替切换,旧内容滑出左侧的同时新内容从右侧滑入。",
  "descriptionEn": "Content swaps with a horizontal slide: old slides out left while new slides in from the right.",
  "category": "transition",
  "tags": [
    "slide",
    "swap",
    "transition",
    "horizontal"
  ],
  "trigger": "manual",
  "difficulty": "intermediate",
  "duration": "400ms",
  "easing": "cubic-bezier(0.16, 1, 0.3, 1)",
  "cssProperties": [
    "opacity",
    "transform"
  ],
  "isGPUAccelerated": true,
  "previewBg": "light",
  "keywords": [
    "slide swap",
    "horizontal transition",
    "tab slide",
    "page transition",
    "carousel"
  ],
  "useCases": [
    "Step wizard navigation",
    "Carousel slide transition",
    "Tab panel switching",
    "Onboarding flow steps"
  ],
  "relatedAnimations": [
    "crossfade",
    "slide-in-left",
    "slide-in-right"
  ],
  "recommendedStyles": [
    "minimalist-flat",
    "corporate-clean"
  ],
  "codeSnippets": [
    {
      "label": "CSS Keyframes",
      "language": "css",
      "code": "@keyframes slide-in-from-right {\n  from {\n    opacity: 0;\n    transform: translateX(30px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slide-out-to-left {\n  from {\n    opacity: 1;\n    transform: translateX(0);\n  }\n  to {\n    opacity: 0;\n    transform: translateX(-30px);\n  }\n}\n\n.slide-swap-enter {\n  --sk-duration: 400ms;\n  --sk-ease: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n  animation: slide-in-from-right var(--sk-duration) var(--sk-ease) both;\n}\n\n.slide-swap-exit {\n  --sk-duration: 400ms;\n  --sk-ease: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n  position: absolute;\n  inset: 0;\n  animation: slide-out-to-left var(--sk-duration) var(--sk-ease) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .slide-swap-enter,\n  .slide-swap-exit {\n    animation: none;\n  }\n}"
    },
    {
      "label": "Tailwind CSS",
      "language": "css",
      "code": "@keyframes slide-in-from-right {\n  from { opacity: 0; transform: translateX(30px); }\n  to { opacity: 1; transform: translateX(0); }\n}\n\n@keyframes slide-out-to-left {\n  from { opacity: 1; transform: translateX(0); }\n  to { opacity: 0; transform: translateX(-30px); }\n}\n\n@utility animate-slide-swap-in {\n  --sk-duration: 400ms;\n  --sk-ease: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n  animation: slide-in-from-right var(--sk-duration) var(--sk-ease) both;\n}\n\n@utility animate-slide-swap-out {\n  --sk-duration: 400ms;\n  --sk-ease: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n  animation: slide-out-to-left var(--sk-duration) var(--sk-ease) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .slide-swap-enter,\n  .slide-swap-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 SlideSwap({ items }: { items: 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        x: [30, 0],\n        opacity: [0, 1],\n        duration: 400,\n        ease: \"out(4)\",\n      });\n\n      const exitingElement = exitingRef.current;\n      if (!exitingElement) return;\n\n      exitingAnimationRef.current?.cancel();\n      const animation = animate(exitingElement, {\n        x: [0, -30],\n        opacity: [1, 0],\n        duration: 400,\n        ease: \"out(4)\",\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 next() {\n    const nextActive = (active + 1) % items.length;\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      <div className=\"relative overflow-hidden\">\n        {exiting !== null && (\n          <div ref={exitingRef} className=\"absolute inset-0\">\n            {items[exiting]}\n          </div>\n        )}\n        <div ref={activeRef}>{items[active]}</div>\n      </div>\n      <button type=\"button\" onClick={next}>\n        Next\n      </button>\n    </div>\n  );\n}"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "import { motion, AnimatePresence } from \"framer-motion\";\n\nconst variants = {\n  enter: { x: 30, opacity: 0 },\n  center: { x: 0, opacity: 1 },\n  exit: { x: -30, opacity: 0 },\n};\n\n<AnimatePresence mode=\"wait\">\n  <motion.div\n    key={step}\n    variants={variants}\n    initial=\"enter\"\n    animate=\"center\"\n    exit=\"exit\"\n    transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}\n  >\n    {content}\n  </motion.div>\n</AnimatePresence>"
    }
  ]
}