弹性回弹
元素被拉伸后像橡皮筋一样弹性回弹到原位,充满物理质感的趣味交互。
交互预览
动效定义与代码
{
"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>"
}
]
}