返回动效库
离场进阶

折叠收起

元素高度从完整状态收缩至零并淡出,适合手风琴、FAQ 折叠面板等场景。

点击时350ms

交互预览

动效定义与代码

{
  "slug": "collapse",
  "name": "折叠收起",
  "nameEn": "Collapse",
  "description": "元素高度从完整状态收缩至零并淡出,适合手风琴、FAQ 折叠面板等场景。",
  "descriptionEn": "Element height shrinks to zero while fading out. Ideal for accordions, FAQ panels, and collapsible sections.",
  "category": "exit",
  "tags": [
    "collapse",
    "height",
    "exit",
    "accordion"
  ],
  "trigger": "on-click",
  "difficulty": "intermediate",
  "duration": "350ms",
  "easing": "cubic-bezier(0.4, 0, 0.2, 1)",
  "cssProperties": [
    "max-height",
    "opacity",
    "padding"
  ],
  "isGPUAccelerated": false,
  "previewBg": "light",
  "keywords": [
    "collapse",
    "accordion",
    "fold",
    "shrink height",
    "toggle",
    "expand"
  ],
  "useCases": [
    "Accordion panel collapse",
    "FAQ section toggle",
    "Expandable card content",
    "Sidebar section folding"
  ],
  "relatedAnimations": [
    "fade-out-down",
    "scale-out"
  ],
  "recommendedStyles": [
    "minimalist-flat",
    "corporate-clean"
  ],
  "codeSnippets": [
    {
      "label": "CSS Keyframes",
      "language": "css",
      "code": "@keyframes collapse {\n  from {\n    max-height: var(--sk-content-height, 200px);\n    opacity: 1;\n    padding-top: var(--sk-padding, 16px);\n    padding-bottom: var(--sk-padding, 16px);\n  }\n  to {\n    max-height: 0;\n    opacity: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n}\n\n.collapse {\n  --sk-duration: 350ms;\n  --sk-delay: 0ms;\n  --sk-ease: cubic-bezier(0.4, 0, 0.2, 1);\n  overflow: hidden;\n  animation: collapse var(--sk-duration) var(--sk-ease) var(--sk-delay) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .collapse {\n    animation: none;\n    max-height: 0;\n    opacity: 0;\n    padding: 0;\n  }\n}"
    },
    {
      "label": "Tailwind CSS",
      "language": "css",
      "code": "@keyframes collapse {\n  from {\n    max-height: var(--sk-content-height, 200px);\n    opacity: 1;\n    padding-top: var(--sk-padding, 16px);\n    padding-bottom: var(--sk-padding, 16px);\n  }\n  to {\n    max-height: 0;\n    opacity: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n}\n\n@utility animate-collapse {\n  --sk-duration: 350ms;\n  --sk-delay: 0ms;\n  --sk-ease: cubic-bezier(0.4, 0, 0.2, 1);\n  overflow: hidden;\n  animation: collapse var(--sk-duration) var(--sk-ease) var(--sk-delay) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .collapse {\n    animation: none;\n    max-height: 0;\n    opacity: 0;\n    padding: 0;\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 CollapsePanel({ children }: { children: ReactNode }) {\n  const [isOpen, setIsOpen] = useState(true);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const animationRef = useRef<AnimeAnimation | null>(null);\n\n  useEffect(() => {\n    return () => {\n      animationRef.current?.cancel();\n    };\n  }, []);\n\n  async function toggle() {\n    const panel = panelRef.current;\n    if (!panel) return;\n\n    const nextOpen = !isOpen;\n    setIsOpen(nextOpen);\n\n    if (prefersReducedMotion()) {\n      panel.style.height = nextOpen ? \"auto\" : \"0px\";\n      panel.style.opacity = nextOpen ? \"1\" : \"0\";\n      return;\n    }\n\n    const { animate } = await import(\"animejs\");\n    if (panelRef.current !== panel) return;\n\n    animationRef.current?.cancel();\n    const startHeight = panel.getBoundingClientRect().height;\n    const targetHeight = nextOpen ? panel.scrollHeight : 0;\n    panel.style.height = startHeight + \"px\";\n\n    const animation = animate(panel, {\n      height: [startHeight + \"px\", targetHeight + \"px\"],\n      opacity: [Number(getComputedStyle(panel).opacity), nextOpen ? 1 : 0],\n      duration: 350,\n      ease: \"inOut(3)\",\n      onComplete: () => {\n        panel.style.height = nextOpen ? \"auto\" : \"0px\";\n        panel.style.opacity = nextOpen ? \"1\" : \"0\";\n      },\n    });\n\n    animationRef.current = animation;\n  }\n\n  return (\n    <div>\n      <button type=\"button\" aria-expanded={isOpen} onClick={() => void toggle()}>\n        Toggle\n      </button>\n      <div ref={panelRef} className=\"overflow-hidden\">\n        {children}\n      </div>\n    </div>\n  );\n}"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "import { motion, AnimatePresence } from \"framer-motion\";\n\n<AnimatePresence>\n  {isOpen && (\n    <motion.div\n      initial={{ height: 0, opacity: 0 }}\n      animate={{ height: \"auto\", opacity: 1 }}\n      exit={{ height: 0, opacity: 0 }}\n      transition={{ duration: 0.35, ease: [0.4, 0, 0.2, 1] }}\n      style={{ overflow: \"hidden\" }}\n    >\n      Content\n    </motion.div>\n  )}\n</AnimatePresence>"
    }
  ]
}