滑动切换
新旧内容以水平滑动方式交替切换,旧内容滑出左侧的同时新内容从右侧滑入。
交互预览
动效定义与代码
{
"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>"
}
]
}