<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Animation on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/animation/index.xml</link><description>Recent content in Animation on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 14 Oct 2025 04:02:41 +0000</lastBuildDate><item><author>Andy Clarke</author><title>Smashing Animations Part 5: Building Adaptive SVGs With `&lt;symbol>`, `&lt;use>`, And CSS Media Queries</title><link>https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/</link><pubDate>Mon, 06 Oct 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/</guid><description>SVGs, they scale, yes, but how else can you make them adapt even better to several screen sizes? Web design pioneer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> explains how he builds what he calls “adaptive SVGs” using &lt;code>&amp;lt;symbol&amp;gt;&lt;/code>, &lt;code>&amp;lt;use&amp;gt;&lt;/code>, and CSS Media Queries.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/" />
              <title>Smashing Animations Part 5: Building Adaptive SVGs With `&lt;symbol&gt;`, `&lt;use&gt;`, And CSS Media Queries</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 5: Building Adaptive SVGs With `&lt;symbol&gt;`, `&lt;use&gt;`, And CSS Media Queries</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-10-06T13:00:00&#43;00:00" class="op-published">2025-10-06T13:00:00+00:00</time>
                  <time datetime="2025-10-06T13:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>I’ve written quite a lot recently about how I <a href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/">prepare and optimise</a> SVG code to use as static graphics or in <a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">animations</a>. I love working with SVG, but there’s always been something about them that bugs me.</p>

<p>To illustrate how I build adaptive SVGs, I’ve selected an episode of <em>The Quick Draw McGraw Show</em> called “<a href="https://yowpyowp.blogspot.com/2012/06/quick-draw-mcgraw-bow-wow-bandit.html">Bow Wow Bandit</a>,” first broadcast in 1959.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png"
			
			sizes="100vw"
			alt="Bow Wow Bandit illustration"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Quick Draw McGraw Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/1-quick-draw-mcgraw-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In it, Quick Draw McGraw enlists his bloodhound Snuffles to rescue his sidekick Baba Looey. Like most Hanna-Barbera title cards of the period, the artwork was made by Lawrence (Art) Goble.</p>

<div class="refs">
  <ul><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</a></li><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/">Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</a></li><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/">Smashing Animations Part 3: SMIL’s Not Dead Baby, SMIL’s Not Dead</a></li><li><a href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/">Smashing Animations Part 4: Optimising SVGs</a></li></ul>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png"
			
			sizes="100vw"
			alt="Quick Draw McGraw character pulling back on a dog leash attached to his bloodhound, Snuffles."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Andy Clarke’s Bow Wow Bandit Toon Title recreation (16:9). (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/2-andy-clarke-bow-wow-bandit-toon-title-recreation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Let’s say I’ve designed an SVG scene like that one that’s based on Bow Wow Bandit, which has a 16:9 aspect ratio with a <code>viewBox</code> size of 1920×1080. This SVG scales up and down (the clue’s in the name), so it looks sharp when it’s gigantic and when it’s minute.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png"
			
			sizes="100vw"
			alt="16:9 aspect ration vs. 3:4."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Left: 16:9 aspect ratio loses its impact. Right: 3:4 format suits the screen size better. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/3-svgs-aspect-ratio.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But on small screens, the 16:9 aspect ratio (<a href="https://stuffandnonsense.co.uk/toon-titles/quick-draw-3a.html">live demo</a>) might not be the best format, and the image loses its impact. Sometimes, a portrait orientation, like 3:4, would suit the screen size better.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="729"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png"
			
			sizes="100vw"
			alt="Andy Clarke’s Bow Wow Bandit Toon Title recreation (3:4)."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Andy Clarke’s Bow Wow Bandit Toon Title recreation (3:4). (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/4-bow-wow-bandit-toon-title-recreation-portrait.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But, herein lies the problem, as it’s not easy to reposition internal elements for different screen sizes using just <code>viewBox</code>. That’s because in SVG, internal element positions are locked to the coordinate system from the original <code>viewBox</code>, so you can’t easily change their layout between, say, desktop and mobile. This is a problem because animations and interactivity often rely on element positions, which break when the <code>viewBox</code> changes.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png"
			
			sizes="100vw"
			alt="Left: 16:9 for larger screens. Right: 3:4 for smaller screens."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Left: 16:9 for larger screens. Right: 3:4 for smaller screens. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/5-svg-smaller-larger-screens.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>My challenge was to serve a 1080×1440 version of Bow Wow Bandit to smaller screens and a different one to larger ones. I wanted the position and size of internal elements &mdash; like Quick Draw McGraw and his dawg Snuffles &mdash; to change to best fit these two layouts. To solve this, I experimented with several alternatives.</p>

<p><strong>Note:</strong> Why are we not just using the <code>&lt;picture&gt;</code> with external SVGs? The <a href="https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/"><code>&lt;picture&gt;</code> element</a> is brilliant for responsive images, but it only works with raster formats (like JPEG or WebP) and external SVG files treated as images. That means that you can’t animate or style internal elements using CSS.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="showing-and-hiding-svg">Showing And Hiding SVG</h2>

<p>The most obvious choice was to include two different SVGs in my markup, one for small screens, the other for larger ones, then show or hide them using <a href="https://www.smashingmagazine.com/2018/02/media-queries-responsive-design-2018/">CSS and Media Queries</a>:</p>

<pre><code class="language-svg">&lt;svg id="svg-small" viewBox="0 0 1080 1440"&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;svg id="svg-large" viewBox="0 0 1920 1080"&gt;
  &lt;!--... --&gt;
&lt;/svg&gt;


#svg-small { display: block; }
#svg-large { display: none; }

@media (min-width: 64rem) {
  #svg-small { display: none; }
  #svg-mobile { display: block; }
}
</code></pre>

<p>But using this method, both SVG versions are loaded, which, when the graphics are complex, means downloading lots and lots and lots of unnecessary code.</p>

<h2 id="replacing-svgs-using-javascript">Replacing SVGs Using JavaScript</h2>

<p>I thought about using JavaScript to swap in the larger SVG at a specified breakpoint:</p>

<pre><code class="language-javascript">if (window.matchMedia('(min-width: 64rem)').matches) {
  svgContainer.innerHTML = desktopSVG; 
} else {
  svgContainer.innerHTML = mobileSVG;
}
</code></pre>

<p>Leaving aside the fact that JavaScript would now be critical to how the design is displayed, both SVGs would usually be loaded anyway, which adds DOM complexity and unnecessary weight. Plus, maintenance becomes a problem as there are now two versions of the artwork to maintain, doubling the time it would take to update something as small as the shape of Quick Draw’s tail.</p>

<h2 id="the-solution-one-svg-symbol-library-and-multiple-uses">The Solution: One SVG Symbol Library And Multiple Uses</h2>

<p>Remember, my goal is to:</p>

<ul>
<li>Serve one version of Bow Wow Bandit to smaller screens,</li>
<li>Serve a different version to larger screens,</li>
<li>Define my artwork just once (DRY), and</li>
<li>Be able to resize and reposition elements.</li>
</ul>

<p>I don’t read about it enough, but the <code>&lt;symbol&gt;</code> element lets you define reusable SVG elements that can be hidden and reused to improve maintainability and reduce code bloat. They’re like components for SVG: <a href="https://css-tricks.com/svg-symbol-good-choice-icons/">create once and use wherever you need them</a>:</p>

<pre><code class="language-svg">&lt;svg xmlns="http://www.w3.org/2000/svg" style="display: none;"&gt;
  &lt;symbol id="quick-draw-body" viewBox="0 0 620 700"&gt;
    &lt;g class="quick-draw-body"&gt;[…]&lt;/g&gt;
  &lt;/symbol&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;use href="#quick-draw-body" /&gt;
</code></pre>

<p>A <code>&lt;symbol&gt;</code> is like storing a character in a library. I can reference it as many times as I need, to keep my code consistent and lightweight. Using <code>&lt;use&gt;</code> elements, I can insert the same symbol multiple times, at different positions or sizes, and even in different SVGs.</p>

<p>Each <code>&lt;symbol&gt;</code> must have its own <code>viewBox</code>, which defines its internal coordinate system. That means paying special attention to how SVG elements are exported from apps like Sketch.</p>

<div class="partners__lead-place"></div>

<h2 id="exporting-for-individual-viewboxes">Exporting For Individual Viewboxes</h2>

<p>I wrote before about <a href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/">how I export elements</a> in layers to make working with them easier. That process is a little different when creating symbols.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png"
			
			sizes="100vw"
			alt=""
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      My usual process of exporting elements from Sketch. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/6-exporting-elements-from-sketch.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Ordinarily, I would export all my elements using the same <code>viewBox</code>size. But when I’m creating a <code>symbol</code>, I need it to have its own specific <code>viewBox</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png"
			
			sizes="100vw"
			alt="Exporting elements from Sketch as individual SVG files."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Exporting elements from Sketch as individual SVG files. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/7-exporting-elements-sketch-individual-svgs-files.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>So I export each element as an individually sized SVG, which gives me the dimensions I need to convert its content into a <code>symbol</code>. Let’s take the SVG of Quick Draw McGraw’s hat, which has a <code>viewBox</code> size of 294×182:</p>

<pre><code class="language-svg">&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 294 182"&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;
</code></pre>

<p>I swap the SVG tags for <code>&lt;symbol&gt;</code> and add its artwork to my SVG library:</p>

<pre><code class="language-svg">&lt;svg xmlns="http://www.w3.org/2000/svg" style="display: none;"&gt;
  &lt;symbol id="quick-draw-hat" viewBox="0 0 294 182"&gt;
    &lt;g class="quick-draw-hat"&gt;[…]&lt;/g&gt;
  &lt;/symbol&gt;
&lt;/svg&gt;
</code></pre>

<p>Then, I repeat the process for all the remaining elements in my artwork. Now, if I ever need to update any of my symbols, the changes will be automatically applied to every instance it’s used.</p>

<h2 id="using-a-symbol-in-multiple-svgs">Using A <code>&lt;symbol&gt;</code> In Multiple SVGs</h2>

<p>I wanted my elements to appear in both versions of Bow Wow Bandit, one arrangement for smaller screens and an alternative arrangement for larger ones. So, I create both SVGs:</p>

<pre><code class="language-svg">&lt;svg class="svg-small" viewBox="0 0 1080 1440"&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;svg class="svg-large" viewBox="0 0 1920 1080"&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;
</code></pre>

<p>…and insert links to my symbols in both:</p>

<pre><code class="language-svg">&lt;svg class="svg-small" viewBox="0 0 1080 1440"&gt;
  &lt;use href="#quick-draw-hat" /&gt;
&lt;/svg&gt;

&lt;svg class="svg-large" viewBox="0 0 1920 1080"&gt;
  &lt;use href="#quick-draw-hat" /&gt;
&lt;/svg&gt;
</code></pre>

<h2 id="positioning-symbols">Positioning Symbols</h2>

<p>Once I’ve placed symbols into my layout using <code>&lt;use&gt;</code>, my next step is to position them, which is especially important if I want alternative layouts for different screen sizes. Symbols behave like <code>&lt;g&gt;</code> groups, so I can scale and move them using attributes like <code>width</code>, <code>height</code>, and <code>transform</code>:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;svg class="svg-small" viewBox="0 0 1080 1440"&gt;
  &lt;use href="#quick-draw-hat" width="294" height="182" transform="translate(-30,610)"/&gt;
&lt;/svg&gt;

&lt;svg class="svg-large" viewBox="0 0 1920 1080"&gt;
  &lt;use href="#quick-draw-hat" width="294" height="182" transform="translate(350,270)"/&gt;
&lt;/svg&gt;
</code></pre>
</div>

<p>I can place each <code>&lt;use&gt;</code> element independently using <code>transform</code>. This is powerful because rather than repositioning elements inside my SVGs, I move the <code>&lt;use&gt;</code> references. My internal layout stays clean, and the file size remains small because I’m not duplicating artwork. A browser only loads it once, which reduces bandwidth and speeds up page rendering. And because I’m always referencing the same <code>symbol</code>, their appearance stays consistent, whatever the screen size.</p>

<h2 id="animating-use-elements">Animating <code>&lt;use&gt;</code> Elements</h2>

<p>Here’s where things got tricky. I wanted to animate parts of my characters &mdash; like Quick Draw’s hat tilting and his legs kicking. But when I added CSS animations targeting internal elements inside a <code>&lt;symbol&gt;</code>, nothing happened.</p>

<p><strong>Tip:</strong> You can animate the <code>&lt;use&gt;</code> element itself, but not elements inside the <code>&lt;symbol&gt;</code>. If you want individual parts to move, make them their own symbols and animate each <code>&lt;use&gt;</code>.</p>

<p>Turns out, you can’t style or animate a <code>&lt;symbol&gt;</code>, because <code>&lt;use&gt;</code> creates shadow DOM clones that aren’t easily targetable. So, I had to get sneaky. Inside each <code>&lt;symbol&gt;</code> in my library SVG, I added a <code>&lt;g&gt;</code> element around the part I wanted to animate:</p>

<pre><code class="language-svg">&lt;symbol id="quick-draw-hat" viewBox="0 0 294 182"&gt;
  &lt;g class="quick-draw-hat"&gt;
    &lt;!-- ... --&gt;
  &lt;/g&gt;
&lt;/symbol&gt;
</code></pre>

<p>…and animated it using an attribute substring selector, targeting the <code>href</code> attribute of the <code>use</code> element:</p>

<pre><code class="language-css">use[href="#quick-draw-hat"] {
  animation-delay: 0.5s;
  animation-direction: alternate;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-name: hat-rock;
  animation-timing-function: ease-in-out;
  transform-origin: center bottom;
}

@keyframes hat-rock {
from { transform: rotate(-2deg); }
to   { transform: rotate(2deg); } }
</code></pre>

<div class="partners__lead-place"></div>

<h2 id="media-queries-for-display-control">Media Queries For Display Control</h2>

<p>Once I’ve created my two visible SVGs &mdash; one for small screens and one for larger ones &mdash; the final step is deciding which version to show at which screen size. I use CSS Media Queries to hide one SVG and show the other. I start by showing the small-screen SVG by default:</p>

<pre><code class="language-css">.svg-small { display: block; }
.svg-large { display: none; }
</code></pre>

<p>Then I use a <code>min-width</code> media query to switch to the large-screen SVG at <code>64rem</code> and above:</p>

<pre><code class="language-css">@media (min-width: 64rem) {
  .svg-small { display: none; }
  .svg-large { display: block; }
}
</code></pre>

<p>This ensures there’s only ever one SVG visible at a time, keeping my layout simple and the DOM free from unnecessary clutter. And because both visible SVGs reference the same hidden <code>&lt;symbol&gt;</code> library, the browser only downloads the artwork once, regardless of how many <code>&lt;use&gt;</code> elements appear across the two layouts.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png"
			
			sizes="100vw"
			alt="The final adaptive SVG."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View the final adaptive SVG on my <a href='https://stuffandnonsense.co.uk/toon-titles/quick-draw-3.html'>Toon Titles website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-5-building-adaptive-svgs/8-final-adaptive-svg.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>By combining <code>&lt;symbol&gt;</code>, <code>&lt;use&gt;</code>, CSS Media Queries, and specific transforms, I can build <strong>adaptive SVGs</strong> that reposition their elements without duplicating content, loading extra assets, or relying on JavaScript. I need to define each graphic only once in a hidden symbol library. Then I can reuse those graphics, as needed, inside several visible SVGs. With CSS doing the layout switching, the <strong>result is fast and flexible</strong>.</p>

<p>It’s a reminder that some of the most powerful techniques on the web don’t need big frameworks or complex tooling &mdash; just a bit of SVG know-how and a clever use of the basics.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Andy Clarke</author><title>Ambient Animations In Web Design: Principles And Implementation (Part 1)</title><link>https://www.smashingmagazine.com/2025/09/ambient-animations-web-design-principles-implementation/</link><pubDate>Mon, 22 Sep 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/09/ambient-animations-web-design-principles-implementation/</guid><description>Creating motion can be tricky. Too much and it’s distracting. Too little and a design feels flat. Ambient animations are the middle ground &amp;mdash; subtle, slow-moving details that add atmosphere without stealing the show. In this article, web design pioneer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> introduces the concept of ambient animations and explains how to implement them.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/09/ambient-animations-web-design-principles-implementation/" />
              <title>Ambient Animations In Web Design: Principles And Implementation (Part 1)</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Ambient Animations In Web Design: Principles And Implementation (Part 1)</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-09-22T13:00:00&#43;00:00" class="op-published">2025-09-22T13:00:00+00:00</time>
                  <time datetime="2025-09-22T13:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Unlike <em>timeline-based</em> animations, which tell stories across a sequence of events, or <em>interaction</em> animations that are triggered when someone touches something, <strong>ambient animations</strong> are the kind of passive movements you might not notice at first. But, they make a design look alive in subtle ways.</p>

<p>In an ambient animation, elements might subtly transition between colours, move slowly, or gradually shift position. Elements can appear and disappear, change size, or they could rotate slowly.</p>

<p>Ambient animations aren’t intrusive; they don’t demand attention, aren’t distracting, and don’t interfere with what someone’s trying to achieve when they use a product or website. They can be playful, too, making someone smile when they catch sight of them. That way, ambient animations <strong>add depth to a brand’s personality</strong>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="399"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png"
			
			sizes="100vw"
			alt="A three-page spread of a Quick Draw McGraw comic book including the animated cover and first two pages."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Hanna-Barbera’s Quick Draw McGraw © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/1-quick-draw-mcgraw-comic-book.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p class="c-pre-sidenote--left">To illustrate the concept of ambient animations, I’ve recreated the cover of a <a href="https://en.wikipedia.org/wiki/Quick_Draw_McGraw"><em>Quick Draw McGraw</em></a> <a href="https://dn720005.ca.archive.org/0/items/QuickDrawMcGrawCharlton/Quick%20Draw%20McGraw%20%233%20%28Charlton%201971%29.pdf">comic book</a> (PDF) as a CSS/SVG animation. The comic was published by Charlton Comics in 1971, and, being printed, these characters didn’t move, making them ideal candidates to transform into ambient animations.</p>
<p class="c-sidenote c-sidenote--right"><strong>FYI</strong>: Original cover artist <a href="https://www.lambiek.net/artists/d/dirgo_ray.htm">Ray Dirgo</a> was best known for his work drawing Hanna-Barbera characters for Charlton Comics during the 1970s. Ray passed away in 2000 at the age of 92. He outlived Charlton Comics, which went out of business in 1986, and DC Comics acquired its characters.</p>

<p><strong>Tip</strong>: You can view the complete ambient animation <a href="https://codepen.io/malarkey/pen/NPGrWVy">code on CodePen</a>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png"
			
			sizes="100vw"
			alt="Quick Draw McGraw ambient animations."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Quick Draw McGraw ambient animations. (<a href='https://codepen.io/malarkey/pen/NPGrWVy'>Live Demo</a>) (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/2-quick-draw-mcgraw-ambient-animations.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="choosing-elements-to-animate">Choosing Elements To Animate</h2>

<p>Not everything on a page or in a graphic needs to move, and part of designing an ambient animation is <strong>knowing when to stop</strong>. The trick is to pick elements that lend themselves naturally to subtle movement, rather than forcing motion into places where it doesn’t belong.</p>

<h3 id="natural-motion-cues">Natural Motion Cues</h3>

<p>When I’m deciding what to animate, I look for natural motion cues and think about when something would move naturally in the real world. I ask myself: <em>“Does this thing have weight?”</em>, <em>“Is it flexible?”</em>, and <em>“Would it move in real life?”</em> If the answer’s <em>“yes,”</em> it’ll probably feel right if it moves. There are several motion cues in Ray Dirgo’s cover artwork.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png"
			
			sizes="100vw"
			alt="Vibrantly illustrated pipe adorned with two feathers on the end against a silhouetted toon title card."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Pipe and feathers swing slightly. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/3-pipe-feathers-toon-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For example, the peace pipe Quick Draw’s puffing on has two feathers hanging from it. They swing slightly left and right by three degrees as the pipe moves, just like real feathers would.</p>

<div class="break-out">
<pre><code class="language-css">&#35;quick-draw-pipe {
  animation: quick-draw-pipe-rotate 6s ease-in-out infinite alternate;
}

@keyframes quick-draw-pipe-rotate {
  0% { transform: rotate(3deg); }
  100% { transform: rotate(-3deg); }
}

&#35;quick-draw-feather-1 {
  animation: quick-draw-feather-1-rotate 3s ease-in-out infinite alternate;
}

&#35;quick-draw-feather-2 {
  animation: quick-draw-feather-2-rotate 3s ease-in-out infinite alternate;
}

@keyframes quick-draw-feather-1-rotate {
  0% { transform: rotate(3deg); }
  100% { transform: rotate(-3deg); }
}

@keyframes quick-draw-feather-2-rotate {
  0% { transform: rotate(-3deg); }
  100% { transform: rotate(3deg); }
}
</code></pre>
</div>

<h3 id="atmosphere-not-action">Atmosphere, Not Action</h3>

<p>I often choose elements or decorative details that add to the vibe but don’t fight for attention.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aAmbient%20animations%20aren%e2%80%99t%20about%20signalling%20to%20someone%20where%20they%20should%20look;%20they%e2%80%99re%20about%20creating%20a%20mood.%20%0a&url=https://smashingmagazine.com%2f2025%2f09%2fambient-animations-web-design-principles-implementation%2f">
      
Ambient animations aren’t about signalling to someone where they should look; they’re about creating a mood. 

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Here, the chief slowly and subtly rises and falls as he puffs on his pipe.</p>

<pre><code class="language-css">&#35;chief {
  animation: chief-rise-fall 3s ease-in-out infinite alternate;
}

@keyframes chief-group-rise-fall {
  0% { transform: translateY(0); }
  100% { transform: translateY(-20px); }
}
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png"
			
			sizes="100vw"
			alt="An illustrated Indian chief seated and puffing on a pipe against a silhouetted toon title card."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The chief rises and falls as he puffs on his pipe. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/4-chief-toon-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For added effect, the feather on his head also moves in time with his rise and fall:</p>

<div class="break-out">
<pre><code class="language-css">&#35;chief-feather-1 {
  animation: chief-feather-1-rotate 3s ease-in-out infinite alternate;
}

&#35;chief-feather-2 {
  animation: chief-feather-2-rotate 3s ease-in-out infinite alternate;
}

@keyframes chief-feather-1-rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(-9deg); }
}

@keyframes chief-feather-2-rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(9deg); }
}
</code></pre>
</div>

<h3 id="playfulness-and-fun">Playfulness And Fun</h3>

<p>One of the things I love most about ambient animations is how they bring fun into a design. They’re an opportunity to <strong>demonstrate personality</strong> through playful details that make people smile when they notice them.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png"
			
			sizes="100vw"
			alt="Closeup of the illustrated chief’s head and face."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The chief’s eyebrows rise and fall, and his eyes cross. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/5-closeup-illustrated-chief-head.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Take a closer look at the chief, and you might spot his eyebrows raising and his eyes crossing as he puffs hard on his pipe. Quick Draw’s eyebrows also bounce at what look like random intervals.</p>

<pre><code class="language-css">&#35;quick-draw-eyebrow {
  animation: quick-draw-eyebrow-raise 5s ease-in-out infinite;
}

@keyframes quick-draw-eyebrow-raise {
  0%, 20%, 60%, 100% { transform: translateY(0); }
  10%, 50%, 80% { transform: translateY(-10px); }
}
</code></pre>

<div class="partners__lead-place"></div>

<h2 id="keep-hierarchy-in-mind">Keep Hierarchy In Mind</h2>

<p>Motion draws the eye, and even subtle movements have a visual weight. So, I reserve the most obvious animations for elements that I need to create the biggest impact.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png"
			
			sizes="100vw"
			alt="Illustrated Quick Draw McGraw holding the feather-adorned pipe with dizzy eyes veering right."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Quick Draw McGraw wobbles under the influence of his pipe. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/6-illustrated-duick-draw-mcgraw.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Smoking his pipe clearly has a big effect on Quick Draw McGraw, so to demonstrate this, I wrapped his elements &mdash; including his pipe and its feathers &mdash; within a new SVG group, and then I made that wobble.</p>

<pre><code class="language-css">&#35;quick-draw-group {
  animation: quick-draw-group-wobble 6s ease-in-out infinite;
}

@keyframes quick-draw-group-wobble {
  0% { transform: rotate(0deg); }
  15% { transform: rotate(2deg); }
  30% { transform: rotate(-2deg); }
  45% { transform: rotate(1deg); }
  60% { transform: rotate(-1deg); }
  75% { transform: rotate(0.5deg); }
  100% { transform: rotate(0deg); }
}
</code></pre>

<p>Then, to emphasise this motion, I mirrored those values to wobble his shadow:</p>

<pre><code class="language-css">&#35;quick-draw-shadow {
  animation: quick-draw-shadow-wobble 6s ease-in-out infinite;
}

@keyframes quick-draw-shadow-wobble {
  0% { transform: rotate(0deg); }
  15% { transform: rotate(-2deg); }
  30% { transform: rotate(2deg); }
  45% { transform: rotate(-1deg); }
  60% { transform: rotate(1deg); }
  75% { transform: rotate(-0.5deg); }
  100% { transform: rotate(0deg); }
}
</code></pre>

<h2 id="apply-restraint">Apply Restraint</h2>

<p>Just because something can be animated doesn’t mean it should be. When creating an ambient animation, I study the image and note the elements where subtle motion might add life. I keep in mind the questions: <em>“What’s the story I’m telling? Where does movement help, and when might it become distracting?”</em></p>

<p>Remember, restraint isn’t just about doing less; it’s about doing the right things less often.</p>

<h2 id="layering-svgs-for-export">Layering SVGs For Export</h2>

<p>In “<a href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/">Smashing Animations Part 4: Optimising SVGs</a>,” I wrote about the process I rely on to <em>“prepare, optimise, and structure SVGs for animation.”</em> When elements are crammed into a single SVG file, they can be a nightmare to navigate. Locating a specific path or group can feel like searching for a needle in a haystack.</p>

<blockquote>That’s why I develop my SVGs in layers, exporting and optimising one set of elements at a time &mdash; always in the order they’ll appear in the final file. This lets me build the master SVG gradually by pasting it in each cleaned-up section.</blockquote>

<p>I start by exporting background elements, optimising them, adding class and ID attributes, and pasting their code into my SVG file.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png"
			
			sizes="100vw"
			alt="The toon title card with the chief and Quick Draw characters cut out with their shapes remaining."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Exporting background elements. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/7-toon-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then, I export elements that often stay static or move as groups, like the chief and Quick Draw McGraw.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png"
			
			sizes="100vw"
			alt="Showing Quick Draw pasted to the toon title card’s foreground, minus details including the pipe he is holding and his eyeballs."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Exporting larger groups. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/8-quick-draw-pasted-toon-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Before finally exporting, naming, and adding details, like Quick Draw’s pipe, eyes, and his stoned sparkles.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="484"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png"
			
			sizes="100vw"
			alt="Showing Quick Draw in the same toon title card but including the details that were left out before."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Adding details. (<a href='https://files.smashing.media/articles/ambient-animations-web-design-principles-implementation/9-quick-draw-toon-title-card-details.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Since I export each layer from the same-sized artboard, I don’t need to worry about alignment or positioning issues as they all slot into place automatically.</p>

<h2 id="implementing-ambient-animations">Implementing Ambient Animations</h2>

<p>You don’t need an animation framework or library to add ambient animations to a project. Most of the time, all you’ll need is a well-prepared SVG and some thoughtful CSS.</p>

<p>But, let’s start with the SVG. The key is to group elements logically and give them meaningful class or ID attributes, which act as animation hooks in the CSS. For this animation, I gave every moving part its own identifier like <code>#quick-draw-tail</code> or <code>#chief-smoke-2</code>. That way, I could target exactly what I needed without digging through the DOM like a raccoon in a trash can.</p>

<p>Once the SVG is set up, CSS does most of the work. I can use <code>@keyframes</code> for more expressive movement, or <code>animation-delay</code> to simulate randomness and stagger timings. The trick is to keep everything subtle and remember I’m not animating for attention, I’m animating for atmosphere.</p>

<p>Remember that most ambient animations loop continuously, so they should be <strong>lightweight</strong> and <strong>performance-friendly</strong>. And of course, <a href="https://www.smashingmagazine.com/2021/10/respecting-users-motion-preferences/">it’s good practice to respect users who’ve asked for less motion</a>. You can wrap your animations in an <code>@media prefers-reduced-motion</code> query so they only run when they’re welcome.</p>

<div class="break-out">
<pre><code class="language-javascript">@media (prefers-reduced-motion: no-preference) {
  &#35;quick-draw-shadow {
    animation: quick-draw-shadow-wobble 6s ease-in-out infinite;
  }
}
</code></pre>
</div>

<p>It’s a small touch that’s easy to implement, and it makes your designs more inclusive.</p>

<div class="partners__lead-place"></div>

<h2 id="ambient-animation-design-principles">Ambient Animation Design Principles</h2>

<p>If you want your animations to feel ambient, more like atmosphere than action, it helps to follow a few principles. These aren’t hard and fast rules, but rather things I’ve learned while animating smoke, sparkles, eyeballs, and eyebrows.</p>

<h3 id="keep-animations-slow-and-smooth">Keep Animations Slow And Smooth</h3>

<p>Ambient animations should feel relaxed, so use <strong>longer durations</strong> and choose <strong>easing curves that feel organic</strong>. I often use <code>ease-in-out</code>, but <a href="https://www.smashingmagazine.com/2022/10/advanced-animations-css/">cubic Bézier curves</a> can also be helpful when you want a more relaxed feel and the kind of movements you might find in nature.</p>

<h3 id="loop-seamlessly-and-avoid-abrupt-changes">Loop Seamlessly And Avoid Abrupt Changes</h3>

<p>Hard resets or sudden jumps can ruin the mood, so if an animation loops, ensure it cycles smoothly. You can do this by <strong>matching start and end keyframes</strong>, or by setting the <code>animation-direction</code> to <code>alternate</code> the value so the animation plays forward, then back.</p>

<h3 id="use-layering-to-build-complexity">Use Layering To Build Complexity</h3>

<p>A single animation might be boring. Five subtle animations, each on separate layers, can feel rich and alive. Think of it like building a sound mix &mdash; you want <strong>variation in rhythm, tone, and timing</strong>. In my animation, sparkles twinkle at varying intervals, smoke curls upward, feathers sway, and eyes boggle. Nothing dominates, and each motion plays its small part in the scene.</p>

<h3 id="avoid-distractions">Avoid Distractions</h3>

<p>The point of an ambient animation is that it doesn’t dominate. It’s a <strong>background element</strong> and not a call to action. If someone’s eyes are drawn to a raised eyebrow, it’s probably too much, so dial back the animation until it feels like something you’d only catch if you’re really looking.</p>

<h3 id="consider-accessibility-and-performance">Consider Accessibility And Performance</h3>

<p>Check <code>prefers-reduced-motion</code>, and don’t assume everyone’s device can handle complex animations. SVG and CSS are light, but things like blur filters and drop shadows, and complex CSS animations can still tax lower-powered devices. When an animation is purely decorative, consider adding <code>aria-hidden=&quot;true&quot;</code> to keep it from cluttering up the accessibility tree.</p>

<h2 id="quick-on-the-draw">Quick On The Draw</h2>

<p>Ambient animation is like seasoning on a great dish. It’s the pinch of salt you barely notice, but you’d miss when it’s gone. It doesn’t shout, it whispers. It doesn’t lead, it lingers. It’s floating smoke, swaying feathers, and sparkles you catch in the corner of your eye. And when it’s done well, ambient animation <strong>adds personality to a design without asking for applause</strong>.</p>

<p>Now, I realise that not everyone needs to animate cartoon characters. So, in part two, I’ll share how I created animations for several recent client projects. Until next time, if you’re crafting an illustration or working with SVG, ask yourself: <strong>What would move if this were real?</strong> Then animate just that. Make it slow and soft. Keep it ambient.</p>

<p>You can view the complete ambient animation <a href="https://codepen.io/malarkey/pen/NPGrWVy">code on CodePen</a>.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Marius Sarca</author><title>Creating Elastic And Bounce Effects With Expressive Animator</title><link>https://www.smashingmagazine.com/2025/09/creating-elastic-bounce-effects-expressive-animator/</link><pubDate>Mon, 15 Sep 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/09/creating-elastic-bounce-effects-expressive-animator/</guid><description>Elastic and bounce effects have long been among the most desirable but time-consuming techniques in motion design. Expressive Animator streamlines the process, making it possible to produce lively animations in seconds, bypassing the tedious work of manual keyframe editing.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/09/creating-elastic-bounce-effects-expressive-animator/" />
              <title>Creating Elastic And Bounce Effects With Expressive Animator</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Creating Elastic And Bounce Effects With Expressive Animator</h1>
                  
                    
                    <address>Marius Sarca</address>
                  
                  <time datetime="2025-09-15T10:00:00&#43;00:00" class="op-published">2025-09-15T10:00:00+00:00</time>
                  <time datetime="2025-09-15T10:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                <p>This article is sponsored by <b>Expressive</b></p>
                

<p>In the world of modern web design, SVG images are used everywhere, from illustrations to icons to background effects, and are universally prized for their crispness and lightweight size. While static SVG images play an important role in web design, most of the time their true potential is unlocked only when they are combined with motion.</p>

<p>Few things add more life and personality to a website than a well-executed SVG animation. But not all animations have the same impact in terms of digital experience. For example, <strong>elastic and bounce effects</strong> have a unique appeal in motion design because they bring a <strong>sense of realism into movement</strong>, making animations more engaging and memorable.</p>

<figure><a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/grumpy-egg.gif"><img src="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/grumpy-egg-800.gif" width="800" height="800" alt="Grumpy Egg" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/grumpy-egg.gif">Large preview</a>)</figcaption></figure>

<p>However, anyone who has dived into animating SVGs knows <a href="https://www.smashingmagazine.com/2023/02/putting-gears-motion-animating-cars-with-html-svg/">the technical hurdles involved</a>. Creating a convincing elastic or bounce effect traditionally requires handling complex CSS keyframes or wrestling with JavaScript animation libraries. Even when using an SVG animation editor, it will most likely require you to manually add the keyframes and adjust the easing functions between them, which can become a time-consuming process of trial and error, no matter the level of experience you have.</p>

<p>This is where Expressive Animator shines. It allows creators to apply elastic and bounce effects <strong>in seconds</strong>, bypassing the tedious work of manual keyframe editing. And the result is always exceptional: animations that feel <em>alive</em>, produced with a fraction of the effort.</p>

<h2 id="using-expressive-animator-to-create-an-elastic-effect">Using Expressive Animator To Create An Elastic Effect</h2>

<p>Creating an elastic effect in Expressive Animator is remarkably simple, fast, and intuitive, since the effect is built right into the software as an easing function. This means you only need two keyframes (start and end) to make the effect, and the software will automatically handle the springy motion in between. Even better, the elastic easing can be applied to <strong>any animatable property</strong> (e.g., position, scale, rotation, opacity, morph, etc.), giving you a consistent way to add it to your animations.</p>

<p>Before we dive into the tutorial, take a look at the video below to see what you will learn to create and the entire process from start to finish.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1116135653"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>First things first, let’s set the scene. For this, we’ll <a href="https://expressive.app/expressive-animator/docs/v1/projects/create/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">create a new project</a> by pressing <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>P</kbd> and configuring it in the “Create New Project” dialog that pops up. For frame size, we’ll choose 1080×1080, for a duration of 00:01:30, and we’ll let the frame rate remain unchanged at 60 frames per second (fps).</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png"
			
			sizes="100vw"
			alt="“Create New Project” dialog"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/01-create-dialog.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once you hit the “Create project” button, you can use the <a href="https://expressive.app/expressive-animator/docs/v1/tools/pen-tool/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Pen</a> and <a href="https://expressive.app/expressive-animator/docs/v1/tools/ellipse-tool/">Ellipse</a> tools to create the artwork that will be animated, or you can simply copy and paste the artwork below.</p>

<figure class="break-out">
	<p data-height="600"
	data-theme-id="light"
	data-slug-hash="pvjmwxv"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Effects With Expressive Animator - Artwork for Animation](https://codepen.io/smashingmag/pen/pvjmwxv).</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvjmwxv">Effects With Expressive Animator - Artwork for Animation</a>.</figcaption>
</figure>

<p>Now that everything has been set up, let’s create the animation. Make sure that snapping and auto-record are enabled, then move the playhead to 01:00f. By <a href="https://expressive.app/expressive-animator/docs/v1/canvas/snapping/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">enabling snapping</a>, you will be able to perfectly align nodes and graphic objects on the canvas. On the other hand, as the name suggests, auto-record tracks every change you make to the artwork and adds the appropriate keyframes on the timeline.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png"
			
			sizes="100vw"
			alt="Screenshot with snapping and auto-record are enabled and the playhead moved to 01:00f"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/02-prepare-scene.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Press the <kbd>A</kbd> key on your keyboard to switch to the <a href="https://expressive.app/expressive-animator/docs/v1/tools/node-tool/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Node tool</a>, then select the String object and move its handle to the center-right point of the artboard. Don’t worry about precision, as the snapping will do all the heavy lifting for you. This will bend the shape and add keyframes for the Morph animator.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png"
			
			sizes="100vw"
			alt="Screenshot with the String object and its handle moved to the center-right point of the artboard"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/03-string.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Next, press the <kbd>V</kbd> key on your keyboard to switch to the <a href="https://expressive.app/expressive-animator/docs/v1/tools/selection-tool/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Selection tool</a>. With this tool enabled, select the Ball, move it to the right, and place it in the middle of the string. Once again, snapping will do all the hard work, allowing you to position the ball exactly where you want to, while auto-recording automatically adds the appropriate keyframes.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png"
			
			sizes="100vw"
			alt="Screenshot with the Ball selected and moved to the middle of the string"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/04-ball.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can now replay the animation and disable auto-recording by clicking on the Auto-Record button again.</p>

<p>As you can see when replaying, the direction in which the String and Ball objects are moving is wrong. Fortunately, we can fix this extremely easily just by reversing the keyframes. To do this, select the keyframes in the timeline and right-click to open the context menu and choose Reverse. This will reverse the keyframes, and if you replay the animation, you will see that the direction is now correct.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png"
			
			sizes="100vw"
			alt="Screenshot with the context menu where you can choose Reverse"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/05-reverse.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With this out of the way, we can finally add the elastic effect. Select all the keyframes in the timeline and click on the Custom easing button to open a dialog with easing options. From the dialog, choose Elastic and set the oscillations to 4 and the stiffness to 2.5.</p>

<p>That’s it! Click anywhere outside the easing dialog to close it and replay the animation to see the result.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png"
			
			sizes="100vw"
			alt="Selected custom easing button that opened a dialog with easing options"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/06-effect.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><a href="https://expressive.app/expressive-animator/docs/v1/export/svg/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">The animation can be exported as well.</a> Press <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>E</kbd> on your keyboard to open the export dialog and choose from various export options, ranging from vectorized formats, such as <a href="https://expressive.app/expressive-animator/docs/v1/export/svg/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">SVG</a> and <a href="https://expressive.app/expressive-animator/docs/v1/export/lottie/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Lottie</a>, to rasterized formats, such as <a href="https://expressive.app/expressive-animator/docs/v1/export/image/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">GIF</a> and <a href="https://expressive.app/expressive-animator/docs/v1/export/video/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">video</a>.</p>

<p>For this specific animation, we’re going to choose the SVG export format. Expressive Animator allows you to choose between three different types of SVG, depending on the technology used for animation: <a href="https://expressive.app/expressive-animator/docs/v1/export/svg/smil/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">SMIL</a>, <a href="https://expressive.app/expressive-animator/docs/v1/export/svg/css/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">CSS</a>, or <a href="https://expressive.app/expressive-animator/docs/v1/export/svg/js/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">JavaScript</a>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="467"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png"
			
			sizes="100vw"
			alt="Export settings in the Expressive Animator"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/07-export.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Each of these technologies has different strengths and weaknesses, but for this tutorial, we are going to choose SMIL. This is because SMIL-based animations are widely supported, even on Safari browsers, and can be used as background images or embedded in HTML pages using the <code>&lt;img&gt;</code>  tag. In fact, <a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/">Andy Clarke recently wrote all about SMIL animations here at Smashing Magazine</a> if you want a full explanation of how it works.</p>

<p>You can visualize the exported SVG in the following CodePen demo:</p>

<figure class="break-out">
	<p data-height="600"
	data-theme-id="light"
	data-slug-hash="GgpaEyG"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Expressive Animator - Exported SVG](https://codepen.io/smashingmag/pen/GgpaEyG).</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgpaEyG">Expressive Animator - Exported SVG</a>.</figcaption>
</figure>

<h2 id="expressive-animator-for-bounce-and-other-effects">Expressive Animator For Bounce And Other Effects</h2>

<p>Adding a bounce effect to an animation is very similar to the process we just covered for creating an elastic effect, since both are built into Expressive Animator as easing functions. Just like elastic, bounce easing can be applied to any animatable property, giving you quick ways to create realistic motion.</p>

<p>Beyond these two effects, Expressive Animator also offers other easing options that can shape the personality of your animation, like Back, Steps, Sinc, just to name a few.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="757"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png"
			
			sizes="100vw"
			alt="Easing functions in the Expressive Animator"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/08-easing-functions.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>Elastic and bounce effects have long been among the most desirable but time-consuming techniques in motion design. By integrating them directly into its easing functions, Expressive Animator removes the complexity of manual keyframe manipulation and transforms what used to be a technical challenge into a creative opportunity.</p>

<p>The best part is that getting started with Expressive Animator comes with zero risk. The software offers a full 7&ndash;day <strong>free trial without requiring an account</strong>, so you can download it instantly and begin experimenting with your own designs right away. After the trial ends, you can buy Expressive Animator with a one-time payment, <strong>no subscription required</strong>. This will give you a perpetual license covering both Windows and macOS.</p>

<p>To help you get started even faster, I’ve prepared some extra resources for you. You’ll find the source files for the animations created in this tutorial, along with a curated list of useful links that will guide you further in exploring Expressive Animator and SVG animation. These materials are meant to give you a solid starting point so you can learn, experiment, and build on your own with confidence.</p>

<ul>
<li>Grumpy Egg: The <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/grumpy-egg.eaf" download><code>.eaf</code></a> source file for the sample animation presented at the beginning of this article.</li>
<li>Elastic Effect: Another <a href="https://files.smashing.media/articles/creating-elastic-bounce-effects-expressive-animator/elastic-effect.eaf" download><code>.eaf</code></a> file, this time for the animation we made in this tutorial.</li>
<li><a href="https://expressive.app/expressive-animator/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Get started with Expressive Animator</a></li>
<li>Expressive Animator <a href="https://expressive.app/expressive-animator/docs/v1/?utm_source=smashingmagazine&amp;utm_medium=blog&amp;utm_campaign=elastic_effect">Documentation</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Blake Lundquist</author><title>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</title><link>https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/</link><pubDate>Wed, 11 Jun 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/</guid><description>In this tutorial, Blake Lundquist walks us through two methods of creating the “moving-highlight” navigation pattern using only plain JavaScript and CSS. The first technique uses the &lt;code>getBoundingClientRect&lt;/code> method to explicitly animate the border between navigation bar items when they are clicked. The second approach achieves the same functionality using the new View Transition API.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/" />
              <title>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</h1>
                  
                    
                    <address>Blake Lundquist</address>
                  
                  <time datetime="2025-06-11T13:00:00&#43;00:00" class="op-published">2025-06-11T13:00:00+00:00</time>
                  <time datetime="2025-06-11T13:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>I recently came across an old jQuery tutorial demonstrating a <strong>“moving highlight” navigation bar</strong> and decided the concept was due for a modern upgrade. With this pattern, the border around the active navigation item animates directly from one element to another as the user clicks on menu items. In 2025, we have much better tools to manipulate the DOM via vanilla JavaScript. New features like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">View Transition API</a> make progressive enhancement more easily achievable and handle a lot of the animation minutiae.</p>

<figure><a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif"><img src="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif" width="800" height="131" alt="An example of a “moving highlight” navigation bar" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif">Large preview</a>)</figcaption></figure>

<p>In this tutorial, I will demonstrate two methods of creating the “moving highlight” navigation bar using plain JavaScript and CSS. The first example uses the <code>getBoundingClientRect</code> method to explicitly animate the border between navigation bar items when they are clicked. The second example achieves the same functionality using the new View Transition API.</p>

<h2 id="the-initial-markup">The Initial Markup</h2>

<p>Let’s assume that we have a single-page application where content changes without the page being reloaded. The starting HTML and CSS are your standard navigation bar with an additional <code>div</code> element containing an <code>id</code> of <code>#highlight</code>. We give the first navigation item a class of <code>.active</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="EajQyBW"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar Starting Markup [forked]](https://codepen.io/smashingmag/pen/EajQyBW) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/EajQyBW">Moving Highlight Navbar Starting Markup [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<p>For this version, we will position the <code>#highlight</code> element around the element with the <code>.active</code> class to create a border. We can utilize <code>absolute</code> positioning and animate the element across the navigation bar to create the desired effect. We’ll hide it off-screen initially by adding <code>left: -200px</code> and include <code>transition</code> styles for all properties so that any changes in the position and size of the element will happen gradually.</p>

<pre><code class="language-css">&#35;highlight {
  z-index: 0;
  position: absolute;
  height: 100%;
  width: 100px;
  left: -200px;
  border: 2px solid green;
  box-sizing: border-box;
  transition: all 0.2s ease;
}
</code></pre>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="add-a-boilerplate-event-handler-for-click-interactions">Add A Boilerplate Event Handler For Click Interactions</h2>

<p>We want the highlight element to animate when a user changes the <code>.active</code> navigation item. Let’s add a <code>click</code> event handler to the <code>nav</code> element, then filter for events caused only by elements matching our desired selector. In this case, we only want to change the <code>.active</code> nav item if the user clicks on a link that does not already have the <code>.active</code> class.</p>

<p>Initially, we can call <code>console.log</code> to ensure the handler fires only when expected:</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  console.log('click');
});
</code></pre>
</div>

<p>Open your browser console and try clicking different items in the navigation bar. You should only see <code>&quot;click&quot;</code> being logged when you select a new item in the navigation bar.</p>

<p>Now that we know our event handler is working on the correct elements let’s add code to move the <code>.active</code> class to the navigation item that was clicked. We can use the object passed into the event handler to find the element that initialized the event and give that element a class of <code>.active</code> after removing it from the previously active item.</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
-  console.log('click');
+  document.querySelector('nav a.active').classList.remove('active');
+  event.target.classList.add('active');
  
});
</code></pre>
</div>

<p>Our <code>#highlight</code> element needs to move across the navigation bar and position itself around the active item. Let’s write a function to calculate a new position and width. Since the <code>#highlight</code> selector has <code>transition</code> styles applied, it will move gradually when its position changes.</p>

<p>Using <code>getBoundingClientRect</code>, we can get information about the position and size of an element. We calculate the width of the active navigation item and its offset from the left boundary of the parent element. Then, we assign styles to the highlight element so that its size and position match.</p>

<div class="break-out">
<pre><code class="language-javascript">// handler for moving the highlight
const moveHighlight = () =&gt; {
  const activeNavItem = document.querySelector('a.active');
  const highlighterElement = document.querySelector('#highlight');
  
  const width = activeNavItem.offsetWidth;

  const itemPos = activeNavItem.getBoundingClientRect();
  const navbarPos = navbar.getBoundingClientRect()
  const relativePosX = itemPos.left - navbarPos.left;

  const styles = {
    left: `${relativePosX}px`,
    width: `${width}px`,
  };

  Object.assign(highlighterElement.style, styles);
}
</code></pre>
</div>

<p>Let’s call our new function when the click event fires:</p>

<div class="break-out">
<pre><code class="language-javascript">navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  document.querySelector('nav a.active').classList.remove('active');
  event.target.classList.add('active');
  
+  moveHighlight();
});
</code></pre>
</div>

<p>Finally, let’s also call the function immediately so that the border moves behind our initial active item when the page first loads:</p>

<pre><code class="language-javascript">// handler for moving the highlight
const moveHighlight = () =&gt; {
 // ...
}

// display the highlight when the page loads
moveHighlight();
</code></pre>

<p>Now, the border moves across the navigation bar when a new item is selected. Try clicking the different navigation links to animate the navigation bar.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="WbvMxqV"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar [forked]](https://codepen.io/smashingmag/pen/WbvMxqV) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/WbvMxqV">Moving Highlight Navbar [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<p>That only took a few lines of vanilla JavaScript and could easily be extended to account for other interactions, like <code>mouseover</code> events. In the next section, we will explore refactoring this feature using the View Transition API.</p>

<div class="partners__lead-place"></div>

<h2 id="using-the-view-transition-api">Using The View Transition API</h2>

<p>The View Transition API provides functionality to create animated transitions between website views. Under the hood, the API creates snapshots of “before” and “after” views and then handles transitioning between them. View transitions are useful for creating animations between documents, providing the <strong>native-app-like user experience</strong> featured in frameworks like <a href="https://docs.astro.build/en/guides/view-transitions/">Astro</a>. However, the API also provides handlers meant for <strong>SPA-style applications</strong>. We will use it to reduce the JavaScript needed in our implementation and more easily create fallback functionality.</p>

<p>For this approach, we no longer need a separate <code>#highlight</code> element. Instead, we can style the <code>.active</code> navigation item directly using pseudo-selectors and let the View Transition API handle the animation between the before-and-after UI states when a new navigation item is clicked.</p>

<p>We’ll start by getting rid of the <code>#highlight</code> element and its associated CSS and replacing it with styles for the <code>nav a::after</code> pseudo-selector:</p>

<pre><code class="language-html">&lt;nav&gt;
  - &lt;div id="highlight"&gt;&lt;/div&gt;
  &lt;a href="#" class="active"&gt;Home&lt;/a&gt;
  &lt;a href="#services"&gt;Services&lt;/a&gt;
  &lt;a href="#about"&gt;About&lt;/a&gt;
  &lt;a href="#contact"&gt;Contact&lt;/a&gt;
&lt;/nav&gt;
</code></pre>

<pre><code class="language-css">- &#35;highlight {
-  z-index: 0;
-  position: absolute;
-  height: 100%;
-  width: 0;
-  left: 0;
-  box-sizing: border-box;
-  transition: all 0.2s ease;
- }

+ nav a::after {
+  content: " ";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border: none;
+  box-sizing: border-box;
+ }
</code></pre>

<p>For the <code>.active</code> class, we include the <code>view-transition-name</code> property, thus unlocking the magic of the View Transition API. Once we trigger the view transition and change the location of the <code>.active</code> navigation item in the DOM, “before” and “after” snapshots will be taken, and the browser will animate the border across the bar. We’ll give our view transition the name of <code>highlight</code>, but we could theoretically give it any name.</p>

<pre><code class="language-css">nav a.active::after {
  border: 2px solid green;
  view-transition-name: highlight;
}
</code></pre>

<p>Once we have a selector that contains a <code>view-transition-name</code> property, the only remaining step is to trigger the transition using the <code>startViewTransition</code> method and pass in a callback function.</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

// Change the active nav item on click
navbar.addEventListener('click', async  function (event) {

  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }
  
  document.startViewTransition(() =&gt; {
    document.querySelector('nav a.active').classList.remove('active');

    event.target.classList.add('active');
  });
});
</code></pre>
</div>

<p>Above is a revised version of the <code>click</code> handler. Instead of doing all the calculations for the size and position of the moving border ourselves, the View Transition API handles all of it for us. We only need to call <code>document.startViewTransition</code> and pass in a callback function to change the item that has the <code>.active</code> class!</p>

<div class="partners__lead-place"></div>

<h2 id="adjusting-the-view-transition">Adjusting The View Transition</h2>

<p>At this point, when clicking on a navigation link, you’ll notice that the transition works, but some strange sizing issues are visible.</p>

<figure><a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues.gif"><img src="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues-800px.gif" width="800" height="163" alt="The view transition with sizing issues" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues.gif">Large preview</a>)</figcaption></figure>

<p>This sizing inconsistency is caused by aspect ratio changes during the course of the view transition. We won’t go into detail here, but <a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/">Jake Archibald has a detailed explanation you can read</a> for more information. In short, to ensure the height of the border stays uniform throughout the transition, we need to declare an explicit <code>height</code> for the <code>::view-transition-old</code> and <code>::view-transition-new</code> pseudo-selectors representing a static snapshot of the old and new view, respectively.</p>

<pre><code class="language-css">::view-transition-old(highlight) {
  height: 100%;
}

::view-transition-new(highlight) {
  height: 100%;
}
</code></pre>

<p>Let’s do some final refactoring to tidy up our code by moving the callback to a separate function and adding a fallback for when view transitions aren’t supported:</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

// change the item that has the .active class applied
const setActiveElement = (elem) =&gt; {
  document.querySelector('nav a.active').classList.remove('active');
  elem.classList.add('active');
}

// Start view transition and pass in a callback on click
navbar.addEventListener('click', async  function (event) {
  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }

  // Fallback for browsers that don't support View Transitions:
  if (!document.startViewTransition) {
    setActiveElement(event.target);
    return;
  }
  
  document.startViewTransition(() =&gt; setActiveElement(event.target));
});
</code></pre>
</div>

<p>Here’s our view transition-powered navigation bar! Observe the smooth transition when you click on the different links.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ogXELKE"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar with View Transition [forked]](https://codepen.io/smashingmag/pen/ogXELKE) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogXELKE">Moving Highlight Navbar with View Transition [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>Animations and transitions between website UI states used to require many kilobytes of external libraries, along with verbose, confusing, and error-prone code, but vanilla JavaScript and CSS have since incorporated features to achieve <strong>native-app-like interactions without breaking the bank</strong>. We demonstrated this by implementing the “moving highlight” navigation pattern using two approaches: CSS transitions combined with the <code>getBoundingClientRect()</code> method and the View Transition API.</p>

<h3 id="resources">Resources</h3>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect"><code>getBoundingClientRect()</code> method documentation</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">View Transition API documentation</a></li>
<li>“<a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/">View Transitions: Handling Aspect Ratio Changes</a>” by Jake Archibald</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Andy Clarke</author><title>Smashing Animations Part 4: Optimising SVGs</title><link>https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/</link><pubDate>Wed, 04 Jun 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/</guid><description>What’s the best way to make your SVGs faster, simpler, and more manageable? In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk/">Andy Clarke&lt;/a> explains the process he relies on &lt;em>to&lt;/em> prepare, optimise, and structure SVGs for animation and beyond.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/" />
              <title>Smashing Animations Part 4: Optimising SVGs</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 4: Optimising SVGs</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-06-04T08:00:00&#43;00:00" class="op-published">2025-06-04T08:00:00+00:00</time>
                  <time datetime="2025-06-04T08:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>SVG animations take me back to the Hanna-Barbera cartoons I watched as a kid. Shows like <em>Wacky Races</em>, <em>The Perils of Penelope Pitstop</em>, and, of course, <a href="https://en.wikipedia.org/wiki/Yogi_Bear"><em>Yogi Bear</em></a>. They inspired me to lovingly recreate some classic <a href="https://stuffandnonsense.co.uk/toon-titles">Toon Titles</a> using CSS, SVG, and SMIL animations.</p>

<p>But getting animations to load quickly and work smoothly needs more than nostalgia. It takes clean design, lean code, and a process that makes complex SVGs easier to animate. Here’s how I do it.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/toon-titles">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png"
			
			sizes="100vw"
			alt="An example of Toon Titles from the website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      There’s now a website where you can see all my <a href='https://stuffandnonsense.co.uk/toon-titles'>Toon Titles</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/1-toon-titles.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="refs">
  <ul><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</a></li><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/">Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</a></li><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/">Smashing Animations Part 3: SMIL’s Not Dead Baby, SMIL’s Not Dead</a></li></ul>
</div>

<p>Whether for personal projects or commercial work, preparing SVGs well ensures they’re accessible. Optimising them ensures they load quickly, especially on mobile, and thinking carefully about how they’re structured makes maintaining them easier. I’ve developed a <strong>process that balances visuals with accessibility and performance</strong> and makes complex SVGs easier to work with.</p>

<p>So, to explain my process, I’ve chosen an episode of <em>The Yogi Bear Show</em> called “Bewitched Bear,” first broadcast in January 1960. In this story, Yogi steals a witch’s broom to help him grab “pic-a-nic” baskets.</p>

<p>“Hey, hey, hey!”</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png"
			
			sizes="100vw"
			alt="An illustration from the “Bewitched Bear” episode of The Yogi Bear Show where bear is on a witch’s broom"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/2-yogi-bear-bewitched-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="start-clean-and-design-with-optimisation-in-mind">Start Clean And Design With Optimisation In Mind</h2>

<p>Keeping things simple is key to making SVGs that are optimised and ready to animate. Tools like Adobe Illustrator convert bitmap images to vectors, but the output often contains too many extraneous groups, layers, and masks. Instead, I start cleaning in Sketch, work from a reference image, and use the Pen tool to create paths.</p>

<blockquote><strong>Tip</strong>: <a href="https://affinity.serif.com/en-gb/designer/">Affinity Designer</a> (UK) and <a href="https://www.sketch.com">Sketch</a> (Netherlands) are alternatives to Adobe Illustrator and Figma. Both are independent and based in Europe. Sketch has been my default design app since Adobe killed Fireworks.</blockquote>

<h2 id="beginning-with-outlines">Beginning With Outlines</h2>

<p>For these Toon Titles illustrations, I first use the Pen tool to draw black outlines with as few anchor points as possible. The more points a shape has, the bigger a file becomes, so simplifying paths and reducing the number of points makes an SVG much smaller, often with no discernible visual difference.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png"
			
			sizes="100vw"
			alt="Two outlines with different anchor points"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <strong>Left</strong>: 160 anchor points. <strong>Right</strong>: 80 points. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/3-outlines-anchor-points.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Bearing in mind that parts of this Yogi illustration will ultimately be animated, I keep outlines for this Bewitched Bear’s body, head, collar, and tie separate so that I can move them independently. The head might nod, the tie could flap, and, like in those classic cartoons, Yogi’s collar will hide the joins between them.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png"
			
			sizes="100vw"
			alt="Separate outlines for body, head, collar and tie, and broom."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Separate outlines for body, head, collar and tie, and broom. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/4-separate-outlines.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="drawing-simple-background-shapes">Drawing Simple Background Shapes</h2>

<p>With the outlines in place, I use the Pen tool again to draw new shapes, which fill the areas with colour. These colours sit behind the outlines, so they don’t need to match them exactly. The fewer anchor points, the smaller the file size.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png"
			
			sizes="100vw"
			alt="Original vector artwork and a simplified version with Adobe Illustrator"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <strong>Left</strong>: Original vector artwork, 8 Kb. <strong>Right</strong>: Simplified using Adobe Illustrator, 2 Kb. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/5-simple-background-shapes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Sadly, neither Affinity Designer nor Sketch has tools that can simplify paths, but if you have it, using Adobe Illustrator can shave a few extra kilobytes off these background shapes.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png"
			
			sizes="100vw"
			alt="An illustration how to simplify paths with Adobe Illustrator"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Adobe Illustrator: Object → Path → Simplify. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/6-adobe-illustrator-simplify-paths.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="optimising-the-code">Optimising The Code</h2>

<p>It’s not just metadata that makes SVG bulkier. The way you export from your design app also affects file size.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png"
			
			sizes="100vw"
			alt="Vector artwork ready for optimisation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Vector artwork ready for optimisation. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/7-vector-artwork-ready-optimisation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Exporting just those simple background shapes from Adobe Illustrator includes unnecessary groups, masks, and bloated path data by default. Sketch’s code is barely any better, and there’s plenty of room for improvement, even in its SVGO Compressor code. I rely on Jake Archibald’s <a href="https://jakearchibald.github.io/svgomg/">SVGOMG</a>, which uses SVGO v3 and consistently delivers the best optimised SVGs.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="439"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png"
			
			sizes="100vw"
			alt="Jake Archibald’s SVGOMG online optimisation tool."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Jake Archibald’s SVGOMG online optimisation tool. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/8-jake-archibald-svgomg-online-optimisation-tool.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div class="partners__lead-place"></div>

<h2 id="layering-svg-elements">Layering SVG Elements</h2>

<p>My process for preparing SVGs for animation goes well beyond drawing vectors and optimising paths &mdash; it also includes how I <strong>structure the code</strong> itself. When every visual element is crammed into a single SVG file, even optimised code can be a nightmare to navigate. Locating a specific path or group often feels like searching for a needle in a haystack.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png"
			
			sizes="100vw"
			alt="Toon Titles recreation of the Yogi Bear title card"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1958). Toon Titles recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/9-yogi-bear-title-card-toon-titles-recreation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>That’s why I develop my SVGs in layers, exporting and optimising one set of elements at a time &mdash; always in the order they’ll appear in the final file. This lets me build the master SVG gradually by pasting it in each cleaned-up section. For example, I start with backgrounds like this gradient and title graphic.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png"
			
			sizes="100vw"
			alt="Gradient background and title graphic."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Gradient background and title graphic. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/10-gradient-background-title-graphic.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Instead of facing a wall of SVG code, I can now easily identify the background gradient’s path and its associated <code>linearGradient</code>, and see the group containing the title graphic. I take this opportunity to add a comment to the code, which will make editing and adding animations to it easier in the future:</p>

<pre><code class="language-svg">&lt;svg ...&gt;
  &lt;defs&gt;
    &lt;!-- ... --&gt;
  &lt;/defs&gt;
  &lt;path fill="url(#grad)" d="…"/&gt;
  &lt;!-- TITLE GRAPHIC --&gt;
  &lt;g&gt;
    &lt;path … /&gt;
    &lt;!-- ... --&gt; 
  &lt;/g&gt;
&lt;/svg&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png"
			
			sizes="100vw"
			alt="Trail with Gaussian Blur."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Trail with Gaussian Blur. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/11-trail-gaussian-blur.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Next, I add the blurred trail from Yogi’s airborne broom. This includes defining a Gaussian Blur filter and placing its path between the background and title layers:</p>

<pre><code class="language-svg">&lt;svg ...&gt;
  &lt;defs&gt;
    &lt;linearGradient id="grad" …&gt;…&lt;/linearGradient&gt;
    &lt;filter id="trail" …&gt;…&lt;/filter&gt;
  &lt;/defs&gt;
  &lt;!-- GRADIENT --&gt;
  &lt;!-- TRAIL --&gt;
  &lt;path filter="url(#trail)" …/&gt;
  &lt;!-- TITLE GRAPHIC --&gt;
&lt;/svg&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png"
			
			sizes="100vw"
			alt="Yogi Bear’s magical stars."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear’s magical stars. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/12-yogi-bear-magical-stars.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then come the magical stars, added in the same sequential fashion:</p>

<pre><code class="language-svg">&lt;svg ...&gt;
  &lt;!-- GRADIENT --&gt;
  &lt;!-- TRAIL --&gt;
  &lt;!-- STARS --&gt;
  &lt;!-- TITLE GRAPHIC --&gt;
&lt;/svg&gt;
</code></pre>

<p>To keep everything organised and animation-ready, I create an empty group that will hold all the parts of Yogi:</p>

<pre><code class="language-svg">&lt;g id="yogi"&gt;...&lt;/g&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png"
			
			sizes="100vw"
			alt="Added Yogi Bear’s component parts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Sequentially adding Yogi Bear’s component parts. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/13-yogi-bear-component-parts.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then I build Yogi from the ground up &mdash; starting with background props, like his broom:</p>

<pre><code class="language-svg">&lt;g id="broom"&gt;...&lt;/g&gt;
</code></pre>

<p>Followed by grouped elements for his body, head, collar, and tie:</p>

<pre><code class="language-svg">&lt;g id="yogi"&gt;
  &lt;g id="broom"&gt;…&lt;/g&gt;
  &lt;g id="body"&gt;…&lt;/g&gt;
  &lt;g id="head"&gt;…&lt;/g&gt;
  &lt;g id="collar"&gt;…&lt;/g&gt;
  &lt;g id="tie"&gt;…&lt;/g&gt;
&lt;/g&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png"
			
			sizes="100vw"
			alt="Toon Titles recreation of the Yogi Bear title card"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1958). Toon Titles recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/14-yogi-bear-title-card-toon-titles-recreation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Since I export each layer from the same-sized artboard, I don’t need to worry about alignment or positioning issues later on &mdash; they’ll all slot into place automatically. I keep my code <strong>clean</strong>, <strong>readable</strong>, and <strong>ordered logically</strong> by layering elements this way. It also makes animating smoother, as each component is easier to identify.</p>

<h2 id="reusing-elements-with-use">Reusing Elements With <code>&lt;use&gt;</code></h2>

<p>When duplicate shapes get reused repeatedly, SVG files can get bulky fast. My recreation of the “Bewitched Bear” title card contains 80 stars in three sizes. Combining all those shapes into one optimised path would bring the file size down to 3KB. But I want to animate individual stars, which would almost double that to 5KB:</p>

<pre><code class="language-svg">&lt;g id="stars"&gt;
 &lt;path class="star-small" fill="#eae3da" d="..."/&gt;
 &lt;path class="star-medium" fill="#eae3da" d="..."/&gt;
 &lt;path class="star-large" fill="#eae3da" d="..."/&gt;
 &lt;!-- ... --&gt;
&lt;/g&gt;
</code></pre>

<p>Moving the stars’ <code>fill</code> attribute values to their parent group reduces the overall weight a little:</p>

<pre><code class="language-svg">&lt;g id="stars" fill="#eae3da"&gt;
 &lt;path class="star-small" d="…"/&gt;
 &lt;path class="star-medium" d="…"/&gt;
 &lt;path class="star-large" d="…"/&gt;
 &lt;!-- ... --&gt;
&lt;/g&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png"
			
			sizes="100vw"
			alt="Yogi Bear’s sparkling stars."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear’s sparkling stars. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/15-yogi-bear-sparkling-stars.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But a more efficient and manageable option is to define each star size as a reusable template:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;defs&gt;
  &lt;path id="star-large" fill="#eae3da" fill-rule="evenodd" d="…"/&gt;
  &lt;path id="star-medium" fill="#eae3da" fill-rule="evenodd" d="…"/&gt;
  &lt;path id="star-small" fill="#eae3da" fill-rule="evenodd" d="…"/&gt;
&lt;/defs&gt;
</code></pre>
</div>

<p>With this setup, changing a star’s design only means updating its template once, and every instance updates automatically. Then, I reference each one using <code>&lt;use&gt;</code> and position them with <code>x</code> and <code>y</code> attributes:</p>

<pre><code class="language-svg">&lt;g id="stars"&gt;
  &lt;!-- Large stars --&gt;
  &lt;use href="#star-large" x="1575" y="495"/&gt;
  &lt;!-- ... --&gt;
  &lt;!-- Medium stars --&gt;
  &lt;use href="#star-medium" x="1453" y="696"/&gt;
  &lt;!-- ... --&gt;
  &lt;!-- Small stars --&gt;
  &lt;use href="#star-small" x="1287" y="741"/&gt;
  &lt;!-- ... --&gt;
&lt;/g&gt;
</code></pre>

<p>This approach makes the SVG easier to manage, lighter to load, and faster to iterate on, especially when working with dozens of repeating elements. Best of all, it keeps the markup clean <strong>without compromising on flexibility or performance</strong>.</p>

<div class="partners__lead-place"></div>

<h2 id="adding-animations">Adding Animations</h2>

<p>The stars trailing behind Yogi’s stolen broom bring so much personality to the animation. I wanted them to sparkle in a seemingly random pattern against the dark blue background, so I started by defining a keyframe animation that cycles through different <code>opacity</code> levels:</p>

<pre><code class="language-css">@keyframes sparkle {
  0%, 100% { opacity: .1; }
  50% { opacity: 1; }
}
</code></pre>

<p>Next, I applied this looping animation to every <code>use</code> element inside my stars group:</p>

<pre><code class="language-css">&#35;stars use {
  animation: sparkle 10s ease-in-out infinite;
}
</code></pre>

<p>The secret to creating a convincing twinkle lies in <strong>variation</strong>. I staggered animation delays and durations across the stars using <code>nth-child</code> selectors, starting with the quickest and most frequent sparkle effects:</p>

<pre><code class="language-css">/&#42; Fast, frequent &#42;/
&#35;stars use:nth-child(n + 1):nth-child(-n + 10) {
  animation-delay: .1s;
  animation-duration: 2s;
}
</code></pre>

<p>From there, I layered in additional timings to mix things up. Some stars sparkle slowly and dramatically, others more randomly, with a variety of rhythms and pauses:</p>

<pre><code class="language-css">/&#42; Medium &#42;/
&#35;stars use:nth-child(n + 11):nth-child(-n + 20) { ... }

/&#42; Slow, dramatic &#42;/
&#35;stars use:nth-child(n + 21):nth-child(-n + 30) { ... }

/&#42; Random &#42;/
&#35;stars use:nth-child(3n + 2) { ... }

/&#42; Alternating &#42;/
&#35;stars use:nth-child(4n + 1) { ... }

/&#42; Scattered &#42;/
&#35;stars use:nth-child(n + 31) { ... }
</code></pre>

<p>By thoughtfully structuring the SVG and reusing elements, I can build complex-looking animations without bloated code, making even a simple effect like changing <code>opacity</code> sparkle.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png"
			
			sizes="100vw"
			alt="Yogi Bear"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Subtle movements bring Yogi Bear to life. (<a href='https://files.smashing.media/articles/smashing-animations-part-4-optimising-svgs/16-subtle-movements-yogi-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then, for added realism, I make Yogi’s head wobble:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes headWobble {
  0% { transform: rotate(-0.8deg) translateY(-0.5px); }
  100% { transform: rotate(0.9deg) translateY(0.3px); }
}

&#35;head {
  animation: headWobble 0.8s cubic-bezier(0.5, 0.15, 0.5, 0.85) infinite alternate;
}
</code></pre>
</div>

<p>His tie waves:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes tieWave {
  0%, 100% { transform: rotateZ(-4deg) rotateY(15deg) scaleX(0.96); }
  33% { transform: rotateZ(5deg) rotateY(-10deg) scaleX(1.05); }
  66% { transform: rotateZ(-2deg) rotateY(5deg) scaleX(0.98); }
}

&#35;tie {
  transform-style: preserve-3d;
  animation: tieWave 10s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
}
</code></pre>
</div>

<p>His broom swings:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes broomSwing {
  0%, 20% { transform: rotate(-5deg); }
  30% { transform: rotate(-4deg); }
  50%, 70% { transform: rotate(5deg); }
  80% { transform: rotate(4deg); }
  100% { transform: rotate(-5deg); }
}

&#35;broom {
  animation: broomSwing 4s cubic-bezier(0.5, 0.05, 0.5, 0.95) infinite;
}
</code></pre>
</div>

<p>And, finally, Yogi himself gently rotates as he flies on his magical broom:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes yogiWobble {
  0% { transform: rotate(-2.8deg) translateY(-0.8px) scale(0.998); }
  30% { transform: rotate(1.5deg) translateY(0.3px); }
  100% { transform: rotate(3.2deg) translateY(1.2px) scale(1.002); }
}

&#35;yogi {
  animation: yogiWobble 3.5s cubic-bezier(.37, .14, .3, .86) infinite alternate;
}
</code></pre>
</div>

<p>All these subtle movements bring Yogi to life. By developing structured SVGs, I can create animations that feel full of character without writing a single line of JavaScript.</p>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="bNdwJBN"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Bewitched Bear CSS/SVG animation [forked]](https://codepen.io/smashingmag/pen/bNdwJBN) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/bNdwJBN">Bewitched Bear CSS/SVG animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>Whether you’re recreating a classic title card or animating icons for an interface, the principles are the same:</p>

<ol>
<li>Start clean,</li>
<li>Optimise early, and</li>
<li>Structure everything with animation in mind.</li>
</ol>

<p>SVGs offer incredible creative freedom, but only if kept <strong>lean</strong> and <strong>manageable</strong>. When you plan your process like a production cell &mdash; layer by layer, element by element &mdash; you’ll spend less time untangling code and more time bringing your work to life.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Andy Clarke</author><title>Smashing Animations Part 3: SMIL’s Not Dead Baby, SMIL’s Not Dead</title><link>https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/</link><pubDate>Wed, 21 May 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/</guid><description>While there are plenty of ways that CSS animations can bring designs to life, adding simple SMIL (Synchronized Multimedia Integration Language) animations in SVG can help them do much more. Andy Clarke explains where SMIL animations in SVG take over where CSS leaves off.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/" />
              <title>Smashing Animations Part 3: SMIL’s Not Dead Baby, SMIL’s Not Dead</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 3: SMIL’s Not Dead Baby, SMIL’s Not Dead</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-05-21T08:00:00&#43;00:00" class="op-published">2025-05-21T08:00:00+00:00</time>
                  <time datetime="2025-05-21T08:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>The SMIL specification was introduced by the W3C in 1998 for synchronizing multimedia. This was long before CSS animations or JavaScript-based animation libraries were available. It was built into SVG 1.1, which is why we can still use it there today.</p>

<p>Now, you might’ve heard that <a href="https://css-tricks.com/smil-is-dead-long-live-smil-a-guide-to-alternatives-to-smil-features">SMIL is dead</a>. However, it’s alive and well since Google reversed a decision to deprecate the technology almost a decade ago. It remains a terrific choice for designers and developers who want simple, semantic ways to add animations to their designs.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/1-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Tip</strong>: <em>There’s now a website where you can see all my <a href="https://stuffandnonsense.co.uk/toon-titles">Toon Titles</a>.</em></p>

<div class="refs">
  <ul><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</a></li><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/">Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</a></li></ul>
</div>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="introducing-mike-worth">Introducing Mike Worth</h2>

<p>I’ve recently been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a bold, retro-style design that showcases his work. I used animations throughout to delight and surprise his audience as they move through his website.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="550"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png"
			
			sizes="100vw"
			alt="Illustrations from Mike Worth’s website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Design by <a href='https://stuffandnonsense.co.uk/'>Andy Clarke, Stuff & Nonsense</a>. Mike Worth’s website will launch in June 2025, but you can see <a href='https://codepen.io/collection/YwMKPb'>examples from this article on CodePen</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/2-mike-worth-website.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Mike loves ’90s animation &mdash; especially <a href="https://en.wikipedia.org/wiki/DuckTales_(1987_TV_series)">Disney’s</a> <a href="https://en.wikipedia.org/wiki/DuckTales_(1987_TV_series)"><em>Duck Tales</em></a>. Unsurprisingly, my taste in cartoons stretches back a little further to <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> shows like Dastardly and Muttley in <em>Their Flying Machines</em>, <em>Scooby-Doo</em>, <em>The Perils of Penelope Pitstop</em>, <em>Wacky Races</em>, and, of course, <a href="https://en.wikipedia.org/wiki/Yogi_Bear"><em>The Yogi Bear Show</em></a>. So, to explain how this era of animation relates to SVG, I’ll be adding SMIL animations in SVG to title cards from some classic Yogi Bear cartoons.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="300"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustrations"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/3-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Fundamentally, animation changes how an element looks and where it appears over time using a few basic techniques. That might be simply shifting an element up or down, left or right, to create the appearance of motion, like Yogi Bear moving across the screen.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png"
			
			sizes="100vw"
			alt="Yogi Bear title card design recreated by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1958). Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/4-yogi-bear-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Rotating objects around a fixed point can create everything, from simple spinning effects to natural-looking movements of totally normal things, like a bear under a parachute falling from the sky.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png"
			
			sizes="100vw"
			alt="Yogi Bear title card recreated by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1958). Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/5-yogi-bear-title-card-design.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Scaling makes an element grow, shrink, or stretch, which can add drama, create perspective, or simulate depth.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png"
			
			sizes="100vw"
			alt="Yogi Bear title card recreated by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1958). Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/6-yogi-bear-title-card.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Changing colour and transitioning opacity can add atmosphere, create a mood, and enhance visual storytelling. Just these basic principles can create animations that attract attention and improve someone’s experience using a design.</p>

<p>These results are all achievable using CSS animations, but some SVG properties can’t be animated using CSS. Luckily, we can do more &mdash; and have much more fun &mdash; using SMIL animations in SVG. We can combine complex animations, move objects along paths, and control when they start, stop, and everything in between.</p>

<p>Animations can be embedded within any SVG element, including <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Basic_shapes">primitive shapes</a> like circles, ellipses, and rectangles. They can also be encapsulated into groups, paths, and polygons:</p>

<pre><code class="language-svg">&lt;circle ...&gt;
  &lt;animate&gt;...&lt;/animate&gt;
&lt;/circle&gt;
</code></pre>

<p>Animations can also be defined outside an element, elsewhere in an SVG, and connected to it using an <code>xlink</code> attribute:</p>

<pre><code class="language-svg">&lt;g id="yogi"&gt;...&lt;/g&gt;
  ...
&lt;animate xlink:href="#yogi"&gt;…&lt;/animate&gt;
</code></pre>

<h2 id="building-an-animation">Building An Animation</h2>

<p><code>&lt;animate&gt;</code> is just one of several animation elements in SVG. Together with an <code>attributeName</code> value, it enables animations based on one or more of an element’s attributes.</p>

<p>Most animation explanations start by moving a primitive shape, like this exciting circle:</p>

<pre><code class="language-svg">&lt;circle
  r="50"
  cx="50" 
  cy="50" 
  fill="#062326" 
  opacity="1"
/&gt;
</code></pre>

<p>Using this <code>attributeName</code> property, I can define which of this circle’s attributes I want to animate, which, in this example, is its <code>cx</code> (x-axis center point) position:</p>

<pre><code class="language-svg">&lt;circle ... &gt;
  &lt;animate attributename="cx"&gt;&lt;/animate&gt;
&lt;/circle&gt;
</code></pre>

<p>On its own, this does precisely nothing until I define three more values. The <code>from</code> keyword specifies the circle’s initial position, <code>to</code>, its final position, and the <code>dur</code>-ation between those two positions:</p>

<pre><code class="language-svg">&lt;circle ... &gt;
  &lt;animate 
  attributename="cx"
  from="50" 
  to="500"
  dur="1s"&gt;
  &lt;/animate&gt;
&lt;/circle&gt;
</code></pre>

<p>If I want more precise control, I can replace <code>from</code> and <code>to</code> with a set of <code>values</code> separated by semicolons:</p>

<pre><code class="language-svg">&lt;circle ... &gt;
  &lt;animate 
  attributename="cx"
  values="50; 250; 500; 250;"
  dur="1s"&gt;
  &lt;/animate&gt;
&lt;/circle&gt;
</code></pre>

<p>Finally, I can define how many times the animation repeats (<code>repeatcount</code>) and even after what period that repeating should stop (<code>repeatdur</code>):</p>

<pre><code class="language-svg">&lt;circle ... &gt;
  &lt;animate 
  attributename="cx"
  values="50; 250; 500; 250;"
  dur="1s"
  repeatcount="indefinite"
  repeatdur="180s"&gt;
&lt;/circle&gt;
</code></pre>

<p>Most SVG elements have attributes that can be animated. This title card from 1959’s <a href="https://yogibear.fandom.com/wiki/Brainy_Bear">“Brainy Bear” episode</a> shows Yogi in a crazy scientist‘s brain experiment. Yogi’s head is under the dome, and energy radiates around him.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration where Yogi’s head is under the dome, and energy radiates around him"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/7-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To create the buzz around Yogi, my SVG includes three <code>path</code> elements, each with <code>opacity</code>, <code>stroke</code>, and <code>stroke-width</code> attributes, which can all be animated:</p>

<pre><code class="language-svg">&lt;path opacity="1" stroke="#fff" stroke-width="5" ... /&gt;
</code></pre>

<p>I animated each path’s <code>opacity</code>, changing its value from <code>1</code> to <code>.5</code> and back again:</p>

<pre><code class="language-svg">&lt;path opacity="1" ... &gt;
  &lt;animate 
    attributename="opacity"
    values="1; .25; 1;"
    dur="1s"
    repeatcount="indefinite"&gt;
  &lt;/animate&gt;
&lt;/path&gt;
</code></pre>

<p>Then, to radiate energy from Yogi, I specified when each animation should <code>begin</code>, using a different value for each <code>path</code>:</p>

<pre><code class="language-svg">&lt;path ... &gt;
  &lt;animate begin="0" … &gt;
&lt;/path&gt;

&lt;path ... &gt;
  &lt;animate begin=".5s" … &gt;
&lt;/path&gt;

&lt;path ... &gt;
  &lt;animate begin="1s" … &gt;
&lt;/path&gt;
</code></pre>

<p>I’ll explain more about the <code>begin</code> property and how to start animations after this short commercial break.</p>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="qEEzYgG"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Brainy Bear SVG animation [forked]](https://codepen.io/smashingmag/pen/qEEzYgG) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/qEEzYgG">Brainy Bear SVG animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>To make animations appear more natural, I can apply more than one <code>animate</code> element and give each one a different <code>attributename</code> value. Those paths also contain a <code>stroke-width</code> attribute, which I can also animate by changing the stroke widths between <code>5</code> and <code>7</code>:</p>

<pre><code class="language-svg">&lt;path ... &gt;
  &lt;animate attributename="opacity" ... &gt;&lt;/animate&gt;
  &lt;animate attributename="stroke-width" ... &gt;&lt;/animate&gt;
&lt;/path&gt;
</code></pre>

<p>Finally, I can animate the dome over Yogi’s head, changing its <code>fill</code> colour between two values over five seconds to create the impression that the crazy scientist’s machine is heating up:</p>

<pre><code class="language-svg">&lt;path fill="#50D9E0" ... &gt;
  &lt;animate
    attributename="fill"
    values="#50D9E0; #E18C50;"
    dur="5s"
    begin="2s"
  &gt;
&lt;/path&gt;
</code></pre>

<p>Implement that code, and you’ll soon notice that the dome returns to its original state after the animation is complete. To retain its colour at the end of the animation, I can add the &mdash; confusingly named &mdash; <code>fill</code> property and a value of <code>freeze.</code> This stops the animation in its final state and prevents it from returning to the original colour:</p>

<pre><code class="language-svg">&lt;path fill="#50D9E0" ... &gt;
  &lt;animate fill="freeze"&gt;
&lt;/path&gt;
</code></pre>

<p>Animating attributes brings these title card designs to life, whether by adjusting the position of a primitive shape, its opacity, and stroke width or by creating complex sequences with staggered timing. But there’s still more I can do, starting with the next animation element, <code>animateTransform</code>.</p>

<div class="partners__lead-place"></div>

<h2 id="animatetransform"><code>animateTransform</code></h2>

<p>If <code>&lt;animate&gt;</code> controls attributes, then <code>animateTransform</code> animates transformations, including rotations, scaling, skewing, and translations. It works by changing the values of a transform property, like this <code>translate</code>:</p>

<pre><code class="language-svg">&lt;path transform="translate(0,0)"/&gt;
</code></pre>

<p>Then, the animation works the same way as <code>&lt;animate&gt;</code>, adding an <code>attributename</code> and specifying the type of transform, in this example, <code>rotate:</code></p>

<pre><code class="language-svg">&lt;animatetransform 
  attributename="transform"
  type="rotate"&gt;
&lt;/animatetransform&gt;
</code></pre>

<p>I can use either <code>from</code> and <code>to</code> or the <code>values</code> attribute to define how an element is transformed.</p>

<ul>
<li><strong>Scale</strong> uses <code>x</code> and <code>y</code> values  (<code>.5</code>, <code>1</code>).</li>
<li><strong>Rotate</strong> uses degrees (<code>0</code>–<code>360</code>) plus optional <code>x</code> and <code>y</code> (<code>360</code>, <code>0</code>, <code>0</code>).</li>
<li><strong>Translate</strong> also uses <code>x</code> and <code>y</code> values  (<code>50</code>, <code>100</code>).</li>
<li><strong>Skew</strong> uses <code>x</code> and <code>y</code> values, too (<code>50</code>, <code>100</code>).</li>
</ul>

<p>What’s interesting about those values is that they can be added to an element’s existing values instead of replacing them. For example, when an attribute contains a <code>translate</code> value of <code>100, 0</code>:</p>

<pre><code class="language-svg">&lt;path transform="translate(100, 0)"/&gt;
</code></pre>

<p>And then I animate that translation horizontally by <code>100</code>:</p>

<pre><code class="language-svg">&lt;animatetransform
  attributename="transform"
  type="translate"
  from="0, 0"
  to="100, 0"
  additive="sum"&gt;
&lt;/animatetransform&gt;
</code></pre>

<p>Using the <code>additive</code> property with a value of <code>sum</code>, the animation values are relative to the original, starting the animation at <code>100</code> and ending at <code>200</code> by adding <code>100</code> to <code>100</code>.</p>

<p>Similarly, if I give the <code>accumulate</code> property a value of <code>sum</code>, each instance of animation will build on the last. So, in an animation where an element is translated by <code>100</code> and repeats five times, each movement will be cumulative, moving the element by <code>500</code>:</p>

<pre><code class="language-svg">&lt;animatetransform
  attributename="transform"
  type="translate"
  from="0, 0"
  to="100, 0"
  additive="sum"
  accumulate="sum" 
/&gt;
</code></pre>

<p>This title card from 1958’s Yogi Bear’s <a href="https://yogibear.fandom.com/wiki/Yogi_Bear%27s_Big_Break">“Big Break” episode</a> shows Yogi floating from the sky under a parachute.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration shows Yogi floating from the sky under a parachute"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/8-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I needed two types of transform animations to generate the effect of Yogi drifting gently downwards: <code>translate</code>, and <code>rotate</code>. I first added an <code>animatetransform</code> element to the group, which contains Yogi and his chute. I defined his initial vertical position &mdash; <code>1200</code> off the top of the <code>viewBox</code> &mdash; then translated his descent to <code>1000</code> over a 15-second duration:</p>

<pre><code class="language-svg">&lt;g transform="translate(1200, -1200)"&gt;
  ...
  &lt;animateTransform
    attributeName="transform"
    type="translate"
    values="500,-1200; 500,1000"
    dur="15s"
    repeatCount="1" 
  /&gt;
&lt;/g&gt;
</code></pre>

<p>Yogi appears to fall from the sky, but the movement looks unrealistic. So, I added a second <code>animatetransform</code> element, this time with an indefinitely repeating +/- 5-degree rotation to swing Yogi from side to side during his descent:</p>

<pre><code class="language-svg">&lt;animateTransform
  attributeName="transform"
  type="rotate"
  values="-5; 5; -5"
  dur="14s"
  repeatCount="indefinite"
  additive="sum" 
/&gt;
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="PwwraNm"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Big Break SVG animation [forked]](https://codepen.io/smashingmag/pen/PwwraNm) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/PwwraNm">Big Break SVG animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="starting-and-stopping">Starting And Stopping</h2>

<p>So far, every animation begins as soon as the page has loaded. But there are ways to not only delay the start of animation but define precisely where it begins, using the begin <code>property</code>:</p>

<p>In this title card from 1959’s <a href="https://yogibear.fandom.com/wiki/Robin_Hood_Yogi">“Robin Hood Yogi”</a>, Yogi shoots an arrow into an apple on Boo-Boo’s head.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration where Yogi shoots an arrow into an apple on Boo-Boo’s head"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/9-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>By default, the arrow is set loose when the page loads. Blink, and you might miss it. To build some anticipation, I can <code>begin</code> the animation two seconds later:</p>

<pre><code class="language-svg">&lt;animatetransform
  attributename="transform"
  type="translate"
  from="0 0"
  to="750 0"
  dur=".25s"
  begin="2s"
  fill="freeze"
/&gt;
</code></pre>

<p>Or, I can let the viewer take the shot when they click the arrow:</p>

<pre><code class="language-svg">&lt;animatetransform
  ...
  begin="click"
/&gt;
</code></pre>

<p>And I can combine the click event and a delay, all with no JavaScript, just a smattering of SMIL:</p>

<pre><code class="language-svg">&lt;animatetransform
  ...
  begin="click + .5s"
/&gt;
</code></pre>

<p>Try this yourself by clicking the arrow:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="OPPeERj"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Robin Hood Yogi CSS animation [forked]](https://codepen.io/smashingmag/pen/OPPeERj) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/OPPeERj">Robin Hood Yogi CSS animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="synchronising-animations">Synchronising Animations</h2>

<p>In his 1958 <a href="https://yogibear.fandom.com/wiki/Pie-Pirates">“Pie-Pirates” episode</a>, Yogi Bear tries to steal a pie and has to outwit a bulldog. The title card &mdash; designed by Lawrence Goble &mdash; shows the chase but, alas, (spoiler alert) no stolen pie.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration showing a bulldog chasing Yogi Bear"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/10-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To bring this title card to life, I needed two groups of paths: one for Yogi and the other for the dog. I translated them both off the left edge of the <code>viewBox</code>:</p>

<pre><code class="language-svg">&lt;g class="dog" transform="translate(-1000, 0)"&gt;
  ...
&lt;/g&gt;

&lt;g class="yogi" transform="translate(-1000, 0)"&gt;
  ...
&lt;/g&gt;
</code></pre>

<p>Then, I applied an <code>animatetransform</code> element to both groups, which moves them back into view:</p>

<pre><code class="language-svg">&lt;!-- yogi --&gt;
&lt;animateTransform
  attributeName="transform"
  type="translate"
  from="-1000,0"
  to="0,0"
  dur="2s"
  fill="freeze"
/&gt;

&lt;!-- dog --&gt;
&lt;animateTransform
  attributeName="transform"
  type="translate"
  from="-1000,0"
  to="0,0"
  dur=".5s"
  fill="freeze"
/&gt;
</code></pre>

<p>This sets up the action, but the effect feels flat, so I added another pair of animations that bounce both characters:</p>

<pre><code class="language-svg">&lt;!-- yogi --&gt;
&lt;animateTransform
  attributeName="transform"
  type="rotate"
  values="-1,0,450; 1,0,450; -1,0,450"
  dur=".25s"
  repeatCount="indefinite"
/&gt;

&lt;!-- dog --&gt;
&lt;animateTransform
  attributeName="transform"
  type="rotate"
  values="-1,0,450; 1,0,450; -1,0,450"
  dur="0.5s"
  repeatCount="indefinite"
/&gt;
</code></pre>

<p>Animations can begin when a page loads, after a specified time, or when clicked. And by naming them, they can also synchronise with other animations.</p>

<p>I wanted Yogi to enter the frame first to build anticipation, with a short pause before other animations begin, synchronising to the moment he’s arrived. First, I added an ID to Yogi’s <code>translate</code> animation:</p>

<pre><code class="language-svg">&lt;animateTransform
  id="yogi"
  type="translate"
  ...
/&gt;
</code></pre>

<blockquote><strong>Watch out</strong>: For a reason, I can’t, for the life of me, explain why Firefox won’t begin animations with an ID when the ID contains a hyphen. This isn’t smarter than the average browser, but replacing hyphens with underscores fixes the problem.</blockquote>

<p>Then, I applied a <code>begin</code> to his <code>rotate</code> animation, which starts playing a half-second after the <code>#yogi</code> animation ends:</p>

<pre><code class="language-svg">&lt;animateTransform
  type="rotate"
  begin="yogi.end + .5s"
  ...
/&gt;
</code></pre>

<p>I can build sophisticated sets of synchronised animations using the <code>begin</code> property and whether a named animation begins or ends. The bulldog chasing Yogi enters the frame two seconds after Yogi begins his entrance:</p>

<pre><code class="language-svg">&lt;animateTransform
  id="dog"
  type="translate"
  begin="yogi.begin + 2s"
  fill="freeze"
  ...
/&gt;
</code></pre>

<p>One second after the dog has caught up with Yogi, a <code>rotate</code> transformation makes him bounce, too:</p>

<pre><code class="language-svg">&lt;animateTransform
  type="rotate"
  ...
  begin="dog.begin + 1s"
  repeatCount="indefinite" 
/&gt;
</code></pre>

<p>The background rectangles whizzing past are also synchronised, this time to one second before the bulldog ends his run:</p>

<pre><code class="language-svg">&lt;rect ...&gt;
  &lt;animateTransform
    begin="dog.end + -1s"
  /&gt;
&lt;/rect&gt;
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="LEEKryp"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Pie-Pirates SVG animation [forked]](https://codepen.io/smashingmag/pen/LEEKryp) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/LEEKryp">Pie-Pirates SVG animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>The timing of this background movement is synchronised with the dog arriving, which, in turn, is relative to Yogi’s arrival, building a sequence of animations that all feel connected.</p>

<div class="partners__lead-place"></div>

<h2 id="animating-along-motion-paths">Animating Along Motion Paths</h2>

<p>Until now, all the animations in these title cards have been up, down, left, right, or one combination or another. But there’s one more aspect of SMIL in SVG, which can add an extra dimension to animations: animating along motion paths using the <code>animatemotion</code> element.</p>

<p><code>animatemotion</code> accepts all the same properties and values as <code>animate</code> and <code>animateTransform</code>, but adds a few more for finer control over direction and timing. <code>animatemotion</code> uses the <code>path</code> property to enable elements to move along a motion path. It also uses the <code>d</code> value for coordinate data in the same way as any conventional path.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="540"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration of the Runaway Bear"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/11-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In <a href="https://yogibear.fandom.com/wiki/The_Runaway_Bear">“The Runaway Bear”</a> from 1959, Yogi must avoid a hunter turning his head into a trophy. I wanted Yogi to leap in and out of the screen by making him follow a path. I also wanted to vary the speed of his dash: speeding up as he enters and exits, and slowing down as he passes the title text.</p>

<p>I first added a <code>path</code> property, using its coordinate data to give Yogi a route to follow, and specified a two-second duration for my animation:</p>

<pre><code class="language-svg">&lt;g&gt;
  &lt;animateMotion
    dur="2s"
    path="..."
  &gt;
  &lt;/animateMotion&gt;
&lt;/g&gt;
</code></pre>

<p>Alternatively, I could add a <code>path</code> element, leave it visible, or prevent it from being rendered by placing it inside a <code>defs</code> element:</p>

<pre><code class="language-svg">&lt;defs&gt;
  &lt;path id="yogi" d="..." /&gt;
&lt;/defs&gt;
</code></pre>

<p>I can then reference that by using a <code>mpath</code> element inside my <code>animateMotion</code>:</p>

<pre><code class="language-svg">&lt;animateMotion
  ...
  &lt;mpath href="#yogi" /&gt;
&lt;/animateMotion&gt;
</code></pre>

<p>I experimented with several paths before settling on the one that delivered the movement shape I was looking for:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png"
			
			sizes="100vw"
			alt="Several variants of the Yogi Bear title card recreated by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Yogi Bear title card design by Lawrence Goble (1959.) Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-3-smil-not-dead/12-yogi-bear-title-card-recreations.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>One was too bouncy, one was too flat, but the third motion path was just right. Almost, as I also wanted to vary the speed of Yogi’s dash: speeding him up as he enters and exits and slowing him down as he passes the title text.</p>

<p>The <code>keyPoints</code> property enabled me to specify points along the motion path and then adjust the duration Yogi spends between them. To keep things simple, I defined five points between <code>0</code> and <code>1</code>:</p>

<pre><code class="language-svg">&lt;animateMotion
  ...
  keyPoints="0; .35; .5; .65; 1;"
&gt;
&lt;/animateMotion&gt;
</code></pre>

<p>Then I added the same number of <code>keyTimes</code> values, separated by semicolons, to control the pacing of this animation:</p>

<pre><code class="language-svg">&lt;animateMotion
  ...
  keyTimes="0; .1; .5; .95; 1;"
&gt;
&lt;/animateMotion&gt;
</code></pre>

<p>Now, Yogi rushes through the first three <code>keyPoints</code>, slows down as he passes the title text, then speeds up again as he exits the <code>viewBox</code>.</p>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="oggryox"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Runaway Bear SVG animation [forked]](https://codepen.io/smashingmag/pen/oggryox) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/oggryox">Runaway Bear SVG animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="smil-s-not-dead-baby-smil-s-not-dead">SMIL’s Not Dead, Baby. SMIL’s Not Dead</h2>

<p>With their ability to control transformations, animate complex motion paths, and synchronise multiple animations, SMIL animations in SVG are still powerful tools. They can bring design to life without needing a framework or relying on JavaScript. It’s compact, which makes it great for small SVG effects.</p>

<p>SMIL includes the <code>begin</code> attribute, which makes chaining animations far more intuitive than with CSS. Plus, SMIL lives inside the SVG file, making it perfect for animations that travel with an asset. So, while SMIL is not modern by today’s standards and may be a little bit niche, it can still be magical.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aDon%e2%80%99t%20let%20the%20misconception%20that%20SMIL%20is%20%e2%80%9cdead%e2%80%9d%20stop%20you%20from%20using%20this%20fantastic%20tool.%0a&url=https://smashingmagazine.com%2f2025%2f05%2fsmashing-animations-part-3-smil-not-dead%2f">
      
Don’t let the misconception that SMIL is “dead” stop you from using this fantastic tool.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Google reversed its decision to deprecate SMIL almost a decade ago, so it remains a terrific choice for designers and developers who want <strong>simple</strong>, <strong>semantic ways</strong> to add animations to their designs.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Andy Clarke</author><title>Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</title><link>https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/</link><pubDate>Wed, 14 May 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/</guid><description>What if you could take your CSS animations beyond simple fades and slides &amp;mdash; adding an extra dimension and a bit of old-school animation magic? In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> will show you how masking can unlock new creative possibilities for CSS animations, making them feel more fluid, layered, and cinematic.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/" />
              <title>Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-05-14T13:00:00&#43;00:00" class="op-published">2025-05-14T13:00:00+00:00</time>
                  <time datetime="2025-05-14T13:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Despite keyframes and scroll-driven events, CSS animations have remained relatively rudimentary. As I wrote in <a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">Part 1</a>, they remind me of the 1960s <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> animated series I grew up watching on TV. Shows like Dastardly and Muttley in <em>Their Flying Machines</em>, <em>Scooby-Doo</em>, <em>The Perils of Penelope Pitstop</em>, <em>Wacky Races</em>, and, of course, <a href="https://en.wikipedia.org/wiki/Yogi_Bear">Yogi Bear</a>.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="640"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg"
			
			sizes="100vw"
			alt="The Yogi Bear Show poster"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/1-yogi-bear-show.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Mike loves ’90s animation &mdash; especially <a href="https://en.wikipedia.org/wiki/DuckTales_(1987_TV_series)">Disney’s <em>Duck Tales</em></a>. So, that is the aesthetic applied throughout the design.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="550"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png"
			
			sizes="100vw"
			alt="Design by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Design by <a href='https://stuffandnonsense.co.uk/'>Andy Clarke, Stuff & Nonsense</a>. Mike Worth’s website will launch in June 2025, but you can see <a href='https://codepen.io/collection/YwMKPb'>examples from this article on CodePen</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/2-yogi-bear-design-andy-clarke.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I used animations throughout and have recently added an extra dimension to them using <strong>masking</strong>. So, to explain how this era of animation relates to masking in CSS, I’ve chosen an episode of <em>The Yogi Bear Show</em>, “Disguise and Gals,” first broadcast in May 1961. In this story, two bank robbers, disguised as little old ladies, hide their loot in a “pic-a-nic” basket in Yogi and Boo-Boo’s cave!</p>

<p>What could <em>possibly</em> go wrong?</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="640"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg"
			
			sizes="100vw"
			alt="The Yogi Bear Show, “Disguise and Gals” episode"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/3-yogi-bear-disguise-gals.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="what-s-a-mask">What’s A Mask?</h2>

<p>One simple masking example comes at the end of “Disguise and Gals” and countless other cartoons. Here, an animated vignette gradually hides more of Yogi’s face. The content behind the mask isn’t erased; it’s hidden.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="198"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg"
			
			sizes="100vw"
			alt="A masking example of Yogi’s face"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/4-yogi-bear-masking-example.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In CSS, <strong>masking controls visibility using a bitmap, vector, or gradient mask image</strong>. When a mask’s filled pixels cover an element, its content will be visible. When they are transparent, it will be hidden, which makes sense. Filled pixels can be any colour, but I always make mine hot pink so that it’s clear to me which areas will be visible.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="264"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg"
			
			sizes="100vw"
			alt="The Yogi Bear’s masking example by Andy Clarke"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/5-yogi-bear-andy-clarke-recreation.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>A <code>clip-path</code> functions similarly to a <code>mask</code> but uses paths to create hard-edged clipping areas. If you want to be picky, masks and clipping paths are technically different, but the goal for using them is usually the same. So, for this article, I’ll refer to them as two entrances to the same cave and call using either “masking.”</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="148"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png"
			
			sizes="100vw"
			alt="A robber rushes the picnic basket into Yogi’s cave"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/6-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In this sequence from “Disguise and Gals,” one of the robbers rushes the picnic basket containing their loot into Yogi’s cave. Masking defines the visible area, creating the illusion that the robber is entering the cave.</p>

<blockquote>How do I choose when to use <code>clip path</code> and when to choose <code>mask</code>?</blockquote>

<p>I’ll explain my reasons in each example.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="219"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png"
			
			sizes="100vw"
			alt="Masking defines the visible area of the cave"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/7-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>When Mike Worth and I discussed working together, we knew we would neither have the budget nor the time to create a short animated cartoon for his website. However, we were keen to explore how animations could bring to life what would’ve otherwise been static images.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png"
			
			sizes="100vw"
			alt="Illustration by Andy Clarke for Mike Worth’s website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen. (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/8-mike-worth-website.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="masking-using-a-clipping-path">Masking Using A Clipping Path</h2>

<p>On Mike’s biography page, his character also enters a cave. The SVG illustration I created contains two groups, one for the background and the other for the orangutan in the foreground:</p>

<pre><code class="language-html">&lt;figure&gt;
  &lt;svg viewBox="0 0 1400 960" id="cave"&gt;
    &lt;g class="background"&gt;…&lt;/g&gt;
    &lt;g class="foreground"&gt;…&lt;/g&gt;
  &lt;/svg&gt;
&lt;/figure&gt;
</code></pre>

<p>I defined a keyframe animation that moves the character from <code>2000px</code> on the right to its natural position in the center of the frame by altering its <code>translate</code> value:</p>

<pre><code class="language-css">@keyframes foreground {
  0% { 
    opacity: .75; 
    translate: 2000px 0;
  }
  60% { 
    opacity: 1;
    translate: 0 0;
  }
  80% {
    opacity: 1; 
    translate: 50px 0;
  }
  100% {
    opacity: 1;
    translate: 0 0;
  }
}
</code></pre>

<p>Then, I applied that animation to the foreground group:</p>

<pre><code class="language-css">.foreground {
  opacity: 0;
  animation: foreground 5s 2s ease-in-out infinite;
}
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="VYYgYRQ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s about page (without clip-path) [forked]](https://codepen.io/smashingmag/pen/VYYgYRQ) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/VYYgYRQ">Mike Worth’s about page (without clip-path) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>While the <code>50px</code> bounce does add a touch of realism to his movement, I wasn’t happy with how the character’s entrance started at the edge of the viewport.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png"
			
			sizes="100vw"
			alt="Character standing at the edge of the illustartion made with masking using a clipping path"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/9-masking-using-clipping-path.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I wanted him to become visible at the edge of the illustration instead. As the edges of the cave walls are hard, I chose a <code>clip-path</code>.</p>

<p>There are several ways to define a <code>clip-path</code> in CSS. I could use a primitive shape like a rectangle, where each of the first four values specifies its corner positions. The <code>round</code> keyword and the value that follows define any rounded corners:</p>

<pre><code class="language-css">clip-path: rect(0px 150px 150px 0px round 5px);
</code></pre>

<p>Or <code>xywh</code> (x, y, width, height) values, which I find easier to read:</p>

<pre><code class="language-css">clip-path: xywh(0 0 150px 150px round 5px);
</code></pre>

<p>I could use a <code>circle</code>:</p>

<pre><code class="language-css">clip-path: circle(60px at center);
</code></pre>

<p>Or an <code>ellipse</code>:</p>

<pre><code class="language-css">clip-path: ellipse(50% 40% at 50% 50%);
</code></pre>

<p>I could use a <code>polygon</code> shape:</p>

<pre><code class="language-css">clip-path: polygon(...);
</code></pre>

<p>Or even the points from a path I created in a graphics app like Sketch:</p>

<pre><code class="language-css">clip-path: path("M ...");
</code></pre>

<p>Finally &mdash; and my choice for this example &mdash; I might use a mask that I defined using paths from an SVG file:</p>

<pre><code class="language-css">clip-path: url(#mask-cave);
</code></pre>

<p>To make the character visible from the edge of the illustration, I added a second SVG. To prevent a browser from displaying it, set both its dimensions to zero:</p>

<pre><code class="language-html">&lt;figure&gt;
  &lt;svg viewBox="0 0 1400 960" id="cave"&gt;...&lt;/svg&gt;
  &lt;svg height="0" width="0" id="mask"&gt;...&lt;/svg&gt;
&lt;/figure&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png"
			
			sizes="100vw"
			alt="A single SVG clipPath"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/10-svg-clippath.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This contains a single SVG <code>clipPath</code>. By placing this inside the <code>defs</code> element, this path isn’t rendered, but it will be available to create my CSS <code>clip-path</code>:</p>

<pre><code class="language-svg">&lt;svg height="0" width="0" id="mask"&gt;
  &lt;defs&gt;
    &lt;clipPath id="mask-cave"&gt;...&lt;/clipPath&gt;
  &lt;/defs&gt;
&lt;/svg&gt;
</code></pre>

<p>I applied the <code>clipPath</code> URL to my illustration, and now Mike’s mascot only becomes visible when he enters the cave:</p>

<pre><code class="language-css">#cave {
  clip-path: url(#mask-cave);
}
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="oggmgKp"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s about page (with clip-path) (without clip-path) [forked]](https://codepen.io/smashingmag/pen/oggmgKp) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/oggmgKp">Mike Worth’s about page (with clip-path) (without clip-path) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<blockquote><strong>Tip</strong>: Implement that code, and you’ll notice there’s a problem when resizing a browser window. While my cave illustration is flexible, the <code>clipPath</code> remains a fixed width.<br /><br />To make <code>clipPath</code> responsive, add <code>clipPathUnits="objectBoundingBox"</code> to the opening tag, then apply two <code>scale</code> values to the <code>transform</code> property. To calculate these values, divide <code>1</code> first by the SVG’s <code>width</code> and then by its <code>height</code>. My SVG’s width is <code>1400px</code>, which produces the first <code>scale</code> value, <code>0.0007142857143</code>.<br/>
<div class="break-out">
<pre><code class="language-svg">&lt;clipPath id="mask-cave"
  clipPathUnits="objectBoundingBox"
  transform="scale(0.0007142857143, 0.001041666667)"&gt;
  ...
&lt;/clipPath&gt;
</code></pre>
</div></blockquote>

<div class="partners__lead-place"></div>

<h2 id="clipping-irregular-shapes">Clipping Irregular Shapes</h2>

<p>I often need to change or alter illustrations &mdash; perhaps by overlaying colours or applying blending modes &mdash; but I want to retain their overall shape.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="181"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png"
			
			sizes="100vw"
			alt="Altered illustrations with overlaying colours"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/11-altered-illustrations.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>While a <code>clipPath</code> will give me the result I’m looking for, the complexity and size of these paths can sometimes negatively affect performance. That’s when I choose a CSS <code>mask</code> as its properties have been baseline and highly usable since 2023.</p>

<p>The <code>mask</code> property is a shorthand and can include values for <code>mask-clip</code>, <code>mask-mode</code>, <code>mask-origin</code>, <code>mask-position</code>, <code>mask-repeat</code>, <code>mask-size</code>, and <code>mask-type</code>. I find it’s best to learn these properties individually to grasp the concept of masks more easily.</p>

<p>Masks control visibility using bitmap, vector, or gradient mask images. Again, when a mask’s filled pixels cover an element, its content will be visible. When they‘re transparent, the content will be hidden. And when parts of a mask are semi-transparent, some of the content will show through. I can use a bitmap format that includes an alpha channel, such as PNG or WebP:</p>

<pre><code class="language-css">mask-image: url(mask.webp);
</code></pre>

<p>I could apply a mask using a vector graphic:</p>

<pre><code class="language-css">mask-image: url(mask.svg);
</code></pre>

<p>Or generate an image using a conical, linear, or radial gradient:</p>

<pre><code class="language-css">mask-image: linear-gradient(#000, transparent); 
</code></pre>

<p>…or:</p>

<div class="break-out">
<pre><code class="language-css">mask-image: radial-gradient(circle, #ff104c 0%, transparent 100%);
</code></pre>
</div>

<p>I might apply more than one mask to an element and mix several image types using what should be a familiar syntax:</p>

<pre><code class="language-css">mask-image:
  image(url(mask.webp)),
  linear-gradient(#000, transparent);
</code></pre>

<p><code>mask</code> shares the same syntax as CSS backgrounds, which makes remembering its properties much easier. To apply a <code>background-image</code>, add its URL value:</p>

<pre><code class="language-css">background-image: url("background.webp");
</code></pre>

<p>To apply a mask, swap the <code>background-image</code> property for <code>mask-image</code>:</p>

<pre><code class="language-css">mask-image: url("mask.webp");
</code></pre>

<p>The <code>mask</code> property also shares the same browser styles as CSS backgrounds, so by default, a mask will repeat horizontally and vertically unless I specify otherwise:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Options: repeat, repeat-x, repeat-y, round, space, no-repeat &#42;/
mask-repeat: no-repeat;
</code></pre>
</div>

<p>It will be placed at the top-left corner unless I alter its position:</p>

<pre><code class="language-css">/&#42; Options: Keywords, units, percentages &#42;/
mask-position: center;
</code></pre>

<p>Plus, I can specify <code>mask-size</code> in the same way as <code>background-size</code>:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Options: Keywords (auto, contain, cover), units, percentages &#42;/
mask-size: cover;
</code></pre>
</div>

<p>Finally, I can define where a mask starts:</p>

<pre><code class="language-css">mask-origin: content-box;
mask-origin: padding-box;
mask-origin: border-box;
</code></pre>

<h2 id="using-a-mask-image">Using A Mask Image</h2>

<p>Mike’s FAQs page includes an animated illustration of his hero standing at a crossroads. My goal was to separate the shape from its content, allowing me to change the illustration throughout the hero’s journey. So, I created a scalable <code>mask-image</code> which defines the visible area and applied it to the figure element:</p>

<pre><code class="language-css">figure {
  mask-image: url(mask.svg);
}
</code></pre>

<p>To ensure the mask matched the illustration’s dimensions, I also set the <code>mask-size</code> to always <code>cover</code> its content:</p>

<pre><code class="language-css">figure {
  mask-size: cover;
}
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="181"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png"
			
			sizes="100vw"
			alt="Mike Worth’s FAQs (mask-image)"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/12-mask-image.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="VYYgLed"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s FAQs (mask-image) [forked]](https://codepen.io/smashingmag/pen/VYYgLed) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/VYYgLed">Mike Worth’s FAQs (mask-image) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>Although “X“ never, ever marks the spot, Mike Worth’s review page illustration sees his orangutan mascot studying his treasure map. I wanted to focus someone’s attention on the center part of the image by using an elliptical shape.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png"
			
			sizes="100vw"
			alt="A gorilla dressed as an explorer studies a treasure map at a desk with an elliptical shape effect"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/13-mike-worth-review-page-illustration.png'>Large preview</a>)
    </figcaption>
  
</figure>

<pre><code class="language-css">figure {
  clip-path: ellipse(45% 35% at 50% 50%);
}
</code></pre>

<p>However, the hard edges of a clip <code>clip-path</code> don’t create the effect I was aiming to achieve:</p>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="dPPaoXK"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s reviews page (ellipse) [forked]](https://codepen.io/smashingmag/pen/dPPaoXK) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/dPPaoXK">Mike Worth’s reviews page (ellipse) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>I experimented by combining a Gaussian blur filter with a mask defined in SVG, first by creating the filter, and then applying it to the mask’s ellipse:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;defs&gt;
  &lt;filter id="mask-blur" x="-50%" y="-50%" width="200%" height="200%"&gt;
  &lt;feGaussianBlur in="SourceGraphic" stdDeviation="10" /&gt;&lt;/filter&gt;
&lt;/defs&gt;

&lt;defs&gt;
  &lt;mask id="mask"&gt;
    &lt;ellipse cx="700" cy="480" fill="#FFF" rx="450" ry="320" filter="url(#mask-blur)"/&gt;
  &lt;/mask&gt;
&lt;/defs&gt;
</code></pre>
</div>

<p>And, while this achieved the result I was looking for, the implementation felt over-engineered for what was, after all, a simple effect. By far, the most efficient, elegant, and responsive implementation used a <code>radial-gradient</code>, achieving the effect I was hoping for with no bitmap or vector image and just a single CSS property:</p>

<div class="break-out">
<pre><code class="language-css">figure {
  mask-image: radial-gradient(ellipse farthest-corner at center center, #000 0%, transparent 75%);
}
</code></pre>
</div>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="MYYLwjj"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s review page (radial gradient mask) [forked]](https://codepen.io/smashingmag/pen/MYYLwjj) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/MYYLwjj">Mike Worth’s review page (radial gradient mask) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>This approach enables me to fine-tune my <code>mask-image</code> size and even animate its colour stops, position, and size when someone interacts with its content.</p>

<div class="partners__lead-place"></div>

<h2 id="layering-multiple-masks">Layering Multiple Masks</h2>

<p>Lighting is especially important on Mike Worth’s review page, where his orangutan hero needs it to study his treasure map. Like background images, I can apply multiple mask images to create the lighting I need.</p>

<p>I could combine two mask images: a circular, semi-transparent <code>radial-gradient</code> to provide the ambiance, plus a <code>linear-gradient</code> at a 45-degree angle for the light rays. I applied them both to the <code>figure</code> element:</p>

<div class="break-out">
<pre><code class="language-css">figure {
  mask-image:
  radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
  linear-gradient(45deg, transparent 40%, #ff104c 50%, #ff104c 50%, transparent 60%);
  mask-repeat: no-repeat;
}
</code></pre>
</div>

<p>I positioned the masks individually, set the <code>radial-gradient</code> size to <code>80%</code>, and the <code>linear-gradient</code> light rays to cover the entire image:</p>

<pre><code class="language-css">figure {
  mask-position: 50% 50%, 0 0;
  mask-size: 80%, cover;
}
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="myyvJra"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s reviews page (layering multiple masks) [forked]](https://codepen.io/smashingmag/pen/myyvJra) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myyvJra">Mike Worth’s reviews page (layering multiple masks) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>But, I needed more precise control over the position of the light rays to create the effect that they’re coming from the desk lamp. So, I replaced the <code>linear-gradient</code> with a soft-edged bitmap image:</p>

<div class="break-out">
<pre><code class="language-css">figure {
  mask-image:
    radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
  url(mask.webp);
  mask-size: 90%, cover;
}
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="180"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png"
			
			sizes="100vw"
			alt="A three-part image demonstrating the use of multiple masks in image editing."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/14-layering-multiple-masks.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Finally, to add an extra touch of realism, I added a keyframe animation &mdash; which changes the <code>mask-size</code> and creates the effect that the lamp light is flickering &mdash; and applied it to the figure:</p>

<pre><code class="language-css">@keyframes lamp-flicker {
  0%, 19.9%, 22%, 62.9%, 64%, 64.9%, 70%, 100% { 
    mask-size: 90%, auto;
  }

  20%, 21.9%, 63%, 63.9%, 65%, 69.9% { 
    mask-size: 90%, 0px;
  }
}

figure {
  animation: lamp-flicker 3s 3s linear infinite;
}
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="yyyZNba"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s review page (multiple mask-images) [forked]](https://codepen.io/smashingmag/pen/yyyZNba) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/yyyZNba">Mike Worth’s review page (multiple mask-images) [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="animating-masks">Animating Masks</h2>

<p>Animating CSS masks creates exciting reveals and <strong>transitions between scenes</strong> and helps someone focus on critical content. It can also bring stories to life, making a person’s experience more engaging and immersive.</p>

<p>In this deleted scene from my design for his website, the orangutan adventurer mascot I created for Mike Worth can be seen driving across the landscape. To help tell the story that he’s being watched from afar by his archnemesis, I wanted to add a binocular-shaped mask.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png"
			
			sizes="100vw"
			alt="An orangutan adventurer mascot driving across the landscape demonstrating the use of a binocular-shaped mask"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/15-binocular-shaped-mask.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I started by creating the binocular shape, complete with some viewfinder markers.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="549"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png"
			
			sizes="100vw"
			alt="A binocular shape"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/16-binocular-shape.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then, I applied that image as a mask, setting its position, repeat, and size values to place it in the center of the figure element:</p>

<pre><code class="language-css">figure {
  mask-image: url(mask.svg);
  mask-position: 50% 50%;
  mask-repeat: no-repeat;
  mask-size: 85%;
}
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="jEEdPLq"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s journey animation with binocular-shaped mask [forked]](https://codepen.io/smashingmag/pen/jEEdPLq) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/jEEdPLq">Mike Worth’s journey animation with binocular-shaped mask [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>However, despite the infinitely scrolling background and the motion of the hero’s bumpy ride, the animation still felt static. So I added a subtle animation that shifts the <code>mask-position</code>, first by creating the keyframes:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes pan-mask {
  0% { mask-position: 45% 45%; } /&#42; Start lower-left &#42;/
  25% { mask-position: 55% 55%; } /&#42; Move to upper-right &#42;/
  50% { mask-position: 43% 52%; } /&#42; Shift more dramatically &#42;/
  75% { mask-position: 57% 48%; } /&#42; More variation &#42;/
  100% { mask-position: 45% 45%; } /&#42; Loop back &#42;/
}
</code></pre>
</div>

<p>Then, I applied it, along with the scrolling background animation, to the figure element:</p>

<pre><code class="language-css">/&#42; Run both animations simultaneously &#42;/
figure {
  animation:
  background-scroll 5s linear infinite,
  pan-mask 6s ease-in-out infinite alternate;
}
</code></pre>

<p>After seeing these results, I wondered whether I could connect someone to the story by linking the <code>mask-position</code> to the movement of their cursor. I added a script that selects the <code>figure</code> element, gets the cursor position relative to the viewport, and dynamically changes the <code>mask-position</code>:</p>

<pre><code class="language-javascript">&lt;script&gt;
  // Select the figure element.
  const figure = document.querySelector('figure');
  document.addEventListener('mousemove', (event) =&gt; {
  
  // Get the cursor position.
  const mouseX = event.clientX;
  const mouseY = event.clientY;
  
  // Normalise the mask-position.
  const maskX = (mouseX / window.innerWidth) &#42; 100;
  const maskY = (mouseY / window.innerHeight) &#42; 100;`
  
  // Dynamically set the mask-position.
  figure.style.maskPosition =${maskX}% ${maskY}%;
  });
&lt;/script&gt;
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="oggmXeM"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s journey animation with binoculars following the cursor [forked]](https://codepen.io/smashingmag/pen/oggmXeM) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/oggmXeM">Mike Worth’s journey animation with binoculars following the cursor [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>With that, there was only one challenge left to complete the effect. Focusing binoculars on a distant target is rarely easy, and when the hero’s archnemesis has hands the size of a silverback gorilla, the task is even more challenging. I extended my script to blur the visible content seen through the binocular-shaped mask and then removed the filter when someone presses their keyboard’s spacebar or presses a mouse button:</p>

<pre><code class="language-javascript">&lt;script&gt;
  // When mouse button pressed, remove blur
  document.addEventListener('mousedown', () =&gt; {
    figure.style.filter = 'blur(0)';
  });
  
  // When mouse button released, reapply blur
  document.addEventListener('mouseup', () =&gt; {
    figure.style.filter = 'blur(5px)';
  });
  
  // When spacebar pressed, remove blur
  document.addEventListener('keydown', (event) =&gt; {
    if (event.key === ' ') {
      figure.style.filter = 'blur(0)';
    }
  });

  // When spacebar released, reapply blur
  document.addEventListener('keyup', (event) =&gt; {
    if (event.key === ' ') {
      figure.style.filter = 'blur(5px)';
    }
  });
&lt;/script&gt;
</code></pre>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="MYYLwEJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s journey animation with focussing blur [forked]](https://codepen.io/smashingmag/pen/MYYLwEJ) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/MYYLwEJ">Mike Worth’s journey animation with focussing blur [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="more-masking-animation">More Masking Animation</h2>

<p>When someone using Mike Worth’s website follows the wrong path or takes a wrong turn, he ends up sinking into hot lava.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="182"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png"
			
			sizes="100vw"
			alt="A cartoon gorilla wearing a fedora and leather jacket is shown inside a circular cutout at the center of a brown background demonstrating a masking effect"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-2-css-masking-add-extra-dimension/17-masking-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To let someone know they might’ve reached the end of their adventure, I wanted to ape the zooming-in effect I started this article with:</p>

<pre><code class="language-html">&lt;figure&gt;
  &lt;svg&gt;…&lt;/svg&gt;
&lt;/figure&gt;
</code></pre>

<p>I created a circular <code>clip-path</code> and set its default size to <code>75%</code>. Then, I defined the animation keyframes to resize the circle from 75% to 15% before attaching it to my figure with a one-second duration and a three-second delay:</p>

<pre><code class="language-css">@keyframes error {
  0% { clip-path: circle(75%); }
  100% { clip-path: circle(15%); }
}

figure {
  clip-path: circle(75%);
  animation: error 1s 3s ease-in forwards;
}
</code></pre>

<p>The animation now focuses someone’s attention on the hapless hero, before he sinks lower and lower into the bubblingly hot lava.</p>

<p>Try this yourself:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="qEEgdxy"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s error page [forked]](https://codepen.io/smashingmag/pen/qEEgdxy) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/qEEgdxy">Mike Worth’s error page [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="bringing-it-all-to-life">Bringing It All To Life</h2>

<p>Masking adds an <strong>extra dimension to web animation</strong> and makes stories more engaging and someone’s experience more compelling &mdash; all while keeping animations efficiently lightweight. Whether you’re revealing content, guiding focus, or adding more depth to a design, masks offer endless creative possibilities. So why not experiment with them in your next project? You might uncover a whole new way to bring your animations to life.</p>

<p>The end. Or is it? …</p>

<p>Mike Worth’s website will launch in June 2025, but you can <a href="https://codepen.io/collection/YwMKPb">see examples from this article on CodePen</a> now.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Andy Clarke</author><title>Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</title><link>https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/</link><pubDate>Wed, 07 May 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/</guid><description>Have you ever thought about how the limitations of early cartoon animations might relate to web design today? From looping backgrounds to minimal frame changes, these retro animation techniques have surprising parallels to modern CSS. In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> shows how he applied these principles to Emmy-winning composer Mike Worth’s new website, using CSS to craft engaging and fun animations that bring his world to life.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/" />
              <title>Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 1: How Classic Cartoons Inspire Modern CSS</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-05-07T08:00:00&#43;00:00" class="op-published">2025-05-07T08:00:00+00:00</time>
                  <time datetime="2025-05-07T08:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Browser makers didn’t take long to add the movement capabilities to CSS. The simple <code>:hover</code> pseudo-class came first, and a bit later, the <code>transition</code>s between two states. Then came the ability to change states across a set of <code>@keyframes</code> and, most recently, scroll-driven animations that link keyframes to the scroll position.</p>

<p>Even with these added capabilities, <strong>CSS animations have remained relatively rudimentary</strong>. They remind me of the <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> animated series I grew up watching on TV.</p>

<p>These animated shorts lacked the budgets given to live-action or animated movies. They were also far lower than those available when William Hanna and Joseph Barbera made Tom and Jerry shorts while working for MGM Cartoons. This meant the animators needed to develop techniques to work around their cost restrictions and the technical limitations of the time.</p>

<p>They used fewer frames per second and far fewer cells. Instead of using a different image for each frame, they repeated each one several times. They reused cells as frequently as possible by zooming and overlaying additional elements to construct a new scene. They kept bodies mainly static and overlayed eyes, mouths, and legs to create the illusion of talking and walking. Instead of reducing the quality of these cartoons, these constraints created a charm often lacking in more recent, bigger-budget, and technically advanced productions.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show illustration"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/3-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The simple and efficient techniques developed by Hanna-Barbera’s animators can be implemented using CSS. Modern layout tools allow web developers to layer elements. Scaleable Vector Graphics (SVG) can contain several frames, and developers needn’t resort to JavaScript; they can use CSS to change an element’s <code>opacity</code>, <code>position</code>, and <code>visibility</code>. But what are some reasons for doing this?</p>

<p>Animations bring static experiences to life. They can <strong>improve usability</strong> by guiding people’s actions and delighting or surprising them when interacting with a design. When carefully considered, animations can reinforce branding and help tell stories about a brand.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="550"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png"
			
			sizes="100vw"
			alt="Design by Andy Clarke of Mike Worth’s website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/4-andy-clarke-design.png'>Large preview</a>)
    </figcaption>
  
</figure>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="introducing-mike-worth">Introducing Mike Worth</h2>

<p>I’ve recently been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a bold, retro-style design that showcases his work. I used CSS animations throughout to delight and surprise his audience as they move through his website.</p>

<p>Mike loves ’80s and ’90s animation &mdash; especially Disney’s <a href="https://en.wikipedia.org/wiki/DuckTales_(1987_TV_series)"><em>Duck Tales</em></a>. Unsurprisingly, my taste in cartoons stretches back a little further to the 1960s <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> shows like Dastardly and Muttley in <em>Their Flying Machines</em>, <em>Scooby-Doo</em>, <em>The Perils of Penelope Pitstop</em>, <em>Wacky Races</em>, and, of course, <a href="https://en.wikipedia.org/wiki/Yogi_Bear">Yogi Bear</a>.</p>

<p>So, to explain how this era of animation relates to CSS, I’ve chosen an episode of <em>The Yogi Bear Show</em>, “<a href="https://www.youtube.com/watch?v=CPnmzcmKgA0">Home Sweet Jellystone</a>,” first broadcast in 1961. In this story, Ranger Smith inherits a mansion and (spoiler alert) leaves Jellystone.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show. An illustration from “Home Sweet Jellystone episode”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/5-yogi-bear-show-home-sweet-jellystone.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="dissecting-movement">Dissecting Movement</h2>

<p>In this episode, Hanna-Barbera’s techniques become apparent as soon as a postman arrives with a telegram for Ranger Smith. The camera pans sideways across a landscape painting by background artist Robert Gentle to create the illusion that the postman is moving.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show, author’s recreation of dissecting movement"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (Author’s recreation) (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/6-yogi-bear-show-author-recreation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The background loops when a scene lasts longer than a single pan of Robert Gentle’s landscape painting, with bushes and trees appearing repeatedly.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="80"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png"
			
			sizes="100vw"
			alt="A closer look at the landscape painting, with bushes and trees appearing repeatedly"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/7-yogi-bear-dissecting-movement.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This can be recreated using a single element and an animation that changes the position of its background image:</p>

<pre><code class="language-css">@keyframes background-scroll {
  0% { background-position: 2750px 0; }
  100% { background-position: 0 0; }
}

div {
  overflow: hidden;
  width: 100vw;
  height: 540px;
  background-image: url("…");
  background-size: 2750px 540px;
  background-repeat: repeat-x;
  animation: background-scroll 5s linear infinite;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="NPPzgyq"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Yogi Bear background image scroll [forked]](https://codepen.io/smashingmag/pen/NPPzgyq) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/NPPzgyq">Yogi Bear background image scroll [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>Although beautifully executed, Robert Gentle’s background paintings were often remarkably simple. The mansion’s interior background rushes past to create the illusion of Ranger Smith dashing across it, so it needed very few details.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="314"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png"
			
			sizes="100vw"
			alt="The mansion’s interior background"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/8-robert-gentle-background-painting.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>The economy of movement</strong> was essential for producing these animated shorts cheaply and efficiently. The postman’s motorcycle bounces, and only his head position and facial expressions change, which adds a subtle hint of realism.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="131"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png"
			
			sizes="100vw"
			alt="An example of several drawings where only head position and facial expressions change to add movement on a motorcycle"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. Sequence shortened. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/9-yogi-bear-sequence-shortened.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Likewise, only Ranger Smith’s facial expression and leg positions change throughout his walk cycle as he dashes through his mansion. The rest of his body stays static.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="131"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png"
			
			sizes="100vw"
			alt="An example of several drawings where only Ranger Smith’s facial expression and leg positions change during a walk"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. Sequence shortened. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/10-yogi-bear-sequence-shortened.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In a discarded scene from my design for his website, the orangutan adventurer mascot I created for Mike Worth can be seen driving across the landscape.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="400"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png"
			
			sizes="100vw"
			alt="An illustration from Mike Worth’s website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/11-mike-worth-website.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I drew directly from <strong>Hanna-Barbera’s bouncing and scrolling technique</strong> for this scene by using two keyframe animations: <code>background-scroll</code> and <code>bumpy-ride</code>. The infinitely scrolling background works just like before:</p>

<pre><code class="language-css">@keyframes background-scroll {
  0% { background-position: 960px 0; }
  100% { background-position: 0 0; }
}
</code></pre>

<p>I created the appearance of his bumpy ride by animating changes to the keyframes’ <code>translate</code> values:</p>

<pre><code class="language-css">@keyframes bumpy-ride {
  0% { translate: 0 0; }
  10% { translate: 0 -5px; }
  20% { translate: 0 3px; }
  30% { translate: 0 -3px; }
  40% { translate: 0 5px; }
  50% { translate: 0 -10px; }
  60% { translate: 0 4px; }
  70% { translate: 0 -2px; }
  80% { translate: 0 7px; }
  90% { translate: 0 -4px; }
  100% { translate: 0 0; }
}

figure {
  /&#42; ... &#42;/
  animation: background-scroll 5s linear infinite;
}

img {
  /&#42; ... &#42;/
  animation: bumpy-ride 1.5s infinite ease-in-out;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ByyVZrB"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth background image scroll [forked]](https://codepen.io/smashingmag/pen/ByyVZrB) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ByyVZrB">Mike Worth background image scroll [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>As Michelle Barker wrote about <a href="https://www.smashingmagazine.com/2021/10/respecting-users-motion-preferences/">here at Smashing Magazine</a> back in 2021:</p>

<blockquote>“When working with motion on the web, it’s important to consider that not everyone experiences it the same way. What might feel smooth and slick to some might be annoying or distracting to others &mdash; or worse, induce feelings of sickness or even cause seizures.”<br /><br />&mdash; Michelle Barker</blockquote>

<p>You can prevent that from happening by turning off animations when someone has chosen reduced motion in their browser by using the <code>prefers-reduced-motion</code> media query:</p>

<pre><code class="language-css">@media (prefers-reduced-motion: reduce) {
  &#42; { animation: none !important; }
}
</code></pre>

<div class="partners__lead-place"></div>

<h2 id="reusing-elements">Reusing Elements</h2>

<p>Since each episode’s budget and production time were limited, William Hanna and Joseph Barbera created a streamlined process for producing their animations. They used as few as 2,000 individual drawings and just a few background paintings per episode, often reusing them on several episodes.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="322"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png"
			
			sizes="100vw"
			alt="A background painting with highlighted trees from the Yogi Bear show"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/12-yogi-bear-background-painting.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Watch the episode and you’ll see these trees appear over and over again throughout “Home Sweet Jellystone.” Behind Yogi and Boo-Boo on the track, in the bushes, and scaled up in this close-up of Boo-Boo:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="196"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show paintings with the same trees"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/1-yogi-bear-show-reusing-elements.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The animators also frequently <strong>layered foreground elements onto these background paintings</strong> to create a variety of new scenes:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="293"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png"
			
			sizes="100vw"
			alt="The Yogi Bear Show paintings with layered foreground elements"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      On the left: The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. On the right: Author’s edit.(<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/2-layered-foreground-elements.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In my deleted scene from Mike Worth’s website, I introduced these rocks into the foreground to add depth to the animation:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="400"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png"
			
			sizes="100vw"
			alt="Painting from Mike Worth’s website with rocks on the background"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/13-layered-foreground-elements.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If I were using bitmap images, this would require just one additional image:</p>

<pre><code class="language-html">&lt;figure&gt;
  &lt;img id="bumpy-ride" src="..." alt="" /&gt;
  &lt;img id="apes-rock" src="..." alt="" /&gt;
&lt;/figure&gt;
</code></pre>

<pre><code class="language-css">figure {
  position: relative; 

  #bumpy-ride { ... }

  #apes-rock {
    position: absolute;
    width: 960px;
    left: calc(50% - 480px);
    bottom: 0;
  }
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="xbbzraj"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth layered animation [forked]](https://codepen.io/smashingmag/pen/xbbzraj) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/xbbzraj">Mike Worth layered animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>Had I continued developing this scene, I might’ve added a slower scrolling animation to those rocks to introduce a parallax effect for even greater realism.</p>

<h2 id="looping-frames-create-movement">Looping Frames Create Movement</h2>

<p>To meet their limited budget and production schedules, Hanna Barbera’s animators carefully planned their animations and cleverly <strong>only animated specific elements</strong>. While heads and facial expressions make characters talk and their legs change to make them walk, most characters’ bodies remain relatively static. So, throughout this entire scene of Ranger Smith walking and talking his way across his cabin, only his face and legs are animated:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="157"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png"
			
			sizes="100vw"
			alt="Looping Frames of Ranger Smith walking, where only his face and legs are animated"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/14-looping-frames.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Likewise, when the ranger reads his telegram, only his eyes and mouth move:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="400"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png"
			
			sizes="100vw"
			alt="Looping Frames of Ranger Smith reading a telegram"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/15-looping-frames-movement.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If you’ve wondered why both Ranger Smith and Yogi Bear wear collars and neckties, it’s so the line between their animated heads and faces and static bodies is obscured:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="600"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png"
			
			sizes="100vw"
			alt="Yogi Bear wearing a collar"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. Author’s recreation. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/16-yogi-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aSVG%20delivers%20incredible%20performance%20and%20also%20offers%20fantastic%20flexibility%20when%20animating%20elements.%20The%20ability%20to%20embed%20one%20SVG%20inside%20another%20and%20to%20manipulate%20groups%20and%20other%20elements%20using%20CSS%20makes%20it%20ideal%20for%20animations.%0a&url=https://smashingmagazine.com%2f2025%2f05%2fsmashing-animations-part-1-classic-cartoons-inspire-css%2f">
      
SVG delivers incredible performance and also offers fantastic flexibility when animating elements. The ability to embed one SVG inside another and to manipulate groups and other elements using CSS makes it ideal for animations.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>I replicated how Hanna-Barbera made Ranger Smith and other characters’ mouths move by first including a group that contains the ranger’s body and head, which remain static throughout. Then, I added six more groups, each containing one frame of his mouth moving:</p>

<pre><code class="language-svg">&lt;svg&gt;
  &lt;!-- static elements --&gt;
  &lt;g&gt;...&lt;/g&gt;

  &lt;!-- animation frames --&gt;
  &lt;g class="frame-1"&gt;...&lt;/g&gt;
  &lt;g class="frame-2"&gt;...&lt;/g&gt;
  &lt;g class="frame-3"&gt;...&lt;/g&gt;
  &lt;g class="frame-4"&gt;...&lt;/g&gt;
  &lt;g class="frame-5"&gt;...&lt;/g&gt;
  &lt;g class="frame-6"&gt;...&lt;/g&gt;
&lt;/svg&gt;
</code></pre>

<p>I used CSS custom properties to define the speed at which characters’ mouths move and how many frames are in the animation:</p>

<pre><code class="language-css">:root {
  --animation-duration: 1s;
  --frame-count: 6;
}
</code></pre>

<p>Then, I applied a keyframe animation to show and hide each frame:</p>

<pre><code class="language-css">@keyframes ranger-talking {
  0% { visibility: visible; }
  16.67% { visibility: hidden; }
  100% { visibility: hidden; }
}

[class&#42;="frame"] {
  visibility: hidden;
  animation: ranger-talking var(--animation-duration) infinite;
}
</code></pre>

<p>Before finally setting a delay, which makes each frame visible at the correct time:</p>

<div class="break-out">
<pre><code class="language-css">.frame-1 {
  animation-delay: calc(var(--animation-duration) &#42; 0 / var(--frame-count));
}

/&#42; ... &#42;/

.frame-6 {
  animation-delay: calc(var(--animation-duration) &#42; 5 / var(--frame-count));
}
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="QwwxgJE"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Ranger Smith talking [forked]](https://codepen.io/smashingmag/pen/QwwxgJE) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/QwwxgJE">Ranger Smith talking [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<div class="partners__lead-place"></div>

<h2 id="working-with-mike-worth">Working With Mike Worth</h2>

<p>When Mike Worth and I sat down to discuss working together, we both understood that there would be neither the budget nor the time to create a short animated cartoon for his website. We also knew that video would be the correct format for a fully animated production, but we were keen to explore how CSS could bring what would’ve otherwise been static images to life. So, this begs the question of <em>why</em> and <em>when</em> to use CSS animations.</p>

<h3 id="ambient-animations">Ambient Animations</h3>

<blockquote>Subtle ambient animations contribute to a website’s atmosphere and help with storytelling without distracting from its content or functionality.</blockquote>

<p>For Mike Worth’s about-page illustration, I shone shafts of light onto a stone tablet to add depth to an otherwise flat image. Inside my SVG, I added a <code>path</code> for the light and reduced its <code>opacity</code> to <code>.25</code>:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;svg&gt;
  &lt;!-- ... --&gt;
  &lt;path class="light-shaft" fill="#F1DCA9" opacity=".25" d=""/&gt;
&lt;/svg&gt;
</code></pre>
</div>

<p>I then defined an SVG filter to blur the edges of my light shafts and linked it to my <code>path</code>:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;defs&gt;
  &lt;filter id="light-shaft" width="100%" height="100%" x="0" y="0" filterUnits="objectBoundingBox"&gt;
  &lt;feGaussianBlur in="SourceGraphic" stdDeviation="20"/&gt;
  &lt;/filter&gt;
&lt;/defs&gt;

&lt;svg&gt;
  &lt;!-- ... --&gt;
  &lt;path class="light-shaft" filter="url(#light-shaft)" … /&gt;
&lt;/svg&gt;
</code></pre>
</div>

<p>Finally, I added a subtle ambient animation that rotates the light shafts and creates a more natural feel:</p>

<pre><code class="language-css">@keyframes shaft-rotate {
  0% { rotate: 2deg; }
  50% { rotate: -2deg; }
  100% { rotate: 2deg; }
}

.light-shaft {
  animation: shaft-rotate 20s infinite;
  transform-origin: 100% 0;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="bNNKROE"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s about page animation [forked]](https://codepen.io/smashingmag/pen/bNNKROE) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/bNNKROE">Mike Worth’s about page animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<p>Can you throw more light on Mike’s navigation?</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ZYYRyVJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Light up Mike Worth about page [forked]](https://codepen.io/smashingmag/pen/ZYYRyVJ) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ZYYRyVJ">Light up Mike Worth about page [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h3 id="animations-on-interactions">Animations On Interactions</h3>

<blockquote>In the same way that a <code>:hover</code> pseudo-class provides valuable visual feedback when someone interacts with an element, CSS animations can create a deeper connection between people and a design.</blockquote>

<p>I included an Easter egg interaction on Mike Worth’s review page illustration. The big red button turns the desk lamp on and off, much to the consternation of Mike’s orangutan mascot, who’s trying to study his map. To implement this, I applied a <code>data-</code> attribute to the SVG illustration:</p>

<pre><code class="language-svg">&lt;svg … data-lights="lights-on"&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;
</code></pre>

<p>And added a red button for any curious visitor to press:</p>

<div class="break-out">
<pre><code class="language-html">&lt;a href="javascript:void(0);" id="light-switch" title="Lights on/off"&gt;
  &lt;path fill="#0a0908" d="..."/&gt;
  &lt;ellipse fill="#9c1621" … /&gt;
  &lt;path fill="#fff" d="..."/&gt;
&lt;/a&gt;
</code></pre>
</div>

<p>When someone presses that red button, the light goes out, which is made possible by changing the value of the SVG’s <code>data-</code> attribute from <code>lights-on</code> to <code>lights-off</code>.</p>

<p>Several elements within the illustration light up when the desk lamp is on. To make this happen, I applied a class value to those specific items:</p>

<pre><code class="language-svg">&lt;path class="lamp-glow" /&gt;
</code></pre>

<p>And used the data-attribute value to toggle the glow on and off when someone presses the lamp’s button:</p>

<pre><code class="language-css">[data-lights="lights-on"] .lamp-glow {
  opacity: 1;
  transition: opacity .25s linear;
}

[data-lights="lights-off"] .lamp-glow {
  opacity: .25;
  transition: opacity .25s linear;
}
</code></pre>

<p>When someone turns the lamp on, it flickers at what appears to be random intervals. I first applied a class value to the flickering elements:</p>

<pre><code class="language-svg">&lt;path class="lamp-flicker" /&gt;
</code></pre>

<p>Then, I hid them when the lamp was turned off:</p>

<pre><code class="language-css">[data-lights="lights-off"] .lamp-flicker {
  visibility: hidden;
}
</code></pre>

<p>Finally, I created a keyframe animation that flickers the lamp light’s <code>opacity</code> at seemingly random intervals:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes lamp-flicker {
  0%, 19.9%, 22%, 62.9%, 64%, 64.9%, 70%, 100% { opacity: 1; }
  20%, 21.9%, 63%, 63.9%, 65%, 69.9% { opacity: .5; }
}

[data-lights="lights-on"] .lamp-flicker {
  animation: lamp-flicker 3s 3s linear infinite;
}
</code></pre>
</div>

<p>Animations can also tempt people to venture deeper into a design, so I made the crystal skull on the desk vibrate to hint at something more to discover:</p>

<pre><code class="language-html">&lt;a href="/easter-egg"&gt;
  &lt;g id="crystal-skull"&gt;...&lt;/g&gt;
&lt;/a&gt;
</code></pre>

<div class="break-out">
<pre><code class="language-css">@keyframes crystal-skull-vibrate {
  0% { translate: 0 0; }
  20% { translate: -2px 2px; }
  40% { translate: -2px -2px; }
  60% { translate: 2px 2px; }
  80% { translate: 2px -2px; }
  100% { translate: 0 0; }
}

&#35;crystal-skull:hover {
  animation: crystal-skull-vibrate .5s ease 0s infinite normal forwards;
}
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YPPvQBg"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s review page animation [forked]](https://codepen.io/smashingmag/pen/YPPvQBg) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YPPvQBg">Mike Worth’s review page animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h3 id="animations-tell-stories">Animations Tell Stories</h3>

<blockquote>When carefully considered, animations can reflect a brand’s identity and help tell its story.</blockquote>

<p>Mike Worth’s brand is high-energy &mdash; just like his personality &mdash; and the story he tells about his work as a video game composer is engaging and playful. Mike wanted every interaction with his website to bring his personality to the screen.</p>

<p>Should someone get lost along their journey, they’ll end up on Mike’s 404 page, where his hero has a sinking feeling. While Mike’s orangutan adventurer slips deeper and deeper into the quicksand, animated bubbles rise:</p>

<pre><code class="language-svg">&lt;g&gt;
  &lt;circle class="four-oh-dear-bubble" ... /&gt;
  &lt;circle class="four-oh-dear-bubble" ... /&gt;
  &lt;circle class="four-oh-dear-bubble" ... /&gt;
  &lt;!-- ... --&gt;
&lt;/g&gt;
</code></pre>

<div class="break-out">
<pre><code class="language-css">@keyframes four-oh-dear-bubbles {
  0% { 
    animation-timing-function: ease-in; 
    opacity: 1; 
    transform: translateY(45px);
  }
  24% { 
    opacity: 1; 
  }
  40% { 
    animation-timing-function: ease-in; 
    translate: 0 24px;
  }
  65% { 
    animation-timing-function: ease-in;
    translate: 0 12px;
  }
  82% { 
    animation-timing-function: ease-in;
    translate: 0 6px;
  }
  93% { 
    animation-timing-function: ease-in;
    translate: 0 4px;
  }
  25%, 55%, 75%, 87% { 
    animation-timing-function: ease-out;
    translate: 0 0;
  }
  100% { 
    animation-timing-function: ease-out;
    opacity: 1; 
    translate: 0 0;
  }
}

.four-oh-dear-bubble {
  animation: four-oh-dear-bubbles 2s ease 0s infinite alternate forwards; }
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ZYYRyPX"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Mike Worth’s 404 page animation [forked]](https://codepen.io/smashingmag/pen/ZYYRyPX) by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ZYYRyPX">Mike Worth’s 404 page animation [forked]</a> by <a href="https://codepen.io/malarkey">Andy Clarke</a>.</figcaption>
</figure>

<h2 id="bringing-it-all-to-life">Bringing It All To Life</h2>

<p>Just as the animators at Hanna-Barbera turned technical limitations into their signature style, CSS animations enable web professionals to craft characterful experiences. By layering elements, looping frames, and applying subtle movement, you can inject personality into a design while improving someone’s experience.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png"
			
			sizes="100vw"
			alt="Yogi Bear’s painting"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/17-yogi-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In my design for Mike Worth’s website, <strong>animation isn’t just for decoration; it tells a compelling story about him and his work</strong>. Every movement reflects his brand identity and makes his website an extension of his creative world.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThink%20beyond%20movement%20the%20next%20time%20you%20reach%20for%20a%20CSS%20animation.%20Consider%20emotions,%20identity,%20and%20mood,%20too.%20After%20all,%20a%20well-considered%20animation%20can%20do%20more%20than%20catch%20someone%e2%80%99s%20eye.%20It%20can%20capture%20their%20imagination.%0a&url=https://smashingmagazine.com%2f2025%2f05%2fsmashing-animations-part-1-classic-cartoons-inspire-css%2f">
      
Think beyond movement the next time you reach for a CSS animation. Consider emotions, identity, and mood, too. After all, a well-considered animation can do more than catch someone’s eye. It can capture their imagination.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png"
			
			sizes="100vw"
			alt="The end painting"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/smashing-animations-part-1-classic-cartoons-inspire-css/18-the-end-painting.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Mike Worth’s website will launch in June 2025, but you can <a href="https://codepen.io/collection/YwMKPb">see examples from this article on CodePen</a> now.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Joas Pambou</author><title>Using Manim For Making UI Animations</title><link>https://www.smashingmagazine.com/2025/04/using-manim-making-ui-animations/</link><pubDate>Tue, 08 Apr 2025 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/04/using-manim-making-ui-animations/</guid><description>Animation makes things clearer, especially for designers and front-end developers working on UI, prototypes, or interactive visuals. Manim is a tool that lets you create smooth and dynamic animations, not just for the design field but also in math, coding, and beyond, to explain complex ideas or simply make everything a little bit more interactive.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/04/using-manim-making-ui-animations/" />
              <title>Using Manim For Making UI Animations</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Using Manim For Making UI Animations</h1>
                  
                    
                    <address>Joas Pambou</address>
                  
                  <time datetime="2025-04-08T15:00:00&#43;00:00" class="op-published">2025-04-08T15:00:00+00:00</time>
                  <time datetime="2025-04-08T15:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Say you are learning to code for the first time, in Python, for example, which is a great starting point for getting into development. You are likely to come across some information like <strong>“a variable stores a value.”</strong> That sounds straightforward, but if you are a beginner just starting, then it can also be a bit confusing. <em>How</em> does a variable store or hold something? <em>What</em> happens when we assign a new value to it?</p>

<p>To figure things out, you could read a bunch and watch tutorials, but sometimes, resources like these don’t help the concept fully click. That’s where animation helps. It has the power to take complex programming concepts and turn them into something visual, dynamic, and easy to grasp.</p>

<p>Let’s break it down with an example: Say we have a box labeled X, first empty, then fill with a value 5, for this example, then update to 12, then 8, then 20, then 3.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073634054"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>This animation shows how a variable stores values and updates over time, step by step. <a href='https://github.com/pontonkid/Manim-Manim/blob/main/Variable'>Source Code</a></figcaption>
	
</figure>

<p>Even if you are unfamiliar with Python, an animation like this makes the concept more obvious, helping you understand how variables work with visual cues. You can now visualize the variables as containers that hold and update values dynamically. It’s way easier to <em>see</em> that than it is to just read about variables.</p>

<p>Well, <strong>Manim isn’t just limited to programming</strong>; it works for math, physics, UI/UX, and more. In trigonometry, you can take something like a “Sine Wave” as an example, which is a smooth, continuous curve that moves up and down in a repeating pattern, and it is found everywhere from sound waves to electrical signals to the motion of a pendulum.</p>

<p>Sounds simple, right? Or maybe a bit confusing, especially if you’re not a math person, but let me help with this:</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073634919"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>A smooth sine wave moving across the screen, illustrating oscillation and periodic motion. <a href='https://github.com/pontonkid/Manim-Manim/blob/main/Sine-Wave'>Source Code</a></figcaption>
	
</figure>

<p>Now, with this, you can see how the wave moves. Instead of just numbers and formulas, you’re watching it happen. And that’s pretty much the idea here! In this article, we’ll explore Manim and <em>how</em> it makes concepts easier to understand through animation.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="manim-manim-what-is-it">Manim, Manim! What Is It?</h2>

<p>By now, you may have a rough idea of what Manim can do, but let’s break it down a little more. What exactly is Manim? Well, it’s two things.</p>

<blockquote>First, Manim is an open-source Python library for creating high-quality mathematical animations.</blockquote>

<p>If you’ve ever watched a <a href="https://www.3blue1brown.com/"><strong>3Blue1Brown</strong></a> video, you’ve seen Manim in action because <strong>Grant Sanderson</strong> originally developed it for his YouTube channel.</p>

<blockquote>Second, Manim is a script-driven animation engine, meaning you write Python code to generate animations instead of dragging and dropping elements like in typical video editing software.</blockquote>

<p>This gives you <strong>precise control over every detail</strong>, including text, color, shape, transformations, timing &mdash; you name it. Whether you’re explaining math, physics, or programming concepts, Manim makes it fairly easy to create clear and dynamic visuals with just a few lines of code. Plus, it works seamlessly with <a href="https://www.latex-project.org/about/">LaTeX</a>, so you can render mathematical equations beautifully without extra effort. That’s why it’s popular among educators, researchers, and content creators.</p>

<p>Of course, Manim isn’t the only tool you can use. If it doesn’t quite fit your needs or the programming language you are most comfortable with, here are some alternatives worth checking out:</p>

<ul>
<li><a href="https://processing.org/"><strong>Processing</strong></a><br />
This is a Java-based coding framework, great for generative art and interactive visuals. If you enjoy experimenting with visual design through code, in Java, to be exact, then Processing gives you a solid foundation.</li>
<li><a href="https://p5js.org/"><strong>p5.js</strong></a><br />
This is a JavaScript library, an alternative for web animations. If you’re a front-end developer working with HTML and CSS, p5.js makes it easy for you to create graphics directly in the browser.</li>
<li><strong>Desmos</strong><br />
This focuses on math visualization. Desmos lets you create interactive graphs and scripted animations directly in the browser. You can use it through <a href="https://www.desmos.com/calculator/cq6zs8fxhp">Desmos Graphs</a>, <a href="https://www.desmos.com/scientific">Desmos Calculator,</a> or the <a href="https://www.desmos.com/api/v1.10/docs/index.html#document-quickstart">Desmos API</a>.</li>
<li><a href="https://docs.blender.org/api/current/info_quickstart.html"><strong>Blender (with Python Scripting)</strong></a><br />
This is mostly known for 3D animation, but with its Python API, you can script animations, including math and physics-based simulations.</li>
</ul>

<p>Now, let’s compare them:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Tool</th>
            <th>Language</th>
      <th>Best For</th>
      <th>Strengths</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Manim</strong></td>
            <td>Python</td>
      <td>Math, physics, programming animations</td>
      <td>High precision, script-driven, LaTeX support</td>
        </tr>
        <tr>
            <td><strong>Processing</strong></td>
            <td>Java</td>
      <td>Generative art, interactive visuals</td>
      <td>Great for creative coding</td>
        </tr>
        <tr>
            <td><strong>p5.js</strong></td>
            <td>JavaScript</td>
      <td>Web-based animations</td>
      <td>Works well with HTML & CSS</td>
        </tr>
    <tr>
            <td><strong>Blender (Python API)</strong></td>
            <td>Python</td>
      <td>3D & math-based animations</td>
      <td>Powerful 3D capabilities, physics simulations</td>
        </tr>
    <tr>
            <td><strong>Desmos</strong></td>
            <td>JavaScript</td>
      <td>Math visualizations</td>
      <td>Browser-based, great for interactive graphs</td>
        </tr>
    </tbody>
</table>

<h2 id="how-to-get-started">How To Get Started</h2>

<p>There are multiple ways to install the library. You can set it up locally, use Conda or Docker, or run it inside Jupyter Notebooks. But if you don’t want to deal with installations, Replit is a great alternative, as it’s a real-time live editor that lets you start coding animations instantly.</p>

<h3 id="1-create-an-account-on-replit-using-github-or-email">1. Create An Account On Replit Using GitHub or Email.</h3>

<p>Once you’re in, your dashboard should look something like this:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="381"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png"
			
			sizes="100vw"
			alt="Replit dashboard"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/using-manim-making-ui-animations/1-create-account-replit.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="2-click-create-app">2. Click “Create App”</h3>

<p>You’ll see three options:</p>

<ol>
<li>“Create With Replit Agent”,</li>
<li>“Choose a Template”,</li>
<li>“Import from GitHub”.</li>
</ol>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="378"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png"
			
			sizes="100vw"
			alt="A screenshot showing three options how to create an new App on Replit"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/using-manim-making-ui-animations/2-replit-create-app.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="3-select-choose-a-template">3. Select “Choose a Template”</h3>

<p>Then, search for Manim and create your app. At this point, you don’t have to do anything else because this sets up everything for you (including the <code>main.py</code> file, a media folder, and all of the required dependencies).</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="380"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png"
			
			sizes="100vw"
			alt="A screenshot showing how to choose a template Manim on Replit"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/using-manim-making-ui-animations/3-replit-choose-template.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><em>Voilà!</em> Now you can start coding your animations right away!</p>

<div class="partners__lead-place"></div>

<h2 id="using-manim-for-math-code-and-ui-ux-visuals">Using Manim For Math, Code, And UI/UX Visuals</h2>

<p>Okay, you know Manim. Whether it’s for math, programming, physics, or even prototyping UI concepts, it’s all about making complex concepts easier to grasp through animation. But how does that work in practice? Let’s go through some ways Manim makes things clearer and more engaging.</p>

<h3 id="1-math-geometry-visuals">1. Math &amp; Geometry Visuals</h3>

<p>Sometimes, math can feel a bit like a puzzle with missing pieces. But with Manim, numbers, shapes, and graphs move, making patterns and relationships easier to grasp. Take graphs, for example. When you tweak a parameter, Manim instantly updates the visualization so you can watch how a function changes over time. And that’s a game-changer for understanding concepts like <strong>derivatives</strong> or <strong>transformations</strong>.</p>

<figure><a href="https://files.smashing.media/articles/using-manim-making-ui-animations/4-manim-graphs.gif"><img src="https://files.smashing.media/articles/using-manim-making-ui-animations/4-manim-graphs-800px.gif" width="800" height="450" alt="Manim graphs" /></a><figcaption>(<a href="https://files.smashing.media/articles/using-manim-making-ui-animations/4-manim-graphs.gif">Large preview</a>)</figcaption></figure>

<p>Geometry concepts also come easier and become even more fun when you can see those shapes move, giving you a clear understanding of rotation or reflection. If you’re drawing a triangle with a compass and straightedge, for example, Manim can animate each step, making it easier to follow along and understand the idea.</p>

<figure><a href="https://files.smashing.media/articles/using-manim-making-ui-animations/5-manim-triangle.gif"><img src="https://files.smashing.media/articles/using-manim-making-ui-animations/5-manim-triangle.gif" width="640" height="360" alt="Manim for drawing triangles" /></a></figure>

<h3 id="2-coding-algorithms">2. Coding &amp; Algorithms</h3>

<p>As you may already know, coding is a process that runs step by step, and Manim makes that easy to see. Whether you are working on the front end or the back end, logic flows in a way that’s not always clear from just reading or writing code. With Manim, you can, for example, watch how a sorting algorithm moves numbers around or simply how a loop runs.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073635454"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption><a href='https://github.com/pontonkid/Manim-Manim/blob/main/Sorting-Algo'>Source Code</a></figcaption>
	
</figure>

<p>The same goes for data structures like linked lists, trees, and more. A binary tree makes more sense when you can see it grow and balance itself. Even complex algorithms like <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm">Dijkstra’s shortest path</a> become clearer when you watch the path being calculated in real time, even if you may not have a background in math.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073635907"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Watch as the tree is explored node by node, showing how data is structured and accessed. <a href='https://github.com/pontonkid/Manim-Manim/blob/main/Binary_Tree.py'>Source Code</a></figcaption>
	
</figure>

<h3 id="3-ui-ux-concepts-motion-design">3. UI/UX Concepts &amp; Motion Design</h3>

<p>Although Manim is not a UI/UX design tool, it can be useful for <strong>demonstrating designs</strong>. Static images can’t always show the full picture, but with Manim, before-and-after comparisons become more dynamic, and of course, it makes it easier to highlight why a new navigation menu, for example, is more intuitive or how a checkout flow reduces friction.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073636288"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption><a href='https://github.com/pontonkid/Manim-Manim/blob/main/UI_Comparison.py'>Source Code</a></figcaption>
	
</figure>

<p>Animated heatmaps can show click patterns over time, helping to spot trends more easily. Conversion funnels become clearer when each stage is animated, revealing exactly where users drop off.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073636879"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption><a href='https://github.com/pontonkid/Manim-Manim/blob/main/user_heatmap.py'>Source Code</a></figcaption>
	
</figure>

<h2 id="let-s-manim">Let’s Manim!</h2>

<p>Well, that’s a lot we covered! By now, you should have Manim installed in whatever way works best for you. But before we jump into the coding part, let’s quickly go over Manim’s core building blocks. Manim’s animations are made of three main concepts:</p>

<ul>
<li>Mobjects,</li>
<li>Animations,</li>
<li>Scenes.</li>
</ul>

<h3 id="1-mobjects-mathematical-objects">1. Mobjects (Mathematical Objects)</h3>

<p>Everything you display in Manim is a Mobject (short for “mathematical object”). There are different types:</p>

<ul>
<li>Basic shapes like  <strong><code>Circle()</code></strong>, <strong><code>Rectangle()</code></strong>, and <strong><code>Arrow()</code></strong>,</li>
<li>Text elements for adding labels, and</li>
<li>Advanced structures like graphs, axes, and bar charts.</li>
</ul>

<p>A mobject is more like a blueprint, and it won’t show up unless you add it to a scene. Here’s a brief example:</p>

<pre><code class="language-python">from manim import *

class MobjectExample(Scene):
  def construct(self):
    circle = Circle()  &#35; Create a circle
    circle.set&#95;fill(BLUE, opacity=0.5)  &#35; Set color and transparency
    self.add(circle)  &#35; Add to the scene
    self.wait(2)
</code></pre>

<p>A blue circle will appear for about two seconds when you run this:</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073637351"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="2-animations">2. Animations</h3>

<p>Animations in Manim, on the other hand, are all about changing these objects over time. Rather than just displaying a sharp edge, we can make it move, rotate, fade, or transform into something else. Really, we do have this much control through the <strong><code>Animation</code></strong> <code>class</code>.</p>

<p>If we use the same circle example from earlier, we can add animations to see how it works and compare the visual differences:</p>

<pre><code class="language-python">from manim import &#42;

class AnimationExample(Scene):
  def construct(self):
    circle = Circle()
    circle.set&#95;fill(BLUE, opacity=0.5) 

    self.play(FadeIn(circle))
    self.play(circle.animate.shift(RIGHT &#42; 2))
    self.play(circle.animate.scale(1.5)) 
    self.play(Rotate(circle, angle=PI/4))  
    self.wait(2)
</code></pre>

<p>Here, we are making a move, scaling up, and rotating. The <code>play()</code> method is what makes animations run. For example, <code>FadeIn(circle)</code> makes the circle gradually appear, and <code>circle.animate.shift(RIGHT * 2)</code> moves it two units to the right. If you want to slow things down, you can add <code>run_time</code> to control the duration, like the following:</p>

<pre><code class="language-python">self.play(circle.animate.scale(2), run&#95;time=3),
</code></pre>

<p>This makes the scaling take three more seconds instead of the default amount of time:</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073637655"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="3-scenes">3. Scenes</h3>

<p>Scenes are what hold everything together. A scene defines what appears, how it animates, and in what order. Every Manim script has a class that is inherited from a <code>Scene</code>, and it contains a <code>construct()</code> method. This is where we write our animation logic. For example,</p>

<pre><code class="language-python">class SimpleScene(Scene):
  def construct(self):
    text = Text("Hello, Manim!")
    self.play(Write(text))
    self.wait(2)
</code></pre>

<p>This creates a simple text animation where the words appear as if being written.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073637982"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<div class="partners__lead-place"></div>

<h2 id="bringing-manim-to-design">Bringing Manim To Design</h2>

<p>As we discussed earlier, Manim is a great tool for UI/UX designers and front-end developers to <strong>visualize user interactions</strong> or to <strong>explain UI concepts</strong>. Think about how users navigate through a website or an app: they click buttons, move between pages, and interact with elements. With Manim, we can animate these interactions and see them play out step by step.</p>

<p>With this in mind, let’s create a simple flow where a user clicks a button, leading to a new page:</p>

<div class="break-out">
<pre><code class="language-python">from manim import &#42;

class UIInteraction(Scene):
  def construct(self):
    &#35; Create a homepage screen
    homepage = Rectangle(width=6, height=3, color=BLUE)
    homepage&#95;label = Text("Home Page").scale(0.8)
    homepage&#95;group = VGroup(homepage, homepage&#95;label)

    &#35; Create a button
    button = RoundedRectangle(width=1.5, height=0.6, color=RED).shift(DOWN &#42; 1)
    button&#95;label = Text("Click Me").scale(0.5).move&#95;to(button)
    button&#95;group = VGroup(button, button&#95;label)

    &#35; Add homepage and button
    self.add(homepage&#95;group, button&#95;group)

    &#35; Simulating a button click
    self.play(button.animate.set&#95;fill(RED, opacity=0.5))  &#35; Button press effect
    self.wait(0.5)  &#35; Pause to simulate user interaction

    &#35; Create a new page (simulating navigation)
    new_page = Rectangle(width=6, height=3, color=GREEN)
    new&#95;page&#95;label = Text("New Page").scale(0.8)
    new&#95;page&#95;group = VGroup(new&#95;page, new&#95;page&#95;label)

    &#35; Animate transition to new page
    self.play(FadeOut(homepage&#95;group, shift=UP),  &#35; Move old page up
      FadeOut(button&#95;group, shift=UP),  &#35; Move button up
      FadeIn(new&#95;page&#95;group, shift=DOWN))  &#35; Bring new page from top
    self.wait(2)
</code></pre>
</div>

<p>The code creates a simple UI animation for a homepage displaying a button. When the button is clicked, it fades slightly to simulate pressing, and then the homepage and button fade out while a new page fades in, creating a transition effect.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073638330"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>If you think of it, scrolling is one of the most natural interactions in modern web and app design. Whether moving between sections on a landing page or smoothly revealing content, well-designed scroll animations make the experience feel fluid. Let me show you:</p>

<div class="break-out">
<pre><code class="language-python">from manim import &#42;

class ScrollEffect(Scene):
  def construct(self):
    &#35; Create three sections to simulate a webpage
    section1 = Rectangle(width=6, height=3, color=BLUE).shift(UP&#42;3)
    section2 = Rectangle(width=6, height=3, color=GREEN)
    section3 = Rectangle(width=6, height=3, color=RED).shift(DOWN&#42;3)

    &#35; Add text to each section
    text1 = Text("Welcome", font&#95;size=32).move&#95;to(section1)
    text2 = Text("About Us", font&#95;size=32).move&#95;to(section2)
    text3 = Text("Contact", font&#95;size=32).move&#95;to(section3)

    self.add(section1, section2, section3, text1, text2, text3)
    self.wait(1)

    &#35; Simulate scrolling down
    self.play(
      section1.animate.shift(DOWN&#42;6),
      section2.animate.shift(DOWN&#42;6),
      section3.animate.shift(DOWN&#42;6),
      text1.animate.shift(DOWN&#42;6),
      text2.animate.shift(DOWN&#42;6),
      text3.animate.shift(DOWN&#42;6),
      run&#95;time=3
    )
    self.wait(1)
</code></pre>
</div>

<p>This animation shows a scrolling effect by moving sections of a webpage upward, simulating how content shifts as a user scrolls. It is a simple way to visualize transitions that make the UI feel smooth and engaging.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1073638729"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Manim makes it easier to show how users interact with a design. You can animate navigations, interactions, and user behaviors to understand better how design works in action. Is there more to explore? Definitely! You can take these simple examples and build on them by adding more complex features.</p>

<p>But what I hope you take away from all of this is that <strong>subtle animations can help communicate and clarify concepts</strong> and that Manim is a library for making those sorts of animations. Traditionally, it’s used to help explain mathematical and scientific concepts, but you can see just how useful it can be to working in front-end development, particularly when it comes to highlighting and visualizing UI changes.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Brecht De Ruyte</author><title>Transitioning Top-Layer Entries And The Display Property In CSS</title><link>https://www.smashingmagazine.com/2025/01/transitioning-top-layer-entries-display-property-css/</link><pubDate>Wed, 29 Jan 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/01/transitioning-top-layer-entries-display-property-css/</guid><description>We are getting spoiled with so many new features involving animations with CSS, from &lt;a href="https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/">scroll-driven animations&lt;/a> to &lt;a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">view transitions&lt;/a>, and plenty of things in between. But it’s not always the big features that make our everyday lives easier; sometimes, it’s those ease-of-life features that truly enhance our projects. In this article, Brecht De Ruyte puts two features on display: &lt;code>@starting-style&lt;/code> and &lt;code>transition-behavior&lt;/code> &amp;mdash; two properties that are absolutely welcome additions to your everyday work with CSS animations.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/01/transitioning-top-layer-entries-display-property-css/" />
              <title>Transitioning Top-Layer Entries And The Display Property In CSS</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Transitioning Top-Layer Entries And The Display Property In CSS</h1>
                  
                    
                    <address>Brecht De Ruyte</address>
                  
                  <time datetime="2025-01-29T10:00:00&#43;00:00" class="op-published">2025-01-29T10:00:00+00:00</time>
                  <time datetime="2025-01-29T10:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Animating from and to <code>display: none;</code> was something we could only achieve with JavaScript to change classes or create other hacks. The reason why we couldn’t do this in CSS is explained in the new <a href="https://www.w3.org/TR/css-transitions-2/#defining-before-change-style">CSS Transitions Level 2 specification</a>:</p>

<blockquote>“In Level 1 of this specification, transitions can only start during a style change event for elements that have a defined before-change style established by the previous style change event. That means a transition could not be started on an element that was not being rendered for the previous style change event.”</blockquote>

<p>In simple terms, this means that we couldn’t start a transition on an element that is hidden or that has just been created.</p>

<h3 id="what-does-transition-behavior-allow-discrete-do">What Does <code>transition-behavior: allow-discrete</code> Do?</h3>

<p><code>allow-discrete</code> is a bit of a strange name for a CSS property value, right? We are going on about transitioning <code>display: none</code>, so why isn’t this named <code>transition-behavior: allow-display</code> instead? The reason is that this does a bit more than handling the CSS <code>display</code> property, as there are other “discrete” properties in CSS. A simple rule of thumb is that discrete properties do not transition but usually flip right away between two states. Other examples of discrete properties are <code>visibility</code> and <code>mix-blend-mode</code>. I’ll include an example of these at the end of this article.</p>

<p>To summarise, setting the <code>transition-behavior</code> property to <code>allow-discrete</code> allows us to tell the browser it can swap the values of a discrete property (e.g., <code>display</code>, <code>visibility</code>, and <code>mix-blend-mode</code>) at the 50% mark instead of the 0% mark of a transition.</p>

<h3 id="what-does-starting-style-do">What Does <code>@starting-style</code> Do?</h3>

<p>The <code>@starting-style</code> rule defines the styles of an element right before it is rendered to the page. This is highly needed in combination with <code>transition-behavior</code> and this is why:</p>

<p>When an item is added to the DOM or is initially set to <code>display: none</code>, it needs some sort of “starting style” from which it needs to transition. To take the example further, <a href="https://css-tricks.com/clarifying-the-relationship-between-popovers-and-dialogs/">popovers and dialog elements</a> are added to a top layer which is a layer that is outside of your document flow, you can kind of look at it as a sibling of the <code>&lt;html&gt;</code> element in your page’s structure. Now, when opening this dialog or popover, they get created inside that top layer, so they don’t have any styles to start transitioning from, which is why we set <code>@starting-style</code>. Don’t worry if all of this sounds a bit confusing. The demos might make it more clearly. The important thing to know is that we can give the browser something to start the animation with since it otherwise has nothing to animate from.</p>

<h3 id="a-note-on-browser-support">A Note On Browser Support</h3>

<p>At the moment of writing, the <code>transition-behavior</code> is available in Chrome, Edge, Safari, and Firefox. It’s the same for <code>@starting-style</code>, but Firefox currently does not support animating from <code>display: none</code>. But remember that everything in this article can be perfectly used as a progressive enhancement.</p>

<p>Now that we have the theory of all this behind us, let’s get practical. I’ll be covering three use cases in this article:</p>

<ul>
<li>Animating from and to <code>display: none</code> in the DOM.</li>
<li>Animating dialogs and popovers entering and exiting the top layer.</li>
<li>More “discrete properties” we can handle.

<br /></li>
</ul>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="animating-from-and-to-display-none-in-the-dom">Animating From And To <code>display: none</code> In The DOM</h2>

<p>For the first example, let’s take a look at <code>@starting-style</code> alone. I created this demo purely to explain the magic. Imagine you want two buttons on a page to add or remove list items inside of an unordered list.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050954730"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>This could be your starting HTML:</p>

<pre><code class="language-html">&lt;button type="button" class="btn-add"&gt;
  Add item
&lt;/button&gt;
&lt;button type="button" class="btn-remove"&gt;
  Remove item
&lt;/button&gt;
&lt;ul role="list"&gt;&lt;/ul&gt;
</code></pre>

<p>Next, we add actions that add or remove those list items. This can be any method of your choosing, but for demo purposes, I quickly wrote a bit of JavaScript for it:</p>

<pre><code class="language-javascript">document.addEventListener("DOMContentLoaded", () =&gt; {
  const addButton = document.querySelector(".btn-add");
  const removeButton = document.querySelector(".btn-remove");
  const list = document.querySelector('ul[role="list"]');

  addButton.addEventListener("click", () =&gt; {
    const newItem = document.createElement("li");
    list.appendChild(newItem);
  });

  removeButton.addEventListener("click", () =&gt; {
    if (list.lastElementChild) {
      list.lastElementChild.classList.add("removing");
      setTimeout(() =&gt; {
        list.removeChild(list.lastElementChild);
      }, 200);
    }
  });
});
</code></pre>

<p>When clicking the <code>addButton</code>, an empty list item gets created inside of the unordered list. When clicking the <code>removeButton</code>, the last item gets a new <code>.removing</code> class and finally gets taken out of the DOM after 200ms.</p>

<p>With this in place, we can write some CSS for our items to animate the removing part:</p>

<pre><code class="language-css">ul {
    li {
      transition: opacity 0.2s, transform 0.2s;

      &.removing {
        opacity: 0;
        transform: translate(0, 50%);
      }
    }
  }
</code></pre>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050955824"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>This is great! Our <code>.removing</code> animation is already looking perfect, but what we were looking for here was a way to animate the entry of items coming inside of our DOM. For this, we will need to define those starting styles, as well as the final state of our list items.</p>

<p>First, let’s update the CSS to have the final state inside of that list item:</p>

<pre><code class="language-css">ul {
    li {
      opacity: 1;
      transform: translate(0, 0);
      transition: opacity 0.2s, transform 0.2s;

      &.removing {
        opacity: 0;
        transform: translate(0, 50%);
      }
    }
  }
</code></pre>

<p>Not much has changed, but now it’s up to us to let the browser know what the starting styles should be. We could set this the same way we did the <code>.removing</code> styles like so:</p>

<pre><code class="language-css">ul {
    li {
      opacity: 1;
      transform: translate(0, 0);
      transition: opacity 0.2s, transform 0.2s;

      @starting-style {
        opacity: 0;
        transform: translate(0, 50%);
      }

      &.removing {
        opacity: 0;
        transform: translate(0, 50%);
      }
    }
  }
</code></pre>

<p>Now we’ve let the browser know that the <code>@starting-style</code> should include zero opacity and be slightly nudged to the bottom using a <code>transform</code>. The final result is something like this:</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050956394"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>But we don’t need to stop there! We could use different animations for entering and exiting. We could, for example, update our starting style to the following:</p>

<pre><code class="language-css">@starting-style {
  opacity: 0;
  transform: translate(0, -50%);
}
</code></pre>

<p>Doing this, the items will enter from the top and exit to the bottom. See the full example in this CodePen:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="XJroPgg"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [@starting-style demo - up-in, down-out [forked]](https://codepen.io/smashingmag/pen/XJroPgg) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/XJroPgg">@starting-style demo - up-in, down-out [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<h3 id="when-to-use-transition-behavior-allow-discrete">When To Use <code>transition-behavior: allow-discrete</code></h3>

<p>In the previous example, we added and removed items from our DOM. In the next demo, we will show and hide items using the CSS <code>display</code> property. The basic setup is pretty much the same, except we will add eight list items to our DOM with the <code>.hidden</code> class attached to it:</p>

<pre><code class="language-html">  &lt;button type="button" class="btn-add"&gt;
    Show item
  &lt;/button&gt;
  &lt;button type="button" class="btn-remove"&gt;
    Hide item
  &lt;/button&gt;

&lt;ul role="list"&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
  &lt;li class="hidden"&gt;&lt;/li&gt;
&lt;/ul&gt;
</code></pre>

<p>Once again, for demo purposes, I added a bit of JavaScript that, this time, removes the <code>.hidden</code> class of the next item when clicking the <code>addButton</code> and adds the <code>hidden</code> class back when clicking the <code>removeButton</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">document.addEventListener("DOMContentLoaded", () =&gt; {
  const addButton = document.querySelector(".btn-add");
  const removeButton = document.querySelector(".btn-remove");
  const listItems = document.querySelectorAll('ul[role="list"] li');

  let activeCount = 0;

  addButton.addEventListener("click", () =&gt; {
    if (activeCount &lt; listItems.length) {
      listItems[activeCount].classList.remove("hidden");
      activeCount++;
    }
  });

  removeButton.addEventListener("click", () =&gt; {
    if (activeCount &gt; 0) {
      activeCount--;
      listItems[activeCount].classList.add("hidden");
    }
  });
});
</code></pre>
</div>

<p>Let’s put together everything we learned so far, add a <code>@starting-style</code> to our items, and do the basic setup in CSS:</p>

<pre><code class="language-css">ul {
    li {
      display: block;
      opacity: 1;
      transform: translate(0, 0);
      transition: opacity 0.2s, transform 0.2s;

      @starting-style {
        opacity: 0;
        transform: translate(0, -50%);
      }

      &.hidden {
        display: none;
        opacity: 0;
        transform: translate(0, 50%);
      }
    }
  }
</code></pre>

<p>This time, we have added the <code>.hidden</code> class, set it to <code>display: none</code>, and added the same <code>opacity</code> and <code>transform</code> declarations as we previously did with the <code>.removing</code> class in the last example. As you might expect, we get a nice fade-in for our items, but removing them is still very abrupt as we set our items directly to <code>display: none</code>.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050961069"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>This is where the <code>transition-behavior</code> property comes into play. To break it down a bit more, let’s remove the <code>transition</code> property shorthand of our previous CSS and open it up a bit:</p>

<pre><code class="language-css">ul {
    li {
      display: block;
      opacity: 1;
      transform: translate(0, 0);
      transition-property: opacity, transform;
      transition-duration: 0.2s;
    }
  }
</code></pre>

<p>All that is left to do is transition the <code>display</code> property and set the <code>transition-behavior</code> property to <code>allow-discrete</code>:</p>

<pre><code class="language-css">ul {
    li {
      display: block;
      opacity: 1;
      transform: translate(0, 0);
      transition-property: opacity, transform, display;
      transition-duration: 0.2s;
      transition-behavior: allow-discrete;
      /&#42; etc. &#42;/
    }
  }
</code></pre>

<p>We are now animating the element from <code>display: none</code>, and the result is exactly as we wanted it:</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050961316"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>We can use the <code>transition</code> shorthand property to make our code a little less verbose:</p>

<div class="break-out">
<pre><code class="language-css">transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete;
</code></pre>
</div>

<p>You can add <code>allow-discrete</code> in there. But if you do, take note that if you declare a shorthand transition after <code>transition-behavior</code>, it will be overruled. So, instead of this:</p>

<pre><code class="language-css">transition-behavior: allow-discrete;
transition: opacity 0.2s, transform 0.2s, display 0.2s;
</code></pre>

<p>…we want to declare <code>transition-behavior</code> <em>after</em> the <code>transition</code> shorthand:</p>

<pre><code class="language-css">transition: opacity 0.2s, transform 0.2s, display 0.2s;
transition-behavior: allow-discrete;
</code></pre>

<p>Otherwise, the <code>transition</code> shorthand property overrides <code>transition-behavior</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="GgKPXda"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [@starting-style and transition-behavior: allow-discrete [forked]](https://codepen.io/smashingmag/pen/GgKPXda) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgKPXda">@starting-style and transition-behavior: allow-discrete [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<div class="partners__lead-place"></div>

<h2 id="animating-dialogs-and-popovers-entering-and-exiting-the-top-layer">Animating Dialogs And Popovers Entering And Exiting The Top Layer</h2>

<p>Let’s add a few use cases with dialogs and popovers. Dialogs and popovers are good examples because they get added to the top layer when opening them.</p>

<h3 id="what-is-that-top-layer">What Is That Top Layer?</h3>

<p>We’ve already likened the “top layer” to a sibling of the <code>&lt;html&gt;</code> element, but you might also think of it as a special layer that sits above everything else on a web page. It&rsquo;s like a transparent sheet that you can place over a drawing. Anything you draw on that sheet will be visible on top of the original drawing.</p>

<p>The original drawing, in this example, is the DOM. This means that the top layer is out of the document flow, which provides us with a few benefits. For example, as I stated before, dialogs and popovers are added to this top layer, and that makes perfect sense because they should always be on top of everything else. No more <code>z-index: 9999</code>!</p>

<p>But it’s more than that:</p>

<ul>
<li><strong><code>z-index</code> is irrelevant</strong>: Elements on the top layer are always on top, regardless of their <code>z-index</code> value.</li>
<li><strong>DOM hierarchy doesn’t matter</strong>: An element’s position in the DOM doesn’t affect its stacking order on the top layer.</li>
<li><strong>Backdrops</strong>: We get access to a new <code>::backdrop</code> pseudo-element that lets us style the area between the top layer and the DOM beneath it.</li>
</ul>

<p>Hopefully, you are starting to understand the importance of the top layer and how we can transition elements in and out of it as we would with popovers and dialogues.</p>

<h3 id="transitioning-the-dialog-element-in-the-top-layer">Transitioning The Dialog Element In The Top Layer</h3>

<p>The following HTML contains a button that opens a <code>&lt;dialog&gt;</code> element, and that <code>&lt;dialog&gt;</code> element contains another button that closes the <code>&lt;dialog&gt;</code>. So, we have one button that opens the <code>&lt;dialog&gt;</code> and one that closes it.</p>

<div class="break-out">
<pre><code class="language-html">&lt;button class="open-dialog" data-target="my-modal"&gt;Show dialog&lt;/button&gt;

&lt;dialog id="my-modal"&gt;
  &lt;p&gt;Hi, there!&lt;/p&gt;
  &lt;button class="outline close-dialog" data-target="my-modal"&gt;
    close
  &lt;/button&gt;
&lt;/dialog&gt;
</code></pre>
</div>

<p>A lot is happening in HTML with <a href="https://utilitybend.com/blog/an-update-on-invokers-invoker-commands-in-html">invoker commands</a> that will make the following step a bit easier, but for now, let’s add a bit of JavaScript to make this modal actually work:</p>

<pre><code class="language-javascript">// Get all open dialog buttons.
const openButtons = document.querySelectorAll(".open-dialog");
// Get all close dialog buttons.
const closeButtons = document.querySelectorAll(".close-dialog");

// Add click event listeners to open buttons.
openButtons.forEach((button) =&lt; {
  button.addEventListener("click", () =&lt; {
    const targetId = button.getAttribute("data-target");
    const dialog = document.getElementById(targetId);
    if (dialog) {
      dialog.showModal();
    }
  });
});

// Add click event listeners to close buttons.
closeButtons.forEach((button) =&lt; {
  button.addEventListener("click", () =&lt; {
    const targetId = button.getAttribute("data-target");
    const dialog = document.getElementById(targetId);
    if (dialog) {
      dialog.close();
    }
  });
});
</code></pre>

<p>I’m using the following styles as a starting point. Notice how I’m styling the <code>::backdrop</code> as an added bonus!</p>

<pre><code class="language-css">dialog {
  padding: 30px;
  width: 100%;
  max-width: 600px;
  background: #fff;
  border-radius: 8px;
  border: 0;
  box-shadow: 
    rgba(0, 0, 0, 0.3) 0px 19px 38px,
    rgba(0, 0, 0, 0.22) 0px 15px 12px;
    
  &::backdrop {
    background-image: linear-gradient(
      45deg in oklab,
      oklch(80% 0.4 222) 0%,
      oklch(35% 0.5 313) 100%
    );
  }
}
</code></pre>

<p>This results in a pretty hard transition for the entry, meaning it’s not very smooth:</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050964394"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>Let’s add transitions to this dialog element and the backdrop. I’m going a bit faster this time because by now, you likely see the pattern and know what’s happening:</p>

<pre><code class="language-css">dialog {
  opacity: 0;
  translate: 0 30%;
  transition-property: opacity, translate, display;
  transition-duration: 0.8s;

  transition-behavior: allow-discrete;
  
  &[open] {
    opacity: 1;
    translate: 0 0;

    @starting-style {
      opacity: 0;
      translate: 0 -30%;
    }
  }
}
</code></pre>

<p>When a dialog is open, the browser slaps an <code>open</code> attribute on it:</p>

<pre><code class="language-html">&lt;dialog open&gt; ... &lt;/dialog&gt;
</code></pre>

<p>And that’s something else we can target with CSS, like <code>dialog[open]</code>. So, in this case, we need to set a <code>@starting-style</code> for when the dialog is in an <code>open</code> state.</p>

<p>Let’s add a transition for our backdrop while we’re at it:</p>

<pre><code class="language-css">dialog {
  /&#42; etc. &#42;/
  &::backdrop {
    opacity: 0;
    transition-property: opacity;
    transition-duration: 1s;
  }

  &[open] {
    /&#42; etc. &#42;/
    &::backdrop {
      opacity: 0.8;

      @starting-style {
        opacity: 0;
      }
    }
  }
}
</code></pre>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/1050964896"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>Now you’re probably thinking: <em>A-ha! But you should have added the <code>display</code> property and the <code>transition-behavior: allow-discrete</code> on the backdrop!</em></p>

<p>But no, that is not the case. Even if I would change my backdrop pseudo-element to the following CSS, the result would stay the same:</p>

<pre><code class="language-css"> &::backdrop {
    opacity: 0;
    transition-property: opacity, display;
    transition-duration: 1s;
    transition-behavior: allow-discrete;
  }
</code></pre>

<p>It turns out that we are working with a <code>::backdrop</code> and when working with a <code>::backdrop</code>, we’re implicitly also working with the CSS <code>overlay</code> property, which specifies whether an element appearing in the top layer is currently rendered in the top layer.</p>

<p>And <code>overlay</code> just so happens to be another discrete property that we need to include in the <code>transition-property</code> declaration:</p>

<pre><code class="language-css">dialog {
  /&#42; etc. &#42;/

&::backdrop {
  transition-property: opacity, display, overlay;
  /&#42; etc. &#42;/
}
</code></pre>

<p>Unfortunately, this is currently only supported in Chromium browsers, but it can be perfectly used as a progressive enhancement.</p>

<p>And, yes, we need to add it to the <code>dialog</code> styles as well:</p>

<pre><code class="language-css">dialog {
  transition-property: opacity, translate, display, overlay;
  /&#42; etc. &#42;/

&::backdrop {
  transition-property: opacity, display, overlay;
  /&#42; etc. &#42;/
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="pvzqOGe"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Dialog: starting-style, transition-behavior, overlay [forked]](https://codepen.io/smashingmag/pen/pvzqOGe) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvzqOGe">Dialog: starting-style, transition-behavior, overlay [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<p>It’s pretty much the same thing for a popover instead of a dialog. I’m using the same technique, only working with popovers this time:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="emObLxe"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Popover transition with @starting-style [forked]](https://codepen.io/smashingmag/pen/emObLxe) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/emObLxe">Popover transition with @starting-style [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<div class="partners__lead-place"></div>

<h2 id="other-discrete-properties">Other Discrete Properties</h2>

<p>There are a few other discrete properties besides the ones we covered here. If you remember the second demo, where we transitioned some items from and to <code>display: none</code>, the same can be achieved with the <code>visibility</code> property instead. This can be handy for those cases where you want items to preserve space for the element’s box, even though it is invisible.</p>

<p>So, here’s the same example, only using <code>visibility</code> instead of <code>display</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="LEPMJqX"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Transitioning the visibility property [forked]](https://codepen.io/smashingmag/pen/LEPMJqX) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/LEPMJqX">Transitioning the visibility property [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<p>The CSS <strong><code>mix-blend-mode</code></strong> property is another one that is considered discrete. To be completely honest, I can’t find a good use case for a demo. But I went ahead and created a somewhat trite example where two <code>mix-blend-mode</code>s switch right in the middle of the transition instead of right away.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="bNbOxZp"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Transitioning mix-blend-mode [forked]](https://codepen.io/smashingmag/pen/bNbOxZp) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/bNbOxZp">Transitioning mix-blend-mode [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>That’s an overview of how we can transition elements in and out of the top layer! In an ideal world, we could get away without needing a completely new property like <code>transition-behavior</code> just to transition otherwise “un-transitionable” properties, but here we are, and I’m glad we have it.</p>

<p>But we also got to learn about <code>@starting-style</code> and how it provides browsers with a set of styles that we can apply to the start of a transition for an element that’s in the top layer. Otherwise, the element has nothing to transition from at first render, and we’d have no way to transition them smoothly in and out of the top layer.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Mariana Beldi</author><title>An Introduction To CSS Scroll-Driven Animations: Scroll And View Progress Timelines</title><link>https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/</link><pubDate>Wed, 11 Dec 2024 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/</guid><description>It’s been 10 years since scroll-driven animations were introduced in a spec proposal, and after five years in development, we’re finally beginning to see pop up in websites. There are &lt;a href="https://codepen.io/andrewrock/pen/NWoRavN">scrolly-telling&lt;/a> and &lt;a href="https://codepen.io/amit_sheen/pen/ZENNgMw">maze games&lt;/a> as well as &lt;a href="https://codepen.io/bramus/pen/GRdGoKy">cover flow animations&lt;/a> and &lt;a href="https://codepen.io/leemeyer/pen/XWvrMBr">3D rotation with scroll&lt;/a>… but what exactly is new here? It’s not like we haven’t seen scroll animations before, &lt;strong>but what we have now requires no JavaScript, no dependencies, no libraries &amp;mdash; just pure CSS&lt;/strong>. And if that’s not exciting enough, these animations run off the &lt;a href="https://www.smashingmagazine.com/2023/10/speedcurve-fight-main-thread/">main thread&lt;/a>, delivering smooth, high-performance, GPU-accelerated experiences.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/12/introduction-css-scroll-driven-animations/" />
              <title>An Introduction To CSS Scroll-Driven Animations: Scroll And View Progress Timelines</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>An Introduction To CSS Scroll-Driven Animations: Scroll And View Progress Timelines</h1>
                  
                    
                    <address>Mariana Beldi</address>
                  
                  <time datetime="2024-12-11T15:00:00&#43;00:00" class="op-published">2024-12-11T15:00:00+00:00</time>
                  <time datetime="2024-12-11T15:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>You can safely use scroll-driven animations in Chrome as of December 2024. Firefox supports them, too, though you’ll need to enable a flag. Safari? Not yet, but don’t worry &mdash; you can still offer a seamless experience across all browsers with a <a href="https://github.com/flackr/scroll-timeline">polyfill</a>. Just keep in mind that adding a polyfill involves a JavaScript library, so you won’t get the same performance boost.</p>

<p>There are plenty of valuable resources to dive into scroll-driven animations, which I’ll be linking throughout the article. My starting point was <a href="https://www.youtube.com/playlist?list=PLNYkxOF6rcICM3ttukz9x5LCNOHfWBVnn">Bramus’ video tutorial</a>, which pairs nicely with <a href="https://css-tricks.com/unleash-the-power-of-scroll-driven-animations/">Geoff’s in-depth notes</a> <a href="https://css-tricks.com/unleash-the-power-of-scroll-driven-animations/">Graham</a> that build on the tutorial.</p>

<p>In this article, we’ll walk through the <a href="https://www.w3.org/TR/scroll-animations-1/">latest published version by the W3C</a> and explore the two types of scroll-driven timelines &mdash; <strong>scroll progress timelines</strong> and <strong>view progress timelines</strong>. By the end, I hope that you are familiar with both timelines, not only being able to tell them apart but also feeling confident enough to use them in your work.</p>

<p><strong>Note</strong>: <em>All demos in this article only work in Chrome 116 or later at the time of writing.</em></p>

<h2 id="scroll-progress-timelines">Scroll Progress Timelines</h2>

<p>The scroll progress timeline links an animation’s timeline to the scroll position of a scroll container along a specific axis. So, the animation is tied directly to scrolling. As you scroll forward, so does the animation. You’ll see me refer to them as <code>scroll-timeline</code> animations in addition to calling them scroll progress timelines.</p>

<p>Just as we have two types of scroll-driven animations, we have two types of <code>scroll-timeline</code> animations: <strong>anonymous timelines</strong> and <strong>named timelines</strong>.</p>

<h3 id="anonymous-scroll-timeline">Anonymous <code>scroll-timeline</code></h3>

<p>Let’s start with a classic example: creating a scroll progress bar at the top of a blog post to track your reading progress.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="RNbRqoj"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scroll Progress Timeline example - before animation-timeline scroll() [forked]](https://codepen.io/smashingmag/pen/RNbRqoj) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/RNbRqoj">Scroll Progress Timeline example - before animation-timeline scroll() [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>In this example, there’s a <code>&lt;div&gt;</code> with the ID “progress.” At the end of the CSS file, you’ll see it has a background color, a defined width and height, and it’s fixed at the top of the page. There’s also an animation that scales it from <code>0</code> to <code>1</code> along the x-axis &mdash; pretty standard if you’re familiar with CSS animations!</p>

<p>Here’s the relevant part of the styles:</p>

<pre><code class="language-css">&#35;progress {
  /&#42; ... &#42;/
  animation: progressBar 1s linear;
}


@keyframes progressBar {
  from { transform: scaleX(0); }
}
</code></pre>

<p>The <code>progressBar</code> animation runs once and lasts one second with a linear timing function. Linking this animation scrolling is just a single line in CSS:</p>

<pre><code class="language-css">animation-timeline: scroll();
</code></pre>

<p>No need to specify seconds for the duration &mdash; the scrolling behavior itself will dictate the timing. And that’s it! You’ve just created your first scroll-driven animation! Notice how the animation’s direction is directly tied to the scrolling direction &mdash; scroll down, and the progress indicator grows wider; scroll up, and it becomes narrower.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ByBzGpO"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scroll Progress Timeline example - animation-timeline scroll() [forked]](https://codepen.io/smashingmag/pen/ByBzGpO) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ByBzGpO">Scroll Progress Timeline example - animation-timeline scroll() [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<h3 id="scroll-timeline-property-parameters"><code>scroll-timeline</code> Property Parameters</h3>

<p>In a <code>scroll-timeline</code> animation, the <code>scroll()</code> function is used inside the <code>animation-timeline</code> property. It only takes two parameters: <code>&lt;scroller&gt;</code> and <code>&lt;axis&gt;</code>.</p>

<ul>
<li><strong><code>&lt;scroller&gt;</code></strong> refers to the scroll container, which can be set as <code>nearest</code> (the default), <code>root</code>, or <code>self</code>.</li>
<li><strong><code>&lt;axis&gt;</code></strong> refers to the scroll axis, which can be <code>block</code> (the default), <code>inline</code>, <code>x</code>, or <code>y</code>.</li>
</ul>

<p>In the reading progress example above, we didn’t declare any of these because we used the defaults. But we could achieve the same result with:</p>

<pre><code class="language-css">animation-timeline: scroll(nearest block);
</code></pre>

<p>Here, the <code>nearest</code> scroll container is the root scroll of the HTML element. So, we could also write it this way instead:</p>

<pre><code class="language-css">animation-timeline: scroll(root block);
</code></pre>

<p>The <code>block</code> axis confirms that the scroll moves top to bottom in a left-to-right writing mode. If the page has a wide horizontal scroll, and we want to animate along that axis, we could use the <code>inline</code> or <code>x</code>  values (depending on whether we want the scrolling direction to always be left-to-right or adapt based on the writing mode).</p>

<p>We’ll dive into <code>self</code> and <code>inline</code> in more examples later, but the best way to learn is to play around with all the combinations, and <a href="https://scroll-driven-animations.style/tools/scroll-timeline/params/">this tool by Bramus</a> lets you do exactly that. Spend a few minutes before we jump into the next property associated with scroll timelines.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h3 id="the-animation-range-property">The <code>animation-range</code> Property</h3>

<p>The <code>animation-range</code> for <code>scroll-timeline</code> defines which part of the scrollable content controls the start and end of an animation’s progress based on the scroll position. It allows you to decide when the animation starts or ends while scrolling through the container.</p>

<p>By default, the <code>animation-range</code> is set to <code>normal</code>, which is shorthand for the following:</p>

<pre><code class="language-css">animation-range-start: normal;
animation-range-end: normal;
</code></pre>

<p>This translates to <code>0%</code> (<code>start</code>) and <code>100%</code> (<code>end</code>) in a <code>scroll-timeline</code> animation:</p>

<pre><code class="language-css">animation-range: normal normal;
</code></pre>

<p>…which is the same as:</p>

<pre><code class="language-css">animation-range: 0% 100%;
</code></pre>

<p>You can declare any <a href="https://css-tricks.com/css-length-units/">CSS length units</a> or even <a href="https://www.smashingmagazine.com/2015/12/getting-started-css-calc-techniques/">calculations</a>. For example, let’s say I have a footer that’s <code>500px</code> tall. It’s filled with banners, ads, and related posts. I don’t want the scroll progress bar to include any of that as part of the reading progress. What I want is for the animation to start at the top and end <code>500px</code> before the bottom. Here we go:</p>

<pre><code class="language-css">animation-range: 0% calc(100% - 500px);
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="azoZQym"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scroll Progress Timeline example - animation-timeline, animation-range [forked]](https://codepen.io/smashingmag/pen/azoZQym) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/azoZQym">Scroll Progress Timeline example - animation-timeline, animation-range [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>Just like that, we’ve covered the key properties of <code>scroll-timeline</code> animations. Ready to take it a step further?</p>

<h3 id="named-scroll-timeline">Named <code>scroll-timeline</code></h3>

<p>Let’s say I want to use the scroll position of a different scroll container for the same animation. The <code>scroll-timeline-name</code> property allows you to specify which scroll container the scroll animation should be linked to. You give it a name (a dashed-ident, e.g., <code>--my-scroll-timeline</code>) that maps to the scroll container you want to use. This container will then control the animation’s progress as the user scrolls through it.</p>

<p>Next, we need to define the scroll axis for this new container by using the <code>scroll-timeline-axis</code>, which tells the animation which axis will trigger the motion. Here’s how it looks in the code:</p>

<pre><code class="language-css">.my-class { 
  /&#42; This is my new scroll-container &#42;/
  scroll-timeline-name: --my-custom-name;
  scroll-timeline-axis: inline;
}
</code></pre>

<p>If you omit the axis, then the default <code>block</code> value will be used. However, you can also use the shorthand <code>scroll-timeline</code> property to combine both the name and axis in a single declaration:</p>

<pre><code class="language-css">.my-class { 
  /&#42; Shorthand for scroll-container with axis &#42;/
  scroll-timeline: --my-custom-name inline;
}
</code></pre>

<p>I think it’s easier to understand all this with a practical example. Here’s the same progress indicator we’ve been working with, but with inline scrolling (i.e., along the x-axis):</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="pvzbQrM"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Named Scroll Progress Timeline [forked]](https://codepen.io/smashingmag/pen/pvzbQrM) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvzbQrM">Named Scroll Progress Timeline [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>We have two animations running:</p>

<ol>
<li>A progress bar grows wider when scrolling in an inline direction.</li>
<li>The container’s background color changes the further you scroll.</li>
</ol>

<p>The HTML structure looks like the following:</p>

<div class="break-out">
<pre><code class="language-html">&lt;div class="gallery"&gt;
  &lt;div class="gallery-scroll-container"&gt;
    &lt;div class="gallery-progress" role="progressbar" aria-label="progress"&gt;&lt;/div&gt;
    &lt;img src="image1.svg" alt="Alt text" draggable="false" width="500"&gt;
    &lt;img src="image2.svg" alt="Alt text" draggable="false" width="500"&gt;
    &lt;img src="image3.svg" alt="Alt text" draggable="false" width="500"&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
</div>

<p>In this case, the <code>gallery-scroll-container</code> has horizontal scrolling and changes its background color as you scroll. Normally, we could just use <code>animation-timeline: scroll(self inline)</code> to achieve this. However, we also want the <code>gallery-progress</code> element to use the same scroll for its animation.</p>

<p>The <code>gallery-progress</code> element is the first inside <code>gallery-scroll-container</code>, and we will lose it when scrolling unless it’s absolutely positioned. But when we do this, the element no longer occupies space in the normal document flow, and that affects how the element behaves with its parent and siblings. We need to specify which scroll container we want it to listen to.</p>

<p>That’s where naming the scroll container comes in handy. By giving <code>gallery-scroll-container</code> a <code>scroll-timeline-name</code> and <code>scroll-timeline-axis</code>, we can ensure both animations sync to the same scroll:</p>

<pre><code class="language-css">.gallery-scroll-container {
  /&#42; ... &#42;/
  animation: bg steps(1);
  scroll-timeline: --scroller inline;
}
</code></pre>

<p>And is using that scrolling to define its own <code>animation-timeline</code>:</p>

<pre><code class="language-css">.gallery-scroll-container {
  /&#42; ... &#42;/
  animation: bg steps(1);
  scroll-timeline: --scroller inline;
  animation-timeline: --scroller;
}
</code></pre>

<p>Now we can scale this name to the progress bar that is using a different animation but listening to the same scroll:</p>

<pre><code class="language-css">.gallery-progress {
  /&#42; ... &#42;/
  animation: progressBar linear;
  animation-timeline: --scroller;
}
</code></pre>

<p>This allows both animations (the growing progress bar and changing background color) to follow the same scroll behavior, even though they are separate elements and animations.</p>

<div class="partners__lead-place"></div>

<h3 id="the-timeline-scope-property">The <code>timeline-scope</code> Property</h3>

<p>What happens if we want to animate something based on the scroll position of a sibling or even a higher ancestor? This is where the <code>timeline-scope</code> property comes into play. It allows us to extend the scope of a <code>scroll-timeline</code> beyond the current element’s subtree. The value of <code>timeline-scope</code> must be a custom identifier, which again is a dashed-ident.</p>

<p>Let’s illustrate this with a new example. This time, scrolling in one container runs an animation inside another container:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="jENrQGo"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scroll Driven Animations - timeline-scope [forked]](https://codepen.io/smashingmag/pen/jENrQGo) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/jENrQGo">Scroll Driven Animations - timeline-scope [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>We can play the animation on the image when scrolling the text container because they are siblings in the HTML structure:</p>

<pre><code class="language-html">&lt;div class="main-container"&gt;
  &lt;div class="sardinas-container"&gt;
    &lt;img ...&gt;
  &lt;/div&gt;

  &lt;div class="scroll-container"&gt;
    &lt;p&gt;Long text...&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>

<p>Here, only the <code>.scroll-container</code> has scrollable content, so let’s start by naming this:</p>

<pre><code class="language-css">.scroll-container {
  /&#42; ... &#42;/
  overflow-y: scroll;
  scroll-timeline: --containerText;
}
</code></pre>

<p>Notice that I haven’t specified the scroll axis, as it defaults to <code>block</code> (vertical scrolling), and that’s the value I want.</p>

<p>Let’s move on to the image inside the <code>sardinas-container</code>. We want this image to animate as we scroll through the <code>scroll-container</code>. I’ve added a <code>scroll-timeline-name</code> to its <code>animation-timeline</code> property:</p>

<pre><code class="language-css">.sardinas-container img {
  /&#42; ... &#42;/
  animation: moveUp steps(6) both;
  animation-timeline: --containerText;
}
</code></pre>

<p>At this point, however, the animation still won’t work because the <code>scroll-container</code> is not directly related to the images. To make this work, we need to extend the <code>scroll-timeline-name</code> so it becomes reachable. This is done by adding the <code>timeline-scope</code> to the parent element (or a higher ancestor) shared by both elements:</p>

<pre><code class="language-css">.main-container {
  /&#42; ... &#42;/
  timeline-scope: --containerText;
}
</code></pre>

<p>With this setup, the scroll of the <code>scroll-container</code> will now control the animation of the image inside the <code>sardinas-container</code>!</p>

<p>Now that we’ve covered how to use <code>timeline-scope</code>, we’re ready to move on to the next type of scroll-driven animations, where the same properties will apply but with slight differences in how they behave.</p>

<h2 id="view-progress-timelines">View Progress Timelines</h2>

<p>We just looked at <strong>scroll progress animations</strong>. That’s the first type of scroll-driven animation of the two. Next, we’re turning our attention to <strong>view progress animations</strong>. There’s a lot of similarities between the two! But they’re different enough to warrant their own section for us to explore how they work. You’ll see me refer to these as <code>view-timeline</code> animations in addition to calling them view progress animations, as they revolve around a <code>view()</code> function.</p>

<p>The <strong>view progress timeline</strong> is the second type of type of scroll-driven animation that we’re looking at. It tracks an element as it enters or exits the scrollport (the visible area of the scrollable content). This behavior is quite similar to <a href="https://css-tricks.com/an-explanation-of-how-the-intersection-observer-watches/?ref=csslayout.news">how an <code>IntersectionObserver</code> works in JavaScript</a> but can be done entirely in CSS.</p>

<p>We have anonymous and named view progress timelines, just as we have anonymous and named scroll progress animations. Let’s unpack those.</p>

<h3 id="anonymous-view-timeline">Anonymous View Timeline</h3>

<p>Here’s a simple example to help us see the basic idea of anonymous view timelines. Notice how the image fades into view when you scroll down to a certain point on the page:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="KwPMrQO"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [View Timeline Animation - view() [forked]](https://codepen.io/smashingmag/pen/KwPMrQO) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/KwPMrQO">View Timeline Animation - view() [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>Let’s say we want to animate an image that fades in as it appears in the scrollport. The image’s opacity will go from <code>0</code> to <code>1</code>. This is how you might write that same animation in classic CSS using <code>@keyframes</code>:</p>

<pre><code class="language-css">img {
  /&#42; ... &#42;/
  animation: fadeIn 1s;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
</code></pre>

<p>That’s great, but we want the image to <code>fadeIn</code> when it’s in view. Otherwise, the animation is sort of like a tree that falls in a forest with no one there to witness it… did the animation ever happen? We’ll never know!</p>

<p>We have a <code>view()</code> function that makes this a view progress animation with a single line of CSS:</p>

<pre><code class="language-css">img {
  /&#42; ... &#42;/
  animation: fadeIn;
  animation-timeline: view();
}
</code></pre>

<p>And notice how we no longer need to declare an <code>animation-duration</code> like we did in classic CSS. The animation is no longer tied by time but by space. The animation is triggered as the image becomes visible in the scrollport.</p>

<h3 id="view-timeline-parameters">View Timeline Parameters</h3>

<p>Just like the <code>scroll-timeline</code> property, the <strong><code>view-timeline</code></strong> property accepts parameters that allow for more customization:</p>

<pre><code class="language-css">animation-timeline: view(<inset> <axis>);
</code></pre>

<ul>
<li><strong><code>&lt;inset&gt;</code></strong><br />
Controls when the animation starts and ends relative to the element’s visibility within the scrollport. It defines the margin between the edges of the scrollport and the element being tracked. The default value is <code>auto</code>, but it can also take length percentages as well as start and end values.</li>
<li><strong><code>&lt;axis&gt;</code></strong><br />
This is similar to the scroll-timeline’s axis parameter. It defines which axis (horizontal or vertical) the animation is tied to. The default is <code>block</code>, which means it tracks the vertical movement. You can also use <code>inline</code> to track horizontal movement or simple <code>x</code> or <code>y</code>.</li>
</ul>

<p>Here’s an example that uses both <code>inset</code> and <code>axis</code> to customize when and how the animation starts:</p>

<pre><code class="language-css">img {
  animation-timeline: view(20% block);
}
</code></pre>

<p>In this case:</p>

<ol>
<li>The animation starts when the image is 20% visible in the scrollport.</li>
<li>The animation is triggered by vertical scrolling (<code>block</code> axis).</li>
</ol>

<h3 id="parallax-effect">Parallax Effect</h3>

<p>With the <code>view()</code> function, it’s also easy to create parallax effects by simply adjusting the animation properties. For example, you can have an element move or scale as it enters the scrollport without any JavaScript:</p>

<pre><code class="language-css">img {
  animation: parallaxMove 1s;
  animation-timeline: view();
}

@keyframes parallaxMove {
  to { transform: translateY(-50px); }
}
</code></pre>

<p>This makes it incredibly simple to create dynamic and engaging scroll animations with just a few lines of CSS.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="mybEQLK"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Parallax effect with CSS Scroll driven animations - view() [forked]](https://codepen.io/smashingmag/pen/mybEQLK) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/mybEQLK">Parallax effect with CSS Scroll driven animations - view() [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<h3 id="the-animation-range-property-1">The <code>animation-range</code> Property</h3>

<p>Using the CSS <strong><code>animation-range</code></strong> property with view timelines defines how much of an element’s visibility within the scrollport controls the start and end points of the animation’s progress. This can be used to fine-tune when the animation begins and ends based on the element’s visibility in the viewport.</p>

<p>While the default value is <code>normal</code>, in view timelines, it translates to tracking the full visibility of the element from the moment it starts entering the scrollport until it fully leaves. This is represented by the following:</p>

<pre><code class="language-css">animation-range: normal normal;
/&#42; Equivalent to &#42;/
animation-range: cover 0% cover 100%;
</code></pre>

<p>Or, more simply:</p>

<pre><code class="language-css">animation-range: cover;
</code></pre>

<p>There are six possible values or <code>timeline-range-names</code>:</p>

<ol>
<li><strong><code>cover</code></strong><br />
Tracks the full visibility of the element, from when it starts entering the scrollport to when it completely leaves it.</li>
<li><strong><code>contain</code></strong><br />
Tracks when the element is fully visible inside the scrollport, from the moment it’s fully contained until it no longer is.</li>
<li><strong><code>entry</code></strong><br />
Tracks the element from the point it starts entering the scrollport until it’s fully inside.</li>
<li><strong><code>exit</code></strong><br />
Tracks the element from the point it starts, leaving the scrollport until it’s fully outside.</li>
<li><strong><code>entry-crossing</code></strong><br />
Tracks the element as it crosses the starting edge of the scrollport, from start to full crossing.</li>
<li><strong><code>exit-crossing</code></strong><br />
Tracks the element as it crosses the end edge of the scrollport, from start to full crossing.</li>
</ol>

<p>You can mix different <code>timeline-range-names</code> to control the start and end points of the animation range. For example, you could make the animation start when the element enters the scrollport and end when it exits:</p>

<pre><code class="language-css">animation-range: entry exit;
</code></pre>

<p>You can also combine these values with percentages to define more custom behavior, such as starting the animation halfway through the element’s entry and ending it halfway through its exit:</p>

<pre><code class="language-css">animation-range: entry 50% exit 50%;
</code></pre>

<p>Exploring all these values and combinations is best done interactively. Tools like Bramus’ <a href="https://scroll-driven-animations.style/tools/view-timeline/ranges/">view-timeline range visualizer</a> make it easier to understand.</p>

<div class="partners__lead-place"></div>

<h3 id="target-range-inside-keyframes">Target Range Inside <code>@keyframes</code></h3>

<p>One of the powerful features of <code>timeline-range-names</code> is their ability to be used inside <code>@keyframes</code>:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="zxOBMaK"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [target range inside @keyframes - view-timeline, timeline-range-name [forked]](https://codepen.io/smashingmag/pen/zxOBMaK) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxOBMaK">target range inside @keyframes - view-timeline, timeline-range-name [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>Two different animations are happening in that demo:</p>

<ol>
<li><strong><code>slideIn</code></strong><br />
When the element enters the scrollport, it scales up and becomes visible.</li>
<li><strong><code>slideOut</code></strong><br />
When the element leaves, it scales down and fades out.</li>
</ol>

<pre><code class="language-css">@keyframes slideIn {
  from {
    transform: scale(.8) translateY(100px); 
    opacity: 0;
  }
  to { 
    transform: scale(1) translateY(0); 
    opacity: 1;
  }
}

@keyframes slideOut {
  from {
    transform: scale(1) translateY(0); 
    opacity: 1;    
  }
  to { 
    transform: scale(.8) translateY(-100px); 
    opacity: 0 
  }
}
</code></pre>

<p>The new thing is that now we can merge these two animations using the <code>entry</code> and <code>exit</code> <code>timeline-range-names</code>, simplifying it into one animation that handles both cases:</p>

<pre><code class="language-css">@keyframes slideInOut {
  /&#42; Animation for when the element enters the scrollport &#42;/
  entry 0% {
    transform: scale(.8) translateY(100px); 
    opacity: 0;
  }
  entry 100% { 
    transform: scale(1) translateY(0); 
    opacity: 1;
  }
  /&#42; Animation for when the element exits the scrollport &#42;/
  exit 0% {
    transform: scale(1) translateY(0); 
    opacity: 1;    
  }
  exit 100% { 
    transform: scale(.8) translateY(-100px); 
    opacity: 0;
  }
}
</code></pre>

<ul>
<li><strong><code>entry 0%</code></strong><br />
Defines the state of the element at the beginning of its entry into the scrollport (scaled down and transparent).</li>
<li><strong><code>entry 100%</code></strong><br />
Defines the state when the element has fully entered the scrollport (fully visible and scaled up).</li>
<li><strong><code>exit 0%</code></strong><br />
Starts tracking the element as it begins to leave the scrollport (visible and scaled up).</li>
<li><strong><code>exit 100%</code></strong><br />
Defines the state when the element has fully left the scrollport (scaled down and transparent).</li>
</ul>

<p>This approach allows us to animate the element’s behavior smoothly as it both enters and leaves the scrollport, all within a single <code>@keyframes</code> block.</p>

<h3 id="named-view-timeline-and-timeline-scope">Named <code>view-timeline</code> And <code>timeline-scope</code></h3>

<p>The concept of using <code>view-timeline</code> with named timelines and linking them across different elements can truly expand the possibilities for scroll-driven animations. In this case, we are linking the scroll-driven animation of images with the animations of unrelated paragraphs in the DOM structure by using <strong>a named <code>view-timeline</code> and <code>timeline-scope</code></strong>.</p>

<p>The <code>view-timeline</code> property works similarly to the <code>scroll-timeline</code> property. It’s the shorthand for declaring the <code>view-timeline-name</code> and <code>view-timeline-axis</code> properties in one line. However, the difference from <code>scroll-timeline</code> is that we can link the animation of an element when the linked elements enter the scrollport. I took the previous demo and added an animation to the paragraphs so you can see how the opacity of the text is animated when scrolling the images on the left:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="KwPMrBP"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [View-timeline, timeline-scope [forked]](https://codepen.io/smashingmag/pen/KwPMrBP) by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/KwPMrBP">View-timeline, timeline-scope [forked]</a> by <a href="https://codepen.io/marianab">Mariana Beldi</a>.</figcaption>
</figure>

<p>This one looks a bit verbose, but I found it hard to come up with a better example to show the power of it. Each image in the vertical scroll container is assigned a named <code>view-timeline</code> with a unique identifier:</p>

<div class="break-out">
<pre><code class="language-css">.vertical-scroll-container img:nth-of-type(1) { view-timeline: --one; }
.vertical-scroll-container img:nth-of-type(2) { view-timeline: --two; }
.vertical-scroll-container img:nth-of-type(3) { view-timeline: --three; }
.vertical-scroll-container img:nth-of-type(4) { view-timeline: --four; }
</code></pre>
</div>

<p>This makes the scroll timeline of each image have its own custom name, such as <code>--one</code> for the first image, <code>--two</code> for the second, and so on.</p>

<p>Next, we connect the animations of the paragraphs to the named timelines of the images. The corresponding paragraph should animate when the images enter the scrollport:</p>

<div class="break-out">
<pre><code class="language-css">.vertical-text p:nth-of-type(1) { animation-timeline: --one; }
.vertical-text p:nth-of-type(2) { animation-timeline: --two; }
.vertical-text p:nth-of-type(3) { animation-timeline: --three; }
.vertical-text p:nth-of-type(4) { animation-timeline: --four; }
</code></pre>
</div>

<p>However, since the images and paragraphs are not directly related in the DOM, we need to declare a <code>timeline-scope</code> on their common ancestor. This ensures that the named timelines (<code>--one</code>, <code>--two</code>, and so on) can be referenced and shared between the elements:</p>

<pre><code class="language-css">.porto {
  /&#42; ... &#42;/
  timeline-scope: --one, --two, --three, --four;
}
</code></pre>

<p>By declaring the <code>timeline-scope</code> with all the named timelines (<code>--one</code>, <code>—two</code>, <code>--three</code>, <code>--four</code>), both the images and the paragraphs can participate in the same scroll-timeline logic, despite being in separate parts of the DOM tree.</p>

<h2 id="final-notes">Final Notes</h2>

<p>We’ve covered the vast majority of what’s currently defined in the <a href="https://drafts.csswg.org/scroll-animations-1/">CSS Scroll-Driven Animations Module Leve 1 specification</a> today in December 2024. But I want to highlight a few key takeaways that helped me better understand these new rules that you may not get directly from the spec:</p>

<ul>
<li><strong>Scroll container essentials</strong><br />
It may seem obvious, but a scroll container is necessary for scroll-driven animations to work. Issues often arise when elements like text or containers are resized or when animations are tested on larger screens, causing the scrollable area to disappear.</li>
<li><strong>Impact of <code>position: absolute</code></strong><br />
Using absolute positioning can sometimes interfere with the intended behavior of scroll-driven animations. The relationship between elements and their parent elements gets tricky when <code>position: absolute</code> is applied.</li>
<li><strong>Tracking an element’s initial state</strong><br />
The browser evaluates the element’s state <em>before</em> any transformations (like <code>translate</code>) are applied. This affects when animations, particularly view timelines, begin. Your animation might trigger earlier or later than expected due to the initial state.</li>
<li><strong>Avoid hiding overflow</strong><br />
Using <code>overflow: hidden</code> can disrupt the scroll-seeking mechanism in scroll-driven animations. The recommended solution is to switch to <code>overflow: clip</code>. Bramus has <a href="https://www.bram.us/2024/02/14/scroll-driven-animations-you-want-overflow-clip-not-overflow-hidden/">a great article about this</a> and <a href="https://www.youtube.com/watch?v=72pUm4tQesw">a video from Kevin Powell</a> also suggests that we may no longer need <code>overflow: hidden</code>.</li>
<li><strong>Performance</strong><br />
For the best results, stick to animating GPU-friendly properties like transforms, opacity, and some filters. These skip the heavy lifting of recalculating layout and repainting. On the other hand, animating things like <code>width</code>, <code>height</code>, or <code>box-shadow</code> can slow things down since they require re-rendering. Bramus <a href="https://www.bram.us/2024/05/30/scroll-driven-animations-with-css-webexpo/">mentioned</a> that soon, more properties &mdash; like <code>background-color</code>, <code>clip-path</code>, <code>width</code>, and <code>height</code> &mdash; will be animatable on the compositor, making the performance even better.</li>
<li><strong>Use <code>will-change</code> wisely</strong><br />
Leverage this property to promote elements to the GPU, but use it sparingly. Overusing <code>will-change</code> can lead to excessive memory usage since the browser reserves resources even if the animations don’t frequently change.</li>
<li><strong>The order matters</strong><br />
If you are using the <code>animation</code> shorthand, always place the <code>animation-timeline</code> after it.</li>
<li><strong>Progressive enhancement and accessibility</strong><br />
Combine media queries for reduced motion preferences with the <code>@supports</code> rule to ensure animations only apply when the user has no motion restrictions, and the browser supports them.</li>
</ul>

<p>For example:</p>

<div class="break-out">
<pre><code class="language-css">@media screen and (prefers-reduce-motion: no-preference) {
  @supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { 
    .my-class {
      animation: moveCard linear both;    
      animation-timeline: view(); 
    }
  } 
}
</code></pre>
</div>

<p>My main struggle while trying to build the demos was more about CSS itself than the scroll animations. Sometimes, building the layout and generating the scroll was more difficult than applying the scroll animation. Also, some things that confused me at the beginning as the spec keeps evolving, and some of these are not there anymore (remember, it has been under development for more than five years now!):</p>

<ul>
<li><strong>x and y axes</strong><br />
These used to be called the “horizontal” and “vertical” axes, and while Firefox may still support the old terminology, it has been updated.</li>
<li><strong>Old <code>@scroll-timeline</code> syntax</strong><br />
In the past, <code>@scroll-timeline</code> was used to declare scroll timelines, but this has changed in the most recent version of the spec.</li>
<li><strong>Scroll-driven vs. scroll-linked animations</strong><br />
Scroll-<em>driven</em> animations were originally called scroll-<em>linked</em> animations. If you come across this older term in articles, double-check whether the content has been updated to reflect the latest spec, particularly with features like <code>timeline-scope</code>.</li>
</ul>

<h3 id="resources">Resources</h3>

<ul>
<li>All demos from this article can be found <a href="https://codepen.io/collection/WvVpQR">in this collection</a>, and I might include <a href="https://codepen.io/marianab/pen/bGXdEoB">more</a> as I experiment further.</li>
<li>A collection of <a href="https://codepen.io/collection/aMgBZp">demos from CodePen</a> that I find interesting (send me yours, and I’ll include it!)</li>
<li>This <a href="https://github.com/w3c/csswg-drafts/labels/scroll-animations-1">GitHub repo</a> is where you can report issues or join discussions about scroll-driven animations.</li>
<li><a href="https://scroll-driven-animations.style/">Demos, tools, videos</a>, and (even) more information from Bramus</li>
<li>Google Chrome <a href="https://www.youtube.com/playlist?list=PLNYkxOF6rcICM3ttukz9x5LCNOHfWBVnn">video tutorial</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Mihai Cora</author><title>Creating Custom Lottie Animations With SVGator</title><link>https://www.smashingmagazine.com/2024/09/creating-custom-lottie-animations-svgator/</link><pubDate>Tue, 17 Sep 2024 11:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/09/creating-custom-lottie-animations-svgator/</guid><description>Creating ready-to-implement Lottie animations with a single tool is now possible thanks to SVGator’s latest feature updates. In this article, you will learn how to create and animate a Lottie using SVGator, an online animation tool that has zero learning curve if you’re familiar with at least one design tool. Have a closer look at the tool’s main functionalities and the straightforward creation process.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/09/creating-custom-lottie-animations-svgator/" />
              <title>Creating Custom Lottie Animations With SVGator</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Creating Custom Lottie Animations With SVGator</h1>
                  
                    
                    <address>Mihai Cora</address>
                  
                  <time datetime="2024-09-17T11:00:00&#43;00:00" class="op-published">2024-09-17T11:00:00+00:00</time>
                  <time datetime="2024-09-17T11:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                <p>This article is sponsored by <b>SVGator</b></p>
                

<p>SVGator has gone through a series of updates since our last article, which was published in 2021, when it was already considered to be the most advanced web-based tool for vector animation. The first step toward more versatile software came with the mobile export feature that made it possible to implement the animations in iOS and Android applications.</p>

<p>The animation tool continued its upgrade with a series of new export options: video formats including MP4, AVI, MKV, MOV, and WebM, as well as image formats such as GIF, Animated PNG, WebP, and image sequence. By covering a larger area of users’ needs, the app now enables anyone to create animated stickers, social media, and newsletter animations, video assets, and many more types of visual content on demand.</p>

<p>The goal of becoming a “one tool for all” still lacked the last piece of the puzzle, namely full support for Lottie files. Lottie, just like SVG, is a vector-based format, but it has even <strong>better comprehensive multi-platform support</strong>, a fact that makes it super popular among developers and design professionals. It is built for use across various platforms, enabling <strong>smooth integration into both web and mobile applications</strong>. Its file size is minimal, it is <strong>infinitely scalable</strong>, and developers find it straightforward to implement once they get familiar with the format. Lottie can incorporate raster graphics and also supports interactivity.</p>

<p>SVGator’s latest version has everything you need for your various applications without the need for any third-party apps or plug-ins.</p>

<p><strong>Note</strong>: <em>You can test all of SVGator’s functionalities free of charge before committing to the Pro plan. However, you can export up to three watermarked files, with videos and GIFs limited to basic quality.</em></p>

<p>In this article, we will follow a creation process made of these steps:</p>

<ul>
<li>Importing an existent Lottie JSON and making some minor adjustments;</li>
<li>Importing new animated assets created with SVGator (using the library);</li>
<li>Creating and animating new elements from scratch;</li>
<li>Exporting the Lottie animation.</li>
</ul>

<h2 id="getting-started-with-svgator">Getting Started With SVGator</h2>

<p>The sign-up process is simple, fast, and straightforward, and no credit card is required. Sign up either with Google or Facebook or, alternatively, by providing your name, email address, and password. Start a project either with a Lottie animation or a static SVG. If you don’t have an existing file, you can design and animate everything starting from a blank canvas.</p>

<p>Now that you’ve created your account, let’s dive right into the fun part. Here’s a preview of how your animation is going to look by the time you’re done following this guide. Neat, right?</p>

<figure><a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/1-final-animation.gif"><img src="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/1-final-animation.gif" width="800" height="450" alt="Final animation" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/1-final-animation.gif">Large preview</a>)</figcaption></figure>

<h2 id="create-a-new-project">Create A New Project</h2>

<p>After logging in and clicking on the <strong>New Project</strong> option, you will be taken to the <strong>New Project Panel</strong>, where you can choose between starting from a blank project or uploading a file. Let’s start this project with an existing Lottie JSON.</p>

<ul>
<li><a href="https://cdn.svgator.com/assets/pub/fast-response.json">Download the Lottie demo</a></li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png"
			
			sizes="100vw"
			alt="A screenshot with the selected Upload button"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/2-upload-file-lottie.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
  <li>Click on the <strong>Upload file</strong> button and navigate to the directory where you have saved your Lottie file.<br />
<br />













<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png"
			
			sizes="100vw"
			alt="A screenshot with the selected Fast response.json file and Open button"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/3-lottie-open-file.png'>Large preview</a>)
    </figcaption>
  
</figure>
  </li>
  <li>Select the “<strong>Fast response.json</strong>” file and click <strong>Open</strong>.<br /> 
<br />
Hit play in the editor, and the animation should look like this:<br />
<br />

<figure><a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/4-animation-svgator.gif"><img src="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/4-animation-svgator.gif" width="800" height="449" alt="An animation opened in SVGator" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/4-animation-svgator.gif"><em>Large preview</em></a>)</figcaption></figure>
  </li>
</ol>

<p><strong>Note</strong>: <em>Make sure to hit</em> <strong><em>Save</em></strong> <em>after each step to make sure you don’t lose any of your progress while working on this project alongside our guide.</em></p>

<h2 id="import-an-animated-asset">Import An Animated Asset</h2>

<p>In this step, you will learn how to use the <strong>Library</strong> to import new assets to your project. You can easily choose from a variety of ready-made SVGs stored in different categories, load new files from your computer (Lottie, static SVG, and images), or save animations from other SVGator projects and reuse them.</p>

<p>In this case, let’s use an animated message bubble previously created and saved to the <strong>Uploads</strong> section of the <strong>Library</strong>.</p>

<p>Learn how to <strong>create and save animated assets</strong> with this <a href="https://www.youtube.com/watch?v=R2Px90nfOEI">short video tutorial</a>.</p>

<ul>
<li><a href="https://cdn.svgator.com/assets/pub/message-bubble.json">Download the message bubble Lottie animation</a></li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png"
			
			sizes="100vw"
			alt="A screenshot with the message bubble Lottie animation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/5-message-bubble-lottie-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
  <li>Navigate to the left sidebar of the app and switch to the Library tab, then click the “+” icon to upload the message bubble asset that you downloaded earlier.</li>
  <li>After it is loaded in the uploads section, simply click on it to add it to your project.<br /><br />All the animated properties of the asset are now present in the timeline, and you can edit them if you want.<br /><br />
  <strong>Note</strong>: <em>Make sure the playhead is at the second “0” before adding the animated asset. When adding an animated asset, it will always start animating from the point where the playhead is placed.</em></li>
  <li>Freely adjust its position and size as you wish.</li>
  <li>With the playhead at the second 0, click on the <strong>Animate</strong> button, then choose <strong>Position</strong>.</li>
</ol>

<p>At this point, you should have the first Position keyframe automatically added at the second 0, and you are ready to start animating.</p>

<h2 id="animate-the-message-bubble">Animate The Message Bubble</h2>

<ol>
  <li>Start by dragging the playhead on the timeline at 0.2 seconds:<br />
<br />













<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the message bubble"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/6-animate-message-bubble.png'>Large preview</a>)
    </figcaption>
  
</figure>
  </li>
  <li>Then, drag the message bubble up a few pixels. The second keyframe will appear in the timeline, marking the element’s new position, thus creating the 2 milliseconds animation.<br /><br /><strong>Note</strong>: <em>You can hit Play at any moment to check how everything looks!</em><br /><br />Next, you can use the Scale animator to make the bubble disappear after the dots representing the typing are done animating by scaling it down to 0 for both the X and Y axes:<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the message bubble"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/7-animate-message-bubble.png'>Large preview</a>)
    </figcaption>
  
</figure>
  </li>
  <li>With the message bubble still selected, drag the playhead at 2.2 seconds, click on <strong>Animate</strong>, and select Scale (or just press <kbd>Shift</kbd> + <kbd>S</kbd> on the keyboard) to set the first Scale keyframe, then drag the playhead at 2.5 seconds.</li>
  <li>Set the scale properties to 0 for both the X and Y axes (in the right side panel). The bubble won’t be visible anymore at this point.<br /><br /><strong>Note</strong>: <em>To maintain the ratio while changing the scale values, make sure you have the Maintain proportions on (the link icon next to the scale inputs).</em><br /><br />To add an extra touch of interest to this scaling motion, add an easing function preset:<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the message bubble"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/8-animate-message-bubble.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>First, jump back to the first Scale keyframe (you can also double-click the keyframe to jump the playhead right at it).</li>
  <li>Open the <strong>Easing Panel</strong> next to the time indicator and scroll down through the presets list, then select <strong>Ease in Back</strong>. Due to its bezier going out of the graph, this easing function will create a bounce-back effect for the scale animation.<br /><br /><strong>Note</strong>: <em>You can adjust the bezier of a selected easing preset and create a new custom function, which will appear at the top of the list.</em><br /><br /><em>Keep in mind that you need at least one keyframe selected if you intend to apply an easing. The easing function will apply from the selected keyframe toward the next keyframe at its right. Of course, you can apply a certain easing for multiple keyframes at once.</em><br /><br />To get a smoother transition when the message bubble disappears, add an Opacity animation of one millisecond at the end of the scaling:<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the message bubble"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/9-animate-message-bubble.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Choose <strong>Opacity</strong> from the animators’ list and set the first keyframe at 2.4 seconds, then drag the playhead at 2.5 seconds to match the ending keyframe from the scale animation above.</li>
  <li>From the <strong>Appearance panel</strong>, drag the <strong>Opacity slider</strong> all the way to the left, at 0%.</li>
</ol>

<h2 id="create-an-email-icon">Create An Email Icon</h2>

<p>For the concept behind this animation to be complete, let’s create (and later animate) a “new email” notification as a response to the character sending that message.</p>

<p>Once again, SVGator’s asset library comes in handy for this step:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to create an email icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/10-create-email-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
  <li>Go to the search bar from the <strong>Library</strong> and type in “<strong>mail</strong>,” then click on the mail asset from the results.</li>
  <li>Place it somewhere above the laptop. Edit the mail icon to better fit the style of the animation:<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/11-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Open the <strong>email</strong> group and select the rectangle from the back.</li>
  <li>Change its fill color to a dark purple.</li>
  <li>Round up the corners using the <strong>Radius</strong> slider.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/12-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Make the element’s design minimal by deleting these two lines from the lower part of the envelope.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot steps showing how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/13-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
    <li>Select the envelope seal flap, which is the <strong>Polyline</strong> element in the group, above the rectangle.</li>
    <li>Add a lighter purple for the fill, set the stroke to 2 px width, and also make it white.<br /><br />To make the animation even more interesting, create a notification alert in the top-right corner of the envelope:<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/14-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Use the <strong>Ellipse tool (O)</strong> from the toolbar on top and draw a circle in the top-right corner of the envelope.</li>
  <li>Choose a nice red color for the fill, and set the stroke to white with a 2 px width.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/15-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Click on the “T” icon to select the <strong>Text</strong> tool.</li>
  <li>Click on the circle and type “1”.</li>
  <li>Set the color to white and click on the “B” icon to make it bold.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/16-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
  <li>Select both the red circle and the number, and group them: right-click, and hit <strong>Group</strong>.<br /><br /> 
You can also hit <kbd>Command</kbd> or <kbd>Ctrl</kbd> + <kbd>G</kbd> on your keyboard. Double-click on the newly created group to rename it to “Notification.”<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to edit the mail icon."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/17-edit-mail-icon-animation.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li>Select both the notification group and email group below and create a new group, which you can name “new email.”</li>
</ol>

<h2 id="animate-the-new-email-group">Animate The New Email Group</h2>

<p>Let’s animate the new email popping out of the laptop right after the character has finished texting his message:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the new email group."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/18-animate-new-email-group.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
  <li>With the “New email” group selected, click twice on the <strong>Move down</strong> icon from the header to place the group last.<br />You can also press <kbd>Command</kbd> or <kbd>Ctrl</kbd> + arrow down on your keyboard.</li>
<li>Drag the group behind the laptop (on the canvas) to hide it entirely, and also scale it down a little.</li>
<li>With the playhead at 3 seconds, add the animators <strong>Scale</strong> and <strong>Position</strong>.<br />You can also do that by pressing <kbd>Shift</kbd> + <kbd>S</kbd> and <kbd>Shift</kbd> + <kbd>P</kbd> on your keyboard.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the new email group."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/19-animate-new-email-group.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li>Drag the playhead at the second 3.3 on the timeline.</li>
<li>Move the New Email group above the laptop and scale it up a bit.</li>
<li>You can also bend the motion path line to create a curved trajectory for the position animation.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png"
			
			sizes="100vw"
			alt="A screenshot showing steps how to animate the new email group."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/20-animate-new-email-group.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li>Select the first keyframes at the second 3.</li>
<li>Open the easing panel.</li>
<li>And click on the <strong>Ease Out Cubic</strong> preset to add it to both keyframes.</li>
</ol>

<h2 id="animate-the-notification">Animate The Notification</h2>

<p>Let’s animate the notification dot separately. We’ll make it pop in while the email group shows up.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png"
			
			sizes="100vw"
			alt="A screenshot showing steps of how to animate the notification."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/21-animate-notification.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
<li>Select the <strong>Notification</strong> group.</li>
<li>Create a scale-up animation for it with 0 for both the X and Y axes at 3.2 and 1 at 3.5 seconds.</li>
<li>Select the first keyframe and, from the easing panel, choose <strong>Ease Out Back</strong>. This easing function will ensure the popping effect.</li>
</ol>

<h2 id="add-expressiveness-to-the-character">Add Expressiveness To The Character</h2>

<p>Make the character smile while looking at the email that just popped out. For this, you need to animate the stroke offset of the mouth:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png"
			
			sizes="100vw"
			alt="A screenshot showing the steps of how to add expressiveness to the character."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/22-add-expressiveness-character.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
  <li>Select the mouth path. You can use the <strong>Node tool</strong> to select it directly with one click.</li>
  <li>Drag the playhead at 3.5 seconds, which is the moment from where the smile will start.</li>
  <li>Select the last keyframe of the <strong>Stroke offset</strong> animator from the timeline and duplicate it at second 3.5, or you can also use <kbd>Ctrl</kbd> or <kbd>Cmd</kbd> + <kbd>D</kbd> for duplication.<br /><br />














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png"
			
			sizes="100vw"
			alt="A screenshot showing the steps of how to add expressiveness to the character."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/23-add-expressiveness-character.png'>Large preview</a>)
    </figcaption>
  
</figure>
</li>
<li>Drag the playhead at second 3.9.</li>
<li>Go to the properties panel and set the Offset to 0. The stroke will now fill the path all the way, creating a stroke offset animation of 4 milliseconds.</li>
</ol>

<h2 id="final-edits">Final Edits</h2>

<p>You can still make all kinds of adjustments to your animation before exporting it. In this case, let’s change the color of the initial Lottie animation we used to start this project:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png"
			
			sizes="100vw"
			alt="A screenshot showing how to change the color of the initial Lottie animation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/24-final-edits-color.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
<li>Use the <strong>Node tool</strong> to select all the green paths that form the character’s arms and torso.</li>
<li>Change the color as you desire.</li>
</ol>

<h2 id="export-lottie">Export Lottie</h2>

<p>Once you’re done editing, you can export the animation by clicking on the top right <strong>Export</strong> button and selecting the Lottie format. Alternatively, you can press <kbd>Command</kbd> or <kbd>Ctrl</kbd> + <kbd>E</kbd> on your keyboard to jump directly to the export panel, from where you can still select the animation you want to export.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png"
			
			sizes="100vw"
			alt="A screenshot showing how to export the animation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/creating-custom-lottie-animations-svgator/25-export-lottie.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ol>
<li>Make sure the Lottie format is selected from the dropdown. In the export panel, you can set a name for the file you are about to export, choose the frame rate and animation speed, or set a background color.</li>
<li>You can preview the Lottie animation with a Lottie player.<br />
<strong>Note</strong>: <em>This step is recommended to make sure all animations are supported in the Lottie format by previewing it on a webpage using the Lottie player. The preview in the export panel isn’t an actual Lottie animation.</em></li>
<li>Get back to the export panel and simply click <strong>Export</strong> to download the Lottie JSON.</li>
</ol>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>Now that you’re done with your animation don’t forget that you have plenty of export options available besides Lottie. You can post the same project on social media in video format, export it as an SVG animation for the web, or turn it into a GIF sticker or any other type of visual you can think of. GIF animations can also be used in Figma presentations and prototypes as a high-fidelity preview of the production-ready Lottie file.</p>

<p>We hope you enjoyed this article and that it will inspire you to create amazing Lottie animations in your next project.</p>

<p>Below, you can find a few useful resources to continue your journey with SVG and SVGator:</p>

<ul>
<li><a href="https://www.svgator.com/tutorials?utm_source=smashingmagazine.com&amp;utm_medium=referral&amp;utm_campaign=lottie-article">SVGator tutorials</a><br />
Check out a series of short video tutorials to help you get started with SVGator.</li>
<li><a href="https://www.svgator.com/help?utm_source=smashingmagazine.com&amp;utm_medium=referral&amp;utm_campaign=lottie-article">SVGator Help Center</a><br />
It answers the most common questions about SVGator, its features, and membership plans.</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk, il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Preethi Sam</author><title>The Times You Need A Custom @property Instead Of A CSS Variable</title><link>https://www.smashingmagazine.com/2024/05/times-need-custom-property-instead-css-variable/</link><pubDate>Mon, 13 May 2024 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/05/times-need-custom-property-instead-css-variable/</guid><description>Custom properties and CSS variables are often used interchangeably when describing placeholder values in CSS despite the fact that they are different but related concepts. Preethi Sam walks through an example that demonstrates where custom properties are more suitable than variables while showcasing the greater freedom and flexibility that custom properties provide for designing complex, refined animations.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/05/times-need-custom-property-instead-css-variable/" />
              <title>The Times You Need A Custom @property Instead Of A CSS Variable</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Times You Need A Custom @property Instead Of A CSS Variable</h1>
                  
                    
                    <address>Preethi Sam</address>
                  
                  <time datetime="2024-05-13T08:00:00&#43;00:00" class="op-published">2024-05-13T08:00:00+00:00</time>
                  <time datetime="2024-05-13T08:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>We generally use a CSS variable as a placeholder for some value we plan to reuse &mdash; to avoid repeating the same value and to easily update that value across the board if it needs to be updated.</p>

<pre><code class="language-css">:root { 
  --mix: color-mix(in srgb, #8A9B0F, #fff 25%);
}

div {
  box-shadow: 0 0 15px 25px var(--mix);
}
</code></pre>

<p>We can register <em>custom</em> properties in CSS using <code>@property</code>. The most common example you’ll likely find demonstrates how <code>@property</code> can <a href="https://css-tricks.com/interpolating-numeric-css-variables/">animate the colors of a gradient</a>, something we’re unable to do otherwise since a CSS variable is recognized as a string and what we need is a number format that can interpolate between two numeric values. That’s where <code>@property</code> allows us to define not only the variable’s <em>value</em> but its <em>syntax,</em> <em>initial value</em>, and <em>inheritance</em>, just like you’ll find documented in CSS specifications.</p>

<p>For example, here’s how we register a custom property called <code>--circleSize</code>, which is formatted as a percentage value that is set to <code>10%</code> by default and is not inherited by child elements.</p>

<pre><code class="language-css">@property --circleSize {
  syntax: "&lt;percentage&gt;";
  inherits: false;
  initial-value: 10%;
}

div { /&#42; red div &#42;/
  clip-path: circle(var(--circleSize) at center bottom);
  transition: --circleSize 300ms linear;
}

section:hover div { 
  --circleSize: 125%; 
}
</code></pre>

<p>In this example, a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/circle"><code>circle()</code></a> function is used to clip the <code>&lt;div&gt;</code> element into &mdash; you guessed it &mdash; a circle. The size value of the <code>circle()</code>’s radius is set to the registered custom property, <code>--circleSize</code>, which is then independently changed on hover using a <code>transition</code>. The result is something close to <a href="https://m3.material.io/foundations/interaction/states/applying-states#d8d475ac-672e-4692-ae60-eb557c4990bc">Material Design’s ripple effect</a>, and we can do it because we’ve told CSS to treat the custom property as a percentage value rather than a string:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="PovwepK"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS @property [forked]](https://codepen.io/smashingmag/pen/PovwepK) by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/PovwepK">CSS @property [forked]</a> by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</figcaption>
</figure>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20freedom%20to%20define%20and%20spec%20our%20own%20CSS%20properties%20gives%20us%20new%20animating%20superpowers%20that%20were%20once%20only%20possible%20with%20JavaScript,%20like%20transitioning%20the%20colors%20of%20a%20gradient.%0a&url=https://smashingmagazine.com%2f2024%2f05%2ftimes-need-custom-property-instead-css-variable%2f">
      
The freedom to define and spec our own CSS properties gives us new animating superpowers that were once only possible with JavaScript, like transitioning the colors of a gradient.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Here’s an idea I have that uses the same basic idea as the ripple, only it chains multiple custom properties together that are formatted as colors, lengths, and angle degrees for a more complex animation where text slides up the container as the text changes colors.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="rNgavyb"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Text animation with @property [forked]](https://codepen.io/smashingmag/pen/rNgavyb) by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/rNgavyb">Text animation with @property [forked]</a> by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</figcaption>
</figure>

<p>Let’s use this demo as an exercise to learn more about defining custom properties with the <code>@property</code> at-rule, combining what we just saw in the ripple with the concept of interpolating gradient values.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="the-html">The HTML</h2>

<pre><code class="language-html">&lt;div class="scrolling-text"&gt;
  &lt;div class="text-container"&gt;
    &lt;div class="text"&gt;
      &lt;ruby&gt;&#22777;&lt;rt&gt;one&lt;/rt&gt;&lt;/ruby&gt;
      &lt;ruby&gt;&#34560;&lt;rt&gt;two&lt;/rt&gt;&lt;/ruby&gt;
      &lt;ruby&gt;&#20841;&lt;rt&gt;three&lt;/rt&gt;&lt;/ruby&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>

<p>The HTML contains Chinese characters we’re going to animate. These Chinese characters are marked up with <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby"><code>&lt;ruby&gt;</code></a> tags so that their English translations can be supplied in <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rt"><code>&lt;rt&gt;</code></a> tags. The idea is that <code>.scrolling-text</code> is the component’s parent container and, in it, is a child element holding the sliding text characters that allow the characters to slide in and out of view.</p>

<h2 id="vertical-sliding">Vertical Sliding</h2>

<p>In CSS, let’s make the characters slide vertically on hover. What we’re making is a container with a fixed height we can use to clip the characters out of view when they overflow the available space.</p>

<pre><code class="language-css">.scrolling-text {
  height: 1lh;
  overflow: hidden;
  width: min-content;
}
.text-container:has(:hover, :focus) .text {
  transform: translateY(-2lh) ;
}
.text {
  transition: transform 2.4s ease-in-out;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="pomvVPx"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Vertical text transition [forked]](https://codepen.io/smashingmag/pen/pomvVPx) by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pomvVPx">Vertical text transition [forked]</a> by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</figcaption>
</figure>

<p>Setting the <code>.scrolling-text</code> container’s width to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/min-content"><code>min-content</code></a> gives the characters a tight fit, stacking them vertically in a single column. The container’s height is set <code>1lh</code>. And since we’ve set <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/overflow"><code>overflow: hidden</code></a> on the container, only one character is shown in the container at any given point in time.</p>

<blockquote><strong>Tip</strong>: You can also use the HTML <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"><code>&lt;pre&gt;</code></a> element or either the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/white-space"><code>white-space</code></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap"><code>text-wrap</code></a> properties to control how text wraps.</blockquote>

<p>On hover, the text moves <code>-2lh</code>, or double the height of a single text character in the opposite, or up, direction. So, basically, we’re sliding things up by two characters in order to animate from the first character to the third character when the container holding the text is in a hovered state.</p>

<div class="partners__lead-place"></div>

<h2 id="applying-gradients-to-text">Applying Gradients To Text</h2>

<p>Here’s a fun bit of styling:</p>

<div class="break-out">
<pre><code class="language-css">.text {
  background: repeating-linear-gradient(
    180deg, 
    rgb(224, 236, 236), 
    rgb(224, 236, 236) 5px, 
    rgb(92, 198, 162) 5px, 
    rgb(92, 198, 162) 6px);
  background-clip: text;
  color: transparent; /&#42; to show the background underneath &#42;/
  background-size: 20% 20%;
}
</code></pre>
</div>

<p>How often do you find yourself using repeating gradients in your work? The fun part, though, is what comes after it. See, we’re setting a <code>transparent</code> color on the text and that allows the <code>repeating-linear-gradient()</code> to show through it. <a href="https://css-tricks.com/the-css-box-model/">But since text is a box like everything else in CSS</a>, we clip the background at the text itself to make it look like the text is cut out of the gradient.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="BaeyxZJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [A gradient text (Note: View in Safari or Chrome) [forked]](https://codepen.io/smashingmag/pen/BaeyxZJ) by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/BaeyxZJ">A gradient text (Note: View in Safari or Chrome) [forked]</a> by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</figcaption>
</figure>

<p>Pretty neat, right? Now, it looks like our text characters have a striped pattern painted on them.</p>

<h2 id="animating-the-gradient">Animating The Gradient</h2>

<p>This is where we take the same animated gradient concept covered in other tutorials and work it into what we’re doing here. For that, we’ll first register some of the <code>repeating-linear-gradient()</code> values as custom properties. But unlike the other implementations, ours is a bit more complex because we will animate several values rather than, say, updating the hue.</p>

<p>Instead, we’re animating two colors, a length, and an angle.</p>

<pre><code class="language-css">@property --c1 {
  syntax: "&lt;color&gt;";
  inherits: false;
  initial-value: rgb(224, 236, 236);
}
@property --c2 {
  syntax: "&lt;color&gt;";
  inherits: false;
  initial-value: rgb(92, 198, 162);
}
@property --l {
  syntax: "&lt;length&gt; | &lt;percentage&gt;";
  inherits: false;
  initial-value: 5px;
}
@property --angle {
  syntax: "&lt;angle&gt;";
  inherits: false;
  initial-value: 180deg;
}

.text {
  background: repeating-linear-gradient(
    var(--angle), 
    var(--c1), 
    var(--c1) 5px, 
    var(--c2) var(--l), 
    var(--c2) 6px);
}
</code></pre>

<p>We want to update the values of our registered custom properties when the container that holds the text is hovered or in focus. All that takes is re-declaring the properties with the updated values.</p>

<pre><code class="language-css">.text-container:has(:hover, :focus) .text {
  --c1: pink;
  --c2: transparent;  
  --l: 100%;
  --angle: 90deg;

  background-size: 50% 100%;
  transform:  translateY(-2lh);
}
</code></pre>

<p>To be super clear about what’s happening, these are the custom properties and values that update on hover:</p>

<ul>
<li><code>--c1</code>: Starts with a color value of <code>rgb(224, 236, 236)</code> and updates to <code>pink</code>.</li>
<li><code>--c2</code>: Starts with a color value of <code>rgb(92, 198, 162)</code> and updates to <code>transparent</code>.</li>
<li><code>--l</code>: Starts with length value <code>5px</code> and updates to <code>100%</code>.</li>
<li><code>--a</code>: Starts with an angle value of <code>180deg</code> and updates to <code>90deg</code>.</li>
</ul>

<p>So, the two colors used in the gradient transition into other colors while the overall size of the gradient increases and rotates. It’s as though we’re choreographing a short dance routine for the gradient.</p>

<div class="partners__lead-place"></div>

<h2 id="refining-the-transition">Refining The Transition</h2>

<p>All the while, the <code>.text</code> element containing the characters slides up to reveal one character at a time. The only thing is that we have to tell CSS what will <code>transition</code> on hover, which we do directly on the <code>.text</code> element:</p>

<div class="break-out">
<pre><code class="language-css">.text {
  transition: --l, --angle, --c1, --c2, background-size, transform 2.4s ease-in-out;
  transition-duration: 2s; 
}
</code></pre>
</div>

<p>Yes, I could just as easily have used the <code>all</code> keyword to select all of the transitioning properties. But I prefer taking the extra step of declaring each one individually. It’s a little habit to keep the browser from having to watch for too many things, which could slow things down even a smidge.</p>

<h2 id="final-demo">Final Demo</h2>

<p>Here’s the final outcome once again:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="qBGEYXO"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Text animation with @property [forked]](https://codepen.io/smashingmag/pen/qBGEYXO) by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/qBGEYXO">Text animation with @property [forked]</a> by <a href="https://codepen.io/rpsthecoder">Preethi Sam</a>.</figcaption>
</figure>

<p>I hope this little exercise not only demonstrates the sorts of fancy things we can make with CSS custom properties but also helps clarify the differences between custom properties and standard variables. Standard variables are excellent placeholders for more maintainable code (and a few <a href="https://css-tricks.com/the-css-custom-property-toggle-trick/">fancy tricks of their own</a>) but when you find yourself needing to update one value in a property that supports multiple values &mdash; such as colors in a gradient &mdash; the <code>@property</code> at-rule is where it’s at because it lets us define variables with a custom specification that sets the variable’s syntax, initial value, and inheritance behavior.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aWhen%20we%20get%20to%20amend%20values%20individually%20and%20independently%20with%20a%20promise%20of%20animation,%20it%20both%20helps%20streamline%20the%20code%20and%20opens%20up%20new%20possibilities%20for%20designing%20elaborate%20animations%20with%20relatively%20nimble%20code.%0a&url=https://smashingmagazine.com%2f2024%2f05%2ftimes-need-custom-property-instead-css-variable%2f">
      
When we get to amend values individually and independently with a promise of animation, it both helps streamline the code and opens up new possibilities for designing elaborate animations with relatively nimble code.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>That’s why <code>@property</code> is a useful CSS standard to keep in mind and keep ready to use when you are thinking about animations that involve isolated value changes.</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2022/10/advanced-animations-css/">How To Create Advanced Animations With CSS</a>,” Yosra Emad</li>
<li>“<a href="https://www.smashingmagazine.com/2021/04/easing-functions-css-animations-transitions/">Understanding Easing Functions For CSS Animations And Transitions</a>,” Adrian Bece</li>
<li>“<a href="https://www.smashingmagazine.com/2023/09/path-css-easing-linear-function/">The Path To Awesome CSS Easing With The linear() Function</a>,” Jhey Tompkins</li>
<li>“<a href="https://www.smashingmagazine.com/2022/01/css-radial-conic-gradient/">A Deep CSS Dive Into Radial And Conic Gradients</a>,” Ahmad Shadeed</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Silvestar Bistrović</author><title>Infinite-Scrolling Logos In Flat HTML And Pure CSS</title><link>https://www.smashingmagazine.com/2024/04/infinite-scrolling-logos-html-css/</link><pubDate>Tue, 02 Apr 2024 12:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/04/infinite-scrolling-logos-html-css/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>Editor&amp;rsquo;s Note:&lt;/strong> This article has been updated with additional accessibility considerations for motion sensitivities.&lt;/p>
&lt;/blockquote>
&lt;p>Remember the HTML &lt;code>&amp;lt;marquee&amp;gt;&lt;/code> element? It’s deprecated, so it’s not like you’re going to use it when you need some sort of horizontal auto-scrolling feature. That’s where CSS comes in because it has all the tools we need to pull it off. Silvestar Bistrović demonstrates a technique that makes it possible with a set of images and as little HTML as possible.&lt;/p></description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/04/infinite-scrolling-logos-html-css/" />
              <title>Infinite-Scrolling Logos In Flat HTML And Pure CSS</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Infinite-Scrolling Logos In Flat HTML And Pure CSS</h1>
                  
                    
                    <address>Silvestar Bistrović</address>
                  
                  <time datetime="2024-04-02T12:00:00&#43;00:00" class="op-published">2024-04-02T12:00:00+00:00</time>
                  <time datetime="2024-04-02T12:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>When I was asked to make an auto-scrolling logo farm, I had to ask myself: “You mean, like a <code>&lt;marquee&gt;</code>?” It’s not the weirdest request, but the thought of a <code>&lt;marquee&gt;</code> conjures up the “old” web days when Geocities ruled. What was next, a repeating sparkling unicorn GIF background?</p>

<p>If you’re tempted to reach for the <code>&lt;marquee&gt;</code> element, don’t. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/marquee">MDN has a stern warning about it right at the top of the page</a>:</p>

<blockquote>“<strong>Deprecated:</strong> This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible […] Be aware that this feature may cease to work at any time.”</blockquote>

<p>That’s fine because whatever infinite scrolling feature <code>&lt;marquee&gt;</code> is offered, we can most certainly pull off in CSS. But when I researched examples to help guide me, I was surprised to find very little on it. Maybe auto-scrolling elements aren’t the rage these days. Perhaps the sheer nature of auto-scrolling behavior is enough of an accessibility red flag to scare us off.</p>

<p>Whatever the case, we have the tools to do this, and I wanted to share how I went about it. This is one of those things that can be done in lots of different ways, leveraging lots of different CSS features. Even though I am not going to exhaustively explore all of them, I think it’s neat to see someone else’s thought process, and that’s what you’re going to get from me in this article.</p>

<h2 id="what-we-re-making">What We’re Making</h2>

<p>But first, here&rsquo;s an example of the finished result:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YzMQMXe"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication [forked]](https://codepen.io/smashingmag/pen/YzMQMXe) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YzMQMXe">CSS only marquee without HTML duplication [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<p>The idea is fairly straightforward. We want some sort of container, and in it, we want a series of images that infinitely scroll without end. In other words, as the last image slides in, we want the first image in the series to directly follow it in an infinite loop.</p>

<p>So, here’s the plan: <strong>We’ll set up the HTML first, then pick at the container and make sure the images are correctly positioned in it before we move on to writing the CSS animation that pulls it all together.</strong></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="existing-examples">Existing Examples</h2>

<p>Like I mentioned, I tried searching for some ideas. While I didn’t find exactly what I was looking for, I did find a few demos that provided a spark of inspiration. What I really wanted was to use CSS only while not having to “clone” the marquee items.</p>

<p>Geoff Graham’s “<a href="https://css-tricks.com/creating-a-css-sliding-background-effect/">Sliding Background Effect</a>” is close to what I wanted. While it is dated, it did help me see how I could intentionally use <code>overflow</code> to allow images to “slide” out of the container and an animation that loops forever. It’s a background image, though, and relies on super-specific numeric values that make it tough to repurpose in other projects.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="LYvLvGz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Untitled [forked]](https://codepen.io/smashingmag/pen/LYvLvGz) by <a href="https://codepen.io/team/css-tricks">@css-tricks</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/LYvLvGz">Untitled [forked]</a> by <a href="https://codepen.io/team/css-tricks">@css-tricks</a>.</figcaption>
</figure>

<p>There’s another great example from <a href="https://codepen.io/Coding_Journey">Coding Journey</a> over at CodePen:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="yLrXrVY"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Marquee-like Content Scrolling [forked]](https://codepen.io/smashingmag/pen/yLrXrVY) by <a href="https://codepen.io/Coding_Journey">Coding Journey</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/yLrXrVY">Marquee-like Content Scrolling [forked]</a> by <a href="https://codepen.io/Coding_Journey">Coding Journey</a>.</figcaption>
</figure>

<p>The effect is what I’m after for sure, but it uses some JavaScript, and even though it’s just a light sprinkle, I would prefer to leave JavaScript out of the mix.</p>

<p>Ryan Mulligan’s “<a href="https://codepen.io/hexagoncircle/pen/wvmjomb?editors=1000">CSS Marquee Logo Wall</a>” is the closest thing. Not only is it a logo farm with individual images, but it demonstrates how CSS masking can be used to hide the images as they slide in and out of the container. I was able to integrate that same idea into my work.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ExJXJZm"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS Marquee Logo Wall [forked]](https://codepen.io/smashingmag/pen/ExJXJZm) by <a href="https://codepen.io/hexagoncircle">Ryan Mulligan</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ExJXJZm">CSS Marquee Logo Wall [forked]</a> by <a href="https://codepen.io/hexagoncircle">Ryan Mulligan</a>.</figcaption>
</figure>

<p>But there’s still something else I’m after. What I would like is the smallest amount of HTML possible, namely markup that does not need to be duplicated to create the impression that there’s an unending number of images. In other words, we should be able to create an infinite-scrolling series of images where the images are the only child elements in the “marquee” container.</p>

<p>I did find a few more examples in other places, but these were enough to point me in the right direction. Follow along with me.</p>

<h2 id="the-html">The HTML</h2>

<p>Let&rsquo;s set up the HTML structure first before anything else. Again, I want this to be as “simple” as possible, meaning very few elements with the shortest family tree possible. We can get by with nothing but the “marquee” container and the logo images in it.</p>

<div class="break-out">
<pre><code class="language-html">&lt;figure class="marquee"&gt;
  &lt;img class="marquee__item" src="logo-1.png" width="100" height="100" alt="Company 1"&gt;
  &lt;img class="marquee__item" src="logo-2.png" width="100" height="100" alt="Company 2"&gt;
  &lt;img class="marquee__item" src="logo-3.png" width="100" height="100" alt="Company 3"&gt;
&lt;/figure&gt;
</code></pre>
</div>

<p>This keeps things as “flat” as possible. There shouldn’t be anything else we need in here to make things work.</p>

<h2 id="setting-up-the-container">Setting Up The Container</h2>

<p>Flexbox might be the simplest approach for establishing a row of images with a gap between them. We don’t even need to tell it to flow in a row direction because that’s the default.</p>

<pre><code class="language-css">.marquee {
  display: flex;
}
</code></pre>

<p>I already know that I plan on using absolute positioning on the image elements, so it makes sense to set relative positioning on the container to, you know, <em>contain</em> them. And since the images are in an absolute position, they have no reserved height or width dimensions that influence the size of the container. So, we’ll have to declare an explicit <code>block-size</code> (the logical equivalent to <code>height</code>). We also need a maximum width so we have a boundary for the images to slide in and out of view, so we’ll use <code>max-inline-size</code> (the logical equivalent to <code>max-width</code>):</p>

<pre><code class="language-css">.marquee {
  --marquee-max-width: 90vw;

  display: flex;
  block-size: var(--marquee-item-height);
  max-inline-size: var(--marquee-max-width);
  position: relative;
}
</code></pre>

<p>Notice I’m using a couple of CSS variables in there: one that defines the marquee’s height based on the height of one of the images (<code>--marquee-item-height</code>) and one that defines the marquee’s maximum width (<code>--marquee-max-width</code>). We can give the marquee’s maximum width a value now, but we’ll need to formally register and assign a value to the image height, which we will do in a bit. I just like knowing what variables I am planning to work with as I go.</p>

<p>Next up, we want the images to be hidden when they are outside of the container. We’ll set the horizontal overflow accordingly:</p>

<pre><code class="language-css">.marquee {
  --marquee-max-width: 90vw;

  display: flex;
  block-size: var(--marquee-item-height);
  max-inline-size: var(--marquee-max-width);
  overflow-x: hidden;
  position: relative;
}
</code></pre>

<p>And I <em>really</em> like the way Ryan Mulligan used a CSS mask. It creates the impression that images are fading in and out of view. So, let’s add that to the mix:</p>

<pre><code class="language-css">.marquee {
  display: flex;
  block-size: var(--marquee-item-height);
  max-inline-size: var(--marquee-max-width);
  overflow-x: hidden;
  position: relative;
  mask-image: linear-gradient(
    to right,
    hsl(0 0% 0% / 0),
    hsl(0 0% 0% / 1) 20%,
    hsl(0 0% 0% / 1) 80%,
    hsl(0 0% 0% / 0)
  );
  position: relative;
}
</code></pre>

<p>Here’s what we have so far:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="LYvjLLG"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication, example 0 [forked]](https://codepen.io/smashingmag/pen/LYvjLLG) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/LYvjLLG">CSS only marquee without HTML duplication, example 0 [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<div class="partners__lead-place"></div>

<h2 id="positioning-the-marquee-items">Positioning The Marquee Items</h2>

<p>Absolute positioning is what allows us to yank the images out of the document flow and manually position them so we can start there.</p>

<pre><code class="language-css">.marquee&#95;&#95;item {
  position: absolute;
}
</code></pre>

<p>That makes it look like the images are completely gone. But they’re there — the images are stacked directly on top of one another.</p>

<p>Remember that CSS variable for our container, <code>--marquee-item-height</code>? Now, we can use it to match the marquee item height:</p>

<pre><code class="language-css">.marquee&#95;&#95;item {
  position: absolute;
  inset-inline-start: var(--marquee-item-offset);
}
</code></pre>

<p>To push marquee images outside the container, we need to define a <code>--marquee-item-offset</code>, but that calculation is not trivial, so we will learn how to do it in the next section. We know what the <code>animation</code> needs to be: something that moves linearly for a certain duration after an initial delay, then goes on infinitely. Let’s plug that in with some variables as temporary placeholders.</p>

<div class="break-out">
<pre><code class="language-css">.marquee&#95;&#95;item {
  position: absolute;
  inset-inline-start: var(--marquee-item-offset);
  animation: go linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
}
</code></pre>
</div>

<p>To animate the marquee items infinitely, we have to define two CSS variables, one for the duration (<code>--marquee-duration</code>) and one for the delay (<code>--marquee-delay</code>). The duration can be any length you want, but the delay should be calculated, which is what we will figure out in the next section.</p>

<div class="break-out">
<pre><code class="language-css">.marquee&#95;&#95;item {
  position: absolute;
  inset-inline-start: var(--marquee-item-offset);
  animation: go linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
  transform: translateX(-50%);
}
</code></pre>
</div>

<p>Finally, we will translate the marquee item by <code>-50%</code> horizontally. This small “hack” handles situations when the image sizes are uneven.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ExJXJMQ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication, example 2 [forked]](https://codepen.io/smashingmag/pen/ExJXJMQ) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ExJXJMQ">CSS only marquee without HTML duplication, example 2 [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<h2 id="animating-the-images">Animating The Images</h2>

<p>To make the animation work, we need the following information:</p>

<ul>
<li>Width of the logos,</li>
<li>Height of the logos,</li>
<li>Number of items, and</li>
<li>Duration of the animation.</li>
</ul>

<p>Let’s use the following configurations for our set of variables:</p>

<pre><code class="language-css">.marquee--8 {
  --marquee-item-width: 100px;
  --marquee-item-height: 100px;
  --marquee-duration: 36s;
  --marquee-items: 8;
}
</code></pre>

<p><strong>Note</strong>: <em>I’m using the BEM modifier <code>.marquee--8</code> to define the animation of the eight logos. We can define the animation keyframes now that we know the <code>--marquee-item-width</code> value.</em></p>

<pre><code class="language-css">@keyframes go {
  to {
    inset-inline-start: calc(var(--marquee-item-width) &#42; -1);
  }
}
</code></pre>

<p>The animation moves the marquee item from right to left, allowing each one to enter into view from the right as it travels out of view over on the left edge and outside of the marquee container.</p>

<p>Now, we need to define the <code>--marquee-item-offset</code>. We want to push the marquee item all the way to the right side of the marquee container, opposite of the animation end state.</p>

<p>You might think the offset should be <code>100% + var(--marquee-item-width)</code>, but that would make the logos overlap on smaller screens. To prevent that, we need to know the minimum width of all logos combined. We do that in the following way:</p>

<pre><code class="language-css">calc(var(--marquee-item-width) &#42; var(--marquee-items))
</code></pre>

<p>But that is not enough. If the marquee container is too big, the logos would take less than the maximum space, and the offset would be within the container, which makes the logos visible inside the marquee container. To prevent that, we will use the <code>max()</code> function like the following:</p>

<pre><code class="language-css">--marquee-item-offset: max(
  calc(var(--marquee-item-width) &#42; var(--marquee-items)),
  calc(100% + var(--marquee-item-width))
);
</code></pre>

<p>The <code>max()</code> function checks which of the two values in its arguments is bigger, the overall width of all logos or the maximum width of the container plus the single logo width, which we defined earlier. The latter will be true on bigger screens and the former on smaller screens.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="BaEZEXN"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication, example 3 [forked]](https://codepen.io/smashingmag/pen/BaEZEXN) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/BaEZEXN">CSS only marquee without HTML duplication, example 3 [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<p>Finally, we will define the complicated animation delay (<code>--marquee-delay</code>) with this formula:</p>

<div class="break-out">
<pre><code class="language-css">--marquee-delay: calc(var(--marquee-duration) / var(--marquee-items) &#42; (var(--marquee-items) - var(--marquee-item-index)) &#42; -1);
</code></pre>
</div>

<p>The delay equals the animation duration divided by a quadratic polynomial (that’s what ChatGPT tells me, at least). The quadratic polynomial is the following part, where we multiply the number of items and number of items minus the current item index:</p>

<div class="break-out">
<pre><code class="language-css">var(--marquee-items) &#42; (var(--marquee-items) - var(--marquee-item-index))
</code></pre>
</div>

<p>Note that we are using a negative delay (<code>* -1</code>) to make the animation start in the “past,” so to speak. The only remaining variable to define is the <code>--marquee-item-index</code> (the current marquee item position):</p>

<pre><code class="language-css">.marquee--8 .marquee&#95;&#95;item:nth-of-type(1) {
  --marquee-item-index: 1;
}
.marquee--8 .marquee&#95;&#95;item:nth-of-type(2) {
  --marquee-item-index: 2;
}

/&#42; etc. &#42;/

.marquee--8 .marquee&#95;&#95;item:nth-of-type(8) {
  --marquee-item-index: 8;
}
</code></pre>

<p>Here’s that final demo once again:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="xxerNKz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication [forked]](https://codepen.io/smashingmag/pen/xxerNKz) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/xxerNKz">CSS only marquee without HTML duplication [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<div class="partners__lead-place"></div>

<h2 id="motion-sensitivities">Motion Sensitivities</h2>

<p>While the animation isn&rsquo;t exactly the most complex and wild thing you&rsquo;ll find, it still could be a trigger for those with motion sensitivities due to a vestibular disorder. We can slow or eliminate the animation with the <code>prefers-reduced-motion</code> media query:</p>

<pre><code class="language-css">@media (prefers-reduced-motion) {
  .marquee__item {
    animation-play-state: paused;
  }
}
</code></pre>

<p>This does the job, but we could do a little better to make sure more of the logos are visible when the animation is off.</p>

<pre><code class="language-css">@media (prefers-reduced-motion) {
  .marquee {
    justify-content: space-evenly;
    mask-image: unset;
  }
 
  .marquee__item {
    position: unset;
    inset-inline-start: unset;
    transform: unset;
  }

  @keyframes go {
    to { 
      inset-inline-start: unset;
    }
  }
}
</code></pre>

<p>A more heavy-handed approach would be to add a button or some other control that toggles between play and pasued states, but whether or not you go that route will depend on your project requirements and whether the animation is essential to your interface.</p>

<h2 id="further-improvements">Further Improvements</h2>

<p>This solution could be better, especially when the logos are not equal widths. To adjust the gaps between inconsistently sized images, we could calculate the delay of the animation more precisely. That is possible because the animation is linear. I’ve tried to find a formula, but I think it needs more fine-tuning, as you can see:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="NWmgVWN"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication, example 4 [forked]](https://codepen.io/smashingmag/pen/NWmgVWN) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/NWmgVWN">CSS only marquee without HTML duplication, example 4 [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<p>Another improvement we can get with a bit of fine-tuning is to prevent big gaps on wide screens. To do that, set the <code>max-inline-size</code> and declare <code>margin-inline: auto</code> on the <code>.marquee</code> container:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="qBwjGBJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS only marquee without HTML duplication, example 5 [forked]](https://codepen.io/smashingmag/pen/qBwjGBJ) by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/qBwjGBJ">CSS only marquee without HTML duplication, example 5 [forked]</a> by <a href="https://codepen.io/CiTA">Silvestar Bistrović</a>.</figcaption>
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>What do you think? Is this something you can see yourself using on a project? Would you approach it differently? I am always happy when I land on something with a clean HTML structure and a pure CSS solution. You can see the final implementation on the <a href="https://heyflow.com">Heyflow website</a>.</p>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li><a href="https://www.smashingmagazine.com/2022/03/designing-better-infinite-scroll/">Infinite Scroll UX Done Right: Guidelines and Best Practices</a>, Vitaly Friedman</li>
<li><a href="https://www.smashingmagazine.com/2022/11/document-object-model-geometry-guide/">Document Object Model (DOM) Geometry: A Beginner’s Introduction And Guide</a>, Pearl Akpan</li>
<li><a href="https://www.smashingmagazine.com/2020/03/infinite-scroll-lazy-image-loading-react/">Implementing Infinite Scroll And Image Lazy Loading In React</a>, Chidi Orji</li>
<li><a href="https://www.smashingmagazine.com/2023/12/css-scroll-snapping-aligned-global-page-layout-case-study/">CSS Scroll Snapping Aligned With Global Page Layout: A Full-Width Slider Case Study</a>, Brecht De Ruyte</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Adrian Bece</author><title>The View Transitions API And Delightful UI Animations (Part 2)</title><link>https://www.smashingmagazine.com/2024/01/view-transitions-api-ui-animations-part2/</link><pubDate>Tue, 02 Jan 2024 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/01/view-transitions-api-ui-animations-part2/</guid><description>The View Transitions API is a new &amp;mdash; but game-changing &amp;mdash; feature that allows us to do the types of reactive state-based UI and page transitions that have traditionally been exclusive to JavaScript frameworks. In the second half of this mini two-part series, Adrian Bece expands on the demos from the first article to demonstrate how the View Transitions API can be used to transition not just elements between two states but the transition between full views and pages in single-page and multi-page applications.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/01/view-transitions-api-ui-animations-part2/" />
              <title>The View Transitions API And Delightful UI Animations (Part 2)</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The View Transitions API And Delightful UI Animations (Part 2)</h1>
                  
                    
                    <address>Adrian Bece</address>
                  
                  <time datetime="2024-01-02T10:00:00&#43;00:00" class="op-published">2024-01-02T10:00:00+00:00</time>
                  <time datetime="2024-01-02T10:00:00&#43;00:00" class="op-modified">2025-10-14T04:02:41+00:00</time>
                </header>
                
                

<p>Last time we met, <a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">I introduced you to the <strong>View Transitions API</strong></a>. We started with a simple default crossfade transition and applied it to different use cases involving elements on a page transitioning between two states. One of those examples took the basic idea of adding products to a shopping cart on an e-commerce site and creating a visual transition that indicates an item added to the cart.</p>

<p>The View Transitions API is still considered an experimental feature that’s currently supported only in Chrome at the time I’m writing this, but I’m providing that demo below as well as a video if your browser is unable to support the API.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="GReJGYV"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Add to cart animation v2 - completed [forked]](https://codepen.io/smashingmag/pen/GReJGYV) by <a href="https://codepen.io/AdrianBece">Adrian Bece</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GReJGYV">Add to cart animation v2 - completed [forked]</a> by <a href="https://codepen.io/AdrianBece">Adrian Bece</a>.</figcaption>
</figure>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898547481"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>That was a fun example! The idea that we can effectively take “snapshots” of an element’s before and after states and generate a transition between them using web standards is amazing.</p>

<p>But did you know that we can do even <em>more</em> than transition elements on the page? In fact, we can transition between two entire <em>pages</em>. The same deal applies to single-page applications (SPA): we can transition between two entire <em>views</em>. The result is an experience that feels a lot like an installed mobile app and the page transitions that have been exclusive to them for some time.</p>

<p>That’s exactly what we’re going to cover in the second half of this two-part series. If you want to <a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">review the first part</a>, please do! If you’re ready to plow ahead, then come and follow along.</p>

<h2 id="a-quick-review">A Quick Review</h2>

<p>The browser does a lot of heavy lifting with the View Transitions API, allowing us to create complex state-based UI and page transitions in a more streamlined way. The API takes a screenshot of the “old” page, performs the DOM update, and when the update is complete, the “new” page is captured.</p>

<p>It’s important to point out that <strong>what we see during the transition is replaced content in CSS</strong>, just the same as other elements, including images, videos, and iframes. That means they are not actually DOM elements, and that’s important because it avoids potential accessibility and usability issues during the transition.</p>

<pre><code class="language-javascript">const transition = document.startViewTransition(() =&gt; {
  /&#42; Take screenshot of an outgoing state &#42;/
  /&#42; Update the DOM - move the item from one container to another &#42;/
   destination.appendChild(card);
  /&#42; Capture the live state and perform a crossfade &#42;/
});
</code></pre>

<p>With this little bit of JavaScript, we can call the <code>document.startViewTransition</code> function, and we get the View Transition API’s default animation that performs a crossfading transition between the outgoing and incoming element states.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898547904"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>What we need to do, though, is tell the View Transitions API to <strong>pay attention to certain UI elements on the page and watch for their position and dimensions</strong>. This is where the CSS <code>view-transition-name</code> property comes in. We apply the property to a single element on the page while the transition function is running; transition names must be unique and applied once per page &mdash; think of them like an <code>id</code> attribute in HTML.</p>

<p>That being said, we can apply unique view transition names to <em>multiple</em> elements during the transition. Let’s select one element for the time being &mdash; we’ll call it <code>.active-element</code> &mdash; and give it a view transition name:</p>

<pre><code class="language-css">.active-item {
  view-transition-name: active-item;
}
</code></pre>

<p>We could do this in JavaScript instead:</p>

<pre><code class="language-javascript">activeItem.style.viewTransitionName = "active-item";
</code></pre>

<p>When we decide to set the transition name, the element itself becomes what’s called a <strong>transition element</strong> between the outgoing and incoming state change. Other elements still receive the crossfade animation between outgoing and incoming states.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898548098"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>From here, we can use CSS to customize the animation. Specifically, we get a whole new set of pseudo-elements we can use to target and select certain parts of the transition. Let’s say we have a “card” component that we’ve identified and given a transition name. We can define a set of CSS <code>@keyframes</code>, set it on the two states (<code>::view-transition-image-pair(card-active)</code>), then configure the animation at different levels, such as applying a certain animation timing function, delay, or duration to the entire transition group (<code>::view-transition-group(*)</code>), the “old” page <code>(::view-transition-old(root)</code>), or the “new” page (<code>::view-transition-new(root)</code>).</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Delay remaining card movement &#42;/
::view-transition-group(&#42;) {
  animation-timing-function: ease-in-out;
  animation-delay: 0.1s;
  animation-duration: 0.2s;
}

/&#42; Delay container shrinking (shrink after cards have moved) &#42;/
::view-transition-old(root),
::view-transition-new(root) {
  animation-delay: 0.2s;
  animation-duration: 0s; /&#42; Avoid crossfade animation, resize instantly &#42;/
}

/&#42; Apply custom keyframe animation to old and new state &#42;/
::view-transition-image-pair(card-active) {
  animation: popIn 0.5s cubic-bezier(0.7, 2.2, 0.5, 2.2);
}

/&#42; Animation keyframes &#42;/
@keyframes popIn { /&#42; ... &#42;/ }
</code></pre>
</div>

<p>Notice how we’re selecting certain UI elements in each pseudo-element, including the entire transition group with the universal selector (<code>*</code>), the <code>root</code> of the old and new states, and the <code>card-active</code> we used to name the watched element.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898548347"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>Before we plow ahead, it’s worth one more reminder that we’re working with an experimental web feature. The latest versions of Chrome, Edge, Opera, and Android Browser <a href="https://caniuse.com/view-transitions">currently support the API</a>. Safari has <a href="https://github.com/WebKit/standards-positions/issues/48">taken a positive position</a> on it, and there’s <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823896">an open ticket for Firefox adoption</a>. We have to wait for these last two browsers to formally support the API before using it in a production environment.</p>

<p>And even though we have Chromium support, that’s got a bit of nuance to it as only Chrome Canary supports multi-page view transitions at the moment behind the <code>view-transition-on-navigation</code> flag. You can paste the following into the Chrome Canary URL bar to access it:</p>

<pre><code class="language-bash">chrome://flags/#view-transition-on-navigation
</code></pre>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="full-view-transitions-for-multi-page-applications">Full View Transitions For Multi-Page Applications</h2>

<p>Let’s start with <strong>multi-page applications (MPA)</strong>. The examples are more straightforward than they are for SPAs, but we can leverage what we learn about MPAs to help understand view transitions in SPAs. Specifically, we are going to build a static site with the <a href="https://www.11ty.dev">Eleventy framework</a> and create different transitions between the site’s different pages.</p>

<p>Again, <strong>MPA view transitions are only supported in Chrome Canary</strong> <code>view-transition-on-navigation</code> flag. So, if you’re following along, be sure you’re using Chrome Canary with the feature flag enabled. Either way, I’ll include videos of what we’re making to help demonstrate the concepts, like this one we’re about to tackle:</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898548828"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>That looks pretty tricky! But we’re starting with the baseline default crossfade animation for this transition and will take things one step at a time to see how everything adds up.</p>

<h3 id="starting-with-the-markup">Starting With The Markup</h3>

<p>I want to showcase vanilla JavaScript and CSS implementations of the API so that you can apply it to your own tech stack without needing to re-configure a bunch of stuff. Even If you are unfamiliar with Eleventy, don’t worry because I’ll use the compiled HTML and CSS code in the examples so you can follow along.</p>

<p>Let’s check out the HTML for a <code>.card</code> element. Once Eleventy generates HTML from the template with the data from our Markdown files, we get the following HTML markup.</p>

<div class="break-out">
<pre><code class="language-html">!-- Item grid element on the listing page (homepage) --&gt;
&lt;a href="/some-path" class="card"&gt;
  &lt;figure class="card&#95;&#95;figure"&gt;
    &lt;picture&gt;
      &lt;!-- Prefer AVIF images --&gt;
      &lt;source type="image/avif" srcset="..."&gt;
        &lt;!-- JPG or PNG fallback --&gt;
      &lt;img class="card&#95;&#95;image" src="..." width="600" height="600"&gt;
    &lt;/picture&gt;
    &lt;figcaption class="card&#95;&#95;content"&gt;
      &lt;h2 class="card&#95;&#95;title"&gt;Reign Of The Reaper&lt;/h2&gt;
      &lt;h3 class="card&#95;&#95;subtitle"&gt;Sorcerer&lt;/h3&gt;
    &lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/a&gt;
</code></pre>
</div>

<p>So, what we’re working with is a link with a <code>.card</code> class wrapped around a <code>&lt;figure&gt;</code> that, in turn, contains the <code>&lt;img&gt;</code> in a <code>&lt;picture&gt;</code> that has the <code>&lt;figcaption&gt;</code> as a sibling that also contains its own stuff, including <code>&lt;h2&gt;</code> and <code>&lt;h3&gt;</code> elements. When clicking the <code>.card</code>, we will transition between the current page and the <code>.card</code>’s corresponding link.</p>

<h3 id="crossfading-transitions">Crossfading Transitions</h3>

<p>Implementing crossfade transitions in MPAs is actually a one-line snippet. In fact, it’s a <code>&lt;meta&gt;</code> tag that we can drop right into the document <code>&lt;head&gt;</code> alongside other meta information.</p>

<pre><code class="language-html">&lt;meta name="view-transition" content="same-origin" /&gt;
</code></pre>

<p>We can still use the <code>document.startViewTransition</code> function to create on-page UI transitions like we did in the examples from the previous article. For crossfade page transitions, we only need to apply this HTML <code>meta</code> tag, and the browser handles the rest for us!</p>

<p>Keep in mind, however, that this is what’s currently only supported in Chrome Canary. The actual implementation might be changed between now and formal adoption. But for now, this is all we need to get the simple crossfade page transitions.</p>

<p>I have to point out how difficult it would be to implement this without the View Transitions API. It’s amazing to see these app-like page transitions between standard HTML documents that run natively in the browser. We’re working directly with the platform!</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898548938"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="transitioning-between-two-pages">Transitioning Between Two Pages</h3>

<p>We’re going to continue configuring our View Transition with CSS animations. Again, it’s awesome that we can resort to using standard CSS <code>@keyframes</code> rather than some library.</p>

<p>First, let’s check out the project pages and how they are linked together. A user is capable of navigating from the homepage to the item details page and back, as well as navigating between two item details pages.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="396"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg"
			
			sizes="100vw"
			alt="Diagram of the page and element transitions."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/1-diagram-page-element-transitions.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Those diagrams illustrate (1) the origin page, (2) the destination page, (3) the type of transition, and (4) the transition elements. The following is a closer look at the transition elements, i.e., the elements that receive the transition and are tracked by the API.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="619"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png"
			
			sizes="100vw"
			alt="Diagramming the transition between two product pages."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/2-diagramming-transition-between-two-product-pages.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>So, what we’re working with are two transition elements: a <strong>header</strong> and a <strong>card component</strong>. We will configure those together one at a time.</p>

<h3 id="header-transition-elements">Header Transition Elements</h3>

<p>The default crossfade transition between the pages has already been set, so let’s start by registering the header as a <em>transition element</em> by assigning it a <code>view-transition-name</code>. First, let’s take a peek at the HTML:</p>

<div class="break-out">
<pre><code class="language-html">&lt;div class="header&#95;&#95;wrapper"&gt;
  &lt;!-- Link back arrow --&gt;
  &lt;a class="header&#95;&#95;link header&#95;&#95;link--dynamic" href="/"&gt;
    &lt;svg ...&gt;&lt;!-- ... --&gt;&lt;/svg&gt;
  &lt;/a&gt;
  &lt;!-- Page title --&gt;
  &lt;h1 class="header&#95;&#95;title"&gt;
    &lt;a href="/" class="header&#95;&#95;link-logo"&gt;
      &lt;span class="header&#95;&#95;logo--deco"&gt;Vinyl&lt;/span&gt;Emporium &lt;/a&gt;
  &lt;/h1&gt;
  &lt;!-- ... --&gt;
&lt;/div&gt;
</code></pre>
</div>

<p>When the user navigates between the homepage and an item details page, the arrow in the header appears and disappears &mdash; depending on which direction we’re moving &mdash; while the title moves slightly to the right. We can use <code>display: none</code> to handle the visibility.</p>

<pre><code class="language-css">/&#42; Hide back arrow on the homepage &#42;/
.home .header&#95;&#95;link--dynamic {
    display: none;
}
</code></pre>

<p>We’re actually registering <em>two</em> transition elements within the header: the arrow (<code>.header__link--dynamic</code>) and the title (<code>.header__title</code>). We use the <code>view-transition-name</code> property on both of them to define the names we want to call those elements in the transition:</p>

<pre><code class="language-css">@supports (view-transition-name: none) {
  .header&#95;&#95;link--dynamic {
    view-transition-name: header-link;
  }
  .header&#95;&#95;title {
    view-transition-name: header-title;
  }
}
</code></pre>

<p>Note how we’re wrapping all of this in a CSS <code>@supports</code> query so it is scoped to browsers that actually support the View Transitions API. So far, so good!</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898551913"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="card-transition-element">Card Transition Element</h3>

<p>Turning our attention to the card component, it’s worth recalling that <strong>all view transitions on a page must have unique names</strong>. So, rather than set the card’s name up front in CSS, let’s instead assign it to the card once the card’s image is clicked to help avoid potential conflicts.</p>

<p>There are different ways to assign a <code>view-transition-name</code> to the clicked card. For example, we can use mouse events. For this demo, however, I’ve decided to use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event">Navigation API</a> because it’s a good excuse to work with it and put its ability to <strong>track back and forward browser navigation</strong> to use. Specifically, we can use it to intercept a navigation event and use a query selector on the card image containing the matching target <code>href</code> that has been clicked on to assign a name for the transitioning element.</p>

<div class="break-out">
<pre><code class="language-javascript">// Utility function for applying view-transition-name to clicked element
function applyTag(url) {
  // Select an image in a link matching the link that has been clicked on.
  const image = document.querySelector(
    `a[href="${url.pathname}"] .card&#95;&#95;image`
  );
  if (!image) return;
  image.style.viewTransitionName = "product-image";
}

// Intercept the navigation event.
navigation.addEventListener("navigate", (event) =&gt; {
  const toUrl = new URL(event.destination.url);

  // Return if origins do not match or if API is not supported.
  if (!document.startViewTransition || location.origin !== toUrl.origin) {
    return;
  }
  applyTag(toUrl);
});
</code></pre>
</div>

<p>The item details page is our destination, and we can assign the <code>view-transition-name</code> property to it directly in CSS since it is always going to be a matching image.</p>

<div class="break-out">
<pre><code class="language-html">&lt;section class="product&#95;&#95;media-wrapper" style="--cover-background-color: #fe917d"&gt;
  &lt;nav class="product&#95;&#95;nav"&gt;
    &lt;span&gt;
      &lt;a class="product&#95;&#95;link product&#95;&#95;link--prev" href="..."&gt;
        &lt;svg ... &gt;&lt;!-- ... --&gt;&lt;/svg&gt;
      &lt;/a&gt;
    &lt;/span&gt;
    &lt;span&gt;
      &lt;a class="product&#95;&#95;link product&#95;&#95;link--next" href="..."&gt;
        &lt;svg ... &gt;&lt;!-- ... --&gt;&lt;/svg&gt;
      &lt;/a&gt;
    &lt;/span&gt;
  &lt;/nav&gt;
  &lt;article class="product&#95;&#95;media"&gt;
    &lt;div class="product&#95;&#95;image"&gt;
      &lt;!-- LP sleeve cover image --&gt;
      &lt;picture&gt;
        &lt;source type="image/avif" srcset="..."&gt;
        &lt;img src="..." width="600" height="600"&gt;
      &lt;/picture&gt;
    &lt;/div&gt;
    &lt;div class="product&#95;&#95;image--deco"&gt;
      &lt;!-- LP image --&gt;
      &lt;picture&gt;
        &lt;source type="image/avif" srcset="..."&gt;
        &lt;img src="..." width="600" height="600"&gt;
      &lt;/picture&gt;
    &lt;/div&gt;
  &lt;/article&gt;
&lt;/section&gt;
</code></pre>
</div>

<p>We can also customize the animations we’ve just created using standard CSS <code>animation</code> properties. For now, let’s merely play around with the animation’s duration and easing function.</p>

<pre><code class="language-css">@supports (view-transition-name: none) {
  .product&#95;&#95;image {
    view-transition-name: product-image;
  }
  ::view-transition-old(&#42;),
  ::view-transition-new(&#42;) {
    animation-timing-function: ease-in-out;
    animation-duration: 0.25s;
  }
  ::view-transition-group(product-image) {
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-duration: 0.4s;
  }
}
</code></pre>

<p>And just like that, we have created a neat page transition! And all we really did was assign a couple of transition elements and adjust their duration and timing functions to get the final result.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898552079"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<div class="partners__lead-place"></div>

<h3 id="working-with-more-complex-animations">Working With More Complex Animations</h3>

<p>Let’s move on to additional, more complex animations that run after the page transition has finished. We won’t actually use them just yet, but we are setting them up so that we <em>can</em> use them for transitioning between two product details pages.</p>

<p><strong>Why are we going with the CSS animations</strong>, all of a sudden? If you recall from <a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">the first article in this two-part series</a>, <strong>the page is not interactive while the View Transitions API is running</strong>. Although the transition animations look smooth and gorgeous, we want to keep them as short as possible so we don’t make the user wait for too long to interact with the page. We also want to be able to <strong>interrupt the animation</strong> when the user clicks on a link.</p>

<p>The following CSS defines two sets of animation <code>@keyframes</code>: one for the album to <code>open</code> up its cover, and another for the album itself to <code>rollOut</code> of the sleeve.</p>

<pre><code class="language-css">/&#42; LP gatefold sleeve open animation and styles &#42;/
.product&#95;&#95;media::before {
  /&#42; Hide until animatton begins (avoid z-index issues) &#42;/
  opacity: 0;
  /&#42; ... &#42;/
  animation: open 0.25s 0.45s ease-out forwards;
}

/&#42; LP roll out animation and styles &#42;/
.product&#95;&#95;image--deco {
  /&#42; Hide until animatton begins (avoid z-index issues) &#42;/
  opacity: 0;
  /&#42; ... &#42;/
  animation: rollOut 0.6s 0.45s ease-out forwards;
}

@keyframes open {
  from {
    opacity: 1;
    transform: rotateZ(0);
  }
  to {
    opacity: 1;
    transform: rotateZ(-1.7deg);
  }
}

@keyframes rollOut {
  from {
    opacity: 1;
    transform: translateX(0) translateY(-50%) rotateZ(-45deg);
  }
  to {
    opacity: 1;
    transform: translateX(55%) translateY(-50%) rotateZ(18deg);
  }
}
</code></pre>

<p>Check it out! Our CSS animations are now included in the transitions.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898552297"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>We aren’t quite done yet. We haven’t actually applied the animations. Let’s do that before playing with the animation a little more.</p>

<h3 id="transitioning-between-two-items-linked-to-different-pages">Transitioning Between Two Items Linked To Different Pages</h3>

<p>OK, so we’ve already completed an example of a view transition between two pages in an MPA. We did it by connecting the site’s global navigation to any product details page when clicking either on the header link or the card component.</p>

<p>We also just created two CSS animations that we haven’t put to use. That is what we’ll do next to set a view transition when navigating between two product pages. For this one, we will create a transition on the product images by clicking on the left or right arrows on either side of the product to view the previous or next product, respectively.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="618"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png"
			
			sizes="100vw"
			alt="Diagramming the transition between product pages."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Notice how we’re now using View Transitions API to reverse the CSS eye-candy animation. (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/3-diagramming-transition-between-product-pages.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To do that, let’s start by defining our transition elements and assign transition names to the elements we’re transitioning between the product image (<code>.product__image--deco</code>) and the product disc behind the image (<code>.product__media::before</code>).</p>

<pre><code class="language-css">@supports (view-transition-name: none) {
  .product&#95;&#95;image--deco {
    view-transition-name: product-lp;
  }
 .product&#95;&#95;media::before {
    view-transition-name: flap;
  }
  ::view-transition-group(product-lp) {
    animation-duration: 0.25s;
    animation-timing-function: ease-in;
  }
  ::view-transition-old(product-lp),
  ::view-transition-new(product-lp) {
    /&#42; Removed the crossfade animation &#42;/
    mix-blend-mode: normal;
    animation: none;
  }
}
</code></pre>

<p>Notice how we had to remove the crossfade animation from the product image’s old (<code>::view-transition-old(product-lp)</code>) and new (<code>::view-transition-new(product-lp)</code>) states. So, for now, at least, the album disc changes instantly the moment it’s positioned back behind the album image.</p>

<p>But doing this messed up the transition between our global header navigation and product details pages. Navigating from the item details page back to the homepage results in the album disc remaining visible until the view transition finishes rather than running when we need it to.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898552887"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>The way we fix this is by removing transitions when returning to a previous state. When we’re working with elaborate page transitions like this one, <strong>we have to be mindful of all the different types of navigation paths that can occur</strong> and ensure transitions run smoothly regardless of which route a user takes or which direction they navigate.</p>

<p>Just like we can assign <code>view-transition-name</code> attributes when needed, we can also remove them to restore the element’s default crossfade transition. Let’s once again use the Navigation API, this time to intercept the navigation event on the item details page. If the user is navigating back to the homepage, we’ll simply set the <code>view-transition-name</code> of the album disc element to <code>none</code> to prevent conflicts.</p>

<div class="break-out">
<pre><code class="language-javascript">function removeTag() {
  const image = document.querySelector(`.product&#95;&#95;image--deco`);
  image.style.viewTransitionName = "none";
}

navigation.addEventListener("navigate", (event) =&gt; {
  const toUrl = new URL(event.destination.url);

  if (!document.startViewTransition || location.origin !== toUrl.origin) {
    return;
  }

  // Remove view-transition-name from the LP if navigating to the homepage.
  if (toUrl.pathname === "/") {
    removeTag();
  }
});
</code></pre>
</div>

<p>Now, all our bases are covered, and we’ve managed to create this seemingly complex page transition with relatively little effort! The crossfade transition between pages works right out of the box with a single meta tag added to the document <code>&lt;head&gt;</code>. All we do from there is set transition names on elements and fiddle with CSS animation properties and <code>@keyframes</code> to make adjustments.</p>

<h3 id="demo">Demo</h3>

<p>The following demo includes the code snippets that are directly relevant to the View Transitions API and its implementation. If you are curious about the complete codebase or want to play around with the example, feel free to <a href="https://github.com/codeAdrian/11ty-vinyl-emporium">check out the source code in this GitHub repository</a>. Otherwise, a live demo is available below:</p>

<ul>
<li><a href="https://vinyl-emporium.vercel.app/">Open Live Demo 1</a></li>
</ul>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898548828"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h2 id="full-view-transitions-for-single-page-applications">Full View Transitions For Single-Page Applications</h2>

<p>The View Transition API gets a little tricky in <strong>single-page applications (SPA)</strong>. Once again, we need to rely on the <code>document.startViewTransition</code> function because everything is handled and rendered with JavaScript. Luckily, routing libraries exist, like <a href="https://github.com/remix-run/react-router">react-router</a>, and they have already <a href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#view-transitions-">implemented page transitions with the View Transitions API</a> as an opt-in. Other libraries are following suit.</p>

<p>In this next tutorial, we’ll use react-router to create the transitions captured in the following video:</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898553477"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>There are a few different types of transitions happening there, and we are going to make all of them. Those include:</p>

<ul>
<li>Transition between category pages;</li>
<li>Transition between a category page and a product details page;</li>
<li>Transition the product image on a product details page to a larger view.</li>
</ul>

<p>We’ll begin by setting up react-router before tackling the first transition between category pages.</p>

<h3 id="react-router-setup">React Router Setup</h3>

<p>Let’s start by setting up our router and main page components. The basic setup is this: we have a homepage that represents one product category, additional pages for other categories, and pages for each individual product.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="314"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png"
			
			sizes="100vw"
			alt="Diagramming the app’s routes."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/4-diagramming-app-routes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Let’s configure the router to match that structure. Each route gets a <a href="https://reactrouter.com/en/main/route/loader"><code>loader</code></a> function to handle page data.</p>

<div class="break-out">
<pre><code class="language-javascript">import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Category, { loader as categoryLoader } from "./pages/Category";
import Details, { loader as detailsLoader } from "./pages/Details";
import Layout from "./components/Layout";

/&#42; Other imports &#42;/

const router = createBrowserRouter([
  {
    /&#42; Shared layout for all routes &#42;/
    element: &lt;Layout /&gt;,
    children: [
      {
        /&#42; Homepage is going to load a default (first) category &#42;/
        path: "/",
        element: &lt;Category /&gt;,
        loader: categoryLoader,
      },
      {
      /&#42; Other categories &#42;/
        path: "/:category",
        element: &lt;Category /&gt;,
        loader: categoryLoader,
      },
      {
        /&#42; Item details page &#42;/
        path: "/:category/product/:slug",
        element: &lt;Details /&gt;,
        loader: detailsLoader,
      },
    ],
  },
]);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  &lt;React.StrictMode&gt;
    &lt;RouterProvider router={router} /&gt;
  &lt;/React.StrictMode&gt;
);
</code></pre>
</div>

<p>With this, we have established the routing structure for the app:</p>

<ul>
<li>Homepage (<code>/</code>);</li>
<li>Category page (<code>/:category</code>);</li>
<li>Product details page (<code>/:category/product/:slug</code>).</li>
</ul>

<p>And depending on which route we are on, the app renders a <code>Layout</code> component. That’s all we need as far as setting up the routes that we’ll use to transition between views. Now, we can start working on our first transition: between two category pages.</p>

<h3 id="transition-between-category-pages">Transition Between Category Pages</h3>

<p>We’ll start by implementing the transition between category pages. The transition performs a crossfade animation between views. The only part of the UI that does not participate in the transition is the bottom border of the category filter menu, which provides a visual indication for the active category filter and moves between the formerly active category filter and the currently active category filter that we will eventually register as a transition element.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="606"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png"
			
			sizes="100vw"
			alt="Diagramming the UI transition when navigating between category views."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The app navigation is a group of category filters where the active category is indicated by a border that transitions when another category is selected. (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/5-diagramming-ui-transition-navigating-between-category-views.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Since we’re using react-router, we get its web-based routing solution, <a href="https://github.com/remix-run/react-router/tree/main/packages/react-router-dom">react-router-dom</a>, baked right in, giving us access to the DOM bindings &mdash; or router components we need to keep the UI in sync with the current route as well as a component for navigational links. That’s also where we gain access to the View Transitions API implementation.</p>

<p>Specifically, we will use the component for navigation links (<code>Link</code>) with the <code>unstable_viewTransition</code> prop that tells the react-router to run the View Transitions API when switching page contents.</p>

<div class="break-out">
<pre><code class="language-javascript">import { Link, useLocation } from "react-router-dom";
/&#42; Other imports &#42;/

const NavLink = ({ slug, title, id }) =&gt; {
  const { pathname } = useLocation();
  /&#42; Check if the current nav link is active &#42;/
  const isMatch = slug === "/" ? pathname === "/" : pathname.includes(slug);
  return (
    &lt;li key={id}&gt;
      &lt;Link
        className={isMatch ? "nav&#95;&#95;link nav&#95;&#95;link--current" : "nav&#95;&#95;link"}
        to={slug}
        unstable&#95;viewTransition
      &gt;
        {title}
      &lt;/Link&gt;
    &lt;/li&gt;
  );
};

const Nav = () =&gt; {
  return 
    &lt;nav className={"nav"}&gt;
      &lt;ul className="nav&#95;&#95;list"&gt;
        {categories.items.map((item) =&gt; (
          &lt;NavLink {...item} /&gt;
        ))}
      &lt;/ul&gt;
    &lt;/nav&gt;
  );
};
</code></pre>
</div>

<p>That is literally all we need to register and run the default crossfading view transition! That’s again because react-router-dom is giving us access to the View Transitions API and does the heavy lifting to abstract the process of setting transitions on elements and views.</p>

<h3 id="creating-the-transition-elements">Creating The Transition Elements</h3>

<p>We only have one UI element that gets its own transition and a name for it, and that’s the visual indicator for the actively selected product category filter in the app’s navigation. While the app transitions between category views, it runs another transition on the active indicator that moves its position from the origin category to the destination category.</p>

<p>I know that I had earlier described that visual indicator as a bottom border, but we’re actually going to establish it as a standard HTML horizontal rule (<code>&lt;hr&gt;</code>) element and conditionally render it depending on the current route. So, basically, the <code>&lt;hr&gt;</code> element is fully removed from the DOM when a view transition is triggered, and we re-render it in the DOM under whatever <code>NavLink</code> component represents the current route.</p>

<p>We want this transition only to run if the navigation is visible, so we’ll use the <code>react-intersection-observer</code> helper to check if the element is visible and, if it is, assign it a <code>viewTransitionName</code> in an inline style.</p>

<div class="break-out">
<pre><code class="language-javascript">import { useInView } from "react-intersection-observer";
/&#42; Other imports &#42;/

const NavLink = ({ slug, title, id }) =&gt; {
  const { pathname } = useLocation();
  const isMatch = slug === "/" ? pathname === "/" : pathname.includes(slug);
  return (
    &lt;li key={id}&gt;
      &lt;Link
        ref={ref}
        className={isMatch ? "nav&#95;&#95;link nav&#95;&#95;link--current" : "nav&#95;&#95;link"}
        to={slug}
        unstable&#95;viewTransition
      &gt;
        {title}
      &lt;/Link&gt;
      {isMatch && (
        &lt;hr
          style={{
            viewTransitionName: inView ? "marker" : "",
          }}
          className="nav&#95;&#95;marker"
        /&gt;
      )}
    &lt;/li&gt;
  );
};
</code></pre>
</div>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898554644"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<div class="partners__lead-place"></div>

<h3 id="transitioning-between-two-product-views">Transitioning Between Two Product Views</h3>

<p>So far, we’ve implemented the default crossfade transition between category views and registered the <code>&lt;hr&gt;</code> element we’re using to indicate the current category view as a transition element. Let’s continue by establishing the transition between two product views.</p>

<p>What we want is to register the product view’s main image element as a transition element each time the user navigates from one product to another and for that transition element to actually transition between views. There’s also a case where users can navigate from a product view to a category view that we need to account for by falling back to a crossfade transition in that circumstance.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="670"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png"
			
			sizes="100vw"
			alt="Diagramming transitioning between two product views"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/6-transitioning-between-two-product-views.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>First, let’s take a look at our <code>Card</code> component used in the category views. Once again, <code>react-router-dom</code> makes our job relatively easy, thanks to the <code>unstable_useViewTransitionState</code> hook. The hook accepts a URL string and returns <code>true</code> if there is an active page transition to the target URL, as well as if the transition is using the View Transitions API.</p>

<p>That’s how we’ll make sure that our active image remains a transition element when navigating between a category view and a product view.</p>

<div class="break-out">
<pre><code class="language-javascript">import { Link, unstable&#95;useViewTransitionState } from "react-router-dom";
/&#42; Other imports &#42;/

const Card = ({ author, category, slug, id, title }) =&gt; {
  /&#42; We'll use the same URL value for the Link and the hook &#42;/
  const url = `/${category}/product/${slug}`;

  /&#42; Check if the transition is running for the item details pageURL &#42;/
  const isTransitioning = unstable&#95;useViewTransitionState(url);

  return (
    &lt;li className="card"&gt;</code>
      <code style="font-weight: bold;">&lt;Link unstable&#95;viewTransition to={url} className="card&#95;&#95;link"&gt;</code>
        <code class="language-javascript">&lt;figure className="card&#95;&#95;figure"&gt;</code>
          <code class="language-javascript">&lt;img</code>
            <code class="language-javascript">className="card&#95;&#95;image"</code>
            <code class="language-javascript">style=&#125;&#125;</code>
              <code class="language-javascript">/&#42; Apply the viewTransitionName if the card has been clicked on &#42;/</code>
              <code class="language-javascript">viewTransitionName: isTransitioning ? "item-image" : "",</code>
            <code class="language-javascript">&#125;&#125;</code>
            <code class="language-javascript">src=&#123;`/assets/$&#123;category&#125;/${id}-min.jpg`&#125;</code>
            <code class="language-javascript">alt=""</code>
          <code class="language-javascript">/&gt;</code>
         <code class="language-javascript">&#123;/&#42; ... &#42;/&#125;</code>
        <code class="language-javascript">&lt;/figure&gt;</code>
        <code class="language-javascript">&lt;div className="card&#95;&#95;deco" /&gt;</code>
      <code class="language-javascript">&lt;/Link&gt;</code>
    <code class="language-javascript">&lt;/li&gt;</code>
  <code class="language-javascript">);</code>
<code class="language-javascript">};</code>

<code class="language-javascript">export default Card;
</code></pre>
</div>

<p>We know which image in the product view is the transition element, so we can apply the <strong><code>viewTransitionName</code></strong> directly to it rather than having to guess:</p>

<div class="break-out">
<pre><code class="language-javascript">import {
  Link,
  useLoaderData,
  unstable&#95;useViewTransitionState,
} from "react-router-dom";
/&#42; Other imports &#42;/

const Details = () =&gt; {
  const data = useLoaderData();
  const { id, category, title, author } = data;
  return (
    &lt;&gt;
      &lt;section className="item"&gt;
        {/&#42; ... &#42;/}
        &lt;article className="item&#95;&#95;layout"&gt;
          &lt;div&gt;
              &lt;img</code>
                <code style="font-weight: bold;">style={{viewTransitionName: "item-image"}}</code>
                <code class="language-javascript">className="item&#95;&#95;image"</code>
                <code class="language-javascript">src={`/assets/${category}/${id}-min.jpg`}</code>
                <code class="language-javascript">alt=""</code>
              <code class="language-javascript">/&gt;</code>
          <code class="language-javascript">&lt;/div&gt;</code>
          <code class="language-javascript">{/&#42; ... &#42;/}</code>
        <code class="language-javascript">&lt;/article&gt;</code>
      <code class="language-javascript">&lt;/section&gt;</code>
    <code class="language-javascript">&lt;/&gt;</code>
  <code class="language-javascript">);</code>
<code class="language-javascript">};</code>

<code class="language-javascript">export default Details;
</code></pre>
</div>

<p>We’re on a good track but have two issues that we need to tackle before moving on to the final transitions.</p>

<p>One is that the <code>Card</code> component’s image (<code>.card__image</code>) contains some CSS that applies a fixed one-to-one <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio">aspect ratio</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit">centering</a> for maintaining consistent dimensions no matter what image file is used. Once the user clicks on the <code>Card</code> &mdash; the <code>.card-image</code> in a category view &mdash; it becomes an <code>.item-image</code> in the product view and should transition into its original state, devoid of those extra styles.</p>

<pre><code class="language-css">
/&#42; Card component image &#42;/
.card&#95;&#95;image {
  object-fit: cover;
  object-position: 50% 50%;
  aspect-ratio: 1;
  /&#42; ... &#42;/
}

/&#42; Product view image &#42;/
.item&#95;&#95;image {
 /&#42; No aspect-ratio applied &#42;/
 /&#42; ... &#42;/
}
</code></pre>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898555931"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>In other words, the transition element is unaware of the CSS that is responsible for those styles and is unable to track it on its own. We need to customize it with CSS pseudo-elements in the same way we did in <a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">the previous article of this two-part series</a>.</p>

<p>Jake Archibald shared this <a href="https://developer.chrome.com/docs/web-platform/view-transitions/#handling-changes-in-aspect-ratio">simple and effective CSS snippet</a> for handling the aspect ratio changes. We’re going to use it here with some minor adjustments for our specific use case.</p>

<div class="break-out">
<pre><code class="language-css">/&#42; This is same as in the Jake Archibald's snippet &#42;/
::view-transition-old(item-image),
::view-transition-new(item-image) {
  /&#42; Prevent the default animation,
  so both views remain opacity:1 throughout the transition &#42;/
  animation: none;
  /&#42; Use normal blending,
  so the new view sits on top and obscures the old view &#42;/
  mix-blend-mode: normal;
  /&#42; Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. &#42;/
  height: 100%;
  /&#42; Clip any overflow of the view &#42;/
  overflow: clip;
}

/&#42; Transition from item details page to category page &#42;/
.category::view-transition-old(item-image) {
  object-fit: cover;
}
.category::view-transition-new(item-image) {
  object-fit: contain;
}
/&#42; Transition from category page to item details page &#42;/
.details::view-transition-old(item-image) {
  object-fit: contain;
}
.details::view-transition-new(item-image) {
  object-fit: cover;
}
</code></pre>
</div>

<p>Next, we’ll use the <code>unstable_useViewTransitionState</code> to conditionally set a <code>viewTransitionName</code> on the image only when the user navigates from the product view back to the category page for that product.</p>

<div class="break-out">
<pre><code class="language-javascript">import {
  Link,
  useLoaderData,
  unstable&#95;useViewTransitionState,
} from "react-router-dom";

/&#42; Other imports &#42;/

const Details = () =&gt; {
  const data = useLoaderData();
  const { id, category, title, author } = data;</code>
  <code style="font-weight: bold;">const categoryUrl = `/${category}`;</code>
  <code style="font-weight: bold;">const isTransitioning = unstable&#95;useViewTransitionState(categoryUrl);</code>
  <code class="language-javascript">return (
    &lt;&gt;
      &lt;section className="item"&gt;
        { /&#42; ... &#42;/ }
        &lt;article className="item&#95;&#95;layout"&gt;
          &lt;div&gt;
            &lt;img</code>
              <code style="font-weight: bold;">style=&#123;&#123;</code>
                <code style="font-weight: bold;">viewTransitionName: isTransitioning ? "item-image" : "",</code>
              <code style="font-weight: bold;">&#125;&#125;</code>
              <code class="language-javascript">className="item&#95;&#95;image"</code>
              <code class="language-javascript">src={`/assets/${category}/${id}-min.jpg`}</code>
              <code class="language-javascript">alt=""</code>
            <code class="language-javascript">/&gt;</code>
          <code class="language-javascript">&lt;/div&gt;</code>
          <code class="language-javascript">{/&#42; ... &#42;/}</code>
        <code class="language-javascript">&lt;/article&gt;</code>
      <code class="language-javascript">&lt;/section&gt;</code>
    <code class="language-javascript">&lt;/&gt;</code>
  <code class="language-javascript">);</code>
<code class="language-javascript">};</code>

<code class="language-javascript">export default Details;
</code></pre>
</div>

<p>Let’s keep this example simple and focus solely on how to conditionally toggle the <code>viewTransitionName</code> parameter based on the target URL.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898556113"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h3 id="transitioning-between-image-states">Transitioning Between Image States</h3>

<p>It’s time for the third and final transition we identified for this example: transitioning the product image on a product details page to a larger view. It’s actually less of a transition between views than it is transitioning between two states of the image element.</p>

<p>We can actually leverage the same <a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">UI transition we created for the image gallery in the last article</a>. That article demonstrated how to transition between two snapshots of an element &mdash; its “old” and “new” states &mdash; using a grid of images. Click an image, and it transitions to a larger scale.</p>

<p>The only difference here is that we have to adapt the work we did in that example to React for this example. Otherwise, the main concept remains exactly the same as what we did in the last article.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="674"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png"
			
			sizes="100vw"
			alt="An image transitioning from a default state to a new, larger state."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/view-transitions-api-ui-animations-part2/7-image-transitioning-fromp-default-state-to-larger-state.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Jake has <a href="https://developer.chrome.com/docs/web-platform/view-transitions/#working-with-frameworks">recommended using React’s <code>flushSync</code> function</a> to make this work. The function <a href="https://react.dev/reference/react-dom/flushSync">forces synchronous and immediate DOM update</a><a href="https://react.dev/reference/react-dom/flushSync">s</a> inside a given callback. It’s meant to be used sparingly, but it’s okay to use it for running the View Transition API as the target component re-renders.</p>

<div class="break-out">
<pre><code class="language-javascript">// Assigns view-transition-name to the image before transition runs
const [isImageTransition, setIsImageTransition] = React.useState(false);

// Applies fixed-positioning and full-width image styles as transition runs
const [isFullImage, setIsFullImage] = React.useState(false);

/&#42; ... &#42;/

// State update function, which triggers the DOM update we want to animate
const toggleImageState = () =&gt; setIsFullImage((state) =&gt; !state);

// Click handler function - toggles both states.
const handleZoom = async () =&gt; {
  // Run API only if available.
  if (document.startViewTransition) {
    // Set image as a transition element.
    setIsImageTransition(true);
    const transition = document.startViewTransition(() =&gt; {
      // Apply DOM updates and force immediate re-render while.
      // View Transitions API is running.
      flushSync(toggleImageState);
    });
    await transition.finished;
    // Cleanup
    setIsImageTransition(false);
  } else {
    // Fallback 
    toggleImageState();
  }
};

/&#42; ... &#42;/
</code></pre>
</div>

<p>With this in place, all we really have to do now is toggle class names and view transition names depending on the state we defined in the previous code.</p>

<div class="break-out">
<pre><code class="language-javascript">import React from "react";
import { flushSync } from "react-dom";

/&#42; Other imports &#42;/

const Details = () =&gt; {
  /&#42; React state, click handlers, util functions... &#42;/

  return (
    &lt;&gt;
      &lt;section className="item"&gt;
        {/&#42; ... &#42;/}
        &lt;article className="item&#95;&#95;layout"&gt;
          &lt;div&gt;</code>
            <code class="language-javascript">&lt;button</code> <code style="font-weight: bold;">onClick={handleZoom}</code> <code class="language-javascript">className="item&#95;&#95;toggle"&gt;</code>
              <code class="language-javascript">&lt;img</code>
                <code style="font-weight: bold;">style=&#123;&#123;</code>
                  <code style="font-weight: bold;">viewTransitionName:</code>
                    <code style="font-weight: bold;">isTransitioning || isImageTransition ? "item-image" : "",</code>
                <code style="font-weight: bold;">&#125;&#125;</code>
                <code style="font-weight: bold;">className=&#123;</code>
                  <code style="font-weight: bold;">isFullImage</code>
                    <code style="font-weight: bold;">? "item&#95;&#95;image item&#95;&#95;image--active"</code>
                    <code style="font-weight: bold;">: "item&#95;&#95;image"</code>
                <code style="font-weight: bold;">&#125;</code>
                <code class="language-javascript">src={`/assets/${category}/${id}-min.jpg`}</code>
                <code class="language-javascript">alt=""</code>
              <code class="language-javascript">/&gt;</code>
            <code class="language-javascript">&lt;/button&gt;</code>
          <code class="language-javascript">&lt;/div&gt;</code>
          <code class="language-javascript">{/&#42; ... &#42;/}</code>
        <code class="language-javascript">&lt;/article&gt;</code>
      <code class="language-javascript">&lt;/section&gt;</code>
      <code class="language-javascript">&lt;aside</code>
        <code style="font-weight: bold;">className=&#123;</code>
          <code style="font-weight: bold;">isFullImage ? "item&#95;&#95;overlay item&#95;&#95;overlay--active" : "item&#95;&#95;overlay"</code>
        <code style="font-weight: bold;">&#125;</code>
      <code class="language-javascript">/&gt;</code>
    <code class="language-javascript">&lt;/&gt;</code>
  <code class="language-javascript">);</code>
<code class="language-javascript">};
</code></pre>
</div>

<p>We are applying <code>viewTransitionName</code> directly on the image’s <code>style</code> attribute. We could have used boolean variables to toggle a CSS class and set a <code>view-transition-name</code> in CSS instead. The only reason I went with inline styles is to show both approaches in these examples. You can use whichever approach fits your project!</p>

<p>Let’s round this out by refining styles for the overlay that sits behind the image when it is expanded:</p>

<pre><code class="language-css">.item&#95;&#95;overlay--active {
  z-index: 2;
  display: block;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
}

.item&#95;&#95;image--active {
  cursor: zoom-out;
  position: absolute;
  z-index: 9;
  top: 50%;
  left: 50%;
  transform: translate3d(-50%, -50%, 0);
  max-width: calc(100vw - 4rem);
  max-height: calc(100vh - 4rem);
}
</code></pre>
    

<h3 id="demo-1">Demo</h3>

<p>The following demonstrates only the code that is directly relevant to the View Transitions API so that it is easier to inspect and use. If you want access to the full code, feel free to get it in <a href="https://github.com/codeAdrian/museum-of-digital-wonders">this GitHub repo</a>.</p>

<ul>
<li><a href="https://museum-of-digital-wonders.vercel.app/illustration">Open Live Demo 2</a></li>
</ul>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/898553477"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>We did a lot of work with the View Transitions API in the second half of this brief two-part article series. Together, we implemented full-view transitions in two different contexts, one in a more traditional multi-page application (i.e., website) and another in a single-page application using React.</p>

<p>We started with transitions in a MPA because the process requires fewer dependencies than working with a framework in a SPA. We were able to set the default crossfade transition between two pages &mdash; a category page and a product page &mdash; and, in the process, we learned how to set view transition names on elements <em>after</em> the transition runs to prevent naming conflicts.</p>

<p>From there, we applied the same concept in a SPA, that is, an application that contains one page but many views. We took a React app for a “Museum of Digital Wonders” and applied transitions between full views, such as navigating between a category view and a product view. We got to see how react-router &mdash; and, by extension, react-router-dom &mdash; is used to define transitions bound to specific routes. We used it not only to set a crossfade transition between category views and between category and product views but also to set a view transition name on UI elements that also transition in the process.</p>

<p>The View Transitions API is powerful, and I hope you see that after reading this series and following along with the examples we covered together. What used to take a hefty amount of JavaScript is now a somewhat trivial task, and the result is a smoother user experience that irons out the process of moving from one page or view to another.</p>

<p>That said, the <strong>View Transitions API’s power and simplicity need the same level of care and consideration for accessibility as any other transition or animation on the web</strong>. That includes things like being mindful of user motion preferences and resisting the temptation to put transitions on everything. There’s a fine balance that comes with making accessible interfaces, and motion is certainly included.</p>

<h3 id="references">References</h3>

<ul>
<li><a href="https://drafts.csswg.org/css-view-transitions-1/">CSS View Transitions Module Level 1 Specification</a> (W3C)</li>
<li><a href="https://github.com/WICG/view-transitions/blob/main/explainer.md">View Transitions API Explainer</a> (GitHub repo)</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API">View Transitions API</a> (MDN)</li>
<li>“<a href="https://developer.chrome.com/docs/web-platform/view-transitions/">Smooth And Simple Transitions With The View Transitions API</a>,” Jake Archibald</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item></channel></rss>