Tobias Ahlin Words by Tobias Ahlin http://tobiasahlin.com Moving along a curved path in CSS with layered animation <div class="blog-banner banner-green"> <div class="blog-banner-content mnc-demo-container"> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> <style> .mnc-demo-container { height: 100px; width: 100px; } .mnc-demo-grid { overflow: hidden; border: 2px solid rgba(0,0,0,0.2); height: 100%; width: 100%; } .mnc-demo-line { width: 100%; height: 2px; background-color: rgba(0,0,0,0.2); position: absolute; } .mnc-demo-line-vertical { width: 2px; height: 100%; background-color: rgba(0,0,0,0.2); position: absolute; } .mnc-demo-dot { will-change: transform; -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); position: absolute; bottom: -10px; left: -10px; } .mnc-demo-dot:after { content: ''; display: block; will-change: transform; width: 20px; height: 20px; border-radius: 20px; background-color: #fff; -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); } .mnc-demo-dot-final-x { -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); } .mnc-demo-dot-final-y { -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); } .mnc-demo-dot-final::after { -webkit-animation: none; animation: none; } @keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @-webkit-keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } } </style> </div> <p>CSS animations and transitions are great for animating something from point A to B. That is, if you want to animate along a straight path. No matter how much you bend your <a href="https://developer.mozilla.org/en/docs/Web/CSS/timing-function">bezier curves</a>, you can’t make something move along a curved path by applying an <code class="highlighter-rouge">animation</code> or a <code class="highlighter-rouge">transition</code> to an object. You can overshoot with custom timing functions, and produce spring-like effects, but the relative movement along the X and Y-axis will always be identical.</p> <p>Instead of turning to JavaScript for producing more natural looking motion, there’s an easy way to work around this limitation: layered animation. By using two or more objects to drive an animation, we get fine grained control over an object’s path, and can apply one timing function for the movement along the X-axis, and another one for the Y-axis.</p> <!--more--> <h2 id="the-problem">The problem</h2> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Regular path</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">What we want</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> <style> .mnc-demo-container-inline { display: inline-block; position: relative; margin-right: 60px; margin-bottom: 30px; margin-top: 15px; } .mnc-demo-label { position: absolute; bottom: -38px; color: #666; font-size: 11px; text-align: center; width: 100%; } .mnc-demo-container-inline .mnc-demo-dot:after { background-color: #333; } .mnc-demo-dot-straight { -webkit-animation: straightLine 2.5s infinite linear; animation: straightLine 2.5s infinite linear; } .mnc-demo-dot-straight:after { -webkit-animation: none; animation: none; } @-webkit-keyframes straightLine { 50% { -webkit-transform: translate3D(100px, -100px, 0); transform: translate3D(100px, -100px, 0); } } @keyframes straightLine { 50% { -webkit-transform: translate3D(100px, -100px, 0); transform: translate3D(100px, -100px, 0); } } </style> </div> <p>Before we dive into the solution, let’s look closer at the problem. CSS <code class="highlighter-rouge">animations</code> and <code class="highlighter-rouge">transitions</code> restrict us to animating along straight paths. How? By always taking the shortest path from point A to point B. That’s great—this is what we’re after in most cases—but we lack a way of telling CSS to “take a nicer path” rather than “take the shortest path”.</p> <p>The most straight-forward way of animating between two points in CSS (with hardware acceleration) is to use <code class="highlighter-rouge">transform</code> to <code class="highlighter-rouge">translate</code> an object over time. This produces movement along a linear path. In this <code class="highlighter-rouge">@keyframes</code> block, we’re going back and forth between (0,0) and (100,-100), as seen in the example above:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="k">@keyframes</span> <span class="n">straightLine</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translate3D</span><span class="p">(</span><span class="m">100px</span><span class="p">,</span> <span class="m">-100px</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="nc">.dot</span> <span class="p">{</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">straightLine</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">linear</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>This isn’t complicated, but let’s stop here for a moment. To understand the solution to the problem we need to, at least visually, take the animation apart.</p> <p>At <code class="highlighter-rouge">0%</code> we start out at (0,0), and at <code class="highlighter-rouge">50%</code> we use <code class="highlighter-rouge">translate3D(100px, -100px, 0)</code> to move to (100,-100), then back again. Put another way, we move the object <code class="highlighter-rouge">100px</code> to the right, and <code class="highlighter-rouge">100px</code> upwards, and these two translations combined move the object at an angle.</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-straightX"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-straightY"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight"></div> </div> <style> .mnc-demo-dot-straightX { -webkit-animation: straightLineX 2.5s infinite linear; animation: straightLineX 2.5s infinite linear; } .mnc-demo-dot-straightY { -webkit-animation: straightLineY 2.5s infinite linear; animation: straightLineY 2.5s infinite linear; } @-webkit-keyframes straightLineX { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes straightLineX { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes straightLineY { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes straightLineY { 50% { -webkit-transform: translateY(-100px); transform: translateY(-100px); } } </style> </div> <h2 id="the-solution-one-timing-function-per-axis">The solution: one timing function per axis</h2> <p>So how do we create a curved path like the one showcased in the earlier example? To create a path that doesn’t go in a straight line, <strong>we want the movement speed along the X-axis and Y-axis to be out of sync</strong>.</p> <p>The previous examples all used <code class="highlighter-rouge">linear</code> timing functions, but if we add a container around the object we want to animate, we can apply one timing function for the X-axis, and another one for the Y-axis. Below, we’re using <code class="highlighter-rouge">ease-in</code> for the X-axis, and <code class="highlighter-rouge">ease-out</code> for the Y-axis:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X: ease-in</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-in"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y: ease-out</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-out"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-combined"></div> </div> <style> .mnc-demo-dot-ease-in { -webkit-animation: combined-xAxis 2.5s infinite ease-in; animation: combined-xAxis 2.5s infinite ease-in; } .mnc-demo-dot-ease-out { -webkit-animation: combined-yAxis 2.5s infinite ease-out; animation: combined-yAxis 2.5s infinite ease-out; } .mnc-demo-dot-combined { -webkit-animation: combined-xAxis 2.5s infinite ease-in; animation: combined-xAxis 2.5s infinite ease-in; } .mnc-demo-dot-combined::after { -webkit-animation: combined-yAxis 2.5s infinite ease-out; animation: combined-yAxis 2.5s infinite ease-out; } @keyframes combined-xAxis { 50% { -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes combined-xAxis { 50% { -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes combined-yAxis { 50% { -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @-webkit-keyframes combined-yAxis { 50% { -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; -webkit-transform: translateY(-100px); transform: translateY(-100px); } } </style> </div> <h2 id="the-implementation-one-object-per-axis">The implementation: one object per axis</h2> <p>Unfortunately, we can’t just stack <code class="highlighter-rouge">transform</code> animations: only the last animation to be declared would run. So, how do we actually combine two animations? We put one object inside another, and run one animation on the container element, and a different one on the child element.</p> <p>In all the examples above where you’ve seen the dot move along a curved path, you’ve seen two separate elements being animated, but the container has been completely transparent. To clearly see how the two objects interact in the curved examples, we can change the container to render a bordered box:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X: container</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-square mnc-demo-dot-straight mnc-demo-dot-ease-in"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y: object</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-out"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-square mnc-demo-dot-combined"></div> </div> <style> .mnc-demo-dot-square { border-radius: 0; border: 1px solid #000; width: 20px; height: 20px; } .mnc-demo-dot-square::after { display: none; } .mnc-demo-dot-combined.mnc-demo-dot-square::after { display: block; } </style> </div> <p>The dot sits inside the bordered box, and follows the box’s motion along the X-axis, while it itself moves up and down along the Y-axis. Remove the border of the box, and we’ve got our curved path. Rather than adding two objects in our HTML, though, we can produce a pseudo-element. If we have this in our HTML:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"dot"</span><span class="nt">&gt;&lt;/div&gt;</span> </code></pre> </div> <p>We can add a pseudo-element like so:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code> <span class="nc">.dot</span> <span class="p">{</span> <span class="c">/* Container. Animate along the X-axis */</span> <span class="p">}</span> <span class="nc">.dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="c">/* Render dot, and animate along Y-axis */</span> <span class="p">}</span> </code></pre> </div> <p>Then we need two separate animation blocks: one for the X-axis, and one for the Y-axis. Notice how one uses <code class="highlighter-rouge">ease-in</code>, while the other one uses <code class="highlighter-rouge">ease-out</code>:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.dot</span> <span class="p">{</span> <span class="c">/* Some layout code… */</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">ease-in</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="c">/* Render dot */</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">ease-out</span><span class="p">;</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">ease-in</span><span class="p">;</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">ease-out</span><span class="p">;</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Put together, with vender-prefixes for WebKit, and with some custom bezier curves instead of <code class="highlighter-rouge">ease-in</code> and <code class="highlighter-rouge">ease-out</code>, we can produce the example showcased at the very beginning of the post:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.demo-dot</span> <span class="p">{</span> <span class="nl">-webkit-animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.demo-dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s2">''</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">-webkit-animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="p">}</span> <span class="k">@-webkit-keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@-webkit-keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Which brings us back to where we started:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-final mnc-demo-dot-final-x"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-final mnc-demo-dot-final-y"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Curved path</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> </div> <p>You may have noticed that we’ve been using <code class="highlighter-rouge">@keyframes</code> blocks in all of the examples so far, but that’s purely because we happened to need several keyframes to move the dot back and forth. Layered animation works just as well with the <code class="highlighter-rouge">transition</code> property, if you only need to animate from point A to point B.</p> <p>If you have an absolutely positioned element, you can achieve a curved path by animating the <code class="highlighter-rouge">left</code> and <code class="highlighter-rouge">bottom</code> properties, of a single element, and avoid the need of a container element. There’s a good reason to avoid doing so, however: it performs significantly worse, and will trigger a redraw for every frame of the animation. Using layered animation with a pseudo-element, and animating the hardware accelerated <code class="highlighter-rouge">translate</code> property, produces a great looking animation while making sure it also performs well.</p> Thu, 07 Jan 2016 11:52:00 +0000 http://tobiasahlin.com/blog/curved-path-animations-in-css/ http://tobiasahlin.com/blog/curved-path-animations-in-css/ How to animate "box-shadow" with silky smooth performance <div class="blog-banner banner-purple"> <div class="blog-banner-content" style="height: 90px;"> <div class="box-shadow-demo"></div> </div> <style> .box-shadow-demo { display: inline-block; background-color: #fff; width: 90px; height: 90px; border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,0.2); position: relative; border-radius: 5px; -webkit-animation: scaleAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); animation: scaleAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); } .box-shadow-demo::after { content: ""; border-radius: 5px; position: absolute; z-index: -1; top: 0; left: 0; width: 100%; height: 100%; box-shadow: 0 5px 15px rgba(0,0,0,0.5); opacity: 0; -webkit-animation: fadeAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); animation: fadeAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); } @-webkit-keyframes fadeAnimation { 0%, 80%, 100% { opacity: 0; } 30%, 50% { opacity: 1; } } @-webkit-keyframes scaleAnimation { 0%, 80%, 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 30%, 50% { -webkit-transform: scale(1.3, 1.3); transform: scale(1.3, 1.3); } } @keyframes fadeAnimation { 0%, 80%, 100% { opacity: 0; } 30%, 50% { opacity: 1; } } @keyframes scaleAnimation { 0%, 80%, 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 30%, 50% { -webkit-transform: scale(1.3, 1.3); transform: scale(1.3, 1.3); } } </style> </div> <p>How do you animate the <code class="highlighter-rouge">box-shadow</code> property in CSS without causing re-paints on every frame, and heavily impacting the performance of your page? Short answer: you don’t. Animating a change of <code class="highlighter-rouge">box-shadow</code> <em>will</em> hurt performance.</p> <p>There’s an easy way of mimicking the same effect, however, with minimal re-paints, that should let your animations run at a solid 60 FPS: animate the <code class="highlighter-rouge">opacity</code> of a pseudo-element.</p> <!--more--> <h2 id="demo">Demo</h2> <p><a href="/demo/animate-box-shadow/"><img src="/static/animate-box-shadow/demo.gif" alt="Recording of box-shadow demo in action" /></a></p> <p><a href="/demo/animate-box-shadow/">Have a look at the demo</a> and compare the two different techniques we’ll be exploring. If the two examples look the same to you, that’s the point. The only difference is how we apply and animate the shadow. On the left we’re animating <code class="highlighter-rouge">box-shadow</code> on <code class="highlighter-rouge">hover</code>, and on the right we’re adding a pseudo-element with <code class="highlighter-rouge">:after</code>, applying the shadow to that, and animating the <code class="highlighter-rouge">opacity</code> of that element.</p> <p>If you bring up your developer tools and hover one of these items, you should see something similar to this (green bars are paints; less is better):</p> <p><a href="/demo/animate-box-shadow/"><img src="/static/animate-box-shadow/animation-performance.png" alt="Animation performance when hovering the different boxes" /></a></p> <p>There are clearly more re-paints when hovering the cards on the left side (animating <code class="highlighter-rouge">box-shadow</code>), compared to hovering the cards on the right side (which animate the <code class="highlighter-rouge">opacity</code> of their pseudo-element).</p> <p>Why are we seeing this effect? There are <a href="http://csstriggers.com">very few CSS properties</a> that can be animated without constantly triggering repaints for every frame, namely <code class="highlighter-rouge">opacity</code> and <code class="highlighter-rouge">transform</code>. We minimize the amount of repaints (and work that your browser has to do) by sticking to only changing these two properties during the animation.</p> <p>This is the <strong>critical difference</strong> between the two techniques, stripping out all of the other layout styles:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* The slow way */</span> <span class="nc">.make-it-slow</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">box-shadow</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">:</span> <span class="p">}</span> <span class="c">/* Transition to a bigger shadow on hover */</span> <span class="nc">.make-it-slow</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* The fast way */</span> <span class="nc">.make-it-fast</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* Pre-render the bigger shadow, but hide it */</span> <span class="nc">.make-it-fast</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">opacity</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">:</span> <span class="p">}</span> <span class="c">/* Transition to showing the bigger shadow on hover */</span> <span class="nc">.make-it-fast</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>In the example that performs better we have two layers: one for the box, and one for the shadow, and only animate the <code class="highlighter-rouge">opacity</code> property of the shadow layer.</p> <h2 id="breaking-it-down">Breaking it down</h2> <p>With the fundamentals in place, let’s look at how to create <a href="/demo/animate-box-shadow/">the 3D card effect showcased in the demo</a>. The first step is to move the shadow to a pseudo-element, like we did above. Let’s also add all of the layout code to create the card:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* All HTML you need is &lt;div class="box"&gt;&lt;/div&gt; */</span> <span class="c">/* Create a simple white box, and add the shadow for the initial state */</span> <span class="nc">.box</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* Create the hidden pseudo-element */</span> <span class="c">/* include the shadow for the end state */</span> <span class="nc">.box</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s2">''</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">-1</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">opacity</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>Note that we’re adding a <code class="highlighter-rouge">transition</code> to both the <code class="highlighter-rouge">.box</code>, and <code class="highlighter-rouge">.box::after</code>, since we’re going to animate both of these elements: <code class="highlighter-rouge">transform</code> for <code class="highlighter-rouge">.box</code>, and <code class="highlighter-rouge">opacity</code> for <code class="highlighter-rouge">.box::after</code>.</p> <p>These styles give us a white box with a subtle <code class="highlighter-rouge">box-shadow</code>. The stronger shadow from <code class="highlighter-rouge">.box::after</code> is completely hidden at this point, and you can’t interact with the box:</p> <div class="post-demo-content"> <div class="box"></div> <style> .box { position: relative; display: inline-block; width: 100px; height: 100px; border-radius: 5px; background-color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.1); transition: all 0.3s ease-in-out; } </style> </div> <p>To create the same effect as in the <a href="/demo/animate-box-shadow/">demo</a>, now all we need to do is to scale up the <code class="highlighter-rouge">.box</code> on hover, and fade in the pseudo-element and its shadow:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* Scale up the box */</span> <span class="nc">.box</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.2</span><span class="p">,</span> <span class="m">1.2</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* Fade in the pseudo-element with the bigger shadow */</span> <span class="nc">.box</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>That’s it! Hover the box to preview the effect:</p> <div class="post-demo-content"> <div class="box2"></div> <style> .box2 { position: relative; display: inline-block; width: 100px; height: 100px; border-radius: 5px; background-color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.1); -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); } /* Create the hidden pseudo-element */ /* include the shadow for the end state */ .box2::after { content: ''; position: absolute; z-index: -1; width: 100%; height: 100%; border-radius: 5px; opacity: 0; box-shadow: 0 5px 15px rgba(0,0,0,0.3); -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) } .box2:hover { -webkit-transform: scale(1.25, 1.25); transform: scale(1.25, 1.25); } .box2:hover::after { opacity: 1; } </style> </div> <p>To summarize, here’s all the CSS, with all vendor prefixes, and some custom easing for additional ✨👌:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.box</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.1</span><span class="p">);</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s1">""</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">-1</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.3</span><span class="p">);</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.25</span><span class="p">,</span> <span class="m">1.25</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.25</span><span class="p">,</span> <span class="m">1.25</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>That’s certainly a lot of CSS to achieve the same effect as simply animating <code class="highlighter-rouge">box-shadow</code>, just with improved performance. Why bother?</p> <p>Even if your desktop likely handles animating <code class="highlighter-rouge">box-shadow</code> without any issues, your phone may not, and even your desktop may start to stutter when animating a more complex layout.</p> <p>Keep transitions and animations to only <code class="highlighter-rouge">transform</code> and <code class="highlighter-rouge">opacity</code>, and you’re certain to achieve the best possible performance, and with that, the best possible user experience.</p> Wed, 18 Nov 2015 14:52:00 +0000 http://tobiasahlin.com/blog/how-to-animate-box-shadow/ http://tobiasahlin.com/blog/how-to-animate-box-shadow/ Google Web Font Inspiration with TypeSource <div class="blog-banner banner-blue"> <div class="blog-banner-content" style="height: 90px;"> <img src="/static/typesource.svg" alt="TypeSource" width="90" /> </div> </div> <p>If you’ve been looking through Google’s directory of <a href="https://www.google.com/fonts">Web Fonts</a> in search for fonts to use in your next project, you might have felt your inspiration quickly drain. It can be difficult to imagine how a font would look in use when you’re left with a long list of black text on white.</p> <p>I built <a href="/typesource/">Typesource</a> to make it easier to find and match Google Web Fonts. I’ve carefully crafted each example, exploring different styles, combinations, and color schemes in every composition. In addition to the feed of inspiration, the HTML/CSS is available for all examples, so you can copy and paste the code into your project and quickly get started with your web type compositions.</p> <!--more--> <p class="center-content"> <a href="/typesource/" class="outlined-button">View TypeSource</a> </p> <p>I’ve posted 10 examples so far, and more are incoming. Here are some highlights:</p> <p><a href="/typesource/11-sense-of-style/"> <img src="/static/typesource/typesource01.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/10-wait-until-monday/"> <img src="/static/typesource/typesource02.jpg" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/09-history-of-swedish-meatballs/"> <img src="/static/typesource/typesource03.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/04-how-climate-change-will-affect-you/"> <img src="/static/typesource/typesource04.jpg" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/05-perfection/"> <img src="/static/typesource/typesource05.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/06-a-search/"> <img src="/static/typesource/typesource06.jpg" alt="Google Web Font example" /> </a></p> <p class="center-content"> <a href="/typesource/" class="outlined-button">See all TypeSource examples</a> </p> Mon, 16 Nov 2015 13:10:00 +0000 http://tobiasahlin.com/blog/google-web-font-inspiration/ http://tobiasahlin.com/blog/google-web-font-inspiration/ Copy Lorem Ipsum from your Menu Bar with Loremify <div class="blog-banner banner-gray"> <div class="blog-banner-content" style="height: 120px;"> <img src="/static/loremify.png" alt="Loremify" width="120" /> </div> </div> <p><a href="http://kallepersson.se">Kalle Persson</a> and I just built and released a small utility app for OS X. <a href="http://loremify.com/">Loremify</a> is a free mac app to quickly copy Lorem Ipsum to your clipboard. It lets you wrap the dummy text in html or markdown, specify the amount of text, and copy it to your clipboard—all in one click. It sits in your OS X menu bar, and it’s <a href="https://itunes.apple.com/app/loremify/id1028877632">available on the App Store</a>.</p> <!--more--> <p><img src="/static/loremify.gif" alt="Demo of Loremify" /></p> <p>Switch to Markdown (or plain text) with the menu in the top left:</p> <p><img src="/static/loremify-markdown.gif" alt="Demo of switching output language in Loremify" /></p> <p>Loremify is still in its infancy, but we’d love to hear from you if you find it useful (or not useful). Reach out on twitter at <a href="http://twitter.com/tobiasahlin">@tobiasahlin</a> and <a href="https://twitter.com/kallepersson">@kallepersson</a>, and let us know what you think.</p> <p><a href="https://itunes.apple.com/app/loremify/id1028877632"><img src="/static/appstore.svg" width="200" class="aligncenter" /></a></p> Sun, 15 Nov 2015 13:10:00 +0000 http://tobiasahlin.com/blog/lorem-ipsum-in-your-menu-bar-with-loremify/ http://tobiasahlin.com/blog/lorem-ipsum-in-your-menu-bar-with-loremify/ Three new SpinKit spinners <div class="blog-banner banner-green"> <div class="sk-spinner-news blog-banner-content"> <div class="sk-news-rect sk-news-rect1"></div> <div class="sk-news-rect sk-news-rect2"></div> <div class="sk-news-rect sk-news-rect3"></div> <div class="sk-news-rect sk-news-rect4"></div> <div class="sk-news-rect sk-news-rect5"></div> </div> <style type="text/css"> .sk-spinner-news { width: 100px; height: 70px; text-align: center; font-size: 15px; } .sk-news-rect { background-color: #fff; height: 100%; width: 8px; display: inline-block; -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; animation: sk-stretchdelay 1.2s infinite ease-in-out; } .sk-news-rect2 { -webkit-animation-delay: -1.1s; animation-delay: -1.1s; } .sk-news-rect3 { -webkit-animation-delay: -1.0s; animation-delay: -1.0s; } .sk-news-rect4 { -webkit-animation-delay: -0.9s; animation-delay: -0.9s; } .sk-news-rect5 { -webkit-animation-delay: -0.8s; animation-delay: -0.8s; } @-webkit-keyframes sk-stretchdelay { 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 20% { -webkit-transform: scaleY(1.0) } } @keyframes sk-stretchdelay { 0%, 40%, 100% { transform: scaleY(0.4); -webkit-transform: scaleY(0.4); } 20% { transform: scaleY(1.0); -webkit-transform: scaleY(1.0); } } </style> </div> <p><a href="http://tobiasahlin.com/spinkit/">SpinKit</a> just got three new additions: a folding cube spinner, a classic circle spinner, and a scaling grid spinner. Just like the other spinners, they are animated using only the <code class="highlighter-rouge">transform</code> and the <code class="highlighter-rouge">opacity</code> properties, making them perform well, and easy to customize: just change the background color to match your site.</p> <!--more--> <p class="center-content"> <a href="/spinkit/" class="outlined-button">View SpinKit</a> </p> <p>If you want to use SpinKit in your project and stay up to date with new changes and additions, the project is now also available on <a href="https://www.npmjs.com/package/spinkit">npm</a> and <a href="http://bower.io">bower</a>. Just run:</p> <div class="highlighter-rouge"><pre class="highlight"><code>npm install spinkit </code></pre> </div> <p>…or:</p> <div class="highlighter-rouge"><pre class="highlight"><code>bower install spinkit </code></pre> </div> <p>With the recent <a href="https://github.com/tobiasahlin/SpinKit/releases">release of SpinKit 1.2</a>, we refactored a lot of the SCSS. The spinners are now easier to customize, and we fixed all known bugs. I highly recommend updating.</p> <p>Do you have a spinner in mind that you’re missing from SpinKit? <a href="http://twitter.com/tobiasahlin">Let me know</a>, and it may just join the SpinKit family in the next update.</p> <p>Happy spinning!</p> Thu, 06 Aug 2015 13:10:00 +0000 http://tobiasahlin.com/blog/spinkit-news/ http://tobiasahlin.com/blog/spinkit-news/ Wake <p>Bakken &amp; Bæck, <a href="https://medium.com/bakken-bæck/introducing-wake-2f3f0dcc1b20">announcing Wake</a>:</p> <blockquote> <p>We couldn’t find anything that would allow us to quickly share visual work as a team, and we needed a better tool to keep us all in sync. Our design process was fragmented across different tools and channels. We were storing files in Dropbox, and giving feedback on email, Skype or Slack.</p> </blockquote> <blockquote> <p>These tools are great for text-based communication, but they were not designed for the type of visual work we do. We needed a solution to keep everyone in the loop built specifically for designers.</p> </blockquote> <blockquote> <p>Together with Chris we started building a prototype for a tool that would improve our own design workflow.</p> </blockquote> <p><a href="http://wake.io">Wake</a> is a new tool by Bakken &amp; Bæck for collaborating around design, that let’s you “explain your thinking and get feedback”. It looks gorgeous.</p> <iframe width="960" height="540" style="height: 540px;" src="https://www.youtube.com/embed/GIalL5fkhPM?rel=0" frameborder="0" allowfullscreen="" class="size-960"></iframe> Wed, 25 Feb 2015 12:28:00 +0000 http://tobiasahlin.com/blog/wake/ http://tobiasahlin.com/blog/wake/ Coding is not the new literacy <blockquote> <p>Alan Kay did a <a href="https://www.youtube.com/watch?v=oKg1hTOQXoY">talk</a> at OOPSLA in 1997 titled “The computer revolution hasn’t happened yet,” in which he argued that we haven’t realized the potential that computers can provide for us. Eighteen years later, I still agree with him - it hasn’t happened yet. And teaching people how to loop over a list won’t make it happen either. To realize the potential of computers, we have to focus on the fundamental skills that allow us to harness external computation. We have to create a new generation of tools that allow us to express our models without switching professions and a new generation of modelers who wield them.</p> </blockquote> <p>— Chris Granger in <a href="http://www.chris-granger.com/2015/01/26/coding-is-not-the-new-literacy/">Coding is not the new literacy</a></p> Wed, 25 Feb 2015 12:15:00 +0000 http://tobiasahlin.com/blog/coding-is-not-the-new-literacy/ http://tobiasahlin.com/blog/coding-is-not-the-new-literacy/ Origami Live <p><a href="https://code.facebook.com/posts/883904991672650/introducing-origami-live/">Brandon Walkin, on Facebook’s engineering blog</a>:</p> <blockquote> <p>Today, we’re excited to release Origami Live for iOS, along with a major new version of Origami for Mac. Origami Live is a new app that lets you use your Origami prototypes on your iPhone or iPad. Alongside it, we’re releasing Origami 2.0, which has a lot of new features, including code exporting, powerful gesture support, Sketch integration, presentation mode, and more.</p> </blockquote> <p>They also launched a <a href="http://facebook.github.io/origami/">new site with tutorials and documentation</a> to get started with Origami.</p> Wed, 25 Feb 2015 12:10:00 +0000 http://tobiasahlin.com/blog/origami-live/ http://tobiasahlin.com/blog/origami-live/ Building the minimum Badass User <iframe src="//player.vimeo.com/video/54469442" width="960" height="540" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe> Wed, 25 Feb 2015 12:03:00 +0000 http://tobiasahlin.com/blog/minimum-badass-user/ http://tobiasahlin.com/blog/minimum-badass-user/ Animating Link Underlines <style> .underlined-example-wrapper { padding: 21px 20px 19px; background-color: rgba(0,0,0,0.5); display: inline-block; } .underlined-example { position: relative; color: #fff; text-decoration: none; font-size: 24px; display:inline-block; } .underlined-example:hover { color: #fff; } .underlined-example:before { content: ""; position: absolute; width: 100%; height: 2px; bottom: 0; left: 0; background-color: #fff; visibility: hidden; -webkit-transition: all 0.3s ease-in-out 0s; -moz-transition: all 0.3s ease-in-out 0s; -o-transition: all 0.3s ease-in-out 0s; transition: all 0.3s ease-in-out 0s; -webkit-transform: scaleX(0); -moz-transform: scaleX(0); -o-transform: scaleX(0); transform: scaleX(0); } .underlined-example:hover:before { visibility: visible; -webkit-transform: scaleX(1); -moz-transform: scaleX(1); -o-transform: scaleX(1); transform: scaleX(1); } </style> <div class="blog-banner banner-gray"> <div class="blog-banner-content" style="height: 70px; pointer-events: auto;"> <div class="underlined-example-wrapper"> <a href="/blog/css-trick-animating-link-underlines/" class="underlined-example">Hover this link</a> </div> </div> </div> <p>I recently added a simple visual effect to this blog that I quickly fell in love with: when you hover blog headers, the link’s underline is revealed by animating it out from the center. You can try it in the banner above.</p> <p>Creating this effect is surprisingly easy, doesn’t require any additional DOM elements to be added through HTML, and falls back nicely for browsers that don’t support CSS animations (it will just show up as a regular underline).</p> <!--more--> <p>The first thing we need to do is turn off <code class="highlighter-rouge">text-decoration</code>, and set the link’s <code class="highlighter-rouge">position</code> to <code class="highlighter-rouge">relative</code>. For simplicitiy’s sake, we’ll also make sure the link doesn’t change color on hover. Here we’re applying the effect to all link elements inside <code class="highlighter-rouge">h2</code>s:</p> <figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">h2</span> <span class="o">&gt;</span> <span class="nt">a</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span> <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span> <span class="nt">h2</span> <span class="o">&gt;</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p>Next, we want to add the border, and hide it through a transformation. We do this by inserting it with <code class="highlighter-rouge">:before</code>, and setting its X scale to <code class="highlighter-rouge">0</code>. As a fallback, we hide it with <code class="highlighter-rouge">visibility: hidden</code> for browsers that don’t support CSS animations.</p> <figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">h2</span> <span class="o">&gt;</span> <span class="nt">a</span><span class="nd">:before</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s1">""</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">2px</span><span class="p">;</span> <span class="nl">bottom</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span> <span class="nl">visibility</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">scaleX</span><span class="p">(</span><span class="m">0</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scaleX</span><span class="p">(</span><span class="m">0</span><span class="p">);</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span> <span class="m">0s</span><span class="p">;</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span> <span class="m">0s</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p>At the very bottom we tell the element to animate all changes applied to it, with a duration of <code class="highlighter-rouge">0.3</code> seconds. For the animation to appear, now we just need to make the element visible again on <code class="highlighter-rouge">hover</code>, and set its X scale back to <code class="highlighter-rouge">1</code>:</p> <figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">h2</span> <span class="o">&gt;</span> <span class="nt">a</span><span class="nd">:hover:before</span> <span class="p">{</span> <span class="nl">visibility</span><span class="p">:</span> <span class="nb">visible</span><span class="p">;</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">scaleX</span><span class="p">(</span><span class="m">1</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scaleX</span><span class="p">(</span><span class="m">1</span><span class="p">);</span> <span class="p">}</span></code></pre></figure> <p>And that’s it! Firefox has had support for <code class="highlighter-rouge">animation</code> and <code class="highlighter-rouge">transform</code> without the <code class="highlighter-rouge">moz</code> prefix <a href="http://caniuse.com/css-animation">since version 16.0</a> (for over a year), so I’ve left out the prefix in the code. If you want to be safe, you should add <code class="highlighter-rouge">-o</code> and <code class="highlighter-rouge">-moz</code> to all animations and transforms.</p> Sat, 04 Jan 2014 10:46:00 +0000 http://tobiasahlin.com/blog/css-trick-animating-link-underlines/ http://tobiasahlin.com/blog/css-trick-animating-link-underlines/