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

弹性回弹

元素被拉伸后像橡皮筋一样弹性回弹到原位,充满物理质感的趣味交互。

点击时800ms

交互预览

动效定义与代码

{
  "slug": "elastic-snap",
  "name": "弹性回弹",
  "nameEn": "Elastic Snap",
  "description": "元素被拉伸后像橡皮筋一样弹性回弹到原位,充满物理质感的趣味交互。",
  "descriptionEn": "Element stretches and snaps back like a rubber band with satisfying elastic physics.",
  "category": "micro-interaction",
  "tags": [
    "elastic",
    "spring",
    "rubber",
    "physics",
    "snap"
  ],
  "trigger": "on-click",
  "difficulty": "intermediate",
  "duration": "800ms",
  "easing": "cubic-bezier(0.68, -0.55, 0.27, 1.55)",
  "cssProperties": [
    "transform"
  ],
  "isGPUAccelerated": true,
  "previewBg": "light",
  "keywords": [
    "elastic",
    "spring",
    "rubber band",
    "snap",
    "physics",
    "stretch"
  ],
  "useCases": [
    "Button press feedback",
    "Toggle switch animation",
    "Pull-to-refresh indicator",
    "Game UI interactions"
  ],
  "relatedAnimations": [
    "bounce-in",
    "shake"
  ],
  "recommendedStyles": [
    "playful-pop",
    "soft-ui",
    "neo-brutalist"
  ],
  "codeSnippets": [
    {
      "label": "CSS Keyframes",
      "language": "css",
      "code": "@keyframes elastic-snap {\n  0% {\n    transform: scale(1);\n  }\n  30% {\n    transform: scaleX(1.2) scaleY(0.8);\n  }\n  50% {\n    transform: scaleX(0.9) scaleY(1.1);\n  }\n  70% {\n    transform: scaleX(1.05) scaleY(0.95);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n\n.elastic-snap {\n  will-change: transform;\n  animation: elastic-snap 800ms cubic-bezier(0.68, -0.55, 0.27, 1.55);\n}\n\n/* Trigger on click via JS: element.classList.add(\"elastic-snap\") */\n\n@media (prefers-reduced-motion: reduce) {\n  .elastic-snap {\n    animation: none;\n  }\n}"
    },
    {
      "label": "Tailwind CSS",
      "language": "css",
      "code": "/* Add to your global CSS or Tailwind v4 theme */\n@keyframes elastic-snap {\n  0% {\n    transform: scale(1);\n  }\n  30% {\n    transform: scaleX(1.2) scaleY(0.8);\n  }\n  50% {\n    transform: scaleX(0.9) scaleY(1.1);\n  }\n  70% {\n    transform: scaleX(1.05) scaleY(0.95);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n\n@utility animate-elastic-snap {\n  will-change: transform;\n  animation: elastic-snap 800ms cubic-bezier(0.68, -0.55, 0.27, 1.55);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .elastic-snap {\n    animation: none;\n  }\n}"
    },
    {
      "label": "AnimeJS",
      "language": "tsx",
      "code": "\"use client\";\n\nimport { useEffect, useRef, 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 ElasticSnapButton({ children }: { children: ReactNode }) {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const animationRef = useRef<AnimeAnimation | null>(null);\n\n  useEffect(() => {\n    return () => {\n      animationRef.current?.cancel();\n    };\n  }, []);\n\n  async function triggerSnap() {\n    const button = buttonRef.current;\n    if (!button || prefersReducedMotion()) return;\n\n    const { animate } = await import(\"animejs\");\n    if (buttonRef.current !== button) return;\n\n    animationRef.current?.cancel();\n    button.style.transform = \"\";\n\n    animationRef.current = animate(button, {\n      scaleX: [1, 1.25, 0.9, 1.08, 0.97, 1],\n      scaleY: [1, 0.9, 1.08, 0.96, 1.02, 1],\n      duration: 800,\n      ease: \"outElastic(1, .55)\",\n    });\n  }\n\n  return (\n    <button\n      ref={buttonRef}\n      type=\"button\"\n      onClick={() => void triggerSnap()}\n      className=\"will-change-transform\"\n    >\n      {children}\n    </button>\n  );\n}"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "import { motion } from \"framer-motion\";\n\n// Tip: respect user's motion preferences\n// import { useReducedMotion } from \"framer-motion\";\n// const reduced = useReducedMotion();\n// Skip elastic animation entirely when reduced is true\n\n<motion.button\n  whileTap={{\n    scaleX: [1, 1.2, 0.9, 1.05, 1],\n    scaleY: [1, 0.8, 1.1, 0.95, 1],\n  }}\n  transition={{\n    duration: 0.8,\n    ease: [0.68, -0.55, 0.27, 1.55],\n    times: [0, 0.3, 0.5, 0.7, 1],\n  }}\n>\n  Click me\n</motion.button>"
    }
  ]
}