{"slug": "squash-and-stretch", "title": "Squash and Stretch", "summary": "The article discusses the \"squash and stretch\" principle from Disney's 12 Basic Principles of Animation and how it can be applied to web development, specifically for animating SVG icons. The author demonstrates this technique using a stretchy arrow icon that elongates and thins on hover, providing code examples and implementation details using CSS transitions or JavaScript. The post emphasizes that this animation principle creates visually pleasing micro-interactions that enhance user experience on websites.", "body_md": "[Introduction]\n\nHave you ever heard of [Disney’s 12 Basic Principles of Animation(opens in new tab)](https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation)?\n\nIt’s a collection of animation best practices created in 1981 by two Disney animators, intended to be used by the folks who produce animated cartoon movies like *Aladdin* or *Beauty And The Beast.*\n\nNot all of the rules are relevant to us, as web developers, but some of them are incredibly useful. In this blog post, I want to share my favourite rule, and show some of the ways I use it in my projects!\n\nIt’s the very first rule, “squash and stretch”. Let’s start with the most common example, a bouncing ball. **Check out what happens as you drag the slider:**\n\nAs you increase the “Squash/Stretch Amount”, the ball starts to flatten like a water balloon when it hits the ground, threatening to burst. As it bounces back up, it elongates, becoming long and skinny.\n\nThis bouncing ball demo is useful in showcasing the general idea: there’s something visually pleasant about an object getting squashed or stretched during motion. In practice, I find myself using this trick a lot more on SVG icons than with bouncing balls, so that’s what we’ll focus on in this tutorial (though I’ll include a full playground for the bouncing ball at the end, for anyone curious!).\n\n[Link to this heading](#stretchy-arrows-1)Stretchy arrows\n\nOn this blog, I have lots of little SVG icons that have subtle micro-interactions. For example, on [the homepage](/), I have little arrows that stretch on hover:\n\nThe arrows aren’t *just* getting longer; to really sell the effect, the tips of the arrow get pulled in slightly, as though the arrow was getting *thinner* as it gets longer.\n\nHere’s a side-by-side comparison showing this effect with/without the stretch effect. Tap each arrow to trigger the effect:Hover over each arrow to see the difference:Trigger each arrow by focusing and pressing `Enter`\n\nWithout Squeeze\n\nWith Squeeze\n\nIsn’t it *so much nicer* with the squeeze? 😄\n\nThings happen pretty quick in this animation, so here’s another demo that lets you scrub through the full stretch effect:\n\nLet’s look at how we can create effects like this!\n\n[Link to this heading](#implementing-stretchy-arrows-2)Implementing stretchy arrows\n\nFirst, we need an icon to work with. My favourite icon pack these days is [ Lucide(opens in new tab)](https://lucide.dev/), a fork of the legendary Feather Icons pack, which adds >1000 new icons in the same lovely style.\n\nSo, let’s grab the [ arrow-right](https://lucide.dev/icons/arrow-right) icon from Lucide, downloading it as an SVG. After reformatting the code and removing unnecessary attributes like\n\n`xmlns`\n\n, here’s what we’re left with:\n\n```\n<svg\n  width=\"24\"\n  height=\"24\"\n  viewBox=\"0 0 24 24\"\n>\n  <path\n    d=\"\n      M 5,12\n      h 14\n    \"\n  />\n  <path\n    d=\"\n      M 12,5\n      l 7,7\n      l -7,7\n    \"\n  />\n</svg>\n```\n\nIn this SVG, we have two `<path>`\n\nelements; the first one draws a straight horizontal line (the main shaft), while the second draws a `>`\n\nshape (the arrow tip).\n\nTo solve this problem, we want to come up with an alternative set of drawing instructions for the hover state. For example, the arrow shaft should grow from a 14px-wide line (`h 14`\n\n) to a 17px-wide one (`h 17`\n\n).\n\n**How do we transition between two SVG paths?** Well, that depends on what level of browser support we’re willing to accept 😅. The simplest solution is to use CSS transitions, but unfortunately, transitions on `<path>`\n\nelements aren’t supported in Safari. This means that browser support is [only around 79%(opens in new tab)](https://caniuse.com/mdn-svg_elements_path_d_path), as of April 2026.\n\nAlternatively, we can use a JavaScript library to handle the transition for us. This means that it’ll work consistently for all users, but at the expense of a larger JS bundle and some additional code complexity.\n\n**Let’s start with the most basic approach.** Here’s a stripped-down implementation:\n\nCode Playground\n\n```\n<style>\n  @media (prefers-reduced-motion: no-preference) {\n    .shaft, .tip {\n      transition: d 300ms;\n    }\n    \n    .btn:hover .shaft,\n    .btn:focus-visible .shaft {\n      d: path(\"\\\n        M 5,12\\\n        h 17\\\n      \");\n    }\n    .btn:hover .tip,\n    .btn:focus-visible .tip {\n      d: path(\"\\\n        M 15,7\\\n        l 7,5\\\n        l -7,5\\\n      \");\n    }\n  }\n</style>\n\n<button class=\"btn\">\n  <svg\n    class=\"arrow\"\n    aria-hidden=\"true\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      class=\"shaft\"\n      d=\"\n        M 5,12\n        h 14\n      \"\n    />\n    <path\n      class=\"tip\"\n      d=\"\n        M 12,5\n        l 7,7\n        l -7,7\n      \"\n    />\n  </svg>\n  \n  <span class=\"visually-hidden\">\n    Provide a description here of what this button does,\n    for assistive technologies like screen readers\n  </span>\n</button>\n```\n\nThe CSS `path()`\n\nfunction allows us to specify an override for the `d`\n\nattribute on SVG path elements. When we hover over the button, we apply new drawing instructions for both `<path>`\n\nnodes. We can then use [CSS transitions](/animation/css-transitions/) to smoothly interpolate between states.\n\nI’m using the [prefers-reduced-motion media query](/react/prefers-reduced-motion/) to make sure that this animation doesn’t trigger for folks with motion sensitivities. For this particular effect, the motion is pretty small/subtle, but there are all sorts of reasons why someone might want to disable motion, so I prefer to err on the side of caution.\n\n**One gotcha to watch out for:** strings in CSS aren’t multi-line. This means when we define our new `path()`\n\noverride, we either have to keep it all in one line (eg. `path(\"M 5,12 h 17\")`\n\n), or use backslashes (`\\`\n\n) to escape the newline characters. Like I mention in my [blog post about paths](/svg/interactive-guide-to-paths/#aside-commas-and-whitespace), I prefer to have each command sit on its own line. In this case, the commands are relatively short, but most SVG paths are considerably longer, and I find that formatting them across multiple lines makes them much easier to read!\n\n[Link to this heading](#using-a-javascript-library-3)Using a JavaScript library\n\nLike I mentioned earlier, this version won’t work in Safari. That’s not *necessarily* a deal-breaker; for purely-cosmetic effects like this, I think we can be a little more relaxed when it comes to our [browser support targets](/css/browser-support/).\n\nThat said, I usually *do* want my micro-interactions to reach as wide an audience as possible! So, let’s look at how we can use [ Motion(opens in new tab)](https://motion.dev/) to implement the same effect:\n\nCode Playground\n\n``` js\nimport { animate } from 'motion';\nimport './reset.css';\nimport './styles.css';\n\nconst btn = document.querySelector('.btn');\nconst shaft = btn.querySelector('.shaft');\nconst tip = btn.querySelector('.tip');\n\nbtn.addEventListener('pointerenter', (event) => {\n  if (checkPrefersReducedMotion()) {\n    return;\n  }\n\n  animate(\n    shaft,\n    {\n      d: `\n        M 5,12\n        h 17\n      `,\n    }\n  );\n  animate(\n    tip,\n    {\n      d: `\n        M 15,7\n        l 7,5\n        l -7,5\n      `,\n    }\n  );\n});\n\nbtn.addEventListener('mouseleave', (event) => {\n  if (checkPrefersReducedMotion()) {\n    return;\n  }\n\n  animate(\n    shaft,\n    {\n      d: `\n        M 5,12\n        h 14\n      `,\n    }\n  );\n  animate(\n    tip,\n    {\n      d: `\n        M 12,5\n        l 7,7\n        l -7,7\n      `,\n    }\n  );\n});\n\nfunction checkPrefersReducedMotion() {\n  return !window.matchMedia(\n    '(prefers-reduced-motion: no-preference)'\n  ).matches;\n}\n```\n\nIf you’re not familiar with Motion, it’s an absolutely lovely animation library. In a former life, it was a React-exclusive library called Framer Motion, but since then, it’s become a vanilla-JS tool with alternative versions for both React and Vue.\n\nThe `animate()`\n\nfunction works by manually calculating the intermediate values for every frame in JavaScript. This *sounds* like it’d be really slow / inefficient, but Motion is well-optimized. It also uses the Web Animations API under the hood, which means we even get the benefit of a separate animation thread! So, even if there’s a bunch of other stuff happening in our application, the animation should still run smoothly.\n\n[Link to this heading](#adding-some-polish--4)Adding some polish ✨\n\nThere are a couple more things we can do to make this look even better. First, let’s use spring physics instead of the default Bézier easing:\n\nBézier easing\n\nSpring easing\n\nUnlike Bézier curves, spring physics are modeled on real-world springs, and the motion they produce tends to feel a lot more natural. Springs work particularly well for squash/stretch effects like this, since it makes the element feel elastic and rubbery.\n\nAnother thing we can do is to move away from a typical state-based hover transition, and focus instead on the hover *event.*\n\nThis is one of my favourite little tricks, and it’s something I cover in-depth in a [dedicated blog post](/react/boop/). Instead of applying the stretchy-arrow variant based on the hover *state*, I can instead trigger it for a brief moment when the hover starts:\n\nState-based\n\nEvent-based\n\nIn the state-based variant, the arrow stays stretched for as long as the cursor remains over the SVG (or, on touchscreens, until the user taps somewhere else). But in this new version, the arrow snaps back almost immediately.\n\nI like this because it’s playful and unexpected. Almost all hover interactions online are state-based, so it really stands out when we do something different!\n\nHere’s a new implementation that integrates these bells and whistles, using the Motion library:\n\nCode Playground\n\n``` js\nimport { animate } from 'motion';\nimport './reset.css';\nimport './styles.css';\n\nconst btn = document.querySelector('.btn');\nconst shaft = btn.querySelector('.shaft');\nconst tip = btn.querySelector('.tip');\n\nconst SPRING_CONFIG = {\n  type: 'spring',\n  stiffness: 300,\n  damping: 12,\n};\n\nbtn.addEventListener('pointerenter', (event) => {\n  if (checkPrefersReducedMotion()) {\n    return;\n  }\n  \n  animate(\n    shaft, {\n      d: `\n        M 5,12\n        h 17`,\n      },\n    SPRING_CONFIG\n  );\n  animate(tip, {\n    d: `\n      M 15,7\n      l 7,5\n      l -7,5\n    `,\n  }, SPRING_CONFIG);\n\n  // Wait a brief moment, and then revert\n  // to the default arrow shape:\n  window.setTimeout(() => {\n    animate(shaft, {\n      d: `\n        M 5,12\n        h 14\n      `,\n    }, SPRING_CONFIG);\n    animate(tip, {\n      d: `\n        M 12,5\n        l 7,7\n        l -7,7\n      `,\n    }, SPRING_CONFIG);\n  }, 150);\n});\n\nfunction checkPrefersReducedMotion() {\n  return !window.matchMedia(\n    '(prefers-reduced-motion: no-preference)'\n  ).matches;\n}\n```\n\n[Link to this heading](#going-deeper-5)Going deeper\n\nThe trick we’ve covered in this blog post is just one of the little strategies I use to add polish to my animations and interactions. For the past 18 months, I’ve been focused on creating the *ultimate* resource when it comes to web animations. **It’s called Whimsical Animations, and registration has just opened!**\n\nIn this course, I’ll teach you everything I’ve learned about creating top-tier animations and interactions using HTML/CSS, JavaScript, SVG, and 2D Canvas. If you’ve ever wondered how I did something on this blog or one of my other projects, there’s a very good chance we cover it in the course. 😄\n\nIn my experience, most front-end developers have a pretty limited set of skills when it comes to animation. It’s not really part of the typical “web developer” toolkit. This is a real shame. We can do *so much cool stuff* when we get beyond the basics of CSS transitions.\n\nWhimsical Animations mostly focuses on implementation, but I also share a bunch of stuff I’ve learned about animation design. **This blog post was actually plucked from the “Animation Design” bonus module!** Most of us don’t have the luxury of working with a motion designer, so I wanted to make sure that this course covered everything you need to start creating incredible effects. ✨\n\nLearn more here:\n\n[Link to this heading](#bonus-bouncing-ball-playground-6)Bonus: bouncing ball playground\n\nIn the very first demo on this blog post, I shared a bouncing ball. Here’s a playground with detailed comments showing how this effect works:\n\nLike I said above, I think that there are *way* more practical ways to use this effect than this 😅. But, if you *do* find yourself needing a bounce animation, I hope this playground helps!\n\nAnd if you do wind up squashing and/or stretching something in your work, I’d love to see it! You can share it with me [on Bluesky(opens in new tab)](https://bsky.app/profile/joshwcomeau.com) or [by email](/contact/).\n\n### Last updated on\n\nMay 5th, 2026", "url": "https://wpnews.pro/news/squash-and-stretch", "canonical_source": "https://www.joshwcomeau.com/animation/squash-and-stretch/", "published_at": "2026-04-13 12:00:00+00:00", "updated_at": "2026-05-22 14:50:03.559604+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Disney"], "alternates": {"html": "https://wpnews.pro/news/squash-and-stretch", "markdown": "https://wpnews.pro/news/squash-and-stretch.md", "text": "https://wpnews.pro/news/squash-and-stretch.txt", "jsonld": "https://wpnews.pro/news/squash-and-stretch.jsonld"}}