<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CSS on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/css/index.xml</link><description>Recent content in CSS on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 09 Feb 2026 03:03:08 +0000</lastBuildDate><item><author>Blake Lundquist</author><title>CSS &lt;code>@scope&lt;/code>: An Alternative To Naming Conventions And Heavy Abstractions</title><link>https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/</link><pubDate>Thu, 05 Feb 2026 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/</guid><description>Prescriptive class name conventions are no longer enough to keep CSS maintainable in a world of increasingly complex interfaces. Can the new &lt;code>@scope&lt;/code> rule finally give developers the confidence to write CSS that can keep up with modern front ends?</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/" />
              <title>CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions</h1>
                  
                    
                    <address>Blake Lundquist</address>
                  
                  <time datetime="2026-02-05T08:00:00&#43;00:00" class="op-published">2026-02-05T08:00:00+00:00</time>
                  <time datetime="2026-02-05T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>When learning the principles of basic CSS, one is taught to write modular, reusable, and descriptive styles to ensure maintainability. But when developers become involved with real-world applications, it often feels impossible to add UI features without styles leaking into unintended areas.</p>

<p>This issue often snowballs into a self-fulfilling loop; styles that are theoretically scoped to one element or class start showing up where they don’t belong. This forces the developer to create even more specific selectors to override the leaked styles, which then accidentally override global styles, and so on.</p>

<p>Rigid class name conventions, such as <a href="https://getbem.com/introduction/">BEM</a>, are one theoretical solution to this issue. The <strong>BEM (Block, Element, Modifier) methodology</strong> is a <a href="https://www.smashingmagazine.com/2012/04/a-new-front-end-methodology-bem/">systematic way of naming CSS classes</a> to ensure reusability and structure within CSS files. Naming conventions like this can <a href="https://www.smashingmagazine.com/2018/06/bem-for-beginners/">reduce cognitive load by leveraging domain language to describe elements and their state</a>, and if implemented correctly, <a href="https://www.smashingmagazine.com/2025/06/css-cascade-layers-bem-utility-classes-specificity-control/">can make styles for large applications easier to maintain</a>.</p>

<p>In the real world, however, it doesn’t always work out like that. Priorities can change, and with change, implementation becomes inconsistent. Small changes to the HTML structure can require many CSS class name revisions. With highly interactive front-end applications, class names following the BEM pattern can become long and unwieldy (e.g., <code>app-user-overview__status--is-authenticating</code>), and not fully adhering to the naming rules breaks the system’s structure, thereby negating its benefits.</p>

<p>Given these challenges, it’s no wonder that developers have turned to frameworks, Tailwind being <a href="https://2024.stateofcss.com/en-US/tools/">the most popular CSS framework</a>. Rather than trying to fight what seems like an unwinnable specificity war between styles, it is easier to give up on the <a href="https://css-tricks.com/the-c-in-css-the-cascade/">CSS Cascade</a> and use tools that guarantee complete isolation.</p>

<h2 id="developers-lean-more-on-utilities">Developers Lean More On Utilities</h2>

<p>How do we know that some developers are keen on avoiding cascaded styles? It’s the rise of “modern” front-end tooling &mdash; like <a href="https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx/">CSS-in-JS frameworks</a> &mdash; designed specifically for that purpose. Working with isolated styles that are tightly scoped to specific components can seem like a breath of fresh air. It removes the need to name things &mdash; <a href="https://24ways.org/2014/naming-things/">still one of the most hated and time-consuming front-end tasks</a> &mdash; and allows developers to be productive without fully understanding or leveraging the benefits of CSS inheritance.</p>

<p>But ditching the CSS Cascade comes with its own problems. For instance, composing styles in JavaScript requires heavy build configurations and often leads to styles awkwardly intermingling with component markup or HTML. Instead of carefully considered naming conventions, we allow build tools to autogenerate selectors and identifiers for us (e.g., <code>.jsx-3130221066</code>), requiring developers to keep up with yet another pseudo-language in and of itself. (As if the cognitive load of understanding what all your component’s <code>useEffect</code>s do weren’t already enough!)</p>

<p>Further abstracting the job of naming classes to tooling means that basic debugging is often constrained to specific application versions compiled for development, rather than leveraging native browser features that support live debugging, such as Developer Tools.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIt%e2%80%99s%20almost%20like%20we%20need%20to%20develop%20tools%20to%20debug%20the%20tools%20we%e2%80%99re%20using%20to%20abstract%20what%20the%20web%20already%20provides%20%e2%80%94%20all%20for%20the%20sake%20of%20running%20away%20from%20the%20%e2%80%9cpain%e2%80%9d%20of%20writing%20standard%20CSS.%0a&url=https://smashingmagazine.com%2f2026%2f02%2fcss-scope-alternative-naming-conventions%2f">
      
It’s almost like we need to develop tools to debug the tools we’re using to abstract what the web already provides — all for the sake of running away from the “pain” of writing standard CSS.

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

<p>Luckily, modern CSS features not only make writing standard CSS more flexible but also give developers like us a great deal more power to manage the cascade and make it work for us. <a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">CSS Cascade Layers</a> are a great example, but there’s another feature that gets a surprising lack of attention &mdash; although that is changing now that it has recently become <strong>Baseline compatible</strong>.</p>

<h2 id="the-css-scope-at-rule">The CSS <code>@scope</code> At-Rule</h2>

<p>I consider the <strong>CSS <code>@scope</code> at-rule</strong> to be a potential cure for the sort of style-leak-induced anxiety we’ve covered, one that does not force us to compromise native web advantages for abstractions and extra build tooling.</p>

<blockquote>“The <code>@scope</code> CSS at-rule enables you to select elements in specific DOM subtrees, targeting elements precisely without writing overly-specific selectors that are hard to override, and without coupling your selectors too tightly to the DOM structure.”<br /><br />&mdash; <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope">MDN</a></blockquote>

<p>In other words, we can work with isolated styles in specific instances <strong>without sacrificing inheritance, cascading, or even the basic separation of concerns</strong> that has been a long-running guiding principle of front-end development.</p>

<p>Plus, it has <a href="https://caniuse.com/css-cascade-scope">excellent browser coverage</a>. In fact, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/146">Firefox 146</a> added support for <code>@scope</code> in December, making it <a href="https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility">Baseline compatible</a> for the first time. Here is a simple comparison between a button using the BEM pattern versus the <code>@scope</code> rule:</p>

<pre><code class="language-html">&lt;!-- BEM --&gt; 
&lt;button class="button button--primary"&gt;
  &lt;span class="button&#95;&#95;text"&gt;Click me&lt;/span&gt;
  &lt;span class="button&#95;&#95;icon"&gt;→&lt;/span&gt;
&lt;/button&gt;

&lt;style&gt;
  .button .button&#95;&#95;text { /&#42; button text styles &#42;/ }
  .button .button&#95;&#95;icon { /&#42; button icon styles &#42;/ }
  .button--primary { primary button styles &#42;/ }
&lt;/style&gt;
</code></pre>

<pre><code class="language-html">&lt;!-- @scope --&gt; 
&lt;button class="primary-button"&gt;
  &lt;span&gt;Click me&lt;/span&gt;
  &lt;span&gt;→&lt;/span&gt;
&lt;/button&gt;

&lt;style&gt;
  @scope (.primary-button) {
    span:first-child { /&#42; button text styles &#42;/ }
    span:last-child { /&#42; button icon styles &#42;/ }
  }
&lt;/style&gt;
</code></pre>

<p>The <code>@scope</code> rule allows for <strong>precision with less complexity</strong>. The developer no longer needs to create boundaries using class names, which, in turn, allows them to write selectors based on native HTML elements, thereby eliminating the need for prescriptive CSS class name patterns. By simply removing the need for class name management, <code>@scope</code> can alleviate the fear associated with CSS in large projects.</p>

<h2 id="basic-usage">Basic Usage</h2>

<p>To get started, add the <code>@scope</code> rule to your CSS and insert a root selector to which styles will be scoped:</p>

<pre><code class="language-css">@scope (&lt;selector&gt;) {
  /&#42; Styles scoped to the &lt;selector&gt; &#42;/
}
</code></pre>

<p>So, for example, if we were to scope styles to a <code>&lt;nav&gt;</code> element, it may look something like this:</p>

<div class="break-out">
<pre><code class="language-css">@scope (nav) {
  a { /&#42; Link styles within nav scope &#42;/ }

  a:active { /&#42; Active link styles &#42;/ }

  a:active::before { /&#42; Active link with pseudo-element for extra styling &#42;/ }

  @media (max-width: 768px) {
    a { /&#42; Responsive adjustments &#42;/ }
  }
}
</code></pre>
</div>

<p>This, on its own, is not a groundbreaking feature. However, a second argument can be added to the scope to create a <strong>lower boundary</strong>, effectively defining the scope’s start and end points.</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Any `a` element inside `ul` will not have the styles applied &#42;/
@scope (nav) to (ul) {
  a {
    font-size: 14px;
  }
}
</code></pre>
</div>

<p>This practice is called <strong>donut scoping</strong>, and <a href="https://css-tricks.com/solved-by-css-donuts-scopes/">there are several approaches</a> one could use, including a series of similar, highly specific selectors coupled tightly to the DOM structure, a <code>:not</code> pseudo-selector, or assigning specific class names to <code>&lt;a&gt;</code> elements within the <code>&lt;nav&gt;</code> to handle the differing CSS.</p>

<p>Regardless of those other approaches, the <code>@scope</code> method is much more concise. More importantly, it prevents the risk of broken styles if classnames change or are misused or if the HTML structure were to be modified. Now that <code>@scope</code> is Baseline compatible, we no longer need workarounds!</p>

<p>We can take this idea further with multiple end boundaries to create a “style figure eight”:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Any &lt;a&gt; or &lt;p&gt; element inside &lt;aside&gt; or &lt;nav&gt; will not have the styles applied &#42;/
@scope (main) to (aside, nav) {
  a {
    font-size: 14px;
  }
  p {
    line-height: 16px;
    color: darkgrey;
  }
}
</code></pre>
</div>

<p>Compare that to a version handled without the <code>@scope</code> rule, where the developer has to “reset” styles to their defaults:</p>

<div class="break-out">
<pre><code class="language-css">main a {
  font-size: 14px;
}

main p {
  line-height: 16px;
  color: darkgrey;
}

main aside a,
main nav a {
  font-size: inherit; /&#42; or whatever the default should be &#42;/
}

main aside p,
main nav p {
  line-height: inherit; /&#42; or whatever the default should be &#42;/
  color: inherit; /&#42; or a specific color &#42;/
}
</code></pre>
</div>

<p>Check out the following example. Do you notice how simple it is to target some nested selectors while exempting others?</p>

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

<p>Consider a scenario where unique styles need to be applied to slotted content within <a href="https://www.smashingmagazine.com/2025/07/web-components-working-with-shadow-dom/">web components</a>. When slotting content into a web component, that content becomes part of the Shadow DOM, but still inherits styles from the parent document. The developer might want to implement different styles depending on which web component the content is slotted into:</p>

<pre><code class="language-html">&lt;!-- Same &lt;user-card&gt; content, different contexts --&gt;
&lt;product-showcase&gt;
  &lt;user-card slot="reviewer"&gt;
    &lt;img src="avatar.jpg" slot="avatar"&gt;
    &lt;span slot="name"&gt;Jane Doe&lt;/span&gt;
  &lt;/user-card&gt;
&lt;/product-showcase&gt;

&lt;team-roster&gt;
  &lt;user-card slot="member"&gt;
    &lt;img src="avatar.jpg" slot="avatar"&gt;
    &lt;span slot="name"&gt;Jane Doe&lt;/span&gt;
  &lt;/user-card&gt;
&lt;/team-roster&gt;
</code></pre>

<p>In this example, the developer might want the <code>&lt;user-card&gt;</code> to have distinct styles only if it is rendered inside <code>&lt;team-roster&gt;</code>:</p>

<pre><code class="language-css">@scope (team-roster) {
  user-card {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }
  
  user-card img {
    border-radius: 50%;
    width: 40px;
    height: 40px;
  }
}
</code></pre>

<h2 id="more-benefits">More Benefits</h2>

<p>There are additional ways that <code>@scope</code> can remove the need for class management without resorting to utilities or JavaScript-generated class names. For example, <code>@scope</code> opens up the possibility to easily <strong>target descendants of any selector</strong>, not just class names:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Only div elements with a direct child button are included in the root scope &#42;/
@scope (div:has(&gt; button)) {
  p {
    font-size: 14px;
  }
}
</code></pre>
</div>

<p>And they <strong>can be nested</strong>, creating scopes within scopes:</p>

<pre><code class="language-css">@scope (main) {
  p {
    font-size: 16px;
    color: black;
  }
  @scope (section) {
    p {
      font-size: 14px;
      color: blue;
    }
    @scope (.highlight) {
      p {
        background-color: yellow;
        font-weight: bold;
      }
    }
  }
}
</code></pre>

<p>Plus, the root scope can be easily referenced within the <code>@scope</code> rule:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Applies to elements inside direct child `section` elements of `main`, but stops at any direct `aside` that is a direct chiled of those sections &#42;/
@scope (main &gt; section) to (:scope &gt; aside) {
  p {
    background-color: lightblue;
    color: blue;
  }
  /&#42; Applies to ul elements that are immediate siblings of root scope  &#42;/
  :scope + ul {
    list-style: none;
  }
}
</code></pre>
</div>

<p>The <code>@scope</code> at-rule also introduces a new <strong>proximity</strong> dimension to CSS specificity resolution. In traditional CSS, when two selectors match the same element, the selector with the higher specificity wins. With <code>@scope</code>, when two elements have equal specificity, the one whose scope root is closer to the matched element wins. This eliminates the need to override parent styles by manually increasing an element’s specificity, since inner components naturally supersede outer element styles.</p>

<div class="break-out">
<pre><code class="language-html">&lt;style&gt;
  @scope (.container) {
    .title { color: green; } 
  }
  &lt;!-- The &lt;h2&gt; is closer to .container than to .sidebar so "color: green" wins. --&gt;
  @scope (.sidebar) {
    .title { color: red; }
  }
&lt;/style&gt;

&lt;div class="sidebar"&gt;
  &lt;div class="container"&gt;
    &lt;h2 class="title"&gt;Hello&lt;/h2&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
</div>
    

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

<p>Utility-first CSS frameworks, such as Tailwind, work well for prototyping and smaller projects. Their benefits quickly diminish, however, when used in larger projects involving more than a couple of developers.</p>

<p>Front-end development has become increasingly overcomplicated in the last few years, and CSS is no exception. While the <code>@scope</code> rule isn’t a cure-all, it can reduce the need for complex tooling. When used in place of, or alongside strategic class naming, <code>@scope</code> can make it easier and more fun to write maintainable CSS.</p>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope">CSS <code>@scope</code></a> (MDN)</li>
<li>“<a href="https://css-tricks.com/almanac/rules/s/scope/">CSS <code>@scope</code></a>”, Juan Diego Rodríguez (CSS-Tricks)</li>
<li><a href="https://www.firefox.com/en-US/firefox/146.0/releasenotes/">Firefox 146 Release Notes</a> (Firefox)</li>
<li><a href="https://caniuse.com/css-cascade-scope">Browser Support</a> (CanIUse)</li>
<li><a href="https://2024.stateofcss.com/en-US/tools/">Popular CSS Frameworks</a> (State of CSS 2024)</li>
<li>“<a href="https://css-tricks.com/the-c-in-css-the-cascade/">The “C” in CSS: Cascade</a>”, Thomas Yip (CSS-Tricks)</li>
<li><a href="https://getbem.com/introduction/">BEM Introduction</a> (Get BEM)</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>Gabriel Shoyombo</author><title>Unstacking CSS Stacking Contexts</title><link>https://www.smashingmagazine.com/2026/01/unstacking-css-stacking-contexts/</link><pubDate>Tue, 27 Jan 2026 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/01/unstacking-css-stacking-contexts/</guid><description>In CSS, we can create “stacking contexts” where elements are visually placed one on top of the next in a three-dimensional sense that creates the perception of depth. Stacking contexts are incredibly useful, but they’re also widely misunderstood and often mistakenly created, leading to a slew of layout issues that can be tricky to solve.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/01/unstacking-css-stacking-contexts/" />
              <title>Unstacking CSS Stacking Contexts</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Unstacking CSS Stacking Contexts</h1>
                  
                    
                    <address>Gabriel Shoyombo</address>
                  
                  <time datetime="2026-01-27T10:00:00&#43;00:00" class="op-published">2026-01-27T10:00:00+00:00</time>
                  <time datetime="2026-01-27T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Have you ever set <code>z-index: 99999</code> on an element in your CSS, and it doesn’t come out on top of other elements? A value that large should easily place that element visually on top of anything else, assuming all the different elements are set at either a lower value or not set at all.</p>

<p>A webpage is usually represented in a two-dimensional space; however, by applying specific CSS properties, an imaginary z-axis plane is introduced to convey depth. This plane is perpendicular to the screen, and from it, the user perceives the order of elements, one on top of the other. The idea behind the imaginary z-axis, the user’s perception of stacked elements, is that the CSS properties that create it combine to form what we call a <strong>stacking context</strong>.</p>

<p>We’re going to talk about how elements are “stacked” on a webpage, what controls the stacking order, and practical approaches to “unstack” elements when needed.</p>

<h2 id="about-stacking-contexts">About Stacking Contexts</h2>

<p>Imagine your webpage as a desk. As you add HTML elements, you’re laying pieces of paper, one after the other, on the desk. The last piece of paper placed is equivalent to the most recently added HTML element, and it sits on top of all the other papers placed before it. This is the normal document flow, even for nested elements. The desk itself represents the root stacking context, formed by the <code>&lt;html&gt;</code> element, which contains all other folders.</p>

<p>Now, specific CSS properties come into play.</p>

<p>Properties like <code>position</code> (with <code>z-index</code>), <code>opacity</code>, <code>transform</code>, and <code>contain</code>) act like a folder. This folder takes an element and all of its children, extracts them from the main stack, and groups them into a separate sub-stack, creating what we call a <strong>stacking context</strong>. For positioned elements, this happens when we declare a <code>z-index</code> value other than <code>auto</code>. For properties like <code>opacity</code>, <code>transform</code>, and <code>filter</code>, the stacking context is created automatically when specific values are applied.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="436"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png"
			
			sizes="100vw"
			alt="Before (global stacking order) and after (stacking context order)"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      When the browser decides what goes on top, it stacks the folders first, not the individual papers inside them. This is “The Golden Rule” of stacking contexts that many developers miss. (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png'>Large preview</a>)
    </figcaption>
  
</figure>

<blockquote>Try to understand this: Once a piece of paper (i.e., a child element) is inside a folder (i.e., the parent’s stacking context), it can never exit that folder or be placed between papers in a different folder. Its <code>z-index</code> is now only relevant inside its own folder.</blockquote>

<p>In the illustration below, Paper B is now within the stacking context of Folder B, and can only be ordered with other papers in the folder.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="436"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png"
			
			sizes="100vw"
			alt="Before (global stacking order) and after (stacking context order)"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Imagine, if you will, that you have two folders on your desk:</p>

<pre><code class="language-html">&lt;div class="folder-a"&gt;Folder A&lt;/div&gt;
&lt;div class="folder-b"&gt;Folder B&lt;/div&gt;
</code></pre>

<pre><code class="language-css">.folder-a { z-index: 1; }
.folder-b { z-index: 2; }
</code></pre>

<p>Let’s update the markup a bit. Inside Folder A is a special page, <code>z-index: 9999</code>. Inside Folder B is a plain page, <code>z-index: 5</code>.</p>

<pre><code class="language-html">&lt;div class="folder-a"&gt;
   &lt;div class="special-page"&gt;Special Page&lt;/div&gt;
&lt;/div&gt;

&lt;div class="folder-b"&gt;
  &lt;div class="plain-page"&gt;Plain Page&lt;/div&gt;
&lt;/div&gt;
</code></pre>

<pre><code class="language-css">.special-page { z-index: 9999; }
.plain-page { z-index: 5; }
</code></pre>

<p>Which page is on top?</p>

<p>It’s the <code>.plain-page</code> in Folder B. The browser ignores the child papers and stacks the two folders first. It sees Folder B (<code>z-index: 2</code>) and places it on top of Folder A (<code>z-index: 1</code>) because we know that two is greater than one. Meanwhile, the <code>.special-page</code> set to <code>z-index: 9999</code> page is at the bottom of the stack even though its <code>z-index</code> is set to the highest possible value.</p>

<p>Stacking contexts can also be nested (folders inside folders), creating a “family tree.” The same principle applies: a child can never escape its parents’ folder.</p>

<p>Now that you get how stacking contexts behave like folders that group and reorder layers, it’s worth asking: why do certain properties &mdash; like <code>transform</code> and <code>opacity</code> &mdash; create new stacking contexts?</p>

<p>Here’s the thing: these properties don’t create stacking contexts because of how they look; they do it because of how the browser works under the hood. When you apply <code>transform</code>, <code>opacity</code>, <code>filter</code>, or <code>perspective</code>, you’re telling the browser, <em>“Hey, this element might move, rotate, or fade, so be ready!”</em></p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="533"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png"
			
			sizes="100vw"
			alt="Diagram illustrating the main document layout with an applied transform that creates a new stacking context, which in turn, runs on the GPU to handle the transformation. It indicates that a new stacking context is promoted for performance."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>When you use these properties, the browser creates a new stacking context to manage rendering more efficiently. This allows the browser to handle animations, transforms, and visual effects independently, reducing the need to recalculate how these elements interact with the rest of the page. Think of it as the browser saying, <em>“I’ll handle this folder separately so I don’t have to reshuffle the entire desk every time something inside it changes.”</em></p>

<p>But there’s a side effect. Once the browser lifts an element into its own layer, it must “flatten” everything within it, creating a new stacking context. It’s like taking a folder off the desk to handle it separately; everything inside that folder gets grouped, and the browser now treats it as a single unit when deciding what sits on top of what.</p>

<p>So even though the <code>transform</code> and <code>opacity</code> properties might not appear to affect the way that elements stack visually, they do, and it’s for performance optimisation. Several other CSS properties can also create stacking contexts for similar reasons. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts">MDN provides a complete list</a> if you want to dig deeper. There are quite a few, which only illustrates how easy it is to inadvertently create a stacking context without knowing it.</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-unstacking-problem">The “Unstacking” Problem</h2>

<p>Stacking issues can arise for many reasons, but some are more common than others. Modal components are a classic pattern because they require toggling the component to “open” on a top layer above all other elements, then removing it from the top layer when it is “closed.”</p>

<p>I’m pretty confident that all of us have run into a situation where we open a modal and, for whatever reason, it doesn’t appear. It’s not that it didn’t open properly, but that it is out of view in a lower layer of the stacking context.</p>

<p>This leaves you to wonder “how come?” since you set:</p>

<div class="break-out">
<pre><code class="language-css">.overlay {
  position: fixed; /&#42; creates the stacking context &#42;/
  z-index: 1; /&#42; puts the element on a layer above everything else &#42;/
  inset: 0; 
  width: 100%; 
  height: 100vh; 
  overflow: hidden;
  background-color: &#35;00000080;
}
</code></pre>
</div>

<p>This looks correct, but if the parent element containing the modal trigger is a child element within another parent element that’s also set to <code>z-index: 1</code>, that technically places the modal in a sublayer obscured by the main folder. Let’s look at that specific scenario and a couple of other common stacking-context pitfalls. I think you’ll see not only how easy it is to inadvertently create stacking contexts, but also how to mismanage them. Also, how you return to a managed state depends on the situation.</p>

<h3 id="scenario-1-the-trapped-modal">Scenario 1: The Trapped Modal</h3>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="pvbddjd"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 1: The Trapped Modal (Problem) [forked]](https://codepen.io/smashingmag/pen/pvbddjd) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvbddjd">Scenario 1: The Trapped Modal (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<p>If you click the “Open Modal” button in the header, you’ll notice that the overlay and modal appear behind the main content. This is because the modal is a child of the header container, which has a lower stacking context order (<code>z-index: 1</code>) than the main container (<code>z-index</code> of <code>2</code>). Despite the modal overlay and the modal having <code>z-index</code> values of <code>9998</code> and <code>9999</code>, respectively, the main container with a <code>z-index: 2</code> still sits right above them.</p>

<h3 id="scenario-2-the-submerged-dropdown">Scenario 2: The Submerged Dropdown</h3>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="zxBPPvm"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 2: The Submerged Dropdown (Problem) [forked]](https://codepen.io/smashingmag/pen/zxBPPvm) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxBPPvm">Scenario 2: The Submerged Dropdown (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<p>Here, we have a similar issue with the first scenario. When you hover over the “services” link, the dropdown shows, but behind the main container. I intentionally set the main container’s <code>margin-top</code> to <code>20px</code> to make the dropdown visible enough for you to see it appear, but keep it just behind the main container. This is another popular issue front-end developers encounter, stemming from context stacking. While it is similar to the first scenario, there’s another approach to resolving it, which will be explored soon.</p>

<h3 id="scenario-3-the-clipped-tooltip">Scenario 3: The Clipped Tooltip</h3>

<p>Now, this is an interesting one. It’s not about which element has the higher <code>z-index</code>. It’s about <code>overflow: hidden</code> doing <a href="https://www.smashingmagazine.com/2021/04/css-overflow-issues/">what it’s designed to do</a>: preventing content from visually escaping its container, even when that content has <code>z-index: 1000</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="GgqOOoo"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 3: The Clipped Tooltip (Problem) [forked]](https://codepen.io/smashingmag/pen/GgqOOoo) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgqOOoo">Scenario 3: The Clipped Tooltip (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<p>Who would have thought <code>overflow: hidden</code> could stop a <code>z-index: 1000</code>? Well, it did stop it, as you can see in the Codepen above.</p>

<p>I think developers trust <code>z-index</code> so much that they expect it to pull them out of any obscurity issue, but in reality, it doesn’t work that way. Not that it isn’t powerful, it’s just that other factors determine its ability to push your element to the top.</p>

<p>Before you slap <code>z-index</code> on that element, remember that while this might get you out of the current jam, it might also throw you into a greater one <a href="https://www.matuzo.at/blog/2025/never-lose-a-z-index-battle-again">that even <code>z-index: infinity</code> won’t get you out of</a>.</p>

<p>Let’s try to understand the problem before attempting to fix it.</p>

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

<h2 id="identifying-the-trapped-layer">Identifying The Trapped Layer</h2>

<p>When you encounter an issue such as those listed above, it is helpful to know that the element isn’t possessed; instead, an ancestor has sinned, and the child is paying the debt. In non-spiritual English terms, the obscured element isn’t the problem; an ancestor element has created a lower-level stacking context that has led the children to be below the children of a parent with a higher-level stacking context.</p>

<p>A good way to track and find that parent is to descend into the browser’s devtools to inspect the element and make your way up, checking each parent level to see which has a property or properties that trigger a stacking context, and find out its position in the order compared to sibling elements. Let’s create a checklist to order our steps.</p>

<h3 id="your-debugging-checklist">Your Debugging Checklist</h3>

<ol>
<li><strong>Inspect the Problem Element.</strong><br />
Right-click your hidden element (the modal, the dropdown menu, the tooltip) and click “Inspect.”</li>
<li><strong>Check its Styles.</strong><br />
In the “Styles” or “Computed” pane, verify that it has the expected high <code>z-index</code> (e.g., <code>z-index: 9999;</code>).</li>
<li><strong>Climb the DOM Tree.</strong><br />
In the “Elements” panel, look at the element’s immediate parent. Click on it.</li>
<li><strong>Investigate the Parent’s Styles.</strong><br />
Look at the parent’s CSS in the “Styles” pane. You are now hunting for any property that creates a new stacking context. Look for any properties related to positioning, visual effects, and containment.</li>
<li><strong>Repeat.</strong><br />
If the immediate parent is clean, click on its parent (the grandparent of your element). Repeat Step 4. Keep climbing the DOM tree, one parent at a time, until you find the culprit.</li>
</ol>

<p>Now, let’s apply this checklist to our three scenarios.</p>

<h3 id="problem-1-the-trapped-modal">Problem 1: The Trapped Modal</h3>

<ol>
<li><strong>Inspect:</strong> We inspect the <code>.modal-content</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 9999</code>. That’s not the problem.</li>
<li><strong>Climb:</strong> We look at its parent, <code>.modal-container</code>. It has no trapping properties.</li>
<li><strong>Climb Again:</strong> We look at its parent, the <strong><code>.header</code></strong>.</li>
<li><strong>Investigate:</strong> We check the styles for <code>.header</code> and find the culprit: <code>position: absolute</code> and <code>z-index: 1</code>. This element is creating a stacking context. We’ve seen our trap! The modal’s <code>z-index: 9999</code> is “trapped” inside a <code>z-index: 1</code> folder.</li>
</ol>

<h3 id="problem-2-the-submerged-dropdown">Problem 2: The Submerged Dropdown</h3>

<ol>
<li><strong>Inspect:</strong> We inspect the <code>.dropdown-menu</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 100</code>.</li>
<li><strong>Climb:</strong> We check its parent <code>li</code>, then its parent <code>ul</code>, then its parent <strong><code>.navbar</code></strong>.</li>
<li>Investigate: We find <code>.navbar</code> has <code>position: relative</code> and <code>z-index: 1</code>. This creates Stacking Context A.</li>
<li><strong>Analyse Siblings:</strong> This isn’t the whole story. Why is it under the content? We now inspect the sibling of <code>.navbar</code>, which is <code>.content</code>. We find it has <code>position: relative</code> and <code>z-index: 2</code> (Stacking Context B). The browser is stacking the “folders”: <code>.content</code> (2) on top of <code>.navbar</code> (1). We’ve found the root cause.</li>
</ol>

<h3 id="problem-3-the-clipped-tooltip">Problem 3: The Clipped Tooltip</h3>

<ol>
<li><strong>Inspect:</strong> We inspect the <code>.tooltip</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 1000</code>.</li>
<li><strong>Climb:</strong> We check its parent, <code>.tooltip-trigger</code>. It’s fine.</li>
<li><strong>Climb Again:</strong> We check its parent, the <strong><code>.card-container</code></strong>.</li>
<li><strong>Investigate:</strong> We scan its styles and find the culprit: <code>overflow: hidden</code>. This is a special type of trap. It clips any child that tries to render outside its boundaries, regardless of <code>z-index</code> values.</li>
</ol>

<h3 id="advanced-tooling">Advanced Tooling</h3>

<p>While climbing the DOM tree works, it can be slow. Here are tools that speed things up.</p>

<h4 id="devtools-3d-view">DevTools 3D View</h4>

<p>Some browsers, such as Microsoft Edge (in the “More Tools” menu) and Firefox (in the “Inspector” tab), include a “3D View” or “Layers” panel. This tool is a lifesaver. It visually explodes the webpage into its different layers, showing you exactly how the stacking contexts are grouped.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="534"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png"
			
			sizes="100vw"
			alt="Microsoft Edge 3D Stacking Context View"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Microsoft Edge 3D Stacking Context View. (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can immediately see your modal trapped in a low-level layer and identify the parent.</p>

<h4 id="browser-extensions">Browser Extensions</h4>

<p>Smart developers have built extensions to help. Tools like this <a href="https://chrome.google.com/webstore/detail/z-context/jigamimbjojkdgnlldajknogfgncplbhhttps://chrome.google.com/webstore/detail/z-context/jigamimbjojkdgnlldajknogfgncplbh">“CSS Stacking Context Inspector” Chrome extension</a> add an extra <code>z-index</code> tab to your DevTools to show you information about elements that create a stacking context.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="341"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png"
			
			sizes="100vw"
			alt="CSS Stacking Context Inspector Chrome extension"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h4 id="ide-extensions">IDE Extensions</h4>

<p>You can even spot issues during development with an extension <a href="https://marketplace.visualstudio.com/items?itemName=mikerheault.vscode-better-css-stacking-contexts">like this one for VS Code</a>, which highlights potential stacking context issues directly in your editor.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="468"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png"
			
			sizes="100vw"
			alt="Better CSS Stacking Contexts Extension"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Better CSS Stacking Contexts Extension. (<a href='https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="unstacking-and-regaining-control">Unstacking And Regaining Control</h2>

<p>After we’ve identified the root cause, the next step is to deal with it. There are several approaches you can take to tackle this problem, and I’ll list them in order. You can choose anyone at any level, though; no one can complain or obstruct another.</p>

<h3 id="change-the-html-structure">Change The HTML Structure</h3>

<p>This is considered the optimal fix. For you to run into a stacking context issue, you must have placed some elements in funny positions within your HTML. Restructuring the page will help you reshape the DOM and eliminate the stacking context problem. Find the problematic element and remove it from the trapping element in the HTML markup. For instance, we can solve the first scenario, “The Trapped Modal,” by moving the <code>.modal-container</code> out of the header and placing it in the <code>&lt;body&gt;</code> element by itself.</p>

<div class="break-out">
<pre><code class="language-html">&lt;header class="header"&gt;
  &lt;h2&gt;Header&lt;/h2&gt;
  &lt;button id="open-modal"&gt;Open Modal&lt;/button&gt;
  &lt;!-- Former position --&gt;
&lt;/header&gt;
&lt;main class="content"&gt;
  &lt;h1&gt;Main Content&lt;/h1&gt;
  &lt;p&gt;This content has a z-index of 2 and will still not cover the modal.&lt;/p&gt;
&lt;/main&gt;

&lt;!-- New position  --&gt;
&lt;div id="modal-container" class="modal-container"&gt;
  &lt;div class="modal-overlay"&gt;&lt;/div&gt;
  &lt;div class="modal-content"&gt;
    &lt;h3&gt;Modal Title&lt;/h3&gt;
    &lt;p&gt;Now, I'm not behind anything. I've gotten a better position as a result of DOM restructuring.&lt;/p&gt;
    &lt;button id="close-modal"&gt;Close&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
</div> 

<p>When you click the “Open Modal” button, the modal is positioned in front of everything else as it’s supposed to be.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="azZVVNP"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 1: The Trapped Modal (Solution) [forked]](https://codepen.io/smashingmag/pen/azZVVNP) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/azZVVNP">Scenario 1: The Trapped Modal (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<h3 id="adjust-the-parent-stacking-context-in-css">Adjust The Parent Stacking Context In CSS</h3>

<p>What if the element is one you can’t move without breaking the layout? It’s better to address the issue: <strong>the parent establishes the context.</strong> Find the CSS property (or properties) responsible for triggering the context and remove it. If it has a purpose and cannot be removed, give the parent a higher <code>z-index</code> value than its sibling elements to lift the entire container. With a higher <code>z-index</code> value, the parent container moves to the top, and its children appear closer to the user.</p>

<p>Based on what we learned in “<a href="/scl/fi/ue0rufxffviprc9858j25/Debugging-CSS-Stacking-Contexts.paper?rlkey=ezbdaiq6mojvb7xzezxlds29b&amp;dl=0#:uid=376729122027939635792428&amp;h2=Problem-2:-The-Submerged-Dropd">The Submerged Dropdown</a>” scenario, we can’t move the dropdown out of the navbar; it wouldn’t make sense. However, we can increase the <code>z-index</code> value of the <code>.navbar</code> container to be greater than the <code>.content</code> element’s <code>z-index</code> value.</p>

<pre><code class="language-css">.navbar {
  background: &#35;333;
  /&#42; z-index: 1; &#42;/
  z-index: 3;
  position: relative;
}
</code></pre>

<p>With this change, the <code>.dropdown-menu</code> now appears in front of the content without any issue.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YPWEEWz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 2: The Submerged Dropdown (Solution) [forked]](https://codepen.io/smashingmag/pen/YPWEEWz) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YPWEEWz">Scenario 2: The Submerged Dropdown (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<h3 id="try-portals-if-using-a-framework">Try Portals, If Using A Framework</h3>

<p>In frameworks like <a href="https://react.dev/reference/react-dom/createPortal">React</a> or <a href="https://www.digitalocean.com/community/tutorials/vuejs-portal-vue">Vue</a>, a Portal is a feature that lets you render a component outside its normal parent hierarchy in the DOM. Portals are like a teleportation device for your components. They let you render a component’s HTML anywhere in the document (typically right into <code>document.body</code>) while keeping it logically connected to its original parent for props, state, and events. This is perfect for escaping stacking context traps since the rendered output literally appears outside the problematic parent container.</p>

<pre><code class="language-javascript">ReactDOM.createPortal(
  &lt;ToolTip /&gt;,
  document.body
);
</code></pre>

<p>This ensures your dropdown content isn’t hidden behind its parent, even if the parent has <code>overflow: hidden</code> or a lower <code>z-index</code>.</p>

<p>In the “The Clipped Tooltip” scenario we looked at earlier, I used a Portal to rescue the tooltip from the <code>overflow: hidden</code> clip by placing it in the document body and positioning it above the trigger within the container.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="myEqqEe"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scenario 3: The Clipped Tooltip (Solution) [forked]](https://codepen.io/smashingmag/pen/myEqqEe) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myEqqEe">Scenario 3: The Clipped Tooltip (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

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

<h2 id="introducing-stacking-context-without-side-effects">Introducing Stacking Context Without Side Effects</h2>

<p>All the approaches explained in the previous section are aimed at “unstacking” elements from problematic stacking contexts, but there are some situations where you’ll actually need or want to create a stacking context.</p>

<p>Creating a new stacking context is easy, but all approaches come with a side effect. That is, except for using <a href="https://css-tricks.com/almanac/properties/i/isolation/"><code>isolation: isolate</code></a>. When applied to an element, the stacking context of that element’s children is determined relative to each child and within that context, rather than being influenced by elements outside of it. A classic example is assigning that element a negative value, such as <code>z-index: -1</code>.</p>

<p>Imagine you have a <code>.card</code> component. You want to add a decorative shape that sits behind the <code>.card</code>’s text, but on top of the card’s background. Without a stacking context on the card, <code>z-index: -1</code> sends the shape to the bottom of the root stacking context (the whole page). This makes it disappear behind the <code>.card</code>’s white background:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="QwEOOEM"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Negative z-index (problem) [forked]](https://codepen.io/smashingmag/pen/QwEOOEM) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/QwEOOEM">Negative z-index (problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<p>To solve this, we declare <code>isolation: isolate</code> on the parent <code>.card</code>:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="MYeOOeG"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Negative z-index (solution) [forked]](https://codepen.io/smashingmag/pen/MYeOOeG) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/MYeOOeG">Negative z-index (solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption>
</figure>

<p>Now, the <code>.card</code> element itself becomes a stacking context. When its child element &mdash; the decorative shape created on the <code>:before</code> pseudo-element &mdash; has <code>z-index: -1</code>, it goes to the very bottom of the parent’s stacking context. It sits perfectly behind the text and on top of the card’s background, as intended.</p>

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

<p>Remember: the next time your <code>z-index</code> seems out of control, it’s a trapped stacking context.</p>

<h3 id="references">References</h3>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context">Stacking context</a> (MDN)</li>
<li><a href="https://web.dev/learn/css/z-index">Z-index and stacking contexts</a> (web.dev)</li>
<li>“<a href="https://www.freecodecamp.org/news/the-css-isolation-property/">How to Create a New Stacking Context with the Isolation Property in CSS</a>”, Natalie Pina</li>
<li>“<a href="https://www.joshwcomeau.com/css/stacking-contexts/">What The Heck, z-index??</a>”, Josh Comeau</li>
</ul>

<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>

<ul>
<li>“<a href="https://www.smashingmagazine.com/2021/02/css-z-index-large-projects/">Managing CSS Z-Index In Large Projects</a>”, Steven Frieson</li>
<li>“<a href="https://www.smashingmagazine.com/2024/09/sticky-headers-full-height-elements-tricky-combination/">Sticky Headers And Full-Height Elements: A Tricky Combination</a>”, Philip Braunen</li>
<li>“<a href="https://www.smashingmagazine.com/2019/04/z-index-component-based-web-application/">Managing Z-Index In A Component-Based Web Application</a>”, Pavel Pomerantsev</li>
<li>“<a href="https://www.smashingmagazine.com/2009/09/the-z-index-css-property-a-comprehensive-look/">The Z-Index CSS Property: A Comprehensive Look</a>”, Louis Lazaris</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 8: Theming Animations Using CSS Relative Colour</title><link>https://www.smashingmagazine.com/2026/01/smashing-animations-part-8-css-relative-colour/</link><pubDate>Wed, 14 Jan 2026 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/01/smashing-animations-part-8-css-relative-colour/</guid><description>CSS relative colour values are now widely supported. In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk/">Andy Clarke&lt;/a> shares practical techniques for using them to theme and animate SVG graphics.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/01/smashing-animations-part-8-css-relative-colour/" />
              <title>Smashing Animations Part 8: Theming Animations Using CSS Relative Colour</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 8: Theming Animations Using CSS Relative Colour</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2026-01-14T10:00:00&#43;00:00" class="op-published">2026-01-14T10:00:00+00:00</time>
                  <time datetime="2026-01-14T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>I’ve recently refreshed the animated graphics on <a href="https://stuffandnonsense.co.uk/">my website</a> with a new theme and a group of pioneering characters, putting into practice plenty of the techniques I shared in <a href="https://www.smashingmagazine.com/author/andy-clarke/">this series</a>. A few of my animations change appearance when someone interacts with them or at different times of day.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/blog">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="341"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png"
			
			sizes="100vw"
			alt="Graphics from Andy’s website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this animated SVG on <a href='https://stuffandnonsense.co.uk/blog'>my website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The colours in the graphic atop <a href="https://stuffandnonsense.co.uk/blog">my blog pages</a> change from morning until night every day. Then, there’s the <a href="https://stuffandnonsense.co.uk/blog/let-it-snow">snow mode</a>, which adds chilly colours and a wintery theme, courtesy of an overlay layer and a blending mode.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="359"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png"
			
			sizes="100vw"
			alt="Snow mode applied to the town background"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Snow mode allows my pioneer town background to adapt throughout the day. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>While working on this, I started to wonder whether CSS relative colour values could give me more control while also simplifying the process.</p>

<p><strong>Note</strong>: <em>In this tutorial, I’ll focus on relative colour values and the OKLCH colour space for theming graphics and animations. If you want to dive deep into relative colour, Ahmad Shadeed created a superb <a href="https://ishadeed.com/article/css-relative-colors/">interactive guide</a>. As for colour spaces, gamuts, and OKLCH, our own Geoff Graham <a href="https://www.smashingmagazine.com/2023/08/oklch-color-spaces-gamuts-css/">wrote</a> about them.</em></p>

<div class="refs">
  <ul><li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/"><strong>Smashing Animations Part 1</strong>: 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/"><strong>Smashing Animations Part 2</strong>: 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/"><strong>Smashing Animations Part 3</strong>: 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/"><strong>Smashing Animations Part 4</strong>: Optimising SVGs</a></li><li><a href="https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/"><strong>Smashing Animations Part 5</strong>: Building Adaptive SVGs With <code>&lt;symbol&gt;</code>, <code>&lt;use&gt;</code>, And CSS Media Queries</a></li><li><a href="https://www.smashingmagazine.com/2025/11/smashing-animations-part-6-svgs-css-custom-properties/"><strong>Smashing Animations Part 6</strong>: Magnificent SVGs With <code>&lt;use&gt;</code> And CSS Custom Properties</a></li><li><a href="https://www.smashingmagazine.com/2025/12/smashing-animations-part-7-recreating-toon-text-css-svg/"><strong>Smashing Animations Part 7</strong>: Recreating Toon Text With CSS And SVG</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="how-cartoon-animation-taught-me-to-reuse-everything">How Cartoon Animation Taught Me To Reuse Everything</h2>

<p>The <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> animated series I grew up watching had budgets far lower than those available when William Hanna and Joseph Barbera produced <em>Tom and Jerry</em> shorts at MGM Cartoons. This meant the animators needed to develop techniques to work around their cost restrictions.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/3-yogi-bear-show.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-8-css-relative-colour/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-8-css-relative-colour/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-8-css-relative-colour/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-8-css-relative-colour/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-8-css-relative-colour/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-8-css-relative-colour/3-yogi-bear-show.png"
			
			sizes="100vw"
			alt="Repeated use of elements in 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-8-css-relative-colour/3-yogi-bear-show.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Repeated use of elements was key. Backgrounds were reused whenever possible, with zooms and overlays helping construct new scenes from the same artwork. It was born of necessity, but it also encouraged thinking in terms of series rather than individual scenes.</p>

<h2 id="the-problem-with-manually-updating-colour-palettes">The problem With Manually Updating Colour Palettes</h2>

<p>Let’s get straight to my challenge. In Toon Titles like this one &mdash; based on the 1959 Yogi Bear Show episode “Lullabye-Bye Bear” &mdash; and my work generally, palettes are limited to a select few colours.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/4-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-8-css-relative-colour/4-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-8-css-relative-colour/4-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-8-css-relative-colour/4-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-8-css-relative-colour/4-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-8-css-relative-colour/4-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-8-css-relative-colour/4-yogi-bear.png"
			
			sizes="100vw"
			alt="Illustration of Yogi Bear asleep in a hammock tied between two thin, white trees. Andy Clarke’s Toon Titles is displayed above Yogi in cartoon-style typography."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this on my <a href='https://stuffandnonsense.co.uk/toon-titles/24b.html'>Toon Titles website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/4-yogi-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I create shades and tints from what I call my “foundation” colour to expand the palette without adding more hues.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.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-8-css-relative-colour/5-colour-palette.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png"
			
			sizes="100vw"
			alt="Colour palette of a foundation colour"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Colour palette with shades and tints of a foundation colour. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In <a href="https://www.sketch.com">Sketch</a>, I work in the <a href="https://www.smashingmagazine.com/2021/07/hsl-colors-css/">HSL colour space</a>, so this process involves increasing or decreasing the lightness value of my foundation colour. Honestly, it’s not an arduous task &mdash; but choosing a different foundation colour requires creating a whole new set of shades and tints. Doing that manually, again and again, quickly becomes laborious.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.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-8-css-relative-colour/6-foundation-colour.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png"
			
			sizes="100vw"
			alt="Shades and tints of a different foundation colour."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Shades and tints of a different foundation colour. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I mentioned the HSL &mdash; <strong>H</strong> (hue), S (saturation), and <strong>L</strong> (lightness) &mdash; colour space, but that’s just one of several ways to describe colour.</p>

<p>RGB &mdash; <strong>R</strong> (red), <strong>G</strong> (green), <strong>B</strong> (blue) &mdash; is probably the most familiar, at least in its Hex form.</p>

<p>There’s also LAB &mdash; <strong>L</strong> (lightness), <strong>A</strong> (green–red), <strong>B</strong> (blue–yellow) &mdash; and the newer, but now widely supported LCH &mdash; <strong>L</strong> (lightness), <strong>C</strong> (chroma), <strong>H</strong> (hue) &mdash; model in its OKLCH form. With LCH &mdash; specifically OKLCH in CSS &mdash; I can adjust the lightness value of my foundation colour.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="334"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png"
			
			sizes="100vw"
			alt="Lightness changes to the foundation colour."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Lightness changes to my foundation colour. Chroma and Hue remain the same. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Or I can alter its <em>chroma</em>. LCH chroma and HSL saturation both describe the intensity or richness of a colour, but they do so in different ways. LCH gives me a wider range and more predictable blending between colours.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="334"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png"
			
			sizes="100vw"
			alt="Chroma changes to the foundation colour"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Chroma changes to my foundation colour. Lightness and Hue remain the same. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I can also alter the hue to create a palette of colours that share the same lightness and chroma values. In both HSL and LCH, the hue spectrum starts at red, moves through green and blue, and returns to red.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="334"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png"
			
			sizes="100vw"
			alt="Hue changes to the foundation colour."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Hue changes to my foundation colour. Lightness and Chrome remain the same. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="why-oklch-changed-how-i-think-about-colour">Why OKLCH Changed How I Think About Colour</h2>

<p>Browser support for the OKLCH colour space <a href="https://caniuse.com/wf-oklab">is now widespread</a>, even if design tools &mdash; including Sketch &mdash; haven’t caught up. Fortunately, that shouldn’t stop you from using OKLCH. Browsers will happily convert Hex, HSL, LAB, and RGB values into OKLCH for you. You can define a CSS custom property with a foundation colour in any space, including Hex:</p>

<pre><code class="language-css">/&#42; Foundation colour &#42;/
--foundation: &#35;5accd6;
</code></pre>

<p>Any colours derived from it will be converted into OKLCH automatically:</p>

<pre><code class="language-css">--foundation-light: oklch(from var(--foundation) [...]; }
--foundation-mid: oklch(from var(--foundation) [...]; }
--foundation-dark: oklch(from var(--foundation) [...]; }
</code></pre>

<h2 id="relative-colour-as-a-design-system">Relative Colour As A Design System</h2>

<p>Think of relative colour as saying: <em>“Take this colour, tweak it, then give me the result.”</em> There are two ways to adjust a colour: absolute changes and proportional changes. They look similar in code, but behave very differently once you start swapping foundation colours. Understanding that difference is what can turn using relative colour into a system.</p>

<pre><code class="language-css">/&#42; Foundation colour &#42;/
--foundation: &#35;5accd6;
</code></pre>

<p>For example, the lightness value of my foundation colour is <code>0.7837</code>, while a darker version has a value of <code>0.5837</code>. To calculate the difference, I subtract the lower value from the higher one and apply the result using a <code>calc()</code> function:</p>

<pre><code class="language-css">--foundation-dark: 
  oklch(from var(--foundation)
  calc(l - 0.20) c h);
</code></pre>

<p>To achieve a lighter colour, I add the difference instead:</p>

<pre><code class="language-css">--foundation-light:
  oklch(from var(--foundation)
  calc(l + 0.10) c h);
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="334"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png"
			
			sizes="100vw"
			alt="Calculations of the difference between the foundation colour and Lightness, Chroma, and Hue-adjusted colours."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Calculating the difference between my foundation colour and Lightness, Chroma, and Hue-adjusted colours. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Chroma adjustments follow the same process. To reduce the intensity of my foundation colour from <code>0.1035</code> to <code>0.0035</code>, I subtract one value from the other:</p>

<pre><code class="language-css">oklch(from var(--foundation)
l calc(c - 0.10) h);
</code></pre>

<p>To create a palette of hues, I calculate the difference between the hue value of my foundation colour (<code>200</code>) and my new hue (<code>260</code>):</p>

<pre><code class="language-css">oklch(from var(--foundation)
l c calc(h + 60));
</code></pre>

<p>Those calculations are absolute. When I subtract a fixed amount, I’m effectively saying, <em>“Always subtract this much.”</em> The same applies when adding fixed values:</p>

<pre><code class="language-css">calc(c - 0.10)
calc(c + 0.10)
</code></pre>

<p>I learned the limits of this approach the hard way. When I relied on subtracting fixed chroma values, colours collapsed towards grey as soon as I changed the foundation. A palette that worked for one colour fell apart for another.</p>

<p>Multiplication behaves differently. When I multiply chroma, I’m telling the browser: <em>“Reduce this colour’s intensity by a proportion.”</em> The relationship between colours remains intact, even when the foundation changes:</p>

<pre><code class="language-css">calc(c &#42; 0.10)
</code></pre>

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

<h2 id="my-move-it-scale-it-rotate-it-rules">My Move It, Scale It, Rotate It Rules</h2>

<ul>
<li><strong>Move</strong> lightness (add or subtract),</li>
<li><strong>Scale</strong> chroma (multiply),</li>
<li><strong>Rotate</strong> hue (add or subtract degrees).</li>
</ul>

<p>I scale chroma because I want intensity changes to stay proportional to the base colour. Hue relationships are rotational, so multiplying hue makes no sense. Lightness is perceptual and absolute &mdash; multiplying it often produces odd results.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="334"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png"
			
			sizes="100vw"
			alt="Lightness: Move it. Chroma: Scale it. Hue: Rotate it"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Lightness: Move it. Chroma: Scale it. Hue: Rotate it. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="from-one-colour-to-an-entire-theme">From One Colour To An Entire Theme</h2>

<p>Relative colour allows me to define a foundation colour and generate every other colour I need &mdash; fills, strokes, gradient stops, shadows &mdash; from it. At that point, colour stops being a palette and starts being a system.</p>

<blockquote>SVG illustrations tend to reuse the same few colours across fills, strokes, and gradients. Relative colour lets you define those relationships once and reuse them everywhere &mdash; much like animators reused backgrounds to create new scenes.</blockquote>

<p>Change the foundation colour once, and every derived colour updates automatically, without recalculating anything by hand. Outside of animated graphics, I could use this same approach to define colours for the states of interactive elements such as buttons and links.</p>

<p>The foundation colour I used in my “Lullabye-Bye Bear” Toon Title is a cyan-looking blue. The background is a radial gradient between my foundation and a darker version.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html">
    
    <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-8-css-relative-colour/12-toon-titles-website.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-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-8-css-relative-colour/12-toon-titles-website.png"
			
			sizes="100vw"
			alt="“Lullabye-Bye Bear” Toon Title"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this on my <a href='https://stuffandnonsense.co.uk/toon-titles/24b.html'>Toon Titles website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To create alternative versions with entirely different moods, I only need to change the foundation colour:</p>

<pre><code class="language-css">--foundation: &#35;5accd6;
--grad-end: var(--foundation);
--grad-start: oklch(from var(--foundation)
  calc(l - 0.2357) calc(c &#42; 0.833) h);
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="171"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-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-8-css-relative-colour/13-toon-titles-website.png"
			
			sizes="100vw"
			alt="Three alternative versions of the “Lullabye-Bye Bear” Toon Title"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Use the colour picker on my <a href='https://stuffandnonsense.co.uk/toon-titles/24b.html'>Toon Titles website</a> to see this in action. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To bind those custom properties to my SVG gradient without duplicating colour values, I replaced hard-coded <code>stop-color</code> values with inline styles:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;defs&gt;
  &lt;radialGradient id="bg-grad" […]&gt;
    &lt;stop offset="0%" style="stop-color: var(--grad-end);" /&gt;
    &lt;stop offset="100%" style="stop-color: var(--grad-start);" /&gt;
  &lt;/radialGradient&gt;
&lt;/defs&gt;
</code></pre>
</div>
  

<pre><code class="language-svg">&lt;path fill="url(#bg-grad)" fill="#5DCDD8" d="[...]"/&gt;
</code></pre>

<p>Next, I needed to ensure that my <a href="https://stuffandnonsense.co.uk/toon-text/index.html">Toon Text</a> always contrasts with whatever foundation colour I choose. A <code>180deg</code> hue rotation produces a complementary colour that certainly pops &mdash; but can vibrate uncomfortably:</p>

<pre><code class="language-css">.text-light {
  fill: oklch(from var(--foundation)
    l c calc(h + 180));
}
</code></pre>

<p>A <code>90°</code> shift produces a vivid secondary colour without being fully complementary:</p>

<pre><code class="language-css">.text-light {
  fill: oklch(from var(--foundation)
    l c calc(h - 90));
}
</code></pre>

<p>My recreation of Quick Draw McGraw’s 1959 Toon Title “El Kabong“ uses the same techniques but with a more varied palette. For example, there’s another radial gradient between the foundation colour and a darker shade.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.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-8-css-relative-colour/14-quick-draw-mcgraw.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png"
			
			sizes="100vw"
			alt="An animated still of Quick Draw McGraw swinging from a rope going from left to right against a purple gradient background. Andy Clarke’s Toon Titles is displayed above him in cartoon-style typography. A silhouetted building and palm tree are positioned in the bottom-right corner."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this on my <a href='https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html'>Toon Titles website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="167"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/15-quick-draw-mcgraw-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-8-css-relative-colour/15-quick-draw-mcgraw-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-8-css-relative-colour/15-quick-draw-mcgraw-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-8-css-relative-colour/15-quick-draw-mcgraw-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-8-css-relative-colour/15-quick-draw-mcgraw-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-8-css-relative-colour/15-quick-draw-mcgraw-toon-titles.png"
			
			sizes="100vw"
			alt="Three alternative versions of Quick Draw McGraw’s 1959 Toon Title “El Kabong“"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Use the colour picker on my <a href='https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html'>Toon Titles website</a> to see this in action. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/15-quick-draw-mcgraw-toon-titles.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The building and tree in the background are simply different shades of the same foundation colour. For those paths, I needed two additional <code>fill</code> colours:</p>

<pre><code class="language-css">.bg-mid {
  fill: oklch(from var(--foundation)
    calc(l - 0.04) calc(c &#42; 0.91) h);
}

.bg-dark {
  fill: oklch(from var(--foundation)
    calc(l - 0.12) calc(c &#42; 0.64) h);
}
</code></pre>

<h2 id="when-the-foundations-start-to-move">When The Foundations Start To Move</h2>

<p>So far, everything I’ve shown has been static. Even when someone uses a colour picker to change the foundation colour, that change happens instantly. But animated graphics rarely stand still &mdash; the clue is in the name. So, if colour is part of the system, there’s no reason it can’t animate, too.</p>

<p>To animate the foundation colour, I first need to split it into its OKLCH channels &mdash; lightness, chroma, and hue. But there’s an important extra step: I need to register those values as <em>typed</em> custom properties. But what does that mean?</p>

<p>By default, a browser doesn’t know whether a CSS custom property value represents a colour, length, number, or something else entirely. That often means <a href="https://css-tricks.com/what-you-need-to-know-about-css-color-interpolation/">they can’t be interpolated smoothly during animation</a>, and jump from one value to the next.</p>

<p>Registering a custom property tells the browser the type of value it represents and how it should behave over time. In this case, I want the browser to treat my colour channels as numbers so they can be animated smoothly.</p>

<pre><code class="language-css">@property --f-l {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.40;
}

@property --f-c {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.11;
}

@property --f-h {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 305;
}
</code></pre>

<p>Once registered, these custom properties behave like native CSS. The browser can interpolate them frame-by-frame. I then rebuild the foundation colour from those channels:</p>

<pre><code class="language-css">--foundation: oklch(var(--f-l) var(--f-c) var(--f-h));
</code></pre>

<p>This makes the foundation colour become animatable, just like any other numeric value. Here’s a simple “breathing” animation that gently shifts lightness over time:</p>

<pre><code class="language-css">@keyframes breathe {
  0%, 100% { --f-l: 0.36; }
  50% { --f-l: 0.46; }
}

.toon-title {
  animation: breathe 10s ease-in-out infinite;
}
</code></pre>

<p>Because every other colour in fills, gradients, and strokes is derived from <code>--foundation</code>, they all animate together, and nothing needs to be updated manually.</p>

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

<h2 id="one-animated-colour-many-effects">One Animated Colour, Many Effects</h2>

<p>At the start of this process, I wondered whether CSS relative colour values could offer more possibilities while also making them simpler to implement. I recently added a new gold mine background to my website’s <a href="https://stuffandnonsense.co.uk/contact">contact page</a>, and the first iteration included oil lamps that glow and swing.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/contact">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="305"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png"
			
			sizes="100vw"
			alt="A group of seven illustrated western characters in an underground gold mine scene."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this animated SVG on <a href='https://stuffandnonsense.co.uk/contact'>my website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I wanted to explore how animating CSS relative colours could make the mine interior more realistic by tinting it with colours from the lamps. I wanted them to affect the world around them, the way real light does. So, rather than animating multiple colours, I built a tiny lighting system that animates just one colour.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="305"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-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-8-css-relative-colour/17-overlay-layer-svg.png"
			
			sizes="100vw"
			alt="Overlay layer applied to the SVG"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Adding an overlay layer to my SVG. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>My first task was to slot an overlay layer between the background and my lamps:</p>

<pre><code class="language-svg">&lt;path 
  id="overlay"
  fill="var(--overlay-tint)" 
  [...] 
  style="mix-blend-mode: color"
/&gt;
</code></pre>

<p>I used <code>mix-blend-mode: color</code> because that tints what’s beneath it while preserving the underlying luminance. As I only want the overlay to be visible when animations are turned on, I made the overlay opt-in:</p>

<pre><code class="language-css">.svg-mine &#35;overlay {
  display: none;
}
  
@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] &#35;overlay {
    display: block;
    opacity: 0.5;
  }
}
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="305"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png"
			
			sizes="100vw"
			alt="An overlay applied to the gold mine scene illuminates the background, making it brighter than the foreground."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The overlay layer tints what’s beneath it. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The overlay was in place, but not yet connected to the lamps. I needed a light source. My lamps are simple, and each one contains a <code>circle</code> element that I blurred with a filter. The <code>filter</code> produces a very soft blur over the entire circle.</p>

<div class="break-out">
<pre><code class="language-svg">&lt;filter id="lamp-glow-1" x="-120%" y="-120%" width="340%" height="340%"&gt;
  &lt;feGaussianBlur in="SourceGraphic" stdDeviation="56"/&gt;
&lt;/filter&gt;
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="305"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png"
			
			sizes="100vw"
			alt="Added oil lamps to the gold mine scene"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Adding oil lamps to my scene. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Instead of animating the overlay and lamps separately, I animate a single “flame” colour token and derive everything else from that. First, I register three typed custom properties for OKLCH channels:</p>

<pre><code class="language-css">@property --fl-l {
  syntax: "&lt;number&gt;"; 
  inherits: true;
  initial-value: 0.86;
}
@property --fl-c {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.12;
}
@property --fl-h {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 95;
}
</code></pre>

<p>I animated those channels, deliberately pushing a few frames towards orange so the flicker reads clearly as firelight:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes flame {
  0%, 100% { --fl-l: 0.86; --fl-c: 0.12; --fl-h: 95; }
  6% { --fl-l: 0.91; --fl-c: 0.10; --fl-h: 92; }
  12% { --fl-l: 0.83; --fl-c: 0.14; --fl-h: 100; }
  18% { --fl-l: 0.88; --fl-c: 0.11; --fl-h: 94; }
  24% { --fl-l: 0.82; --fl-c: 0.16; --fl-h: 82; }
  30% { --fl-l: 0.90; --fl-c: 0.12; --fl-h: 90; }
  36% { --fl-l: 0.79; --fl-c: 0.17; --fl-h: 76; }
  44% { --fl-l: 0.87; --fl-c: 0.12; --fl-h: 96; }
  52% { --fl-l: 0.81; --fl-c: 0.15; --fl-h: 102; }
  60% { --fl-l: 0.89; --fl-c: 0.11; --fl-h: 93; }
  68% { --fl-l: 0.83; --fl-c: 0.16; --fl-h: 85; }
  76% { --fl-l: 0.91; --fl-c: 0.10; --fl-h: 91; }
  84% { --fl-l: 0.85; --fl-c: 0.14; --fl-h: 98; }
  92% { --fl-l: 0.80; --fl-c: 0.17; --fl-h: 74; }
}
</code></pre>
</div>

<p>Then I scoped that animation to the SVG, so the shared variables are available to both the lamps and my overlay:</p>

<div class="break-out">
<pre><code class="language-css">@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] {
    animation: flame 3.6s infinite linear;
    isolation: isolate;

    /&#42; Build a flame colour from animated channels &#42;/
    --flame: oklch(var(--fl-l) var(--fl-c) var(--fl-h));

    /&#42; Lamp colour derived from flame &#42;/
    --lamp-core: oklch(from var(--flame) calc(l + 0.05) calc(c &#42; 0.70) h);
  
    /&#42; Overlay tint derived from the same flame &#42;/
    --overlay-tint: oklch(from var(--flame)
      calc(l + 0.06) calc(c &#42; 0.65) calc(h - 10));
  }
}
</code></pre>
</div>

<p>Finally, I applied those derived colours to the glowing lamps and the overlay they affect:</p>

<pre><code class="language-css">@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] &#35;mine-lamp-1 &gt; circle,
  .svg-mine[data-animations=on] &#35;mine-lamp-2 &gt; circle {
    fill: var(--lamp-core);
  }
  
  .svg-mine[data-animations=on] &#35;overlay {
    display: block;
    fill: var(--overlay-tint);
    opacity: 0.5;
  }
}
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="305"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png"
			
			sizes="100vw"
			alt="The lamps and overlay are connected."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The lamps and overlay are connected. (<a href='https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>When the flame shifts toward orange, the lamps warm up, and the scene warms with them. When the flame cools, everything settles together. The best part is that nothing is written manually. If I change the foundation colour or tweak the flame animation ranges, the entire lighting system updates simultaneously.</p>

<p>You can see <a href="https://stuffandnonsense.co.uk/contact">the final result on my website</a>.</p>

<h2 id="reuse-repurpose-revisited">Reuse, Repurpose, Revisited</h2>

<p>Those Hanna-Barbera animators were forced to repurpose elements out of necessity, but I reuse colours because it makes my work <strong>more consistent</strong> and <strong>easier to maintain</strong>. CSS relative colour values allow me to:</p>

<ul>
<li>Define a single foundation colour,</li>
<li>Describe how other colours relate to it,</li>
<li>Reuse those relationships everywhere, and</li>
<li>Animate the system by changing one value.</li>
</ul>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aRelative%20colour%20doesn%e2%80%99t%20just%20make%20theming%20easier.%20It%20encourages%20a%20way%20of%20thinking%20where%20colour,%20like%20motion,%20is%20intentional%20%e2%80%94%20and%20where%20changing%20one%20value%20can%20transform%20an%20entire%20scene%20without%20rewriting%20the%20work%20beneath%20it.%0a&url=https://smashingmagazine.com%2f2026%2f01%2fsmashing-animations-part-8-css-relative-colour%2f">
      
Relative colour doesn’t just make theming easier. It encourages a way of thinking where colour, like motion, is intentional — and where changing one value can transform an entire scene without rewriting the work beneath it.

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

<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 7: Recreating Toon Text With CSS And SVG</title><link>https://www.smashingmagazine.com/2025/12/smashing-animations-part-7-recreating-toon-text-css-svg/</link><pubDate>Wed, 17 Dec 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/12/smashing-animations-part-7-recreating-toon-text-css-svg/</guid><description>In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> shows his techniques for creating &lt;a href="https://stuffandnonsense.co.uk/toon-text/index.html">Toon Text titles&lt;/a> using modern CSS and SVG.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/12/smashing-animations-part-7-recreating-toon-text-css-svg/" />
              <title>Smashing Animations Part 7: Recreating Toon Text With CSS And SVG</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 7: Recreating Toon Text With CSS And SVG</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-12-17T10:00:00&#43;00:00" class="op-published">2025-12-17T10:00:00+00:00</time>
                  <time datetime="2025-12-17T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>After finishing a project that required me to learn everything I could about CSS and SVG animations, I started writing this series about Smashing Animations and “<a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">How Classic Cartoons Inspire Modern CSS</a>.” To round off this year, I want to show you how to use modern CSS to create that element that makes Toon Titles so impactful: their typography.</p>

<h2 id="title-artwork-design">Title Artwork Design</h2>

<p>In the silent era of the 1920s and early ’30s, the typography of a film’s title card created a mood, set the scene, and reminded an audience of the type of film they’d paid to see.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="156"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png"
			
			sizes="100vw"
			alt="Typographic title cards from the early years of cinema"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Typographic title cards from the early years of cinema. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Cartoon title cards were also branding, mood, and scene-setting, all rolled into one. In the early years, when major studio budgets were bigger, these title cards were often illustrative and painterly.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.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-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png"
			
			sizes="100vw"
			alt="Top: William Hanna and Joseph Barbera’s 1940s Tom &amp; Jerry title cards. Bottom: Colour versions released in 1957. © Warner Bros. Entertainment Inc."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Top: William Hanna and Joseph Barbera’s 1940s Tom & Jerry title cards. Bottom: Colour versions released in 1957. © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But when television boomed during the 1950s, budgets dropped, and cards designed by artists like Lawrence “Art” Goble adopted a new visual language, becoming more graphic, stylised, and less intricate.</p>

<p><strong>Note:</strong> <em>Lawrence “Art” Goble is one of the often overlooked heroes of mid-century American animation. He primarily worked for Hanna-Barbera during its most influential years of the 1950s and 1960s.</em></p>

<p>Goble wasn’t a character animator. His role was to create atmosphere, so he designed environments for <em>The Flintstones</em>, <em>Huckleberry Hound</em>, <em>Quick Draw McGraw</em>, and <em>Yogi Bear</em>, as well as the opening title cards that set the tone. His title cards, featuring paintings with a logo overlaid, helped define the iconic look of Hanna-Barbera.</p>

<p>Goble’s artwork for characters such as Quick Draw McGraw and Yogi Bear was effective on smaller TV screens. Rather than reproducing a still from the cartoon, he focused on presenting a single, strong idea &mdash; often in silhouette &mdash; that captured its essence. In “The Buzzin’ Bear,” Yogi buzzes by in a helicopter. He bounces away, pic-a-nic basket in hand, in “Bear on a Picnic,” and for his “Prize Fight Fright,” Yogi boxes the title text.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.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-7-recreating-toon-text-css-svg/3-title-cards-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-7-recreating-toon-text-css-svg/3-title-cards-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-7-recreating-toon-text-css-svg/3-title-cards-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-7-recreating-toon-text-css-svg/3-title-cards-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-7-recreating-toon-text-css-svg/3-title-cards-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-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.png"
			
			sizes="100vw"
			alt="Title cards for Hanna-Barbera’s Yogi Bear."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Yogi Bear. © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With little or no motion to rely on, Goble’s single frames had to create a mood, set the scene, and describe a story. They did this using flat colours, graphic shapes, and typography that was frequently integrated into the artwork.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="225"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png"
			
			sizes="100vw"
			alt="Title cards for Hanna-Barbera’s Quick Draw McGraw."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Quick Draw McGraw. © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>As designers who work on the web, toon titles can teach us plenty about how to convey a brand’s personality, make a first impression, and set expectations for someone’s experience using a product or website. We can learn from the artists’ techniques to create effective banners, landing-page headers, and even good ol’ fashioned splash screens.</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="toon-title-typography">Toon Title Typography</h2>

<p>Cartoon title cards show how merging type with imagery delivers the punch a header or hero needs. With a handful of <code>text-shadow</code>, <code>text-stroke</code>, and <code>transform</code> tricks, modern CSS lets you tap into that same energy.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="455"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png"
			
			sizes="100vw"
			alt="Title cards for Hanna-Barbera’s Augie Doggie."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Augie Doggie. © Warner Bros. Entertainment Inc. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="the-toon-text-title-generator">The Toon Text Title Generator</h2>

<p>Partway through writing this article, I realised it would be useful to have a tool for generating text styled like the cartoon titles I love so much. <a href="https://stuffandnonsense.co.uk/toon-text/tool.html">So I made one.</a></p>

<p>My Toon Text Title Generator lets you experiment with colours, strokes, and multiple text shadows. You can adjust paint order, apply letter spacing, preview your text in a selection of sample fonts, and then copy the generated CSS straight to your clipboard to use in a project.</p>

<h2 id="toon-title-css">Toon Title CSS</h2>

<p>You can simply copy-paste the CSS that the Toon Text Title Generator provides you. But let’s look closer at what it does.</p>

<h3 id="text-shadow">Text shadow</h3>

<p>Look at the type in this title from Augie Doggie’s episode “Yuk-Yuk Duck,” with its pale yellow letters and dark, hard, offset shadow that lifts it off the background and creates the illusion of depth.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You probably already know that <code>text-shadow</code> accepts four values: (1) horizontal and (2) vertical offsets, (3) blur, and (4) a colour which can be solid or semi-transparent. Those offset values can be positive or negative, so I can replicate “Yuk-Yuk Duck” using a hard shadow pulled down and to the right:</p>

<pre><code class="language-css">color: &#35;f7f76d;
text-shadow: 5px 5px 0 &#35;1e1904;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>On the other hand, this “Pint Giant” title has a different feel with its negative semi-soft shadow:</p>

<pre><code class="language-css">color: &#35;c2a872;
text-shadow:
  -7px 5px 0 &#35;b100e,
  0 -5px 10px &#35;546c6f;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To add extra depth and create more interesting effects, I can layer multiple shadows. For “Let’s Duck Out,” I combine four shadows: the first a solid shadow with a negative horizontal offset to lift the text off the background, followed by progressively softer shadows to create a blur around it:</p>

<pre><code class="language-css">color: &#35;6F4D80;
text-shadow:
  -5px 5px 0 &#35;260e1e, /&#42; Shadow 1 &#42;/
  0 0 15px &#35;e9ce96,   /&#42; Shadow 2 &#42;/
  0 0 30px &#35;e9ce96,   /&#42; Shadow 3 &#42;/
  0 0 30px &#35;e9ce96;   /&#42; Shadow 4 &#42;/
</code></pre>

<p>These shadows show that using <code>text-shadow</code> isn’t just about creating lighting effects, as they can also be decorative and add personality.</p>

<h3 id="text-stroke">Text Stroke</h3>

<p>Many cartoon title cards feature letters with a bold outline that makes them stand out from the background. I can recreate this effect using <code>text-stroke</code>. For a long time, this property was only available via a <code>-webkit-</code> prefix, but that also means it’s now supported across modern browsers.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><code>text-stroke</code> is a shorthand for two properties. The first, <code>text-stroke-width</code>, draws a contour around individual letters, while the second, <code>text-stroke-color</code>, controls its colour. For “Whatever Goes Pup,” I added a <code>4px</code> blue stroke to the yellow text:</p>

<pre><code class="language-css">color: &#35;eff0cd;
-webkit-text-stroke: 4px &#35;7890b5;
text-stroke: 4px &#35;7890b5;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Strokes can be especially useful when they’re combined with shadows, so for “Growing, Growing, Gone,” I added a thin <code>3px</code> stroke to a barely blurred <code>1px</code> shadow to create this three-dimensional text effect:</p>

<pre><code class="language-css">color: &#35;fbb999;
text-shadow: 3px 5px 1px &#35;5160b1;
-webkit-text-stroke: 3px &#35;984336;
text-stroke: 3px &#35;984336;
</code></pre>

<h3 id="paint-order">Paint Order</h3>

<p>Using <code>text-stroke</code> doesn’t always produce the expected result, especially with thinner letters and thicker strokes, because by default the browser draws a stroke over the fill. Sadly, CSS still does not permit me to adjust stroke placement as I often do in Sketch. However, the <code>paint-order</code> property has values that allow me to place the stroke behind, rather than in front of, the fill.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png"
			
			sizes="100vw"
			alt="Left: paint-order: stroke; Right: paint-order: fill."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Left: <code>paint-order: stroke</code>. Right: <code>paint-order: fill</code>. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><code>paint-order: stroke</code> paints the stroke first, then the fill, whereas <code>paint-order: fill</code> does the opposite:</p>

<pre><code class="language-css">color: &#35;fbb999;
paint-order: fill;
text-shadow: 3px 5px 1px &#35;5160b1;
text-stroke-color:&#35;984336;
text-stroke-width: 3px;
</code></pre>

<p>An effective stroke keeps letters readable, adds weight, and &mdash; when combined with shadows and paint order &mdash; gives flat text real presence.</p>

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

<h2 id="backgrounds-inside-text">Backgrounds Inside Text</h2>

<p>Many cartoon title cards go beyond flat colour by adding texture, gradients, or illustrated detail to the lettering. Sometimes that’s a texture, other times it might be a gradient with a subtle tonal shift. On the web, I can recreate this effect by using a background image or gradient behind the text, and then clipping it to the shape of the letters. This relies on two properties working together: <code>background-clip: text</code> and <code>text-fill-color: transparent</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>First, I apply a background behind the text. This can be a bitmap or vector image or a CSS gradient. For this example from the Quick Draw McGraw episode “Baba Bait,” the title text includes a subtle top–bottom gradient from dark to light:</p>

<pre><code class="language-css">background: linear-gradient(0deg, &#35;667b6a, &#35;1d271a);
</code></pre>

<p>Next, I clip that background to the glyphs and make the text transparent so the background shows through:</p>

<pre><code class="language-css">-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
</code></pre>

<p>With just those two lines, the background is no longer painted behind the text; instead, it’s painted within it. This technique works especially well when combined with strokes and shadows. A clipped gradient provides the lettering with colour and texture, a stroke keeps its edges sharp, and a shadow elevates it from the background. Together, they recreate the layered look of hand-painted title cards using nothing more than a little CSS. As always, test clipped text carefully, as browser quirks can sometimes affect shadows and rendering.</p>

<h3 id="splitting-text-into-individual-characters">Splitting Text Into Individual Characters</h3>

<p>Sometimes I don’t want to style a whole word or heading. I want to style individual letters &mdash; to nudge a character into place, give one glyph extra weight, or animate a few letters independently.</p>

<p>In plain HTML and CSS, there’s only one reliable way to do that: wrap each character in its own <code>span</code> element. I could do that manually, but that would be fragile, hard to maintain, and would quickly fall apart when copy changes. Instead, when I need per-letter control, I use a text-splitting library like <a href="https://www.spltjs.com">splt.js</a> (although other solutions are available). This takes a text node and automatically wraps words or characters, giving me extra hooks to animate and style without messing up my markup.</p>

<p>It’s an approach that keeps my HTML readable and semantic, while giving me the fine-grained control I need to recreate the uneven, characterful typography you see in classic cartoon title cards. However, this approach comes with accessibility caveats, as most screen readers read text nodes in order. So this:</p>

<pre><code class="language-html">&lt;h2&gt;Hum Sweet Hum&lt;/h2&gt;
</code></pre>

<p>…reads as you’d expect:</p>

<blockquote>Hum Sweet Hum</blockquote>

<p>But this:</p>

<pre><code class="language-html">&lt;h2&gt;
&lt;span&gt;H&lt;/span&gt;
&lt;span&gt;u&lt;/span&gt;
&lt;span&gt;m&lt;/span&gt;
&lt;!-- etc. --&gt;
&lt;/h2&gt;
</code></pre>

<p>…can be interpreted differently depending on the browser and screen reader. Some will concatenate the letters and read the words correctly. Others may pause between letters, which in a worst-case scenario might sound like:</p>

<blockquote>“H…” “U…” “M…”</blockquote>

<p>Sadly, some splitting solutions don’t deliver an always accessible result, so I’ve written my own text splitter, <a href="https://stuffandnonsense.co.uk/toon-text/splinter.html#section-install">splinter.js</a>, which is currently in beta.</p>

<h3 id="transforming-individual-letters">Transforming Individual Letters</h3>

<p>To activate my Toon Text Splitter, I add a <code>data-</code> attribute to the element I want to split:</p>

<pre><code class="language-html">&lt;h2 data-split="toon"&gt;Hum Sweet Hum&lt;/h2&gt;
</code></pre>

<p>First, my script separates each word into individual letters and wraps them in a <code>span</code> element with class and ARIA attributes applied:</p>

<pre><code class="language-html">&lt;span class="toon-char" aria-hidden="true"&gt;H&lt;/span&gt;
&lt;span class="toon-char" aria-hidden="true"&gt;u&lt;/span&gt;
&lt;span class="toon-char" aria-hidden="true"&gt;m&lt;/span&gt;
</code></pre>

<p>The script then takes the initial content of the split element and adds it as an aria attribute to help maintain accessibility:</p>

<div class="break-out">
<pre><code class="language-html">&lt;h2 data-split="toon" aria-label="Hum Sweet Hum"&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;H&lt;/span&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;u&lt;/span&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;m&lt;/span&gt;
&lt;/h2&gt;
</code></pre>
</div>

<p>With those class attributes applied, I can then style individual characters as I choose.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For example, for “Hum Sweet Hum,” I want to replicate how its letters shift away from the baseline. After using my Toon Text Splitter, I applied four different <code>translate</code> values using several <code>:nth-child</code> selectors to create a semi-random look:</p>

<pre><code class="language-css">/&#42; 4th, 8th, 12th... &#42;/
.toon-char:nth-child(4n) { translate: 0 -8px; }
/&#42; 1st, 5th, 9th... &#42;/
.toon-char:nth-child(4n+1) { translate: 0 -4px; }
/&#42; 2nd, 6th, 10th... &#42;/
.toon-char:nth-child(4n+2) { translate: 0 4px; }
/&#42; 3rd, 7th, 11th... &#42;/
.toon-char:nth-child(4n+3) { translate: 0 8px; }
</code></pre>

<p>But <code>translate</code> is only one property I can use to <code>transform</code> my toon text.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I could also rotate those individual characters for an even more chaotic look:</p>

<pre><code class="language-css">/&#42; 4th, 8th, 12th... &#42;/
.toon-line .toon-char:nth-child(4n) { rotate: -4deg; }
/&#42; 1st, 5th, 9th... &#42;/
.toon-char:nth-child(4n+1) { rotate: -8deg; }
/&#42; 2nd, 6th, 10th... &#42;/
.toon-char:nth-child(4n+2) { rotate: 4deg; }
/&#42; 3rd, 7th, 11th... &#42;/
.toon-char:nth-child(4n+3) { rotate: 8deg; }
</code></pre>

<p>But <code>translate</code> is only one property I can use to <code>transform</code> my toon text. I could also <code>rotate</code> those individual characters for an even more chaotic look:</p>

<pre><code class="language-css">/&#42; 4th, 8th, 12th... &#42;/
.toon-line .toon-char:nth-child(4n) {
rotate: -4deg; }

/&#42; 1st, 5th, 9th... &#42;/
.toon-char:nth-child(4n+1) {
rotate: -8deg; }

/&#42; 2nd, 6th, 10th... &#42;/
.toon-char:nth-child(4n+2) {
rotate: 4deg; }

/&#42; 3rd, 7th, 11th... &#42;/
.toon-char:nth-child(4n+3) {
rotate: 8deg; }
</code></pre>

<p>And, of course, I could add animations to jiggle those characters and bring my toon text style titles to life. First, I created a keyframe animation that rotates the characters:</p>

<div class="break-out">
<pre><code class="language-css">@keyframes jiggle {
0%, 100% { transform: rotate(var(--base-rotate, 0deg)); }
25% { transform: rotate(calc(var(--base-rotate, 0deg) + 3deg)); }
50% { transform: rotate(calc(var(--base-rotate, 0deg) - 2deg)); }
75% { transform: rotate(calc(var(--base-rotate, 0deg) + 1deg)); }
}
</code></pre>
</div>

<p>Before applying it to the <code>span</code> elements created by my Toon Text Splitter:</p>

<pre><code class="language-css">.toon-char {
animation: jiggle 3s infinite ease-in-out;
transform-origin: center bottom; }
</code></pre> 

<p>And finally, setting the rotation amount and a delay before each character begins to jiggle:</p>

<pre><code class="language-css">.toon-char:nth-child(4n) { --base-rotate: -2deg; }
.toon-char:nth-child(4n+1) { --base-rotate: -4deg; }
.toon-char:nth-child(4n+2) { --base-rotate: 2deg; }
.toon-char:nth-child(4n+3) { --base-rotate: 4deg; }

.toon-char:nth-child(4n) { animation-delay: 0.1s; }
.toon-char:nth-child(4n+1) { animation-delay: 0.3s; }
.toon-char:nth-child(4n+2) { animation-delay: 0.5s; }
.toon-char:nth-child(4n+3) { animation-delay: 0.7s; }
</code></pre> 














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="317"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png"
			
			sizes="100vw"
			alt="Example from Andy&#39;s Toon Text collection."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href='https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png'>Large preview</a>)
    </figcaption>
  
</figure>

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

<h2 id="one-frame-to-make-an-impression">One Frame To Make An Impression</h2>

<p>Cartoon title artists had one frame to make an impression, and their typography was as important as the artwork they painted. The same is true on the web.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aA%20well-designed%20header%20or%20hero%20area%20needs%20clarity,%20character,%20and%20confidence%20%e2%80%94%20not%20simply%20a%20faded%20full-width%20background%20image.%0a&url=https://smashingmagazine.com%2f2025%2f12%2fsmashing-animations-part-7-recreating-toon-text-css-svg%2f">
      
A well-designed header or hero area needs clarity, character, and confidence — not simply a faded full-width background image.

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

<p>With a few carefully chosen CSS properties &mdash; shadows, strokes, clipped backgrounds, and some restrained animation &mdash; we can recreate that same impact. I love toon text not because I’m nostalgic, but because its design is intentional. Make deliberate choices, and let a little toon text typography add punch to your 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>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Brecht De Ruyte</author><title>State, Logic, And Native Power: CSS Wrapped 2025</title><link>https://www.smashingmagazine.com/2025/12/state-logic-native-power-css-wrapped-2025/</link><pubDate>Tue, 09 Dec 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/12/state-logic-native-power-css-wrapped-2025/</guid><description>CSS Wrapped 2025 is out! We’re entering a world where CSS can increasingly handle logic, state, and complex interactions once reserved for JavaScript. It’s no longer just about styling documents, but about crafting dynamic, ergonomic, and robust applications with a native toolkit more powerful than ever. Here’s an unpacking of the highlights and how they connect to the broader evolution of modern CSS.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/12/state-logic-native-power-css-wrapped-2025/" />
              <title>State, Logic, And Native Power: CSS Wrapped 2025</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>State, Logic, And Native Power: CSS Wrapped 2025</h1>
                  
                    
                    <address>Brecht De Ruyte</address>
                  
                  <time datetime="2025-12-09T10:00:00&#43;00:00" class="op-published">2025-12-09T10:00:00+00:00</time>
                  <time datetime="2025-12-09T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>If I were to divide CSS evolutions into categories, we have moved far beyond the days when we simply asked for <code>border-radius</code> to feel like we were living in the future. We are currently living in a moment where the platform is handing us tools that don’t just tweak the visual layer, but fundamentally redefine how we architect interfaces. I thought the number of features announced in 2024 couldn’t be topped. I’ve never been so happily wrong.</p>

<p>The Chrome team’s “<a href="https://chrome.dev/css-wrapped-2025/"><strong>CSS Wrapped 2025</strong></a>” is not just a list of features; it is a manifesto for a dynamic, native web. As someone who has spent a couple of years documenting these evolutions &mdash; from <a href="https://www.smashingmagazine.com/2024/08/css5-era-evolution/">defining “CSS5” eras</a> to the intricacies of <a href="https://www.smashingmagazine.com/2024/05/modern-css-layouts-no-framework/">modern layout utilities</a> &mdash; I find myself looking at this year’s wrap-up with a huge sense of excitement. We are seeing a shift towards “Optimized Ergonomics” and “Next-gen interactions” that allow us to stop fighting the code and start sculpting interfaces in their natural state.</p>

<p>In this article, you can find <strong>a comprehensive look at the standout features from Chrome’s report</strong>, viewed through the lens of my recent experiments and hopes for the future of the platform.</p>

<h2 id="the-component-revolution-finally-a-native-customizable-select">The Component Revolution: Finally, A Native Customizable Select</h2>

<p>For years, we have relied on heavy JavaScript libraries to style dropdowns, a “decades-old problem” that the platform has finally solved. As I detailed in <a href="https://utilitybend.com/blog/the-customizable-select-part-one-history-trickery-and-styling-the-select-with-css">my deep dive into the history of the customizable select</a> (and related articles), this has been a long road involving <a href="https://open-ui.org/">Open UI</a>, bikeshedding names like <code>&lt;selectmenu&gt;</code> and <code>&lt;selectlist&gt;</code>, and finally landing on a solution that re-uses the existing <code>&lt;select&gt;</code> element.</p>

<p>The introduction of <code>appearance: base-select</code> is a strong foundation. It allows us to fully customize the <code>&lt;select&gt;</code> element &mdash; including the button and the dropdown list (via <code>::picker(select)</code>) &mdash; using standard CSS. Crucially, this is built with progressive enhancement in mind. By wrapping our styles in a feature query, we ensure a seamless experience across all browsers.</p>

<p>We can opt in to this new behavior without breaking older browsers:</p>

<pre><code class="language-css">select {
  /&#42; Opt-in for the new customizable select &#42;/
  @supports (appearance: base-select) {
    &, &::picker(select) {
      appearance: base-select;
    }
  }
}
</code></pre>

<p>The fantastic addition to allow rich content inside options, such as images or flags, is a lot of fun. We can create all sorts of selects nowadays:</p>

<ul>
<li><strong>Demo:</strong> I created a <a href="https://codepen.io/utilitybend/pen/ByawgNN">Poké-adventure demo</a> showing how the new <code>&lt;selectedcontent&gt;</code> element can clone rich content (like a Pokéball icon) from an option directly into the button.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="JoXwwoZ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [A customizable select with images inside of the options and the selectedcontent [forked]](https://codepen.io/smashingmag/pen/JoXwwoZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/JoXwwoZ">A customizable select with images inside of the options and the selectedcontent [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<ul>
<li><strong>Demo:</strong> A comprehensive look at <a href="https://codepen.io/utilitybend/pen/GgRrLWb">styling the select with only pseudo-elements</a>.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="pvyqqJR"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [A customizable select with only pseudo-elements [forked]](https://codepen.io/smashingmag/pen/pvyqqJR) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvyqqJR">A customizable select with only pseudo-elements [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<ul>
<li><strong>Demo:</strong> Or you can kick it up a notch with this <a href="https://codepen.io/utilitybend/pen/ByoBMBm">Menu selection demo using optgroups</a>.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="myPaaJZ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [An actual Select Menu with optgroups [forked]](https://codepen.io/smashingmag/pen/myPaaJZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myPaaJZ">An actual Select Menu with optgroups [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<p>This feature alone signals a massive shift in how we will build forms, reducing dependencies and technical debt.</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="scroll-markers-and-the-death-of-the-javascript-carousel">Scroll Markers And The Death Of The JavaScript Carousel</h2>

<p>Creating carousels has historically been a friction point between developers and clients. Clients love them, developers dread the JavaScript required to make them accessible and performant. The arrival of <code>::scroll-marker</code> and <code>::scroll-button()</code> pseudo-elements changes this dynamic entirely.</p>

<p>These features allow us to create navigation dots and scroll buttons purely with CSS, linked natively to the scroll container. As I wrote on my blog, this was <a href="https://utilitybend.com/blog/love-at-first-slide-creating-a-carousel-purely-out-of-css">Love at first slide</a>. The ability to create a fully functional, accessible slider without a single line of JavaScript is not just convenient; it is a triumph for performance. There are some accessibility concerns around this feature, and even though these are valid, there might be a way for us developers to make it work. The good thing is, all these UI changes are making it a lot easier than custom DOM manipulation and dragging around aria tags, but I digress…</p>

<p>We can now group markers automatically using <code>scroll-marker-group</code> and style the buttons using anchor positioning to place them exactly where we want.</p>

<div class="break-out">
<pre><code class="language-css">.carousel {
  overflow-x: auto;
  scroll-marker-group: after; /&#42; Creates the container for dots &#42;/

  /&#42; Create the buttons &#42;/
  &::scroll-button(inline-end),
  &::scroll-button(inline-start) {
    content: " ";
    position: absolute;
    /&#42; Use anchor positioning to center them &#42;/
    position-anchor: --carousel;
    top: anchor(center);
  }

  /&#42; Create the markers on the children &#42;/
  div {
    &::scroll-marker {
      content: " ";
      width: 24px;
      border-radius: 50%;
      cursor: pointer;
    }
    /&#42; Highlight the active marker &#42;/
    &::scroll-marker:target-current {
      background: white;
    }
  }
}
</code></pre>
</div>

<ul>
<li><strong>Demo:</strong> My experiment creating a <a href="https://codepen.io/utilitybend/pen/vEBQxNb">carousel purely out of HTML and CSS</a>, using anchor positioning to place the buttons.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ogxJJjQ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Carousel Pure HTML and CSS [forked]](https://codepen.io/smashingmag/pen/ogxJJjQ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogxJJjQ">Carousel Pure HTML and CSS [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/bNbXZWb">Webshop slick slider remake</a> using <code>attr()</code> to pull background images dynamically into the markers.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="gbrZZPY"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Webshop slick slider remake in CSS [forked]](https://codepen.io/smashingmag/pen/gbrZZPY) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/gbrZZPY">Webshop slick slider remake in CSS [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<h2 id="state-queries-sticky-thing-stuck-snappy-thing-snapped">State Queries: Sticky Thing Stuck? Snappy Thing Snapped?</h2>

<p>For a long time, we have lacked the ability to know if a <a href="https://utilitybend.com/blog/is-the-sticky-thing-stuck-is-the-snappy-item-snapped-a-look-at-state-queries-in-css">“sticky thing is stuck” or if a “snappy item is snapped”</a> without relying on IntersectionObserver hacks. Chrome 133 introduced scroll-state queries, allowing us to query these states declaratively.</p>

<p>By setting <code>container-type: scroll-state</code>, we can now style children based on whether they are stuck, snapped, or overflowing. This is a massive “quality of life” improvement that I have been eagerly waiting for since CSS Day 2023. It has even evolved a lot since we can also see the direction of the scroll, lovely!</p>

<p>For a simple example: we can finally apply a shadow to a header <em>only</em> when it is actually sticking to the top of the viewport:</p>

<pre><code class="language-css">.header-container {
  container-type: scroll-state;
  position: sticky;
  top: 0;

  header {
    transition: box-shadow 0.5s ease-out;
    /&#42; The query checks the state of the container &#42;/
    @container scroll-state(stuck: top) {
      box-shadow: rgba(0, 0, 0, 0.6) 0px 12px 28px 0px;
    }
  }
}
</code></pre>

<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/XWLQPOe">sticky header</a> that only applies a shadow when it is actually stuck.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="raeooxY"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Sticky headers with scroll-state query, checking if the sticky element is stuck [forked]](https://codepen.io/smashingmag/pen/raeooxY) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/raeooxY">Sticky headers with scroll-state query, checking if the sticky element is stuck [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/MWMZoqp">Pokémon-themed list</a> that uses scroll-state queries combined with anchor positioning to move a frame over the currently snapped character.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="vEGvvLM"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Scroll-state query to check which item is snapped with CSS, Pokemon version [forked]](https://codepen.io/smashingmag/pen/vEGvvLM) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vEGvvLM">Scroll-state query to check which item is snapped with CSS, Pokemon version [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

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

<h2 id="optimized-ergonomics-logic-in-css">Optimized Ergonomics: Logic In CSS</h2>

<p>The “Optimized Ergonomics” section of CSS Wrapped highlights features that make our workflows more intuitive. Three features stand out as transformative for how we write logic:</p>

<ol>
<li><strong><code>if()</code> Statements</strong><br />
We are finally getting conditionals in CSS. The <code>if()</code> function acts like a ternary operator for stylesheets, allowing us to apply values based on media, support, or style queries inline. This reduces the need for verbose <code>@media</code> blocks for single property changes.</li>
<li><strong><code>@function</code> functions</strong><br />
We can finally move some logic to a different place, resulting in some cleaner files, a real quality of life feature.</li>
<li><strong><code>sibling-index()</code> and <code>sibling-count()</code></strong><br />
These tree-counting functions solve the issue of staggering animations or styling items based on list size. As I explored in <a href="https://utilitybend.com/blog/styling-siblings-with-css-has-never-been-easier-experimenting-with-sibling-count-and-sibling-index">Styling siblings with CSS has never been easier</a>, this eliminates the need to hard-code custom properties (like <code>--index: 1</code>) in our HTML.</li>
</ol>

<h3 id="example-calculating-layouts">Example: Calculating Layouts</h3>

<p>We can now write concise mathematical formulas. For example, staggering an animation for cards entering the screen becomes trivial:</p>

<pre><code class="language-css">.card-container &gt; &#42; {
  animation: reveal 0.6s ease-out forwards;
  /&#42; No more manual --index variables! &#42;/
  animation-delay: calc(sibling-index() &#42; 0.1s);
}
</code></pre>

<p>I even experimented with using these functions along with trigonometry to place items in a perfect circle without any JavaScript.</p>

<ul>
<li><strong>Demo:</strong> <a href="https://codepen.io/utilitybend/pen/wBKQPLr">Staggering card animations dynamically</a>.</li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="RNaEERz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Stagger cards using sibling-index() [forked]](https://codepen.io/smashingmag/pen/RNaEERz) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/RNaEERz">Stagger cards using sibling-index() [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<ul>
<li><strong>Demo:</strong> Placing items in a <a href="https://codepen.io/utilitybend/pen/VYvVXLN">perfect circle</a> using <code>sibling-index</code>, <code>sibling-count</code>, and the new CSS <code>@function</code> feature.
<br /></li>
</ul>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="XJdoojZ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [The circle using sibling-index, sibling-count and functions [forked]](https://codepen.io/smashingmag/pen/XJdoojZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/XJdoojZ">The circle using sibling-index, sibling-count and functions [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption>
</figure>

<h2 id="my-css-to-do-list-features-i-can-t-wait-to-try">My CSS To-Do List: Features I Can’t Wait To Try</h2>

<p>While I have been busy sculpting selects and transitions, the “CSS Wrapped 2025” report is packed with other goodies that I haven’t had the chance to fire up in CodePen yet. These are high on my list for my next experiments:</p>

<h3 id="anchored-container-queries">Anchored Container Queries</h3>

<p>I used CSS Anchor Positioning for the buttons in my carousel demo, but “CSS Wrapped” highlights an evolution of this: <strong>Anchored Container Queries</strong>. This solves a problem we’ve all had with tooltips: if the browser flips the tooltip from top to bottom because of space constraints, the “arrow” often stays pointing the wrong way. With anchored container queries (<code>@container anchored(fallback: flip-block)</code>), we can style the element based on which fallback position the browser actually chose.</p>

<h3 id="nested-view-transition-groups">Nested View Transition Groups</h3>

<p>View Transitions have been a revolution, but they came with a specific trade-off: they flattened the element tree, which often broke 3D transforms or overflow: clip. I always had a feeling that it was missing something, and this might just be the answer. By using <code>view-transition-group: nearest</code>, we can finally nest transition groups within each other.</p>

<p>This allows us to maintain clipping effects or 3D rotations during a transition &mdash; something that was previously impossible because the elements were hoisted up to the top level.</p>

<pre><code class="language-css">.card img {
  view-transition-name: photo;
  view-transition-group: nearest; /&#42; Keep it nested! &#42;/
}
</code></pre>

<h3 id="typography-and-shapes">Typography and Shapes</h3>

<p>Finally, the ergonomist in me is itching to try <strong>Text Box Trim</strong>, which promises to remove that annoying extra whitespace above and below text content (the leading) to finally achieve perfect vertical alignment. And for the creative side, <code>corner-shape</code> and the <code>shape()</code> function are opening up non-rectangular layouts, allowing for “squaricles” and complex paths that respond to CSS variables. That being said, I can’t wait to have a design full of squircles!</p>

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

<h2 id="a-hopeful-future">A Hopeful Future</h2>

<p>We are witnessing a world where <strong>CSS is becoming capable of handling logic, state, and complex interactions that previously belonged to JavaScript</strong>. Features like <code>moveBefore</code> (preserving DOM state for iframes/videos) and <code>attr()</code> (using types beyond strings for colors and grids) further cement this reality.</p>

<p>While some of these features are currently experimental or specific to Chrome, the momentum is undeniable. We must hope for continued support across all browsers through initiatives like Interop to ensure these capabilities become the baseline. That being said, having browser engines is just as important as having all these awesome features in “Chrome first”. These new features need to be discussed, tinkered with, and tested before ever landing in browsers.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIt%20is%20a%20fantastic%20moment%20to%20get%20into%20CSS.%20We%20are%20no%20longer%20just%20styling%20documents;%20we%20are%20crafting%20dynamic,%20ergonomic,%20and%20robust%20applications%20with%20a%20native%20toolkit%20that%20is%20more%20powerful%20than%20ever.%0a&url=https://smashingmagazine.com%2f2025%2f12%2fstate-logic-native-power-css-wrapped-2025%2f">
      
It is a fantastic moment to get into CSS. We are no longer just styling documents; we are crafting dynamic, ergonomic, and robust applications with a native toolkit that is more powerful than ever.

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

<p>Let’s get going with this new era and spread the word.</p>

<p>This is <a href="https://chrome.dev/css-wrapped-2025/">CSS Wrapped</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>Patrick Brosset</author><title>Masonry: Things You Won’t Need A Library For Anymore</title><link>https://www.smashingmagazine.com/2025/12/masonry-things-you-wont-need-library-anymore/</link><pubDate>Tue, 02 Dec 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/12/masonry-things-you-wont-need-library-anymore/</guid><description>CSS Masonry is almost here! Patrick Brosset takes a deep dive into what this long-awaited feature means for web developers and how you could make use of it in your own work.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/12/masonry-things-you-wont-need-library-anymore/" />
              <title>Masonry: Things You Won’t Need A Library For Anymore</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Masonry: Things You Won’t Need A Library For Anymore</h1>
                  
                    
                    <address>Patrick Brosset</address>
                  
                  <time datetime="2025-12-02T10:00:00&#43;00:00" class="op-published">2025-12-02T10:00:00+00:00</time>
                  <time datetime="2025-12-02T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>About 15 years ago, I was working at a company where we built apps for travel agents, airport workers, and airline companies. We also built our own in-house framework for UI components and single-page app capabilities.</p>

<p>We had components for everything: fields, buttons, tabs, ranges, datatables, menus, datepickers, selects, and multiselects. We even had a div component. Our div component was great by the way, it allowed us to do rounded corners on all browsers, which, believe it or not, wasn&rsquo;t an easy thing to do at the time.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="407"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png"
			
			sizes="100vw"
			alt="Div component, which allows to do rounded corners"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Our work took place at a point in our history when JS, Ajax, and dynamic HTML were seen as a revolution that brought us into the future. Suddenly, we could update a page dynamically, get data from a server, and avoid having to navigate to other pages, which was seen as slow and flashed a big white rectangle on the screen between the two pages.</p>

<p>There was a phrase, made popular by Jeff Atwood (the founder of StackOverflow), which read:</p>

<blockquote>“Any application that can be written in JavaScript will eventually be written in JavaScript.”<br /><br />&mdash; <a href='https://blog.codinghorror.com/all-programming-is-web-programming/#:~:text=any%20application%20that%C2%A0can%C2%A0be%20written%20in%20JavaScript%2C%C2%A0will%C2%A0eventually%20be%20written%20in%20JavaScript'>Jeff Atwood</a></blockquote>

<p>To us at the time, this felt like a dare to actually go and create those apps. It felt like a blanket approval to do everything with JS.</p>

<p>So we did everything with JS, and we didn’t really take the time to research other ways of doing things. We didn’t really feel the incentive to properly learn what HTML and CSS could do. We didn’t really perceive the web as an evolving app platform in its entirety. We mostly saw it as something we needed to work around, especially when it came to browser support. We could just throw more JS at it to get things done.</p>

<p>Would taking the time to learn more about how the web worked and what was available on the platform have helped me? Sure, I could probably have shaved a bunch of code that wasn’t truly needed. But, at the time, maybe not that much.</p>

<p>You see, browser differences were pretty significant back then. This was a time when Internet Explorer was still the dominant browser, with Firefox being the close second, but starting to lose market share due to Chrome rapidly gaining popularity. Although Chrome and Firefox were quite good at agreeing on web standards, the environments in which our apps were running meant that we had to support IE6 for a long time. Even when we were allowed to support IE8, we still had to deal with a lot of differences between browsers. Not only that, but the web of the time just didn&rsquo;t have that many capabilities built right into the platform.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://gs.statcounter.com/browser-market-share/all/worldwide/2010">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="492"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png"
			
			sizes="100vw"
			alt=""
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://gs.statcounter.com/browser-market-share/all/worldwide/2010'>statcounter</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Fast forward to today. Things have changed tremendously. Not only do we have more of these capabilities than ever before, but the rate at which they become available has increased as well.</p>

<p>Let me ask the question again, then: Would taking the time to learn more about how the web works and what is available on the platform help you today? Absolutely yes. Learning to understand and use the web platform today puts you at a huge advantage over other developers.</p>

<p>Whether you work on performance, accessibility, responsiveness, all of them together, or just shipping UI features, if you want to do it as a responsible engineer, knowing the tools that are available to you helps you reach your goals faster and better.</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="some-things-you-might-not-need-a-library-for-anymore">Some Things You Might Not Need A Library For Anymore</h2>

<p>Knowing what browsers support today, the question, then, is: What can we ditch? Do we need a div component to do rounded corners in 2025? Of course, we don’t. The <code>border-radius</code> property has been supported by all currently used browsers for more than 15 years at this point. And <code>corner-shape</code> is also coming soon, for even fancier corners.</p>

<p>Let’s take a look at relatively recent features that are now available in all major browsers, and which you can use to replace existing dependencies in your codebase.</p>

<p>The point isn&rsquo;t to immediately ditch all your beloved libraries and rewrite your codebase. As for everything else, you’ll need to take browser support into account first and decide based on other factors specific to your project. The following features are implemented in the three main browser engines (Chromium, WebKit, and Gecko), but you might have different browser support requirements that prevent you from using them right away. Now is still a good time to learn about these features, though, and perhaps plan to use them at some point.</p>

<h3 id="popovers-and-dialogs">Popovers And Dialogs</h3>

<p>The <a href="https://developer.mozilla.org/docs/Web/API/Popover_API">Popover API</a>, the <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/dialog"><code>&lt;dialog&gt;</code> HTML element</a>, and the <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/::backdrop"><code>::backdrop</code> pseudo-element</a> can help you get rid of dependencies on popup, tooltip, and dialog libraries, such as <a href="https://floating-ui.com/">Floating UI</a>, <a href="https://atomiks.github.io/tippyjs/">Tippy.js</a>, <a href="https://tetherjs.dev/docs/welcome/">Tether</a>, or <a href="https://react-tooltip.com/">React Tooltip</a>.</p>

<p>They handle accessibility and focus management for you, out of the box, are highly customizable by using CSS, and can easily be animated.</p>

<h3 id="accordions">Accordions</h3>

<p>The <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/details"><code>&lt;details&gt;</code> element</a>, its <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/details#name"><code>name</code> attribute</a> for mutually exclusive elements, and the <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/::details-content"><code>::details-content</code></a> pseudo-element remove the need for accordion components like the <a href="https://getbootstrap.com/docs/5.3/components/accordion/">Bootstrap Accordion</a> or the <a href="https://mui.com/material-ui/react-accordion/">React Accordion component</a>.</p>

<p>Just using the platform here means it’s easier for folks who know HTML/CSS to understand your code without having to first learn to use a specific library. It also means you’re immune to breaking changes in the library or the discontinuation of that library. And, of course, it means less code to download and run. Mutually exclusive details elements don’t need JS to open, close, or animate.</p>

<h3 id="css-syntax">CSS Syntax</h3>

<p><a href="https://developer.mozilla.org/docs/Web/CSS/@layer">Cascade layers</a>, for a more organized CSS codebase, <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/Nesting_selector">CSS nesting</a>, for more compact CSS, new color functions, <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_colors/Relative_colors">relative colors</a>, and <a href="https://developer.mozilla.org/docs/Web/CSS/color_value/color-mix"><code>color-mix</code></a>, new Maths functions like <a href="https://developer.mozilla.org/docs/Web/CSS/abs"><code>abs()</code></a>, <a href="https://developer.mozilla.org/docs/Web/CSS/sign"><code>sign()</code></a>, <a href="https://developer.mozilla.org/docs/Web/CSS/pow"><code>pow()</code></a> and others help reduce dependencies on <a href="https://css-tricks.com/is-it-time-to-un-sass/">CSS pre-processors</a>, utility libraries like Bootstrap and Tailwind, or even runtime CSS-in-JS libraries.</p>

<p>The game changer <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/:has"><code>:has()</code></a>, one of the most requested features for a long time, removes the need for more complicated JS-based solutions.</p>

<h3 id="js-utilities">JS Utilities</h3>

<p>Modern Array methods like <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast"><code>findLast()</code></a>, or <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/at"><code>at()</code></a>, as well as Set methods like <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/difference"><code>difference()</code></a>, <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection"><code>intersection()</code></a>, <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/union"><code>union()</code></a> and others can reduce dependencies on libraries like <a href="https://lodash.com/">Lodash</a>.</p>

<h3 id="container-queries">Container Queries</h3>

<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_containment/Container_queries">Container queries</a> make UI components respond to things other than the viewport size, and therefore make them more reusable across different contexts.</p>

<p>No need to use a JS-heavy UI library for this anymore, and no need to use a <a href="https://github.com/GoogleChromeLabs/container-query-polyfill">polyfill</a> either.</p>

<h3 id="layout">Layout</h3>

<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout">Grid</a>, <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout/Subgrid">subgrid</a>, <a href="https://developer.mozilla.org/docs/Learn_web_development/Core/CSS_layout/Flexbox">flexbox</a>, or <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/columns">multi-column</a> have been around for a long time now, but looking at the <a href="https://2025.stateofcss.com/en-US">results of the State of CSS surveys</a>, it’s clear that developers tend to be very cautious with adopting new things, and wait for a very long time before they do.</p>

<p>These features have been <a href="https://web-platform-dx.github.io/web-features/">Baseline</a> for a long time and you could use them to get rid of dependencies on things like the <a href="https://getbootstrap.com/docs/5.3/layout/grid/">Bootstrap’s grid system</a>, <a href="https://get.foundation/sites/docs/flexbox-utilities.html">Foundation Framework’s flexbox utilities</a>, <a href="https://bulma.io/documentation/grid/fixed-grid/">Bulma fixed grid</a>, <a href="https://materializecss.com/grid.html">Materialize grid</a>, or <a href="https://tailwindcss.com/docs/columns">Tailwind columns</a>.</p>

<p>I’m not saying you should drop your framework. Your team adopted it for a reason, and removing it might be a big project. But looking at what the web platform can offer without a third-party wrapper on top comes with a lot of benefits.</p>

<h2 id="things-you-might-not-need-anymore-in-the-near-future">Things You Might Not Need Anymore In The Near Future</h2>

<p>Now, let’s take a quick look at some of the things you will not need a library for in the near future. That is to say, the things below are not quite ready for mass adoption, but being aware of them and planning for potential later use can be helpful.</p>

<h3 id="anchor-positioning">Anchor Positioning</h3>

<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_anchor_positioning">CSS anchor positioning</a> handles the positioning of popovers and tooltips relative to other elements, and takes care of keeping them in view, even when moving, scrolling, or resizing the page.</p>

<p>This is a great complement to the Popover API mentioned before, which will make it even easier to migrate away from more performance-intensive JS solutions.</p>

<h3 id="navigation-api">Navigation API</h3>

<p>The <a href="https://developer.mozilla.org/docs/Web/API/Navigation_API">Navigation API</a> can be used to handle navigation in single-page apps and might be a great complement, or even a replacement, to <a href="https://reactrouter.com/">React Router</a>, <a href="https://nextjs.org/docs/routing/introduction">Next.js routing</a>, or <a href="https://angular.io/guide/router">Angular routing tasks</a>.</p>

<h3 id="view-transitions-api">View Transitions API</h3>

<p>The <a href="https://developer.mozilla.org/docs/Web/API/View_Transition_API">View Transitions API</a> can animate between the different states of a page. On a single-page application, this makes smooth transitions between states very easy, and can help you get rid of animation libraries such as <a href="https://animejs.com/">Anime.js</a>, <a href="https://greensock.com/gsap/">GSAP</a>, or <a href="https://motion.dev/">Motion.dev</a>.</p>

<p>Even better, the API can also be used with multiple-page applications.</p>

<p>Remember earlier, when I said that the reason we built single-page apps at the company where I worked 15 years ago was to avoid the white flash of page reloads when navigating? Had that API been available at the time, we would have been able to achieve beautiful page transition effects without a single-page framework and without a huge initial download of the entire app.</p>

<h3 id="scroll-driven-animations">Scroll-driven Animations</h3>

<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations">Scroll-driven animations</a> run on the user’s scroll position, rather than over time, making them a great solution for storytelling and product tours.</p>

<p>Some people <a href="https://gt-era.com/">have gone a bit over the top</a> with it, but when used well, this can be a very effective design tool, and can help get rid of libraries like: <a href="https://scrollrevealjs.org/">ScrollReveal</a>, <a href="https://gsap.com/scroll/">GSAP Scroll</a>, or <a href="https://wowjs.uk/">WOW.js</a>.</p>

<h3 id="customizable-selects">Customizable Selects</h3>

<p>A <a href="https://developer.mozilla.org/docs/Learn_web_development/Extensions/Forms/Customizable_select">customizable select</a> is a normal <code>&lt;select&gt;</code> element that lets you fully customize its appearance and content, while ensuring accessibility and performance benefits.</p>

<p>This has been a long time coming, and a highly requested feature, and it’s amazing to see it come to the web platform soon. With a built-in customizable select, you can finally ditch all this hard-to-maintain JS code for your custom select components.</p>

<h3 id="css-masonry">CSS Masonry</h3>

<p><a href="https://developer.chrome.com/blog/masonry-update">CSS Masonry</a> is another upcoming web platform feature that I want to spend more time on.</p>

<p>With CSS Masonry, you can achieve layouts that are very hard, or even impossible, with flex, grid, or other built-in CSS layout primitives. Developers often resort to using third-party libraries to achieve Masonry layouts, such as the <a href="https://masonry.desandro.com/">Masonry JS library</a>.</p>

<p>But, more on that later. Let’s wrap this point up before moving on to Masonry.</p>

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

<h2 id="why-you-should-care">Why You Should Care</h2>

<p>The job market is full of web developers with experience in JavaScript and the latest frameworks of the day. So, really, what’s the point in learning to use the web platform primitives more, if you can do the same things with the libraries, utilities, and frameworks you already know today?</p>

<p>When an entire industry relies on these frameworks, and you can just pull in the right library, shouldn’t browser vendors just work with these libraries to make them load and run faster, rather than trying to convince developers to use the platform instead?</p>

<p>First of all, we do work with library authors, and we do make frameworks better by learning about what they use and improving those areas.</p>

<p>But secondly, “just using the platform” can bring pretty significant benefits.</p>

<h3 id="sending-less-code-to-devices">Sending Less Code To Devices</h3>

<p>The main benefit is that you end up sending far less code to your clients’ devices.</p>

<p>According to the <a href="https://almanac.httparchive.org/en/2024/">2024 Web Almanac</a>, the average number of HTTP requests is around 70 per site, <a href="https://almanac.httparchive.org/en/2024/javascript#how-many-javascript-requests-per-page">most of which is due to JavaScript with 23 requests</a>. In 2024, JS overtook images as the dominant file type too. The median number of page requests for JS files is 23, up 8% since 2022.</p>

<p>And page size continues to grow year over year. The median page weight is around 2MB now, which is 1.8MB more than it was 10 years ago.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://almanac.httparchive.org/en/2024/page-weight">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="462"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png"
			
			sizes="100vw"
			alt="Median page weight"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://almanac.httparchive.org/en/2024/page-weight'>Web Almanac</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Sure, your internet connection speed has probably increased, too, but that’s not the case for everyone. And not everyone has the same device capabilities either.</p>

<p>Pulling in third-party code for things you can do with the platform, instead, most probably means you ship more code, and therefore reach fewer customers than you normally would. On the web, bad loading performance leads to large abandonment rates and hurts brand reputation.</p>

<h3 id="running-less-code-on-devices">Running Less Code On Devices</h3>

<p>Furthermore, the code you do ship on your customers’ devices likely runs faster if it uses fewer JavaScript abstractions on top of the platform. It’s also probably more responsive and more accessible by default. All of this leads to more and happier customers.</p>

<p>Check my colleague Alex Russell’s <a href="https://infrequently.org/2024/01/performance-inequality-gap-2024/">yearly performance inequality gap blog</a>, which shows that premium devices are largely absent from markets with billions of users due to wealth inequality. And this gap is only growing over time.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://infrequently.org/2024/01/performance-inequality-gap-2024/#device-performance">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="452"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png"
			
			sizes="100vw"
			alt="Device performance scores"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://infrequently.org/2024/01/performance-inequality-gap-2024/#device-performance'>Infrequently Noted</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="built-in-masonry-layout">Built-in Masonry Layout</h2>

<p>One web platform feature that’s coming soon and which I’m very excited about is CSS Masonry.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="459"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png"
			
			sizes="100vw"
			alt="CSS Masonry"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Let me start by explaining what Masonry is.</p>

<h3 id="what-is-masonry">What Is Masonry</h3>

<blockquote>Masonry is a type of layout that was made popular by Pinterest years ago. It creates independent tracks of content within which items pack themselves up as close to the start of the track as they can.</blockquote>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="604"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png"
			
			sizes="100vw"
			alt=""
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <a href='pinterest.com'>Pinterest</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Many people see Masonry as a great option for portfolios and photo galleries, which it certainly can do. But Masonry is <strong>more flexible</strong> than what you see on Pinterest, and it’s <strong>not limited to just waterfall-like layouts</strong>.</p>

<p>In a Masonry layout:</p>

<ul>
<li>Tracks can be columns or rows:</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.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/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png"
			
			sizes="100vw"
			alt="Masonry layout with columns and rows"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li>Tracks of content don’t all have to be the same size:</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="664"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png"
			
			sizes="100vw"
			alt="Masonry layout with tracks of different sizes"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li>Items can span multiple tracks:</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="565"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png"
			
			sizes="100vw"
			alt="Masonry layout with multiple tracks"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png'>Large preview</a>)
    </figcaption>
  
</figure>

<ul>
<li>Items can be placed on specific tracks; they don’t have to always follow the automatic placement algorithm:</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="628"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png"
			
			sizes="100vw"
			alt="Masonry layout with items on specific tracks"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="demos">Demos</h3>

<p>Here are a few simple demos I made by using the upcoming implementation of CSS Masonry in Chromium.</p>

<p><a href="https://microsoftedge.github.io/Demos/css-masonry/new-york.html">A photo gallery demo</a>, showing how items (the title in this case) can span multiple tracks:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="560"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png"
			
			sizes="100vw"
			alt="A photo gallery demo, showing items on multiple tracks"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Another <a href="https://microsoftedge.github.io/Demos/css-masonry/nature.html">photo gallery showing tracks of different sizes</a>:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="437"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png"
			
			sizes="100vw"
			alt="A photo gallery showing tracks of different sizes"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>A <a href="https://microsoftedge.github.io/Demos/css-masonry/the-daily-oddity.html">news site layout</a> with some tracks wider than others, and some items spanning the entire width of the layout:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="607"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png"
			
			sizes="100vw"
			alt="A news site layout with some tracks wider than others"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>A <a href="https://microsoftedge.github.io/Demos/css-masonry/kanban.html">kanban board</a> showing that items can be placed onto specific tracks:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="320"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png"
			
			sizes="100vw"
			alt="A kanban board with items on specific tracks"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <em>The previous demos were made with a version of Chromium that’s not yet available to most web users, because CSS Masonry is only just starting to be implemented in browsers.</em></p>

<p>However, web developers have been happily using libraries to create Masonry layouts for years already.</p>

<h3 id="sites-using-masonry-today">Sites Using Masonry Today</h3>

<p>Indeed, Masonry is pretty common on the web today. Here are a few examples I found besides Pinterest:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="458"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png"
			
			sizes="100vw"
			alt="Erik Johansson&#39;s photography site"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://www.erikjo.com/work'>Erik Johansson</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="456"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png"
			
			sizes="100vw"
			alt="Kristian Hammerstad&#39;s site"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://www.kristianhammerstad.com/'>Kristian Hammerstad</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="479"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png"
			
			sizes="100vw"
			alt="L&#39;usine a Gouzou&#39;s catalogue"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://lusineagouzou.fr/catalogue'>L'usine a Gouzou</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>And a few more, less obvious, examples:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="428"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png"
			
			sizes="100vw"
			alt="Masonry layout from Agora"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A row-direction Masonry layout from <a href='http://agora.io/en/'>www.agora.io</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="633"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png"
			
			sizes="100vw"
			alt="Different size tracks from The Free Dictionary"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Different size tracks from <a href='https://www.thefreedictionary.com/'>www.thefreedictionary.com</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="605"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png"
			
			sizes="100vw"
			alt="Masonry layout of OneSignal"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://onesignal.com/'>OneSignal</a>. (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>So, how were these layouts created?</p>

<h2 id="workarounds">Workarounds</h2>

<p>One trick that I’ve seen used is using a Flexbox layout instead, changing its direction to column, and setting it to wrap.</p>

<p>This way, you can place items of different heights in multiple, independent columns, giving the impression of a Masonry layout:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="578"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png"
			
			sizes="100vw"
			alt="Flexbox layout"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>There are, however, two limitations with this workaround:</p>

<ol>
<li>The order of items is different from what it would be with a real Masonry layout. With Flexbox, items fill the first column first and, when it’s full, then go to the next column. With Masonry, items would stack in whichever track (or column in this case) has more space available.</li>
<li>But also, and perhaps more importantly, this workaround requires that you set a fixed height to the Flexbox container; otherwise, no wrapping would occur.</li>
</ol>

<h2 id="third-party-masonry-libraries">Third-party Masonry Libraries</h2>

<p>For more advanced cases, developers have been using libraries.</p>

<p>The most well-known and popular library for this is simply called <a href="https://masonry.desandro.com/">Masonry</a>, and it gets downloaded about 200,000 times per week <a href="https://www.npmjs.com/package/masonry-layout">according to NPM</a>.</p>

<p>Squarespace also provides a <a href="https://www.beyondspace.studio/blog/squarespace-masonry-gallery-layout-guide#method-2-using-gallery-section">layout component that renders a Masonry layout</a>, for a no-code alternative, and many sites use it.</p>

<p>Both of these options use JavaScript code to place items in the layout.</p>

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

<h2 id="built-in-masonry">Built-in Masonry</h2>

<p>I’m really excited that Masonry is now starting to appear in browsers as a built-in CSS feature. Over time, you will be able to use Masonry just like you do Grid or Flexbox, that is, without needing any workarounds or third-party code.</p>

<p>My team at Microsoft has been implementing built-in Masonry support in the Chromium open source project, which Edge, Chrome, and many other browsers are based on. Mozilla was actually the first browser vendor to <a href="https://github.com/w3c/csswg-drafts/issues/4650">propose an experimental implementation of Masonry</a> back in 2020. And <a href="https://webkit.org/blog/15269/help-us-invent-masonry-layouts-for-css-grid-level-3/">Apple has also been very interested</a> in making this new web layout primitive happen.</p>

<p>The work to standardize the feature is also moving ahead, with agreement within the CSS working group about the general direction and even a new display type <a href="https://github.com/w3c/csswg-drafts/issues/12022#issuecomment-3525043825"><code>display: grid-lanes</code></a>.</p>

<p>If you want to learn more about Masonry and track progress, check out my <a href="https://patrickbrosset.com/lab/css-masonry-resources/">CSS Masonry resources</a> page.</p>

<p>In time, when Masonry becomes a Baseline feature, just like Grid or Flexbox, we’ll be able to simply use it and benefit from:</p>

<ul>
<li>Better performance,</li>
<li>Better responsiveness,</li>
<li>Ease of use and simpler code.</li>
</ul>

<p>Let’s take a closer look at these.</p>

<h3 id="better-performance">Better Performance</h3>

<p>Making your own Masonry-like layout system, or using a third-party library instead, means you’ll have to run JavaScript code to place items on the screen. This also means that this code will be <em>render blocking</em>. Indeed, either nothing will appear, or things won’t be in the right places or of the right sizes, until that JavaScript code has run.</p>

<p>Masonry layout is often used for the main part of a web page, which means the code would be making your main content appear later than it could otherwise have, degrading your <a href="https://web.dev/articles/lcp#what-is-lcp">LCP, or Largest Contentful Paint metric</a>, which plays a big role in perceived performance and search engine optimization.</p>

<p>I tested the Masonry JS library with a simple layout and by simulating a slow 4G connection in DevTools. The library is not very big (24KB, 7.8KB gzipped), but it took 600ms to load under my test conditions.</p>

<p>Here is a performance recording showing that long 600ms load time for the Masonry library, and that no other rendering activity happened while that was happening:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="541"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png"
			
			sizes="100vw"
			alt="A performance recording showing 600ms load time for the Masonry library"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In addition, after the initial load time, the downloaded script then needed to be parsed, compiled, and then run. All of which, as mentioned before, was blocking the rendering of the page.</p>

<p>With a built-in Masonry implementation in the browser, we won’t have a script to load and run. The browser engine will just do its thing during the initial page rendering step.</p>

<h3 id="better-responsiveness">Better Responsiveness</h3>

<p>Similar to when a page first loads, resizing the browser window leads to rendering the layout in that page again. At this point, though, if the page is using the Masonry JS library, there’s no need to load the script again, because it’s already here. However, the code that moves items in the right places needs to run.</p>

<p>Now this particular library seems to be pretty fast at doing this when the page loads. However, it animates the items when they need to move to a different place on window resize, and this makes a big difference.</p>

<p>Of course, users don’t spend time resizing their browser windows as much as we developers do. But this animated resizing experience can be pretty jarring and adds to the perceived time it takes for the page to adapt to its new size.</p>

<h3 id="ease-of-use-and-simpler-code">Ease Of Use And Simpler Code</h3>

<p>How easy it is to use a web feature and how simple the code looks are important factors that can make a big difference for your team. They can’t ever be as important as the final user experience, of course, but developer experience impacts maintainability. Using a built-in web feature comes with important benefits on that front:</p>

<ul>
<li>Developers who already know HTML, CSS, and JS will most likely be able to use that feature easily because it’s been designed to integrate well and be consistent with the rest of the web platform.</li>
<li>There’s no risk of breaking changes being introduced in how the feature is used.</li>
<li>There’s almost zero risk of that feature becoming deprecated or unmaintained.</li>
</ul>

<p>In the case of built-in Masonry, because it’s a layout primitive, you use it from CSS, just like Grid or Flexbox, no JS involved. Also, other layout-related CSS properties, such as gap, work as you’d expect them to. There are no tricks or workarounds to know about, and the things you do learn are documented on MDN.</p>

<p>For the Masonry JS lib, initialization is a bit complex: it requires a data attribute with a specific syntax, along with hidden HTML elements to set the column and gap sizes.</p>

<p>Plus, if you want to span columns, you need to include the gap size yourself to avoid problems:</p>

<div class="break-out">
<pre><code class="language-html">&lt;script src="https://unpkg.com/masonry-layout@4.2.2/dist/masonry.pkgd.min.js"&gt;&lt;/script&gt;
&lt;style&gt;
  .track-sizer,
  .item {
    width: 20%;
  }
  .gutter-sizer {
    width: 1rem;
  }
  .item {
    height: 100px;
    margin-block-end: 1rem;
  }
  .item:nth-child(odd) {
    height: 200px;
  }
  .item--width2 {
    width: calc(40% + 1rem);
  }
&lt;/style&gt;

&lt;div class="container"
  data-masonry='{ "itemSelector": ".item", "columnWidth": ".track-sizer", "percentPosition": true, "gutter": ".gutter-sizer" }'&gt;
  &lt;div class="track-sizer"&gt;&lt;/div&gt;
  &lt;div class="gutter-sizer"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  &lt;div class="item item--width2"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  ...
&lt;/div&gt;
</code></pre>
</div>

<p>Let’s compare this to what a built-in Masonry implementation would look like:</p>

<pre><code class="language-html">&lt;style&gt;
  .container {
    display: grid-lanes;
    grid-lanes: repeat(4, 20%);
    gap: 1rem;
  }
  .item {
    height: 100px;
  }
  .item:nth-child(odd) {
    height: 200px;
  }
  .item--width2 {
    grid-column: span 2;
  }
&lt;/style&gt;

&lt;div class="container"&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  &lt;div class="item item--width2"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  ...
&lt;/div&gt;
</code></pre>

<p>Simpler, more compact code that can just use things like <code>gap</code> and where spanning tracks is done with <code>span 2</code>, just like in grid, and doesn’t require you to calculate the right width that includes the gap size.</p>

<h2 id="how-to-know-what-s-available-and-when-it-s-available">How To Know What’s Available And When It’s Available?</h2>

<p>Overall, the question isn’t really if you should use built-in Masonry over a JS library, but rather <em>when</em>. The Masonry JS library is amazing and has been filling a gap in the web platform for many years, and for many happy developers and users. It has a few drawbacks if you compare it to a built-in Masonry implementation, of course, but those are not important if that implementation isn’t ready.</p>

<p>It’s easy for me to list these cool new web platform features because I work at a browser vendor, and I therefore tend to know what’s coming. But developers often share, survey after survey, that keeping track of new things is hard. <strong>Staying informed is difficult</strong>, and companies don’t always prioritize learning anyway.</p>

<p>To help with this, here are a few resources that provide updates in simple and compact ways so you can get the information you need quickly:</p>

<ul>
<li><a href="https://web-platform-dx.github.io/web-features-explorer/">The Web platform features explorer site</a>:

<ul>
<li>You might be interested in its <a href="https://web-platform-dx.github.io/web-features-explorer/release-notes/october-2025/">release notes</a> page.</li>
<li>And, if you like RSS, check out <a href="https://web-platform-dx.github.io/web-features-explorer/release-notes.xml">the release notes feed</a>, as well as the Baseline <a href="https://web-platform-dx.github.io/web-features-explorer/newly-available.xml">Newly Available</a> and <a href="https://web-platform-dx.github.io/web-features-explorer/widely-available.xml">Widely Available</a> feeds.</li>
</ul></li>
<li><a href="https://webstatus.dev/">The Web Platform Status dashboard</a>:

<ul>
<li>You might like its various <a href="https://webstatus.dev/?q=baseline_date%3A2025-01-01..2025-12-31">Baseline year</a> pages.</li>
</ul></li>
<li><a href="https://chromestatus.com/roadmap">Chrome Platform Status’ roadmap page</a>.</li>
</ul>

<p>If you have a bit more time, you might also be interested in browser vendors’ release notes:</p>

<ul>
<li><a href="https://developer.chrome.com/release-notes">Chrome</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-edge/web-platform/release-notes/">Edge</a></li>
<li><a href="https://www.firefox.com/en-US/releases/">Firefox</a></li>
<li><a href="https://developer.apple.com/documentation/safari-release-notes">Safari</a></li>
</ul>

<p>For even more resources, check out my <a href="https://patrickbrosset.com/lab/navigating-the-web-platform/">Navigating the Web Platform Cheatsheet</a>.</p>

<h2 id="my-thing-is-still-not-implemented">My Thing Is Still Not Implemented</h2>

<p>That’s the other side of the problem. Even if you do find the time, energy, and ways to keep track, there’s still frustration with getting your voice heard and your favorite features implemented.</p>

<p>Maybe you’ve been waiting for years for a specific bug to be resolved, or a specific feature to ship in a browser where it’s still missing.</p>

<p>What I’ll say is <strong>browser vendors do listen</strong>. I’m part of several cross-organization teams where we discuss developer signals and feedback all the time. We look at many different sources of feedback, both internal at each browser vendor and external/public on forums, open source projects, blogs, and surveys. And, we’re always trying to create better ways for developers to share their specific needs and use cases.</p>

<p>So, if you can, please demand more from browser vendors and pressure us to implement the features you need. I get that it takes time, and can also be intimidating (not to mention a high barrier to entry), but it also works.</p>

<p>Here are a few ways you can get your (or your company’s) voice heard: Take the annual <a href="https://stateofjs.com/">State of JS</a>, <a href="https://stateofcss.com/">State of CSS</a>, and <a href="https://stateofhtml.com/">State of HTML</a> surveys. They play a big role in how browser vendors prioritize their work.</p>

<p>If you need a specific standard-based API to be implemented consistently across browsers, consider submitting a proposal at the next <a href="https://github.com/web-platform-tests/interop/">Interop project</a> iteration. It requires more time, but consider how <a href="https://docs.google.com/document/d/1ICqlNtdRXlhIlRuXFr1BRgy68R6Q5AwPv2b4hsIWUMY/edit">Shopify</a> and <a href="https://www.rumvision.com/blog/interop-2026-key-apis-for-sitespeed-and-rum/">RUMvision</a> shared their wish lists for Interop 2026. Detailed information like this can be very useful for browser vendors to prioritize.</p>

<p>For more useful links to influence browser vendors, check out my <a href="https://patrickbrosset.com/lab/navigating-the-web-platform/">Navigating the Web Platform Cheatsheet</a>.</p>

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

<p>To close, I hope this article has left you with a few things to think about:</p>

<ul>
<li>Excitement for Masonry and other upcoming web features.</li>
<li>A few web features you might want to start using.</li>
<li>A few pieces of custom or 3rd-party code you might be able to remove in favor of built-in features.</li>
<li>A few ways to keep track of what’s coming and influence browser vendors.</li>
</ul>

<p>More importantly, I hope I’ve convinced you of the benefits of using the web platform to its full potential.</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>Amit Sheen</author><title>Keyframes Tokens: Standardizing Animation Across Projects</title><link>https://www.smashingmagazine.com/2025/11/keyframes-tokens-standardizing-animation-across-projects/</link><pubDate>Fri, 21 Nov 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/keyframes-tokens-standardizing-animation-across-projects/</guid><description>Animations can be one of the most joyful parts of building interfaces, but without structure, they can also become one of the biggest sources of frustration. By consolidating and standardizing keyframes, you take something that is usually messy and hard to manage and turn it into a clear, predictable system.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/keyframes-tokens-standardizing-animation-across-projects/" />
              <title>Keyframes Tokens: Standardizing Animation Across Projects</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Keyframes Tokens: Standardizing Animation Across Projects</h1>
                  
                    
                    <address>Amit Sheen</address>
                  
                  <time datetime="2025-11-21T08:00:00&#43;00:00" class="op-published">2025-11-21T08:00:00+00:00</time>
                  <time datetime="2025-11-21T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Picture this: you join a new project, dive into the codebase, and within the first few hours, you discover something frustratingly familiar. Scattered throughout the stylesheets, you find multiple <code>@keyframes</code> definitions for the same basic animations. Three different fade-in effects, two or three slide variations, a handful of zoom animations, and at least two different spin animations because, well, why not?</p>

<pre><code class="language-css">@keyframes pulse {
  from {
    scale: 1;
  }
  to {
    scale: 1.1;
  }
}

@keyframes bigger-pulse {
  0%, 20%, 100% { 
    scale: 1; 
  }
  10%, 40% { 
    scale: 1.2; 
  }
}
</code></pre>

<p>If this scenario sounds familiar, you’re not alone. In my experience across various projects, one of the most consistent quick wins I can deliver is <strong>consolidating and standardizing keyframes</strong>. It’s become such a reliable pattern that I now look forward to this cleanup as one of my first tasks on any new codebase.</p>

<h2 id="the-logic-behind-the-chaos">The Logic Behind The Chaos</h2>

<p>This redundancy makes perfect sense when you think about it. We all use the same fundamental animations in our day-to-day work: fades, slides, zooms, spins, and other common effects. These animations are pretty straightforward, and it&rsquo;s easy to whip up a quick <code>@keyframes</code> definition to get the job done.</p>

<p>Without a centralized animation system, developers naturally write these keyframes from scratch, unaware that similar animations already exist elsewhere in the codebase. This is especially common when working in component-based architectures (which most of us do these days), as teams often work in parallel across different parts of the application.</p>

<p>The result? Animation chaos.</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-small-problem">The Small Problem</h2>

<p>The most obvious issues with keyframes duplication are wasted development time and unnecessary code bloat. Multiple keyframe definitions mean multiple places to update when requirements change. Need to adjust the timing of your fade animation? You’ll need to hunt down every instance across your codebase. Want to standardize easing functions? Good luck finding all the variations. This multiplication of maintenance points makes even simple animation updates a time-consuming task.</p>

<h2 id="the-bigger-problem">The Bigger Problem</h2>

<p>This keyframes duplication creates a much more insidious problem lurking beneath the surface: <strong>the global scope trap.</strong> Even when working with component-based architectures, CSS keyframes are always defined in the global scope. This means all keyframes apply to all components. Always. Yes, your animation doesn&rsquo;t necessarily use the keyframes you defined in your component. It uses the last keyframes that match that exact same name that were loaded into the global scope.</p>

<p>As long as all your keyframes are identical, this might seem like a minor issue. But the moment you want to customize an animation for a specific use case, you’re in trouble, or worse, you’ll be the one causing them.</p>

<p>Either your animation won’t work because another component loaded after yours, overwriting your keyframes, or your component loads last and accidentally changes the animation behavior for every other component using that keyframe&rsquo;s name, and you may not even realize it.</p>

<p>Here’s a simple example that demonstrates the problem:</p>

<pre><code class="language-css">.component-one {
  /&#42; component styles &#42;/
  animation: pulse 1s ease-in-out infinite alternate;
}

/&#42; this @keyframes definition will not work &#42;/
@keyframes pulse {
  from {
    scale: 1;
  }
  to {
    scale: 1.1;
  }
} 

/&#42; later in the code... &#42;/

.component-two {
  /&#42; component styles &#42;/
  animation: pulse 1s ease-in-out infinite;
}

/&#42; this keyframes will apply to both components &#42;/
@keyframes pulse {
  0%, 20%, 100% { 
    scale: 1; 
  }
  10%, 40% { 
    scale: 1.2; 
  }
}
</code></pre>

<p>Both components use the same animation name, but the second <code>@keyframes</code> definition overwrites the first one. Now both <code>component-one</code> and <code>component-two</code> will use the second keyframes, regardless of which component defined which keyframes.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="JoXrOqz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 1 [forked]](https://codepen.io/smashingmag/pen/JoXrOqz) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/JoXrOqz">Keyframes Tokens - Demo 1 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>The worst part? This often works perfectly in local development but breaks mysteriously in production when build processes change the loading order of your stylesheets. You end up with animations that behave differently depending on which components are loaded and in what sequence.</p>

<h2 id="the-solution-unified-keyframes">The Solution: Unified Keyframes</h2>

<p>The answer to this chaos is surprisingly simple: <strong>predefined dynamic keyframes stored in a shared stylesheet</strong>. Instead of letting every component define its own animations, we create centralized keyframes that are well-documented, easy to use, maintainable, and tailored to the specific needs of your project.</p>

<p>Think of it as <strong>keyframes tokens</strong>. Just as we <a href="https://www.smashingmagazine.com/2024/05/naming-best-practices/">use tokens for colors and spacing</a>, and many of us already use tokens for animation properties, like duration and easing functions, why not use tokens for keyframes as well?</p>

<p>This approach can integrate naturally with any current design token workflow you’re using, while solving both the small problem (code duplication) and the bigger problem (global scope conflicts) in one go.</p>

<p><strong>The idea is straightforward:</strong> create a single source of truth for all our common animations. This shared stylesheet contains carefully crafted keyframes that cover the animation patterns our project actually uses. No more guessing whether a fade animation already exists somewhere in our codebase. No more accidentally overwriting animations from other components.</p>

<p>But here’s the key: these aren’t just static copy-paste animations. They’re designed to be dynamic and customizable through CSS custom properties, allowing us to maintain consistency while still having the flexibility to adapt animations to specific use cases, like if you need a slightly bigger “pulse” animation in one place.</p>

<h2 id="building-the-first-keyframes-token">Building The First Keyframes Token</h2>

<p>One of the first low-hanging fruits we should tackle is the “fade-in” animation. In one of my recent projects, I found over a dozen separate fade-in definitions, and yes, they all simply animated the <code>opacity</code> from <code>0</code> to <code>1</code>.</p>

<p>So, let’s create a new stylesheet, call it <code>kf-tokens.css</code>, import it into our project, and place our keyframes with proper comments inside of it.</p>

<pre><code class="language-css">/&#42; keyframes-tokens.css &#42;/

/&#42;
 &#42; Fade In - fade entrance animation
 &#42; Usage: animation: kf-fade-in 0.3s ease-out;
 &#42;/
@keyframes kf-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
</code></pre>

<p>This single <code>@keyframes</code> declaration replaces all those scattered fade-in animations across our codebase. Clean, simple, and globally applicable. And now that we have this token defined, we can use it from any component throughout our project:</p>

<pre><code class="language-css">.modal {
  animation: kf-fade-in 0.3s ease-out;
}

.tooltip {
  animation: kf-fade-in 0.2s ease-in-out;
}

.notification {
  animation: kf-fade-in 0.5s ease-out;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="yyOzPdv"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 2 [forked]](https://codepen.io/smashingmag/pen/yyOzPdv) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/yyOzPdv">Keyframes Tokens - Demo 2 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p><strong>Note:</strong> <em>We’re using a <code>kf-</code> prefix in all our <code>@keyframes</code> names. This prefix serves as a namespace that prevents naming conflicts with existing animations in the project and makes it immediately clear that these keyframes come from our keyframes tokens file.</em></p>

<h2 id="making-a-dynamic-slide">Making A Dynamic Slide</h2>

<p>The <code>kf-fade-in</code> keyframes work great because it&rsquo;s simple and there&rsquo;s little room to mess things up. In other animations, however, we need to be much more dynamic, and here we can leverage the enormous power of <a href="https://www.smashingmagazine.com/2017/04/start-using-css-custom-properties/">CSS custom properties</a>. This is where keyframes tokens really shine compared to scattered static animations.</p>

<p>Let’s take a common scenario: “slide-in” animations. But slide in from where? <code>100px</code> from the right? <code>50%</code> from the left? Should it enter from the top of the screen? Or maybe float in from the bottom? So many possibilities, but instead of creating separate keyframes for each direction and each variation, we can build one flexible token that adapts to all scenarios:</p>

<pre><code class="language-css">/&#42;
 &#42; Slide In - directional slide animation
 &#42; Use --kf-slide-from to control direction
 &#42; Default: slides in from left (-100%)
 &#42; Usage: 
 &#42;   animation: kf-slide-in 0.3s ease-out;
 &#42;   --kf-slide-from: -100px 0; // slide from left
 &#42;   --kf-slide-from: 100px 0;  // slide from right
 &#42;   --kf-slide-from: 0 -50px;  // slide from top
 &#42;/

@keyframes kf-slide-in {
  from {
    translate: var(--kf-slide-from, -100% 0);
  }
  to {
    translate: 0 0;
  }
}
</code></pre>

<p>Now we can use this single <code>@keyframes</code> token for any slide direction simply by changing the <code>--kf-slide-from</code> custom property:</p>

<pre><code class="language-css">.sidebar {
  animation: kf-slide-in 0.3s ease-out;
  /&#42; Uses default value: slides from left &#42;/
}

.notification {
  animation: kf-slide-in 0.4s ease-out;
  --kf-slide-from: 0 -50px; /&#42; slide from top &#42;/
}

.modal {
  animation:
    kf-fade-in 0.5s,
    kf-slide-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-slide-from: 50px 50px; /&#42; slide from bottom-right &#42;/
}
</code></pre>

<p>This approach gives us incredible flexibility while maintaining consistency. One keyframe declaration, infinite possibilities.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="raeGYXr"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 3 [forked]](https://codepen.io/smashingmag/pen/raeGYXr) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/raeGYXr">Keyframes Tokens - Demo 3 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>And if we want to make our animations even more flexible, allowing for “slide-out” effects as well, we can simply add a <code>--kf-slide-to</code> custom property, similar to what we’ll see in the next section.</p>

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

<h2 id="bidirectional-zoom-keyframes">Bidirectional Zoom Keyframes</h2>

<p>Another common animation that gets duplicated across projects is “zoom” effects. Whether it’s a subtle scale-up for toast messages, a dramatic zoom-in for modals, or a gentle scale-down effect for headings, zoom animations are everywhere.</p>

<p>Instead of creating separate keyframes for each scale value, let’s build one flexible set of <code>kf-zoom</code> keyframes:</p>

<div class="break-out">
<pre><code class="language-css">/&#42;
 &#42; Zoom - scale animation
 &#42; Use --kf-zoom-from and --kf-zoom-to to control scale values
 &#42; Default: zooms from 80% to 100% (0.8 to 1)
 &#42; Usage:
 &#42;   animation: kf-zoom 0.2s ease-out;
 &#42;   --kf-zoom-from: 0.5; --kf-zoom-to: 1;   // zoom from 50% to 100%
 &#42;   --kf-zoom-from: 1; --kf-zoom-to: 0;     // zoom from 100% to 0%
 &#42;   --kf-zoom-from: 1; --kf-zoom-to: 1.1;   // zoom from 100% to 110%
 &#42;/

@keyframes kf-zoom {
  from {
    scale: var(--kf-zoom-from, 0.8);
  }
  to {
    scale: var(--kf-zoom-to, 1);
  }
}
</code></pre>
</div>

<p>With one definition, we can achieve any zoom variation we need:</p>

<pre><code class="language-css">.toast {
  animation:
    kf-slide-in 0.2s,
    kf-zoom 0.4s ease-out;
  --kf-slide-from: 0 100%; /&#42; slide from top &#42;/
  /&#42; Uses default zoom: scales from 80% to 100% &#42;/
}

.modal {
  animation: kf-zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-zoom-from: 0; /&#42; dramatic zoom from 0% to 100% &#42;/
}

.heading {
  animation:
    kf-fade-in 2s,
    kf-zoom 2s ease-in;
  --kf-zoom-from: 1.2; 
  --kf-zoom-to: 0.8; /&#42; gentle scale down &#42;/
}
</code></pre>

<p>The default of <code>0.8</code> (80%) works perfectly for most UI elements, like toast messages and cards, while still being easy to customize for special cases.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="WbwZdQZ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 4 [forked]](https://codepen.io/smashingmag/pen/WbwZdQZ) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/WbwZdQZ">Keyframes Tokens - Demo 4 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>You might have noticed something interesting in the recent examples: we&rsquo;ve been <strong>combining animations</strong>. One of the key advantages of working with <code>@keyframes</code> tokens is that they’re designed to integrate seamlessly with each other. This smooth composition is intentional, not accidental.</p>

<p>We’ll discuss animation composition in more detail later, including where they can become problematic, but most combinations are straightforward and easy to implement.</p>

<p><strong>Note:</strong> <em>While writing this article, and maybe because of writing it, I found myself rethinking the whole idea of entrance animations. With all the recent advances in CSS, do we still need them at all? Luckily, Adam Argyle explored the same questions and expressed them brilliantly <a href="https://nerdy.dev/using-starting-style-and-transition-behavior-for-enter-and-exit-stage-effects">in his blog</a>. This doesn’t contradict what’s written here, but it does present an approach worth considering, especially if your projects rely heavily on entrance animations.</em></p>

<h2 id="continuous-animations">Continuous Animations</h2>

<p>While entrance animations, like “fade”, “slide”, and “zoom” happen once and then stop, continuous animations loop indefinitely to draw attention or indicate ongoing activity. The two most common continuous animations I encounter are “spin” (for loading indicators) and “pulse” (for highlighting important elements).</p>

<p>These animations present unique challenges when it comes to creating keyframes tokens. Unlike entrance animations that typically go from one state to another, continuous animations need to be highly customizable in their behavior patterns.</p>

<h3 id="the-spin-doctor">The Spin Doctor</h3>

<p>Every project seems to use multiple spin animations. Some spin clockwise, others counterclockwise. Some do a single 360-degree rotation, others do multiple turns for a faster effect. Instead of creating separate keyframes for each variation, let’s build one flexible spin that handles all scenarios:</p>

<div class="break-out">
<pre><code class="language-css">/&#42;
 &#42; Spin - rotation animation
 &#42; Use --kf-spin-from and --kf-spin-to to control rotation range
 &#42; Use --kf-spin-turns to control rotation amount
 &#42; Default: rotates from 0deg to 360deg (1 full rotation)
 &#42; Usage:
 &#42;   animation: kf-spin 1s linear infinite;
 &#42;   --kf-spin-turns: 2;   // 2 full rotations
 &#42;   --kf-spin-from: 0deg; --kf-spin-to: 180deg;  // half rotation
 &#42;   --kf-spin-from: 0deg; --kf-spin-to: -360deg; // counterclockwise
 &#42;/

@keyframes kf-spin {
  from {
    rotate: var(--kf-spin-from, 0deg);
  }
  to {
    rotate: calc(var(--kf-spin-from, 0deg) + var(--kf-spin-to, 360deg) &#42; var(--kf-spin-turns, 1));
  }
}
</code></pre>
</div>

<p>Now we can create any spin variation we like:</p>

<div class="break-out">
<pre><code class="language-css">.loading-spinner {
  animation: kf-spin 1s linear infinite;
  /&#42; Uses default: rotates from 0deg to 360deg &#42;/
} 

.fast-loader {
  animation: kf-spin 1.2s ease-in-out infinite alternate;
  --kf-spin-turns: 3; /&#42; 3 full rotations for each direction per cycle &#42;/
}

.steped-reverse {
  animation: kf-spin 1.5s steps(8) infinite;
  --kf-spin-to: -360deg; /&#42; counterclockwise &#42;/
}

.subtle-wiggle {
  animation: kf-spin 2s ease-in-out infinite alternate;
  --kf-spin-from: -16deg;
  --kf-spin-to: 32deg; /&#42; wiggle 36 deg: between -18deg and +18deg &#42;/
}
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="MYyErbq"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 5 [forked]](https://codepen.io/smashingmag/pen/MYyErbq) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/MYyErbq">Keyframes Tokens - Demo 5 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>The beauty of this approach is that the same keyframes work for loading spinners, rotating icons, wiggle effects, and even complex multi-turn animations.</p>

<h2 id="the-pulse-paradox">The Pulse Paradox</h2>

<p>Pulse animations are trickier because they can “pulse” different properties. Some pulse the <code>scale</code>, others pulse the <code>opacity</code>, and some pulse <code>color</code> properties like brightness or saturation. Rather than creating separate keyframes for each property, we can create keyframes that work with any CSS property.</p>

<p>Here&rsquo;s an example of a pulse keyframe with <code>scale</code> and <code>opacity</code> options:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; 
 &#42; Pulse - pulsing animation
 &#42; Use --kf-pulse-scale-from and --kf-pulse-scale-to to control scale range
 &#42; Use --kf-pulse-opacity-from and --kf-pulse-opacity-to to control opacity range
 &#42; Default: no pulse (all values 1)
 &#42; Usage:
 &#42;   animation: kf-pulse 2s ease-in-out infinite alternate;
 &#42;   --kf-pulse-scale-from: 0.95; --kf-pulse-scale-to: 1.05; // scale pulse
 &#42;   --kf-pulse-opacity-from: 0.7; --kf-pulse-opacity-to: 1; // opacity pulse
 &#42;/

@keyframes kf-pulse {
  from {
    scale: var(--kf-pulse-scale-from, 1);
    opacity: var(--kf-pulse-opacity-from, 1);
  }
  to {
    scale: var(--kf-pulse-scale-to, 1);
    opacity: var(--kf-pulse-opacity-to, 1);
  }
}
</code></pre>
</div>

<p>This creates a flexible pulse that can animate multiple properties:</p>

<pre><code class="language-css">.call-to-action { 
  animation: kf-pulse 0.6s infinite alternate;
  --kf-pulse-opacity-from: 0.5; /&#42; opacity pulse &#42;/
}

.notification-dot {
  animation: kf-pulse 0.6s ease-in-out infinite alternate;
  --kf-pulse-scale-from: 0.9; 
  --kf-pulse-scale-to: 1.1; /&#42; scale pulse &#42;/
}

.text-highlight {
  animation: kf-pulse 1.5s ease-out infinite;
  --kf-pulse-scale-from: 0.8;
  --kf-pulse-opacity-from: 0.2;
  /&#42; scale and opacity pulse &#42;/
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="xbVXpRo"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 6 [forked]](https://codepen.io/smashingmag/pen/xbVXpRo) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/xbVXpRo">Keyframes Tokens - Demo 6 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>This single <code>kf-pulse</code> keyframe can handle everything from subtle attention grabs to dramatic highlights, all while being easy to customize.</p>

<h2 id="advanced-easing">Advanced Easing</h2>

<p>One of the great things about using keyframes tokens is how easy it is to expand our animation library and provide effects that most developers would not bother to write from scratch, like <em>elastic</em> or <em>bounce</em>.</p>

<p>Here is an example of a simple “bounce” keyframes token that uses a <code>--kf-bounce-from</code> custom property to control the jump height.</p>

<pre><code class="language-css">/&#42;
 &#42; Bounce - bouncing entrance animation
 &#42; Use --kf-bounce-from to control jump height
 &#42; Default: jumps from 100vh (off screen)
 &#42; Usage:
 &#42;   animation: kf-bounce 3s ease-in;
 &#42;   --kf-bounce-from: 200px; // jump from 200px height
 &#42;/

@keyframes kf-bounce {
  0% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -1);
  }

  34% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.4);
  }

  55% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.2);
  }

  72% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.1);
  }

  85% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.05);
  }

  94% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.025);
  }

  99% {
    translate: 0 calc(var(--kf-bounce-from, 100vh) &#42; -0.0125);
  }

  22%, 45%, 64%, 79%, 90%, 97%, 100% {
    translate: 0 0;
    animation-timing-function: ease-out;
  }
}
</code></pre>

<p>Animations like “elastic” are a bit trickier because of the calculations inside the keyframes. We need to define <code>--kf-elastic-from-X</code> and <code>--kf-elastic-from-Y</code> separately (both are optional), and together they let us create an elastic entrance from any point on the screen.</p>

<div class="break-out">
<pre><code class="language-css">/&#42;
 &#42; Elastic In - elastic entrance animation
 &#42; Use --kf-elastic-from-X and --kf-elastic-from-Y to control start position
 &#42; Default: enters from top center (0, -100vh)
 &#42; Usage:
 &#42;   animation: kf-elastic-in 2s ease-in-out both;
 &#42;   --kf-elastic-from-X: -50px;
 &#42;   --kf-elastic-from-Y: -200px; // enter from (-50px, -200px)
 &#42;/

@keyframes kf-elastic-in {
  0% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; 1) calc(var(--kf-elastic-from-Y, 0px) &#42; 1);
  }

  16% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; -0.3227) calc(var(--kf-elastic-from-Y, 0px) &#42; -0.3227);
  }

  28% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; 0.1312) calc(var(--kf-elastic-from-Y, 0px) &#42; 0.1312);
  }

  44% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; -0.0463) calc(var(--kf-elastic-from-Y, 0px) &#42; -0.0463);
  }

  59% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; 0.0164) calc(var(--kf-elastic-from-Y, 0px) &#42; 0.0164);
  }

  73% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; -0.0058) calc(var(--kf-elastic-from-Y, 0px) &#42; -0.0058);
  }

  88% {
    translate: calc(var(--kf-elastic-from-X, -50vw) &#42; 0.0020) calc(var(--kf-elastic-from-Y, 0px) &#42; 0.0020);
  }

  100% {
    translate: 0 0;
  }
}
</code></pre>
</div>

<p>This approach makes it easy to reuse and customize advanced keyframes across our project, just by changing a single custom property.</p>

<div class="break-out">
<pre><code class="language-css">.bounce-and-zoom {
  animation:
    kf-bounce 3s ease-in,
    kf-zoom 3s linear;
  --kf-zoom-from: 0;
}

.bounce-and-slide {
  animation-composition: add; /&#42; Both animations use `translate` &#42;/
  animation:
    kf-bounce 3s ease-in,
    kf-slide-in 3s ease-out;
  --kf-slide-from: -200px;
}

.elastic-in {
  animation: kf-elastic-in 2s ease-in-out both;
}
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="QwNqadQ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 7 [forked]](https://codepen.io/smashingmag/pen/QwNqadQ) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/QwNqadQ">Keyframes Tokens - Demo 7 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>Up to this point, we’ve seen how we can consolidate keyframes in a smart and efficient way. Of course, you might want to tweak things to better fit your project’s needs, but we’ve covered examples of several common animations and everyday use cases. And with these keyframes tokens in place, we now have powerful building blocks for creating consistent, maintainable animations across the entire project. No more duplicated keyframes, no more global scope conflicts. Just a clean, convenient way to handle all our animation needs.</p>

<p>But the real question is: <strong>How do we compose these building blocks together?</strong></p>

<h2 id="putting-it-all-together">Putting It All Together</h2>

<p>We’ve seen that combining basic keyframes tokens is simple. We don’t need anything special but to define the first animation, define the second one, set the variables as needed, and that’s it.</p>

<pre><code class="language-css">/&#42; Fade in + slide in &#42;/
.toast {
  animation:
    kf-fade-in 0.4s,
    kf-slide-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-slide-from: 0 40px;
}

/&#42; Zoom in + fade in &#42;/
.modal {
  animation:
    kf-fade-in 0.3s,
    kf-zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-zoom-from: 0.7;
  --kf-zoom-to: 1;
}

/&#42; Slide in + pulse &#42;/
.notification {
  animation:
    kf-slide-in 0.5s,
    kf-pulse 1.2s ease-in-out infinite alternate;
  --kf-slide-from: -100px 0;
  --kf-pulse-scale-from: 0.95;
  --kf-pulse-scale-to: 1.05;
}
</code></pre>

<p>These combinations work beautifully because each animation targets a different property: <code>opacity</code>, <code>transform</code> (<code>translate</code>/<code>scale</code>), etc. But sometimes there are conflicts, and we need to know why and how to deal with them.</p>

<p>When two animations try to animate the same property &mdash; for example, both animating <code>scale</code> or both animating <code>opacity</code> &mdash; the result will not be what you expect. By default, only one of the animations is actually applied to that property, which is the last one in the <code>animation</code> list. This is a limitation of how CSS handles multiple animations on the same property.</p>

<p>For example, this will not work as intended because only the <code>kf-pulse</code> animation will apply.</p>

<pre><code class="language-css">.bad-combo {
  animation:
    kf-zoom 0.5s forwards,
    kf-pulse 1.2s infinite alternate;
  --kf-zoom-from: 0.5;
  --kf-zoom-to: 1.2;
  --kf-pulse-scale-from: 0.8;
  --kf-pulse-scale-to: 1.1;
}
</code></pre>

<h2 id="animation-addition">Animation Addition</h2>

<p>The simplest and most direct way to handle multiple animations that affect the same property is to use the <code>animation-composition</code> property. In the last example above, the <code>kf-pulse</code> animation replaces the <code>kf-zoom</code> animation, so we will not see the initial zoom and will not get the expected <code>scale</code> <code>to</code> of <code>1.2</code>.</p>

<p>By setting the <code>animation-composition</code> to <code>add</code>, we tell the browser to <em>combine</em> both animations. This gives us the result we want.</p>

<pre><code class="language-css">.component-two {
  animation-composition: add;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YPqrYZw"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 8 [forked]](https://codepen.io/smashingmag/pen/YPqrYZw) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YPqrYZw">Keyframes Tokens - Demo 8 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>This approach works well for most cases where we want to combine effects on the same property. It is also useful when we need to combine animations with static property values.</p>

<p>For example, if we have an element that uses the <code>translate</code> property to position it exactly where we want, and then we want to animate it in with the <code>kf-slide-in</code> keyframes, we get a nasty visible jump without <code>animation-composition</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="myPBpWr"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 9 [forked]](https://codepen.io/smashingmag/pen/myPBpWr) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myPBpWr">Keyframes Tokens - Demo 9 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>With <code>animation-composition</code> set to <code>add</code>, the animation is smoothly combined with the existing transform, so the element stays in place and animates as expected.</p>

<h2 id="animation-stagger">Animation Stagger</h2>

<p>Another way of handling multiple animations is to “stagger” them &mdash; that is, start the second animation slightly after the first one finishes. It is not a solution that works for every case, but it is useful when we have an entrance animation followed by a continuous animation.</p>

<pre><code class="language-css">/&#42; fade in + opacity pulse &#42;/
.notification {
  animation:
    kf-fade-in 2s ease-out,
    kf-pulse 0.5s 2s ease-in-out infinite alternate;
  --kf-pulse-opacity-to: 0.5;
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="bNpoaqo"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 10 [forked]](https://codepen.io/smashingmag/pen/bNpoaqo) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/bNpoaqo">Keyframes Tokens - Demo 10 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<h2 id="order-matters">Order Matters</h2>

<p>A large part of the animations we work with use the <code>transform</code> property. In most cases, this is simply more convenient. It also has a performance advantage as transform animations can be GPU-accelerated. But if we use transforms, we need to accept that the order in which we perform our transformations matters. <em>A lot</em>.</p>

<p>In our keyframes so far, we’ve used <em>individual transforms</em>. According to the specs, these are always applied in a fixed order: first, the element gets <code>translate</code>, then <code>rotate</code>, then <code>scale</code>. This makes sense and is what most of us expect.</p>

<p>However, if we use the <code>transform</code> property, the order in which the functions are written is the order in which they are applied. In this case, if we move something 100 pixels on the X-axis and then rotate it by 45 degrees, it is <em>not</em> the same as first rotating it by 45 degrees and then moving it 100 pixels.</p>

<pre><code class="language-css">/&#42; Pink square: First translate, then rotate &#42;/ 
.example-one {
  transform: translateX(100px) rotate(45deg);
}

/&#42; Green square: First rotate, then translate &#42;/
.example-two { 
  transform: rotate(45deg) translateX(100px);
}
</code></pre>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="zxqEpZb"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 11 [forked]](https://codepen.io/smashingmag/pen/zxqEpZb) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxqEpZb">Keyframes Tokens - Demo 11 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>But according to the <code>transform</code> order, all individual transforms &mdash; everything we’ve used for the keyframes tokens &mdash; happens before the transform functions. That means anything you set in the <code>transform</code> property will happen <em>after</em> the animations. But if you set, for example, <code>translate</code> together with the <code>kf-spin</code> keyframes, the <code>translate</code> will happen <em>before</em> the animation. Confused yet?!</p>

<p>This leads to situations where static values can cause different results for the same animation, like in the following case:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; Common animation for both spinners &#42;/ 
.spinner {
  animation: kf-spin 1s linear infinite;
}

/&#42; Pink spinner: translate before rotate (individual transform) &#42;/
.spinner-pink {
  translate: 100% 50%;
}

/&#42; Green spinner: rotate then translate (function order) &#42;/
.spinner-green {
  transform: translate(100%, 50%);
}
</code></pre>
</div>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="NPNaXjw"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Keyframes Tokens - Demo 12 [forked]](https://codepen.io/smashingmag/pen/NPNaXjw) by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/NPNaXjw">Keyframes Tokens - Demo 12 [forked]</a> by <a href="https://codepen.io/amit_sheen">Amit Sheen</a>.</figcaption>
</figure>

<p>You can see that the first spinner (pink) gets a <code>translate</code> that happens before the <code>rotate</code> of <code>kf-spin</code>, so it first moves to its place and then spins. The second spinner (green) gets a <code>translate()</code> function that happens after the individual transform, so the element first spins, then moves relative to its current angle, and we get that wide orbit effect.</p>

<p><strong>No, this is not a bug</strong>. It is just one of those things we need to know about CSS and keep in mind when working with multiple animations or multiple transforms. If needed, you can also create an additional set of <code>kf-spin-alt</code> keyframes that rotate elements using the <code>rotate()</code> function.</p>

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

<h2 id="reduced-motion">Reduced Motion</h2>

<p>And while we’re talking about alternative keyframes, we cannot ignore the “no animation” option. One of the biggest advantages of using keyframes tokens is that <strong>accessibility</strong> can be baked in, and it is actually quite easy to do. By designing our keyframes with accessibility in mind, we can ensure that <a href="https://www.smashingmagazine.com/2021/10/respecting-users-motion-preferences/">users who prefer reduced motion get a smoother, less distracting experience</a>, without extra work or code duplication.</p>

<p>The exact meaning of “Reduced Motion” can change a bit from one animation to another, and from project to project, but here are a few important points to keep in mind:</p>

<h3 id="muting-keyframes">Muting Keyframes</h3>

<p>While some animations can be softened or slowed down, there are others that should disappear completely when reduced motion is requested. Pulse animations are a good example. To make sure these animations do not run in reduced motion mode, we can simply wrap them in the appropriate media query.</p>

<pre><code class="language-css">
@media (prefers-reduced-motion: no-preference) {
  @keyfrmaes kf-pulse {
    from {
      scale: var(--kf-pulse-scale-from, 1);
      opacity: var(--kf-pulse-opacity-from, 1);
    }
    to {
      scale: var(--kf-pulse-scale-to, 1);
      opacity: var(--kf-pulse-opacity-to, 1);
    }
  }
}
</code></pre>

<p>This ensures that users who have set <code>prefers-reduced-motion</code> to <code>reduce</code> will not see the animation and will get an experience that matches their preference.</p>

<h3 id="instant-in">Instant In</h3>

<p>There are some keyframes we cannot simply remove, such as entrance animations. The value must change, must animate; otherwise, the element won&rsquo;t have the correct values. But in reduced motion, this transition from the initial value should be instant.</p>

<p>To achieve this, we’ll define an extra set of keyframes where the value jumps immediately to the end state. These become our default keyframes. Then, we’ll add the regular keyframes inside a media query for <code>prefers-reduced-motion</code> set to <code>no-preference</code>, just like in the previous example.</p>

<pre><code class="language-css">/&#42; pop in instantly for reduced motion &#42;/
@keyframes kf-zoom {
  from, to {
    scale: var(--kf-zoom-to, 1);
  }
}

@media (prefers-reduced-motion: no-preference) {
  /&#42; Original zoom keyframes &#42;/
  @keyframes kf-zoom {
    from {
      scale: var(--kf-zoom-from, 0.8);
    }
    to {
      scale: var(--kf-zoom-to, 1);
    }
  }
}
</code></pre>

<p>This way, users who prefer reduced motion will see the element appear instantly in its final state, while everyone else gets the animated transition.</p>

<h3 id="the-soft-approach">The Soft Approach</h3>

<p>There are cases where we do want to keep some movement, but much softer and calmer than the original animation. For example, we can replace a bounce entrance with a gentle fade-in.</p>

<pre><code class="language-css">
@keyframes kf-bounce {
  /&#42; Soft fade-in for reduced motion &#42;/
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes kf-bounce {
    /&#42; Original bounce keyframes &#42;/
  }
}
</code></pre>

<p>Now, users with reduced motion enabled still get a sense of appearance, but without the intense movement of a bounce or elastic animation.</p>

<p>With the building blocks in place, the next question is how to make them part of the actual workflow. Writing flexible keyframes is one thing, but making them reliable across a large project requires a few strategies that I had to learn the hard way.</p>

<h2 id="implementation-strategies-best-practices">Implementation Strategies &amp; Best Practices</h2>

<p>Once we have a solid library of keyframes tokens, the real challenge is how to bring them into everyday work.</p>

<ul>
<li>The temptation is to drop all keyframes in at once and declare the problem solved, but in practice I have found that <strong>the best results come from gradual adoption</strong>. Start with the most common animations, such as fade or slide. These are easy wins that show immediate value without requiring big rewrites.</li>
<li><strong>Naming is another point that deserves attention.</strong> A consistent prefix or namespace makes it obvious which animations are tokens and which are local one-offs. It also prevents accidental collisions and helps new team members recognize the shared system at a glance.</li>
<li><strong>Documentation is just as important as the code itself.</strong> Even a short comment above each keyframes token can save hours of guessing later. A developer should be able to open the tokens file, scan for the effect they need, and copy the usage pattern straight into their component.</li>
<li><strong>Flexibility is what makes this approach worth the effort.</strong> By exposing sensible custom properties, we give teams room to adapt the animation without breaking the system. At the same time, try not to overcomplicate. Provide the knobs that matter and keep the rest opinionated.</li>
<li>Finally, <strong>remember accessibility</strong>. Not every animation needs a reduced motion alternative, but many do. Baking in these adjustments early means we never have to retrofit them later, and it shows a level of care that our users will notice even if they never mention it.</li>
</ul>

<p>In my experience, treating keyframes tokens as part of our design tokens workflow is what makes them stick. Once they are in place, they stop feeling like special effects and become part of the design language, a natural extension of how the product moves and responds.</p>

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

<p>Animations can be one of the most joyful parts of building interfaces, but without structure, they can also become one of the biggest sources of frustration. By treating keyframes as tokens, you take something that is usually messy and hard to manage and turn it into a clear, predictable system.</p>

<p>The real value is not just in saving a few lines of code. It is in the <strong>confidence</strong> that when you use a fade, slide, zoom, or spin, you know exactly how it will behave across the project. It is in the <strong>flexibility</strong> that comes from custom properties without the chaos of endless variations. And it is in the <strong>accessibility</strong> built into the foundation rather than added as an afterthought.</p>

<p>I have seen these ideas work in different teams and different codebases, and the pattern is always the same.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aOnce%20the%20tokens%20are%20in%20place,%20keyframes%20stop%20being%20a%20scattered%20collection%20of%20tricks%20and%20become%20part%20of%20the%20design%20language.%20They%20make%20the%20product%20feel%20more%20intentional,%20more%20consistent,%20and%20more%20alive.%0a&url=https://smashingmagazine.com%2f2025%2f11%2fkeyframes-tokens-standardizing-animation-across-projects%2f">
      
Once the tokens are in place, keyframes stop being a scattered collection of tricks and become part of the design language. They make the product feel more intentional, more consistent, and more alive.

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

<p>If you take one thing from this article, let it be this: <strong>animations deserve the same care and structure we already give to colors, typography, and spacing</strong>. A small investment in keyframes tokens pays off every time your interface moves.</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>Godstime Aburu</author><title>CSS Gamepad API Visual Debugging With CSS Layers</title><link>https://www.smashingmagazine.com/2025/11/css-gamepad-api-visual-debugging-css-layers/</link><pubDate>Fri, 14 Nov 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/css-gamepad-api-visual-debugging-css-layers/</guid><description>Debugging controllers can be a real pain. Here’s a deep dive into how CSS helps clean it up and how to build a reusable visual debugger for your own projects.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/css-gamepad-api-visual-debugging-css-layers/" />
              <title>CSS Gamepad API Visual Debugging With CSS Layers</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>CSS Gamepad API Visual Debugging With CSS Layers</h1>
                  
                    
                    <address>Godstime Aburu</address>
                  
                  <time datetime="2025-11-14T13:00:00&#43;00:00" class="op-published">2025-11-14T13:00:00+00:00</time>
                  <time datetime="2025-11-14T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>When you plug in a controller, you mash buttons, move the sticks, pull the triggers… and as a developer, you see none of it. The browser’s picking it up, sure, but unless you’re logging numbers in the console, it’s invisible. That’s the headache with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">Gamepad API</a>.</p>

<p>It’s been around for years, and it’s actually pretty powerful. You can read buttons, sticks, triggers, the works. But most people don’t touch it. Why? Because there’s no feedback. No panel in developer tools. No clear way to know if the controller’s even doing what you think. It feels like flying blind.</p>

<p>That bugged me enough to build a little tool: <strong>Gamepad Cascade Debugger</strong>. Instead of staring at console output, you get a live, interactive view of the controller. Press something and it reacts on the screen. And with <a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">CSS Cascade Layers</a>, the styles stay organized, so it’s cleaner to debug.</p>

<p>In this post, I’ll show you why debugging controllers is such a pain, how CSS helps clean it up, and how you can build a reusable visual debugger for your own projects.</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="F8fwVDNM0OI"
      
			videotitle="Live Demo of the Gamepad Debugger showing recording, exporting, and ghost replay in action."
		></lite-youtube>
	</div>
	
		<figcaption>Live Demo of the Gamepad Debugger showing recording, exporting, and ghost replay in action.</figcaption>
	
</figure>

<p>By the end, you’ll know how to:</p>

<ul>
<li>Spot the tricky parts of debugging controller input.</li>
<li>Use Cascade Layers to tame messy CSS.</li>
<li>Build a live Gamepad debugger.</li>
<li>Add extra functionalities like recording, replaying, and taking snapshots.</li>
</ul>

<p>Alright, let’s dive in.</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="why-debugging-gamepad-input-is-hard">Why Debugging Gamepad Input Is Hard</h2>

<p>Just the thought of building a game or web app where a player uses a controller instead of a mouse could make you nervous. You need to be able to respond to actions like:</p>

<ul>
<li>Did they press <code>A</code> or <code>B</code>?</li>
<li>Is the joystick tilted halfway or fully?</li>
<li>How hard is the trigger pulled?</li>
</ul>

<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">Gamepad API</a> exposes and displays all of the information you need, but only as arrays of numbers. Each button has a value (e.g., <code>0</code> for not pressed, <code>1</code> for fully pressed, and decimals for pressure-sensitive triggers), and each joystick reports its position on the X and Y axes.</p>

<p>Here’s what it looks like in raw form:</p>

<pre><code class="language-css">// Example: Reading the first connected gamepad
const gamepad = navigator.getGamepads()[0];
 
console.log(gamepad.buttons.map(b =&gt; b.value));
// [0, 0, 1, 0, 0, 0.5, 0, ...]
 
console.log(gamepad.axes);
// [-0.24, 0.98, -0.02, 0.00]
 </code></pre> 

<p>Is it useful? Technically, yes. Easy to debug? Not at all.</p>

<h3 id="problem-1-invisible-state">Problem 1: Invisible State</h3>

<p>When you press a physical button, you feel the click, right? But in your code, nothing moves on screen unless you manually wire up a display. Unlike keyboard events (which show in browser dev tools) or mouse clicks (which fire visible events), gamepad input has no built-in visual feedback.</p>

<p>To illustrate the difference, here’s how other input methods give you immediate feedback:</p>

<div class="break-out">
<pre><code class="language-css">// Keyboard events are visible and easy to track
document.addEventListener('keydown', (e) =&gt; {
  console.log('Key pressed:', e.key);
  // Outputs: "Key pressed: a"
  // You can see this in DevTools, and many tools show keyboard input
});

// Mouse clicks provide clear event data
document.addEventListener('click', (e) =&gt; {
  console.log('Clicked at:', e.clientX, e.clientY);
  // Outputs: "Clicked at: 245, 389"
  // Visual feedback is immediate
});

// But gamepad input? Silent and invisible.
const gamepad = navigator.getGamepads()[0];
if (gamepad) {
  console.log(gamepad.buttons[0]); 
  // Outputs: GamepadButton {pressed: false, touched: false, value: 0}
  // No events, no DevTools panel, just polling
}
</code></pre>
</div>

<p>The gamepad doesn’t fire events when buttons are pressed. You have to constantly poll it using <code>requestAnimationFrame</code>, checking values manually. There’s no built-in visualization, no dev tools integration, nothing.</p>

<p>This forces you to keep going back and forth between your console and your controller just to keep logging values, interpreting numbers, and mentally mapping them back to physical actions.</p>

<h3 id="problem-2-too-many-inputs">Problem 2: Too Many Inputs</h3>

<p>A modern controller can have up to 15+ buttons and 4+ axes. That’s over a dozen values updating at once.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="500"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg"
			
			sizes="100vw"
			alt="Xbox vs. PlayStation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Both Xbox and PlayStation controllers pack 15+ buttons each, and they’re laid out differently. Debugging across platforms means handling all that variety. (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/1-xbox-playstation.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Even if you are able to log them all, you’ll quickly end up with unreadable console spam. For example:</p>

<pre><code class="language-javascript">[0,0,1,0,0,0.5,0,...]
[0,0,0,0,1,0,0,...]
[0,0,1,0,0,0,0,...]
</code></pre>

<p>Can you tell what button was pressed? Maybe, but only after straining your eyes and missing a few inputs. So, no, debugging doesn’t come easily when it comes to reading inputs.</p>

<h3 id="problem-3-lack-of-structure">Problem 3: Lack Of Structure</h3>

<p>Even if you throw together a quick visualizer, styles can quickly get messy. Default, active, and debug states can overlap, and without a clear structure, your CSS becomes brittle and hard to extend.</p>

<p><a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">CSS Cascade Layers</a> can help. They group styles into “layers” that are ordered by priority, so you stop fighting specificity and guessing, <em>“Why isn’t my debug style showing?”</em> Instead, you maintain separate concerns:</p>

<ul>
<li><strong>Base</strong>: The controller’s standard, initial appearance.</li>
<li><strong>Active</strong>: Highlights for pressed buttons and moved sticks.</li>
<li><strong>Debug</strong>: Overlays for developers (e.g., numeric readouts, guides, and so on).</li>
</ul>

<p>If we were to define layers in CSS according to this, we’d have:</p>

<pre><code class="language-css">/&#42; lowest to highest priority &#42;/
@layer base, active, debug;

@layer base {
  /&#42; ... &#42;/
}

@layer active {
  /&#42; ... &#42;/
}

@layer debug {
  /&#42; ... &#42;/
}
</code></pre>

<p>Because each layer stacks predictably, you always know which rules win. That predictability makes debugging not just easier, but actually manageable.</p>

<p>We’ve covered the problem (invisible, messy input) and the approach (a visual debugger built with Cascade Layers). Now we’ll walk through the step-by-step process to build the debugger.</p>

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

<h2 id="the-debugger-concept">The Debugger Concept</h2>

<p>The easiest way to make hidden input visible is to just draw it on the screen. That’s what this debugger does. Buttons, triggers, and joysticks all get a visual.</p>

<ul>
<li><strong>Press <code>A</code></strong>: A circle lights up.</li>
<li><strong>Nudge the stick</strong>: The circle slides around.</li>
<li><strong>Pull a trigger halfway</strong>: A bar fills halfway.</li>
</ul>

<p>Now you’re not staring at 0s and 1s, but actually watching the controller react live.</p>

<p>Of course, once you start piling on states like default, pressed, debug info, maybe even a recording mode, the CSS starts getting larger and more complex. That’s where cascade layers come in handy. Here’s a stripped-down example:</p>

<pre><code class="language-css">@layer base {
  .button {
    background: &#35;222;
    border-radius: 50%;
    width: 40px;
    height: 40px;
  }
}
 
@layer active {
  .button.pressed {
    background: &#35;0f0; /&#42; bright green &#42;/
  }
}
 
@layer debug {
  .button::after {
    content: attr(data-value);
    font-size: 12px;
    color: &#35;fff;
  }
}
</code></pre>

<p>The layer order matters: <code>base</code> → <code>active</code> → <code>debug</code>.</p>

<ul>
<li><code>base</code> draws the controller.</li>
<li><code>active</code> handles pressed states.</li>
<li><code>debug</code> throws on overlays.</li>
</ul>

<p>Breaking it up like this means you’re not fighting weird specificity wars. Each layer has its place, and you always know what wins.</p>

<h2 id="building-it-out">Building It Out</h2>

<p>Let’s get something on screen first. It doesn’t need to look good &mdash; just needs to exist so we have something to work with.</p>

<div class="break-out">
<pre><code class="language-html">&lt;h1&gt;Gamepad Cascade Debugger&lt;/h1&gt;

&lt;!-- Main controller container --&gt;
&lt;div id="controller"&gt;
  &lt;!-- Action buttons --&gt;
  &lt;div id="btn-a" class="button"&gt;A&lt;/div&gt;
  &lt;div id="btn-b" class="button"&gt;B&lt;/div&gt;
  &lt;div id="btn-x" class="button"&gt;X&lt;/div&gt;
  
  &lt;!-- Pause/menu button (represented as two bars) --&gt;
  &lt;div&gt;
    &lt;div id="pause1" class="pause"&gt;&lt;/div&gt;
    &lt;div id="pause2" class="pause"&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;!-- Toggle button to start/stop the debugger --&gt;
&lt;button id="toggle"&gt;Toggle Debug&lt;/button&gt;

&lt;!-- Status display for showing which buttons are pressed --&gt;
&lt;div id="status"&gt;Debugger inactive&lt;/div&gt;

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
</div>

<p>That’s literally just boxes. Not exciting yet, but it gives us handles to grab later with CSS and JavaScript.</p>

<p>Okay, I’m using cascade layers here because it keeps stuff organized once you add more states. Here’s a rough pass:</p>

<div class="break-out">
<pre><code class="language-css">/&#42; ===================================
   CASCADE LAYERS SETUP
   Order matters: base → active → debug
   =================================== &#42;/

/&#42; Define layer order upfront &#42;/
@layer base, active, debug;

/&#42; Layer 1: Base styles - default appearance &#42;/
@layer base {
  .button {
    background: &#35;333;
    border-radius: 50%;
    width: 70px;
    height: 70px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  
  .pause {
    width: 20px;
    height: 70px;
    background: &#35;333;
    display: inline-block;
  }
}

/&#42; Layer 2: Active states - handles pressed buttons &#42;/
@layer active {
  .button.active {
    background: &#35;0f0; /&#42; Bright green when pressed &#42;/
    transform: scale(1.1); /&#42; Slightly enlarges the button &#42;/
  }
  
  .pause.active {
    background: &#35;0f0;
    transform: scaleY(1.1); /&#42; Stretches vertically when pressed &#42;/
  }
}

/&#42; Layer 3: Debug overlays - developer info &#42;/
@layer debug {
  .button::after {
    content: attr(data-value); /&#42; Shows the numeric value &#42;/
    font-size: 12px;
    color: &#35;fff;
  }
}
</code></pre>
</div>

<p>The beauty of this approach is that each layer has a clear purpose. The <code>base</code> layer can never override <code>active,</code> and <code>active</code> can never override <code>debug</code>, regardless of specificity. This eliminates the CSS specificity wars that usually plague debugging tools.</p>

<p>Now it looks like some clusters are sitting on a dark background. Honestly, not too bad.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="402"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png"
			
			sizes="100vw"
			alt="The debugger’s initial state showing the button layout (A, B, X, and pause bars)"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/2-debugger-initial-state.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="adding-the-javascript">Adding the JavaScript</h3>

<p>JavaScript time. This is where the controller actually does something. We’ll build this step by step.</p>

<h4 id="step-1-set-up-state-management">Step 1: Set Up State Management</h4>

<p>First, we need variables to track the debugger’s state:</p>

<pre><code class="language-javascript">// ===================================
// STATE MANAGEMENT
// ===================================

let running = false; // Tracks whether the debugger is active
let rafId; // Stores the requestAnimationFrame ID for cancellation
</code></pre>

<p>These variables control the animation loop that continuously reads gamepad input.</p>

<h4 id="step-2-grab-dom-references">Step 2: Grab DOM References</h4>

<p>Next, we get references to all the HTML elements we’ll be updating:</p>

<pre><code class="language-javascript">// ===================================
// DOM ELEMENT REFERENCES
// ===================================

const btnA = document.getElementById("btn-a");
const btnB = document.getElementById("btn-b");
const btnX = document.getElementById("btn-x");
const pause1 = document.getElementById("pause1");
const pause2 = document.getElementById("pause2");
const status = document.getElementById("status");
</code></pre>

<p>Storing these references up front is more efficient than querying the DOM repeatedly.</p>

<h4 id="step-3-add-keyboard-fallback">Step 3: Add Keyboard Fallback</h4>

<p>For testing without a physical controller, we’ll map keyboard keys to buttons:</p>

<pre><code class="language-javascript">// ===================================
// KEYBOARD FALLBACK (for testing without a controller)
// ===================================

const keyMap = {
  "a": btnA,
  "b": btnB,
  "x": btnX,
  "p": [pause1, pause2] // 'p' key controls both pause bars
};
</code></pre>

<p>This lets us test the UI by pressing keys on a keyboard.</p>

<h4 id="step-4-create-the-main-update-loop">Step 4: Create The Main Update Loop</h4>

<p>Here’s where the magic happens. This function runs continuously and reads gamepad state:</p>

<pre><code class="language-javascript">// ===================================
// MAIN GAMEPAD UPDATE LOOP
// ===================================

function updateGamepad() {
  // Get all connected gamepads
  const gamepads = navigator.getGamepads();
  if (!gamepads) return;

  // Use the first connected gamepad
  const gp = gamepads[0];

  if (gp) {
    // Update button states by toggling the "active" class
    btnA.classList.toggle("active", gp.buttons[0].pressed);
    btnB.classList.toggle("active", gp.buttons[1].pressed);
    btnX.classList.toggle("active", gp.buttons[2].pressed);

    // Handle pause button (button index 9 on most controllers)
    const pausePressed = gp.buttons[9].pressed;
    pause1.classList.toggle("active", pausePressed);
    pause2.classList.toggle("active", pausePressed);

    // Build a list of currently pressed buttons for status display
    let pressed = [];
    gp.buttons.forEach((btn, i) =&gt; {
      if (btn.pressed) pressed.push("Button " + i);
    });

    // Update status text if any buttons are pressed
    if (pressed.length &gt; 0) {
      status.textContent = "Pressed: " + pressed.join(", ");
    }
  }

  // Continue the loop if debugger is running
  if (running) {
    rafId = requestAnimationFrame(updateGamepad);
  }
}
</code></pre>

<p>The <code>classList.toggle()</code> method adds or removes the <code>active</code> class based on whether the button is pressed, which triggers our CSS layer styles.</p>

<h4 id="step-5-handle-keyboard-events">Step 5: Handle Keyboard Events</h4>

<p>These event listeners make the keyboard fallback work:</p>

<pre><code class="language-javascript">// ===================================
// KEYBOARD EVENT HANDLERS
// ===================================

document.addEventListener("keydown", (e) =&gt; {
  if (keyMap[e.key]) {
    // Handle single or multiple elements
    if (Array.isArray(keyMap[e.key])) {
      keyMap[e.key].forEach(el =&gt; el.classList.add("active"));
    } else {
      keyMap[e.key].classList.add("active");
    }
    status.textContent = "Key pressed: " + e.key.toUpperCase();
  }
});

document.addEventListener("keyup", (e) =&gt; {
  if (keyMap[e.key]) {
    // Remove active state when key is released
    if (Array.isArray(keyMap[e.key])) {
      keyMap[e.key].forEach(el =&gt; el.classList.remove("active"));
    } else {
      keyMap[e.key].classList.remove("active");
    }
    status.textContent = "Key released: " + e.key.toUpperCase();
  }
});
</code></pre>

<h4 id="step-6-add-start-stop-control">Step 6: Add Start/Stop Control</h4>

<p>Finally, we need a way to toggle the debugger on and off:</p>

<pre><code class="language-javascript">// ===================================
// TOGGLE DEBUGGER ON/OFF
// ===================================

document.getElementById("toggle").addEventListener("click", () =&gt; {
  running = !running; // Flip the running state

  if (running) {
    status.textContent = "Debugger running...";
    updateGamepad(); // Start the update loop
  } else {
    status.textContent = "Debugger inactive";
    cancelAnimationFrame(rafId); // Stop the loop
  }
});
</code></pre>

<p>So yeah, press a button and it glows. Push the stick and it moves. That’s it.</p>

<p>One more thing: raw values. Sometimes you just want to see numbers, not lights.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="387"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg"
			
			sizes="100vw"
			alt="The Gamepad Cascade Debugger in its idle state with no inputs detected (Pressed buttons: 0)."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The Gamepad Cascade Debugger in its idle state with no inputs detected (Pressed buttons: 0). (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/3-gamepad-cascade-debugger.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>At this stage, you should see:</p>

<ul>
<li>A simple on-screen controller,</li>
<li>Buttons that react as you interact with them, and</li>
<li>An optional debug readout showing pressed button indices.</li>
</ul>

<p>To make this less abstract, here’s a quick demo of the on-screen controller reacting in real time:</p>


<figure class="video-embed-container break-out">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="gHUKp4Zu-wM"
      
			videotitle="Live demo of the on-screen controller lighting up as buttons are pressed and released."
		></lite-youtube>
	</div>
	
		<figcaption>Live demo of the on-screen controller lighting up as buttons are pressed and released.</figcaption>
	
</figure>

<p>That’s the whole foundation. From here, we can start layering in extra stuff, like record/replay and snapshots.</p>

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

<h2 id="enhancements-from-toy-to-tool">Enhancements: From Toy To Tool</h2>

<p>A static visualizer is helpful, but we as developers often need more than a snapshot of the controller’s state. We want history, analysis, and replay. Let’s add those layers on top of our debugger.</p>

<h3 id="1-recording-stopping-input-logs">1. Recording &amp; Stopping Input Logs</h3>

<p>We can add two buttons:</p>

<div class="break-out">
<pre><code class="language-html">&lt;div class="controls"&gt;
  &lt;button id="start-record" class="btn"&gt;Start Recording&lt;/button&gt;
  &lt;button id="stop-record" class="btn" disabled&gt;Stop Recording&lt;/button&gt;
&lt;/div&gt;
</code></pre>
</div>

<h4 id="step-1-set-up-recording-state">Step 1: Set Up Recording State</h4>

<p>First, let’s set up the variables we need to track recordings:</p>

<pre><code class="language-javascript">// ===================================
// RECORDING STATE
// ===================================

let recording = false; // Tracks if we're currently recording
let frames = []; // Array to store captured input frames

// Get button references
const startBtn = document.getElementById("start-record");
const stopBtn = document.getElementById("stop-record");
</code></pre>

<p>The <code>frames</code> array will store snapshots of the gamepad state at each frame, creating a complete timeline of input.</p>

<h4 id="step-2-handle-start-recording">Step 2: Handle Start Recording</h4>

<p>When the user clicks “Start Recording,” we initialize a new recording session:</p>

<pre><code class="language-javascript">// ===================================
// START RECORDING
// ===================================

startBtn.addEventListener("click", () =&gt; {
  frames = []; // Clear any previous recording
  recording = true;

  // Update UI: disable start, enable stop
  stopBtn.disabled = false;
  startBtn.disabled = true;

  console.log("Recording started...");
});
</code></pre>

<h4 id="step-3-handle-stop-recording">Step 3: Handle Stop Recording</h4>

<p>To stop recording, we flip the state back and re-enable the Start button:</p>

<pre><code class="language-javascript">// ===================================
// STOP RECORDING
// ===================================

stopBtn.addEventListener("click", () =&gt; {
  recording = false;

  // Update UI: enable start, disable stop
  stopBtn.disabled = true;
  startBtn.disabled = false;

  console.log("Recording stopped. Frames captured:", frames.length);
});
</code></pre>

<h4 id="step-4-capture-frames-during-gameplay">Step 4: Capture Frames During Gameplay</h4>

<p>Finally, we need to actually capture frames during the update loop. Add this inside the <code>updateGamepad()</code> function:</p>

<pre><code class="language-javascript">// ===================================
// CAPTURE FRAMES (add this inside updateGamepad loop)
// ===================================

if (recording && gp) {
  // Store a snapshot of the current gamepad state
  frames.push({
    t: performance.now(), // Timestamp for accurate replay
    buttons: gp.buttons.map(b =&gt; ({ 
      pressed: b.pressed, 
      value: b.value 
    })),
    axes: [...gp.axes] // Copy the axes array
  });
}
</code></pre>

<p>Each frame captures the exact state of every button and joystick at that moment in time.</p>

<p>Once wired up, the interface displays a simple recording panel. You get a Start button to begin logging input, while the recording state, frame count, and duration remain at zero until recording begins. The following figure shows the debugger in its initial idle state.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="533"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg"
			
			sizes="100vw"
			alt="Recording panel in its idle state, with only the start button active"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Recording panel in its idle state, with only the start button active. (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/4-recording-panel.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now, pressing <strong>Start Recording</strong> logs everything until you hit <strong>Stop Recording</strong>.</p>

<h3 id="2-exporting-data-to-csv-json">2. Exporting Data to CSV/JSON</h3>

<p>Once we have a log, we’ll want to save it.</p>

<div class="break-out">
<pre><code class="language-html">&lt;div class="controls"&gt;
  &lt;button id="export-json" class="btn"&gt;Export JSON&lt;/button&gt;
  &lt;button id="export-csv" class="btn"&gt;Export CSV&lt;/button&gt;
&lt;/div&gt;
</code></pre>
</div>

<h4 id="step-1-create-the-download-helper">Step 1: Create The Download Helper</h4>

<p>First, we need a helper function that handles file downloads in the browser:</p>

<pre><code class="language-javascript">// ===================================
// FILE DOWNLOAD HELPER
// ===================================

function downloadFile(filename, content, type = "text/plain") {
  // Create a blob from the content
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);

  // Create a temporary download link and click it
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();

  // Clean up the object URL after download
  setTimeout(() =&gt; URL.revokeObjectURL(url), 100);
}
</code></pre>

<p>This function works by creating a Blob (binary large object) from your data, generating a temporary URL for it, and programmatically clicking a download link. The cleanup ensures we don’t leak memory.</p>

<h4 id="step-2-handle-json-export">Step 2: Handle JSON Export</h4>

<p>JSON is perfect for preserving the complete data structure:</p>

<div class="break-out">
<pre><code class="language-javascript">// ===================================
// EXPORT AS JSON
// ===================================

document.getElementById("export-json").addEventListener("click", () =&gt; {
  // Check if there's anything to export
  if (!frames.length) {
    console.warn("No recording available to export.");
    return;
  }

  // Create a payload with metadata and frames
  const payload = {
    createdAt: new Date().toISOString(),
    frames
  };

  // Download as formatted JSON
  downloadFile(
    "gamepad-log.json", 
    JSON.stringify(payload, null, 2), 
    "application/json"
  );
});
</code></pre>
</div>

<p>The JSON format keeps everything structured and easily parseable, making it ideal for loading back into dev tools or sharing with teammates.</p>

<h4 id="step-3-handle-csv-export">Step 3: Handle CSV Export</h4>

<p>For CSV exports, we need to flatten the hierarchical data into rows and columns:</p>

<div class="break-out">
<pre><code class="language-javascript">// ===================================
// EXPORT AS CSV
// ===================================

document.getElementById("export-csv").addEventListener("click", () =&gt; {
  // Check if there's anything to export
  if (!frames.length) {
    console.warn("No recording available to export.");
    return;
  }

  // Build CSV header row (columns for timestamp, all buttons, all axes)
  const headerButtons = frames[0].buttons.map((&#95;, i) =&gt; `btn${i}`);
  const headerAxes = frames[0].axes.map((&#95;, i) =&gt; `axis${i}`);
  const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";

  // Build CSV data rows
  const rows = frames.map(f =&gt; {
    const btnVals = f.buttons.map(b =&gt; b.value);
    return [f.t, ...btnVals, ...f.axes].join(",");
  }).join("\n");

  // Download as CSV
  downloadFile("gamepad-log.csv", header + rows, "text/csv");
});
</code></pre>
</div>

<p>CSV is brilliant for data analysis because it opens directly in Excel or Google Sheets, letting you create charts, filter data, or spot patterns visually.</p>

<p>Now that the export buttons are in, you’ll see two new options on the panel: <strong>Export JSON</strong> and <strong>Export CSV</strong>. JSON is nice if you want to throw the raw log back into your dev tools or poke around the structure. CSV, on the other hand, opens straight into Excel or Google Sheets so you can chart, filter, or compare inputs. The following figure shows what the panel looks like with those extra controls.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="533"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg"
			
			sizes="100vw"
			alt="Export panel with JSON and CSV buttons for saving logs"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Export panel with JSON and CSV buttons for saving logs. (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/5-export-panel.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="3-snapshot-system">3. Snapshot System</h3>

<p>Sometimes you don’t need a full recording, just a quick “screenshot” of input states. That’s where a <strong>Take Snapshot</strong> button helps.</p>

<pre><code class="language-html">&lt;div class="controls"&gt;
  &lt;button id="snapshot" class="btn"&gt;Take Snapshot&lt;/button&gt;
&lt;/div&gt;
</code></pre>

<p>And the JavaScript:</p>

<div class="break-out">
<pre><code class="language-javascript">// ===================================
// TAKE SNAPSHOT
// ===================================

document.getElementById("snapshot").addEventListener("click", () =&gt; {
  // Get all connected gamepads
  const pads = navigator.getGamepads();
  const activePads = [];
  
  // Loop through and capture the state of each connected gamepad
  for (const gp of pads) {
    if (!gp) continue; // Skip empty slots
    
    activePads.push({
      id: gp.id, // Controller name/model
      timestamp: performance.now(),
      buttons: gp.buttons.map(b =&gt; ({ 
        pressed: b.pressed, 
        value: b.value 
      })),
      axes: [...gp.axes]
    });
  }
  
  // Check if any gamepads were found
  if (!activePads.length) {
    console.warn("No gamepads connected for snapshot.");
    alert("No controller detected!");
    return;
  }
  
  // Log and notify user
  console.log("Snapshot:", activePads);
  alert(`Snapshot taken! Captured ${activePads.length} controller(s).`);
});
</code></pre>
</div>

<p>Snapshots freeze the exact state of your controller at one moment in time.</p>

<h3 id="4-ghost-input-replay">4. Ghost Input Replay</h3>

<p>Now for the fun one: ghost input replay. This takes a log and plays it back visually as if a phantom player was using the controller.</p>

<div class="break-out">
<pre><code class="language-html">&lt;div class="controls"&gt;
  &lt;button id="replay" class="btn"&gt;Replay Last Recording&lt;/button&gt;
&lt;/div&gt;
</code></pre>
</div>

<p>JavaScript for replay:</p>

<pre><code class="language-javascript">// ===================================
// GHOST REPLAY
// ===================================

document.getElementById("replay").addEventListener("click", () =&gt; {
  // Ensure we have a recording to replay
  if (!frames.length) {
    alert("No recording to replay!");
    return;
  }
  
  console.log("Starting ghost replay...");
  
  // Track timing for synced playback
  let startTime = performance.now();
  let frameIndex = 0;
  
  // Replay animation loop
  function step() {
    const now = performance.now();
    const elapsed = now - startTime;
    
    // Process all frames that should have occurred by now
    while (frameIndex &lt; frames.length && frames[frameIndex].t &lt;= elapsed) {
      const frame = frames[frameIndex];
      
      // Update UI with the recorded button states
      btnA.classList.toggle("active", frame.buttons[0].pressed);
      btnB.classList.toggle("active", frame.buttons[1].pressed);
      btnX.classList.toggle("active", frame.buttons[2].pressed);
      
      // Update status display
      let pressed = [];
      frame.buttons.forEach((btn, i) =&gt; {
        if (btn.pressed) pressed.push("Button " + i);
      });
      if (pressed.length &gt; 0) {
        status.textContent = "Ghost: " + pressed.join(", ");
      }
      
      frameIndex++;
    }
    
    // Continue loop if there are more frames
    if (frameIndex &lt; frames.length) {
      requestAnimationFrame(step);
    } else {
      console.log("Replay finished.");
      status.textContent = "Replay complete";
    }
  }
  
  // Start the replay
  step();
});
</code></pre>

<p>To make debugging a bit more hands-on, I added a ghost replay. Once you’ve recorded a session, you can hit replay and watch the UI act it out, almost like a phantom player is running the pad. A new <strong>Replay Ghost</strong> button shows up in the panel for this.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="533"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg"
			
			sizes="100vw"
			alt="Ghost replay mode with a session playing back on the debugger."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Ghost replay mode with a session playing back on the debugger. (<a href='https://files.smashing.media/articles/css-gamepad-api-visual-debugging-css-layers/6-ghost-replay-mode.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Hit <strong>Record</strong>, mess around with the controller a bit, stop, then replay. The UI just echoes everything you did, like a ghost following your inputs.</p>

<p>Why bother with these extras?</p>

<ul>
<li><strong>Recording/export</strong> makes it easy for testers to show exactly what happened.</li>
<li><strong>Snapshots</strong> freeze a moment in time, super useful when you’re chasing odd bugs.</li>
<li><strong>Ghost replay</strong> is great for tutorials, accessibility checks, or just comparing control setups side by side.</li>
</ul>

<p>At this point, it’s not just a neat demo anymore, but something you could actually put to work.</p>

<h2 id="real-world-use-cases">Real-World Use Cases</h2>

<p>Now we’ve got this debugger that can do a lot. It shows live input, records logs, exports them, and even replays stuff. But the real question is: who actually cares? Who’s this useful for?</p>

<h3 id="game-developers">Game Developers</h3>

<p>Controllers are part of the job, but debugging them? Usually a pain. Imagine you’re testing a fighting game combo, like <code>↓ →</code> + <code>punch</code>. Instead of praying, you pressed it the same way twice, you record it once, and replay it. Done. Or you swap <code>JSON</code> logs with a teammate to check if your multiplayer code reacts the same on their machine. That’s huge.</p>

<h3 id="accessibility-practitioners">Accessibility Practitioners</h3>

<p>This one’s close to my heart. Not everyone plays with a “standard” controller. Adaptive controllers throw out weird signals sometimes. With this tool, you can see exactly what’s happening. Teachers, researchers, whoever. They can grab logs, compare them, or replay inputs side-by-side. Suddenly, invisible stuff becomes obvious.</p>

<h3 id="quality-assurance-testing">Quality Assurance Testing</h3>

<p>Testers usually write notes like “I mashed buttons here and it broke.” Not very helpful. Now? They can capture the exact presses, export the log, and send it off. No guessing.</p>

<h3 id="educators">Educators</h3>

<p>If you’re making tutorials or YouTube vids, ghost replay is gold. You can literally say, “Here’s what I did with the controller,” while the UI shows it happening. Makes explanations way clearer.</p>

<h3 id="beyond-games">Beyond Games</h3>

<p>And yeah, this isn’t just about games. People have used controllers for robots, art projects, and accessibility interfaces. Same issue every time: what is the browser actually seeing? With this, you don’t have to guess.</p>

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

<p>Debugging a controller input has always felt like flying blind. Unlike the DOM or CSS, there’s no built-in inspector for gamepads; it’s just raw numbers in the console, easily lost in the noise.</p>

<p>With a few hundred lines of HTML, CSS, and JavaScript, we built something different:</p>

<ul>
<li><strong>A visual debugger</strong> that makes invisible inputs visible.</li>
<li><strong>A layered CSS system</strong> that keeps the UI clean and debuggable.</li>
<li><strong>A set of enhancements</strong> (recording, exporting, snapshots, ghost replay) that elevate it from demo to developer tool.</li>
</ul>

<p>This project shows how far you can go by mixing the Web Platform’s power with a little creativity in CSS Cascade Layers.</p>

<p>The tool I just explained in its entirety is open-source. You can <a href="https://github.com/BboyGT/gamepad-cascade-debugger/tree/main/gamepad-cascade-debugger-final">clone the GitHub repo</a> and try it for yourself.</p>

<p>But more importantly, you can make it your own. Add your own layers. Build your own replay logic. Integrate it with your game prototype. Or even use it in ways I haven’t imagined. For teaching, accessibility, or data analysis.</p>

<p>At the end of the day, this isn’t just about debugging gamepads. It’s about <strong>shining a light on hidden inputs</strong>, and giving developers the confidence to work with hardware that the web still doesn’t fully embrace.</p>

<p>So, plug in your controller, open up your editor, and start experimenting. You might be surprised at what your browser and your CSS can truly accomplish.</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>Bryan Rasmussen</author><title>Older Tech In The Browser Stack</title><link>https://www.smashingmagazine.com/2025/11/older-tech-browser-stack/</link><pubDate>Thu, 13 Nov 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/older-tech-browser-stack/</guid><description>There are many existing web features and technologies in the wild that you may never touch directly in your day-to-day work. Perhaps you’re fairly new to web development and are simply unaware of them because you’re steeped in the abstraction of a specific framework that doesn’t require you to know it deeply, or even at all. Bryan Rasmussen looks specifically at XPath and demonstrates how it can be used alongside CSS to query elements.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/older-tech-browser-stack/" />
              <title>Older Tech In The Browser Stack</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Older Tech In The Browser Stack</h1>
                  
                    
                    <address>Bryan Rasmussen</address>
                  
                  <time datetime="2025-11-13T08:00:00&#43;00:00" class="op-published">2025-11-13T08:00:00+00:00</time>
                  <time datetime="2025-11-13T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>I’ve been in front-end development long enough to see a trend over the years: younger developers working with a new paradigm of programming without understanding the historical context of it.</p>

<p>It is, of course, perfectly understandable to <em>not</em> know something. The web is a very big place with a diverse set of skills and specialties, and we don’t always know what we don’t know. Learning in this field is an ongoing journey rather than something that happens once and ends.</p>

<p>Case in point: Someone on my team asked if it was possible to tell if users navigate away from a particular tab in the UI. I pointed out JavaScript’s <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event"><code>beforeunload</code> event</a>. But those who have tackled this before know this is possible because they have been hit with alerts about unsaved data on other sites, for which <code>beforeunload</code> is a typical use case. I also pointed out the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event"><code>pageHide</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event"><code>visibilityChange</code></a> events to my colleague for good measure.</p>

<p>How did I know about that? Because it came up in another project, not because I studied up on it when initially learning JavaScript.</p>

<p>The fact is that modern front-end frameworks are standing on the shoulders of the technology giants that preceded them. They abstract development practices, often for a better developer experience that reduces, or even eliminates, the need to know or touch what have traditionally been essential front-end concepts everyone probably ought to know.</p>

<p>Consider the <a href="https://css-tricks.com/an-introduction-and-guide-to-the-css-object-model-cssom/">CSS Object Model (CSSOM)</a>. You might expect that anyone working in CSS and JavaScript has a bunch of hands-on CSSOM experience, but that’s not always going to be the case.</p>

<p>There was a React project for an e-commerce site I worked on where we needed to load a stylesheet for the currently selected payment provider. The problem was that the stylesheet was loading on every page when it was only really needed on a specific page. The developer tasked with making this happen hadn’t ever loaded a stylesheet dynamically. Again, this is totally understandable when React abstracts away the traditional approach you might have reached for.</p>

<p>The CSSOM is likely not something you need in your everyday work. But it is likely you will need to interact with it at some point, even in a one-off instance.</p>

<p>These experiences inspired me to write this article. There are many existing web features and technologies in the wild that you may never touch directly in your day-to-day work. Perhaps you’re fairly new to web development and are simply unaware of them because you’re steeped in the abstraction of a specific framework that doesn’t require you to know it deeply, or even at all.</p>

<p>I’m speaking specifically about <a href="https://developer.mozilla.org/en-US/docs/Web/XML/Guides/XML_introduction">XML</a>, which many of us know is an ancient language not totally dissimilar from HTML.</p>

<p>I’m bringing this up because of recent WHATWG discussions <a href="https://github.com/whatwg/html/issues/11523">suggesting</a> that a significant chunk of the XML stack known as <a href="https://developer.mozilla.org/en-US/docs/Web/XML/XSLT">XSLT</a> programming should be removed from browsers. This is exactly the sort of older, existing technology we’ve had for years that could be used for something as practical as the CSSOM situation my team was in.</p>

<p>Have you worked with XSLT before? Let’s see if we lean heavily into this older technology and leverage its features outside the context of XML to tackle real-world problems today.</p>

<h2 id="xpath-the-central-api">XPath: The Central API</h2>

<p>The most important XML technology that is perhaps the most useful outside of a straight XML perspective is <strong>XPath</strong>, a query language that allows you to find any node or attribute in a markup tree with one root element. I have a personal affection for XSLT, but that also relies on XPath, and personal affection must be put aside in ranking importance.</p>

<p>The argument for removing XSLT does not make any mention of XPath, so I suppose it is still allowed. That’s good because XPath is the central and most important API in this suite of technologies, especially when trying to find something to use outside normal XML usage. It is important because, while CSS selectors can be used to find most of the elements in your page, they cannot find them all. Furthermore, CSS selectors cannot be used to find an element based on its current position in the DOM.</p>

<p>XPath can.</p>

<p>Now, some of you reading this might know XPath, and some might not. XPath is a pretty big area of technology, and I can’t really teach all the basics and also show you cool things to do with it in a single article like this. I actually tried writing that article, but the average Smashing Magazine publication doesn’t go over 5,000 words. I was already at more than 2,000 words while only halfway through the basics.</p>

<p>So, I’m going to start doing cool stuff with XPath and give you some links that you can use for the basics if you find this stuff interesting.</p>

<h2 id="combining-xpath-css">Combining XPath &amp; CSS</h2>

<p>XPath can do lots of things that CSS selectors can’t when querying elements. But CSS selectors can also do a few things that XPath can’t, namely, query elements by class name.</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>CSS</th>
            <th>XPath</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><code>.myClass</code></td>
            <td><code>/&#42;[contains(@class, "myClass")]</code></td>
        </tr>
    </tbody>
</table>

<p>In this example, CSS queries elements that contain a <code>.myClass</code> classname. Meanwhile, the XPath example queries elements that contain an attribute class with the string “<code>myClass</code>”. In other words, it selects elements with <code>myClass</code> in any attribute, including elements with the <code>.myClass</code> classname &mdash; as well as elements with “<code>myClass</code>” in the string, such as <code>.myClass2</code>. XPath is broader in that sense.</p>

<p>So, no. I’m not suggesting that we ought to toss out CSS and start selecting all elements via XPath. That’s not the point.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20point%20is%20that%20XPath%20can%20do%20things%20that%20CSS%20cannot%20and%20could%20still%20be%20very%20useful,%20even%20though%20it%20is%20an%20older%20technology%20in%20the%20browser%20stack%20and%20may%20not%20seem%20obvious%20at%20first%20glance.%0a&url=https://smashingmagazine.com%2f2025%2f11%2folder-tech-browser-stack%2f">
      
The point is that XPath can do things that CSS cannot and could still be very useful, even though it is an older technology in the browser stack and may not seem obvious at first glance.

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

<p>Let’s use the two technologies together not only because we can, but because we’ll learn something about XPath in the process, making it another tool in your stack &mdash; one you might not have known has been there all along!</p>

<p>The problem is that JavaScript’s <code>document.evaluate</code> method and the various query selector methods we use with the CSS APIs for JavaScript are incompatible.</p>

<p>I have made a compatible querying API to get us started, though admittedly, I have not put a lot of thought into it since it’s a departure from what we’re doing here. Here’s a fairly simple working example of a reusable query constructor:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="jEqEyEx"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [queryXPath [forked]](https://codepen.io/smashingmag/pen/jEqEyEx) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/jEqEyEx">queryXPath [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>I’ve added two methods on the document object: <code>queryCSSSelectors</code> (which is essentially <code>querySelectorAll</code>) and <code>queryXPaths</code>. Both of these return a <code>queryResults</code> object:</p>

<div class="break-out">
<pre><code class="language-javascript">{
  queryType: nodes | string | number | boolean,
  results: any[] // html elements, xml elements, strings, numbers, booleans,
  queryCSSSelectors: (query: string, amend: boolean) =&gt; queryResults,
  queryXpaths: (query: string, amend: boolean) =&gt; queryResults
}
</code></pre>
</div>
  

<p>The <code>queryCSSSelectors</code> and <code>queryXpaths</code> functions run the query you give them over the elements in the results array, as long as the results array is of type <code>nodes</code>, of course. Otherwise, it will return a <code>queryResult</code> with an empty array and a type of <code>nodes</code>. If the <code>amend</code> property is set to <code>true</code>, the functions will change their own <code>queryResults</code>.</p>

<p><strong>Under no circumstances should this be used in a production environment.</strong> I am doing it this way purely to demonstrate the various effects of using the two query APIs together.</p>

<h2 id="example-queries">Example Queries</h2>

<p>I want to show a few examples of different XPath queries that demonstrate some of the powerful things they can do and how they can be used in place of other approaches.</p>

<p>The first example is <code>//li/text()</code>. This queries all <code>li</code> elements and returns their text nodes. So, if we were to query the following HTML:</p>

<pre><code class="language-html">&lt;ul&gt;
  &lt;li&gt;one&lt;/li&gt;
  &lt;li&gt;two&lt;/li&gt;
  &lt;li&gt;three&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
  

<p>…this is what is returned:</p>

<div class="break-out">
<pre><code class="language-json">{"queryType":"xpathEvaluate","results":["one","two","three"],"resultType":"string"}
</code></pre>
</div>
  

<p>In other words, we get the following array: <code>[&quot;one&quot;,&quot;two&quot;,&quot;three&quot;]</code>.</p>

<p>Normally, you would query for the <code>li</code> elements to get that, turn the result of that query into an array, map the array, and return the text node of each element. But we can do that more concisely with XPath:</p>

<pre><code class="language-javascript">document.queryXPaths("//li/text()").results.
</code></pre>

<p>Notice that the way to get a text node is to use <code>text()</code>, which looks like a function signature &mdash; and it is. It returns the text node of an element. In our example, there are three <code>li</code> elements in the markup, each containing text (<code>&quot;one&quot;</code>, <code>&quot;two&quot;</code>, and <code>&quot;three&quot;</code>).</p>

<p>Let’s look at one more example of a <code>text()</code> query. Assume this is our markup:</p>

<pre><code class="language-html">&lt;pa href="/login.html"&gt;Sign In&lt;/a&gt;
</code></pre>
  

<p>Let’s write a query that returns the <code>href</code> attribute value:</p>

<pre><code class="language-javascript">document.queryXPaths("//a[text() = 'Sign In']/@href").results.
</code></pre>

<p>This is an XPath query on the current document, just like the last example, but this time we return the <code>href</code> attribute of a link (<code>a</code> element) that contains the text “Sign In”. The actual returned result is <code>[&quot;/login.html&quot;]</code>.</p>

<h2 id="xpath-functions-overview">XPath Functions Overview</h2>

<p>There are a number of XPath functions, and you’re probably unfamiliar with them. There are several, I think, that are worth knowing about, including the following:</p>

<ul>
<li><strong><code>starts-with</code></strong><br />
If a text starts with a particular other text example, <code>starts-with(@href, 'http:')</code> returns <code>true</code> if an <code>href</code> attribute starts with <code>http:</code>.</li>
<li><strong><code>contains</code></strong><br />
If a text contains a particular other text example, <code>contains(text(), &quot;Smashing Magazine&quot;)</code> returns <code>true</code> if a text node contains the words “Smashing Magazine” in it anywhere.</li>
<li><strong><code>count</code></strong><br />
Returns a count of how many matches there are to a query. For example, <code>count(//*[starts-with(@href, 'http:'])</code> returns a count of how many links in the context node have elements with an <code>href</code> attribute that contains the text beginning with the <code>http:</code>.</li>
<li><strong><code>substring</code></strong><br />
Works like JavaScript <code>substring</code>, except you pass the string as an argument. For example, <code>substring(&quot;my text&quot;, 2, 4)</code> returns <code>&quot;y t&quot;</code>.</li>
<li><strong><code>substring-before</code></strong><br />
Returns the part of a string before another string. For example, <code>substing-before(&quot;my text&quot;, &quot; &quot;)</code> returns <code>&quot;my&quot;</code>. Similarly, <code>substring-before(&quot;hi&quot;,&quot;bye&quot;)</code> returns an empty string.</li>
<li><strong><code>substring-after</code></strong><br />
Returns the part of a string after another string. For example, <code>substing-after(&quot;my text&quot;, &quot; &quot;)</code> returns <code>&quot;text&quot;</code>. Similarly, <code>substring-after(&quot;hi&quot;,&quot;bye&quot;)</code>returns an empty string.</li>
<li><strong><code>normalize-space</code></strong><br />
Returns the argument string with whitespace normalized by stripping leading and trailing whitespace and replacing sequences of whitespace characters by a single space.</li>
<li><strong><code>not</code></strong><br />
Returns a boolean <code>true</code> if the argument is false, otherwise <code>false</code>.</li>
<li><strong><code>true</code></strong><br />
Returns boolean <code>true</code>.</li>
<li><strong><code>false</code></strong><br />
Returns boolean <code>false</code>.</li>
<li><strong><code>concat</code></strong><br />
The same thing as JavaScript <code>concat</code>, except you do not run it as a method on a string. Instead, you put in all the strings you want to concatenate.</li>
<li><strong><code>string-length</code></strong><br />
This is not the same as JavaScript <code>string-length</code>, but rather returns the length of the string it is given as an argument.</li>
<li><strong><code>translate</code></strong><br />
This takes a string and changes the second argument to the third argument. For example, <code>translate(&quot;abcdef&quot;, &quot;abc&quot;, &quot;XYZ&quot;)</code> outputs <code>XYZdef</code>.</li>
</ul>

<p>Aside from these particular XPath functions, there are a number of other functions that work just the same as their JavaScript counterparts &mdash; or counterparts in basically any programming language &mdash; that you would probably also find useful, such as <code>floor</code>, <code>ceiling</code>, <code>round</code>, <code>sum</code>, and so on.</p>

<p>The following demo illustrates each of these functions:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="emZmgzX"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [XPath Numerical functions [forked]](https://codepen.io/smashingmag/pen/emZmgzX) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/emZmgzX">XPath Numerical functions [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>Note that, like most of the string manipulation functions, many of the numerical ones take a <strong>single input</strong>. This is, of course, because they are supposed to be used for querying, as in the last XPath example:</p>

<pre><code class="language-html">//li[floor(text()) &gt; 250]/@val
</code></pre>

<p>If you use them, as most of the examples do, you will end up running it on the first node that matches the path.</p>

<p>There are also some type conversion functions you should probably avoid because JavaScript already has its own type conversion problems. But there can be times when you want to convert a string to a number in order to check it against some other number.</p>

<p>Functions that set the type of something are boolean, number, string, and node. These are the important XPath datatypes.</p>

<p>And as you might imagine, most of these functions can be used on datatypes that are not DOM nodes. For example, <code>substring-after</code> takes a string as we’ve already covered, but it could be the string from an <code>href</code> attribute. It can also just be a string:</p>

<div class="break-out">
<pre><code class="language-javascript">const testSubstringAfter = document.queryXPaths("substring-after('hello world',' ')");
</code></pre>
</div>

<p>Obviously, this example will give us back the results array as <code>[&quot;world&quot;]</code>. To show this in action, I have made a demo page using functions against things that are not DOM nodes:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="qEZERqd"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [queryXPath [forked]](https://codepen.io/smashingmag/pen/qEZERqd) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/qEZERqd">queryXPath [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>You should note the surprising aspect of the <code>translate</code> function, which is that if you have a character in the second argument (i.e., the list of characters you want translated) and no matching character to translate to, that character gets removed from the output.</p>

<p>Thus, this:</p>

<div class="break-out">
<pre><code class="language-javascript">translate('Hello, My Name is Inigo Montoya, you killed my father, prepare to die','abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,','&#42;')
</code></pre>
</div>

<p>…results in the string, including spaces:</p>

<pre><code class="language-json">[" &#42; &#42;  &#42;&#42; "]
</code></pre>

<p>This means that the letter “a” is being translated to an asterisk (<code>*</code>), but every other character that does not have a translation given the target string is completely removed. The whitespace is all we have left between the translated “a” characters.</p>

<p>Then again, this query:</p>

<div class="break-out">
<pre><code class="language-javascript">translate('Hello, My Name is Inigo Montoya, you killed my father, prepare to die','abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,','&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;&#42;')")
</code></pre>
</div>

<p>…does not have the problem and outputs a result that looks like this:</p>

<div class="break-out">
<pre><code class="language-javascript">"&#42;&#42;&#42;&#42;&#42; &#42;&#42; &#42;&#42;&#42;&#42; &#42;&#42; &#42;&#42;&#42;&#42;&#42; &#42;&#42;&#42;&#42;&#42;&#42;&#42; &#42;&#42;&#42; &#42;&#42;&#42;&#42;&#42;&#42; &#42;&#42; &#42;&#42;&#42;&#42;&#42;&#42; &#42;&#42;&#42;&#42;&#42;&#42;&#42; &#42;&#42; &#42;&#42;&#42;"
</code></pre>
</div>
  

<p>It might strike you that there is no easy way in JavaScript to do exactly what the XPath <code>translate</code> function does, although for many use cases, <code>replaceAll</code> with regular expressions can handle it.</p>

<p>You could use the same approach I have demonstrated, but that is suboptimal if all you want is to translate the strings. The following demo wraps XPath’s <code>translate</code> function to provide a JavaScript version:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ZYWYLyZ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [translate function [forked]](https://codepen.io/smashingmag/pen/ZYWYLyZ) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ZYWYLyZ">translate function [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>Where might you use something like this? Consider <a href="https://en.wikipedia.org/wiki/Caesar_cipher">Caesar Cipher</a> encryption with a three-place offset (e.g., top-of-the-line encryption from 48 B.C.):</p>

<div class="break-out">
<pre><code class="language-javascript">translate("Caesar is planning to cross the Rubicon!", 
 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
  "XYZABCDEFGHIJKLMNOPQRSTUVWxyzabcdefghijklmnopqrstuvw")
</code></pre>
</div>

<p>The input text “Caesar is planning to cross the Rubicon!” results in “Zxbpxo fp mixkkfkd ql zolpp qeb Oryfzlk!”</p>

<p>To give another quick example of different possibilities, I made a <code>metal</code> function that takes a string input and uses a <code>translate</code> function to return the text, including all characters that take umlauts.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YPqPNrN"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [metal function [forked]](https://codepen.io/smashingmag/pen/YPqPNrN) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YPqPNrN">metal function [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<div class="break-out">
<pre><code class="language-javascript">const metal = (str) =&gt; {
  return translate(str, "AOUaou","ÄÖÜäöü");
}
</code></pre>
</div>
  

<p>And, if given the text “Motley Crue rules, rock on dudes!”, returns “Mötley Crüe rüles, röck ön düdes!”</p>

<p>Obviously, one might have all sorts of parody uses of this function. If that’s you, then this <a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/HeavyMetalUmlaut">TVTropes article</a> ought to provide you with plenty of inspiration.</p>

<h2 id="using-css-with-xpath">Using CSS With XPath</h2>

<p>Remember our main reason for using CSS selectors together with XPath: CSS pretty much understands what a class is, whereas the best you can do with XPath is string comparisons of the class attribute. That will work in most cases.</p>

<p>But if you were to ever run into a situation where, say, someone created classes named <code>.primaryLinks</code> and <code>.primaryLinks2</code> and you were using XPath to get the <code>.primaryLinks</code> class, then you would likely run into problems. As long as there’s nothing silly like that, you would probably use XPath. But I am sad to report that I have worked at places where people do those types of silly things.</p>

<p>Here’s another demo using CSS and XPath together. It shows what happens when we use the code to run an XPath on a context node that is not the document’s node.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ogxgBpz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [css and xpath together [forked]](https://codepen.io/smashingmag/pen/ogxgBpz) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogxgBpz">css and xpath together [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>The CSS query is <code>.relatedarticles a</code>, which fetches the two <code>a</code> elements in a <code>div</code> assigned a <code>.relatedarticles</code> class.</p>

<p>After that are three “bad” queries, that is to say, queries that do not do what we want them to do when running with these elements as the context node.</p>

<p>I can explain why they are behaving differently than you might expect. The three bad queries in question are:</p>

<ul>
<li><code>//text()</code>: Returns all the text in the document.</li>
<li><code>//a/text()</code>: Returns all the text inside of links in the document.</li>
<li><code>./a/text()</code>: Returns no results.</li>
</ul>

<p>The reason for these results is that while your context is <code>a</code> elements returned from the CSS query, <code>//</code> goes against the whole document. This is the strength of XPath; CSS cannot go from a node up to an ancestor and then to a sibling of that ancestor, and walk down to a descendant of that sibling. But XPath can.</p>

<p>Meanwhile, <code>./</code> queries the children of the current node, where the dot (<code>.</code>) represents the current node, and the forward slash (<code>/</code>) represents going to some child node &mdash; whether it is an attribute, element, or text is determined by the next part of the path. But there is no child <code>a</code> element selected by the CSS query, thus that query also returns nothing.</p>

<p>There are three good queries in that last demo:</p>

<ul>
<li><code>.//text()</code>,</li>
<li><code>./text()</code>,</li>
<li><code>normalize-space(./text())</code>.</li>
</ul>

<p>The <code>normalize-space</code> query demonstrates XPath function usage, but also fixes a problem included in the other queries. The HTML is structured like this:</p>

<div class="break-out">
<pre><code class="language-html">&lt;a href="https://www.smashingmagazine.com/2018/04/feature-testing-selenium-webdriver/"&gt;
  Automating Your Feature Testing With Selenium WebDriver
&lt;/a&gt;
</code></pre>
</div>
  

<p>The query returns a line feed at the beginning and end of the text node, and <code>normalize-space</code> removes this.</p>

<p>Using any XPath function that returns something other than a boolean with an input XPath applies to other functions. The following demo shows a number of examples:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="JoXYGeN"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [xpath functions examples [forked]](https://codepen.io/smashingmag/pen/JoXYGeN) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/JoXYGeN">xpath functions examples [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>The first example shows a problem you should watch out for. Specifically, the following code:</p>

<div class="break-out">
<pre><code class="language-javascript">document.queryXPaths("substring-after(//a/@href,'https://')");
</code></pre>
</div>
  

<p>…returns one string:</p>

<div class="break-out">
<pre><code class="language-html">"www.smashingmagazine.com/2018/04/feature-testing-selenium-webdriver/"
</code></pre>
</div>

<p>It makes sense, right? These functions do not return arrays but rather single strings or single numbers. Running the function anywhere with multiple results only returns the first result.</p>

<p>The second result shows what we really want:</p>

<div class="break-out">
<pre><code class="language-javascript">document.queryCSSSelectors("a").queryXPaths("substring-after(./@href,'https://')");
</code></pre>
</div>

<p>Which returns an array of two strings:</p>

<div class="break-out">
<pre><code class="language-json">["www.smashingmagazine.com/2018/04/feature-testing-selenium-webdriver/","www.smashingmagazine.com/2022/11/automated-test-results-improve-accessibility/"]
</code></pre>
</div>

<p>XPath functions can be nested just like functions in JavaScript. So, if we know the Smashing Magazine URL structure, we could do the following (using template literals is recommended):</p>

<pre><code class="language-javascript">`translate(
    substring(
      substring-after(./@href, ‘www.smashingmagazine.com/')
    ,9),
 '/','')`
</code></pre>
  

<p>This is getting a bit too complex to the extent that it needs comments describing what it does: take all of the URL from the <code>href</code> attribute after <code>www.smashingmagazine.com/</code>, remove the first nine characters, then translate the forward slash (<code>/</code>) character to nothing so as to get rid of the ending forward slash.</p>

<p>The resulting array:</p>

<div class="break-out">
<pre><code class="language-json">["feature-testing-selenium-webdriver","automated-test-results-improve-accessibility"]
</code></pre>
</div>
  

<h2 id="more-xpath-use-cases">More XPath Use Cases</h2>

<p>XPath can really shine in <strong>testing</strong>. The reason is not difficult to see, as XPath can be used to get every element in the DOM, from any position in the DOM, whereas CSS cannot.</p>

<p>You cannot count on CSS classes remaining consistent in many modern build systems, but with XPath, we are able to make more robust matches as to what the text content of an element is, regardless of a changing DOM structure.</p>

<p>There has been <a href="https://ieeexplore.ieee.org/document/6983884">research on techniques</a> that allow you to make resilient XPath tests. Nothing is worse than having tests flake out and fail just because a CSS selector no longer works because something has been renamed or removed.</p>

<p>XPath is also really great at <strong>multiple locator extraction</strong>. There is more than one way to use XPath queries to match an element. The same is true with CSS. But XPath queries can drill into things in a more targeted way that limits what gets returned, allowing you to find a specific match where there may be several possible matches.</p>

<p>For example, we can use XPath to return a specific <code>h2</code> element that is contained inside a <code>div</code> that immediately follows a sibling <code>div</code> that, in turn, contains a child image element with a <code>data-testID=&quot;leader&quot;</code> attribute on it:</p>

<pre><code class="language-html">&lt;div&gt;
  &lt;div&gt;
    &lt;h1&gt;don't get this headline&lt;/h1&gt;
  &lt;/div&gt;
  
  &lt;div&gt;
    &lt;h2&gt;Don't get this headline either&lt;/h2&gt;
  &lt;/div&gt;
  
  &lt;div&gt;
    &lt;h2&gt;The header for the leader image&lt;/h2&gt;
  &lt;/div&gt;
  
  &lt;div&gt;
    &lt;img data-testID="leader" src="image.jpg"/&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
  

<p>This is the query:</p>

<pre><code class="language-javascript">document.queryXPaths(`
  //div[
    following-sibling::div[1]
    /img[@data-testID='leader']
  ]
  /h2/
  text()
`);
</code></pre>
  

<p>Let’s drop in a demo to see how that all comes together:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="zxqxNev"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Complex H2 Query [forked]](https://codepen.io/smashingmag/pen/zxqxNev) by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxqxNev">Complex H2 Query [forked]</a> by <a href="https://codepen.io/bryanrasmussen">Bryan Rasmussen</a>.</figcaption>
</figure>

<p>So, yes. There are lots of possible paths to any element in a test using XPath.</p>

<h2 id="xslt-1-0-deprecation">XSLT 1.0 Deprecation</h2>

<p>I mentioned early on that <a href="https://xslt.rip/">the Chrome team plans on removing XSLT 1.0 support from the browser</a>. That’s important because XSLT 1.0 uses XML-focused programming for document transformation that, in turn, relies on XPath 1.0, which is what is found in most browsers.</p>

<p>When that happens, we’ll lose a key component of XPath. But given the fact that XPath is really great for writing tests, I find it unlikely that XPath as a whole will disappear anytime soon.</p>

<p>That said, I’ve noticed that people get interested in a feature when it’s taken away. And that’s certainly true in the case of XSLT 1.0 being deprecated. <a href="https://news.ycombinator.com/item?id=45006098">There’s an entire discussion happening over at Hacker News</a> filled with arguments against the deprecation. The post itself is a great example of creating a blogging framework with XSLT. You can read the discussion for yourself, but it gets into how JavaScript might be used as a shim for XLST to handle those sorts of cases.</p>

<p>I have also <a href="https://www.saxonica.com/saxonjs/documentation3/index.html#!browser">seen suggestions</a> that browsers should use SaxonJS, which is a port to JavaScript’s Saxon XSLT, XQUERY, and XPath engines. That’s an interesting idea, especially as Saxon-JS implements the current version of these specifications, whereas there is no browser that implements any version of XPath or XSLT beyond 1.0, and none that implements XQuery.</p>

<p>I reached out to <a href="https://norm.tovey-walsh.com">Norm Tovey-Walsh</a> at Saxonica, the company behind SaxonJS and other versions of the Saxon engine. He said:</p>

<blockquote>“If any browser vendor was interested in taking SaxonJS as a starting point for integrating modern XML technologies into the browser, we’d be thrilled to discuss it with them.”<br /><br />&mdash; <a href="https://norm.tovey-walsh.com">Norm Tovey-Walsh</a></blockquote>

<p>But also added:</p>

<blockquote>“I would be very surprised if anyone thought that taking SaxonJS in its current form and dropping it into the browser build unchanged would be the ideal approach. A browser vendor, by nature of the fact that they build the browser, could approach the integration at a much deeper level than we can ‘from the outside’.”<br /><br />&mdash; <a href="https://norm.tovey-walsh.com">Norm Tovey-Walsh</a></blockquote>

<p>It’s worth noting that Tovey-Walsh’s comments came about a week before the XSLT deprecation announcement.</p>

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

<p>I could go on and on. But I hope this has demonstrated the <strong>power of XPath</strong> and given you plenty of examples demonstrating how to use it for achieving great things. It’s a perfect example of older technology in the browser stack that still has plenty of <strong>utility</strong> today, even if you’ve never known it existed or never considered reaching for it.</p>

<h3 id="further-reading">Further Reading</h3>

<ul>
<li>“<a href="https://dl.acm.org/doi/full/10.1145/3700523.3700536">Enhancing the Resiliency of Automated Web Tests with Natural Language</a>” (ACM Digital Library) by Maroun Ayli, Youssef Bakouny, Nader Jalloul, and Rima Kilany<br />
<em>This article provides many XPath examples for writing resilient tests.</em></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/XML/XPath">XPath</a> (MDN)<br />
<em>This is an excellent place to start if you want a technical explanation detailing how XPath works.</em></li>
<li><a href="http://www.zvon.org/xxl/XPathTutorial/General/examples.html">XPath Tutorial</a> (ZVON)<br />
<em>I’ve found this tutorial to be the most helpful in my own learning, thanks to a wealth of examples and clear explanations.</em></li>
<li><a href="https://xpather.com">XPather</a><br />
<em>This interactive tool lets you work directly with the code.</em></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 6: Magnificent SVGs With `&lt;use>` And CSS Custom Properties</title><link>https://www.smashingmagazine.com/2025/11/smashing-animations-part-6-svgs-css-custom-properties/</link><pubDate>Fri, 07 Nov 2025 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/smashing-animations-part-6-svgs-css-custom-properties/</guid><description>SVG is one of those web technologies that’s both elegant and, at times, infuriating. In this article, pioneering author and web designer &lt;a href="https://stuffandnonsense.co.uk">Andy Clarke&lt;/a> explains his technique for animating SVG elements that are hidden in the Shadow DOM.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/smashing-animations-part-6-svgs-css-custom-properties/" />
              <title>Smashing Animations Part 6: Magnificent SVGs With `&lt;use&gt;` And CSS Custom Properties</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Smashing Animations Part 6: Magnificent SVGs With `&lt;use&gt;` And CSS Custom Properties</h1>
                  
                    
                    <address>Andy Clarke</address>
                  
                  <time datetime="2025-11-07T15:00:00&#43;00:00" class="op-published">2025-11-07T15:00:00+00:00</time>
                  <time datetime="2025-11-07T15:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>I explained recently how I use <code>&lt;symbol&gt;</code>, <code>&lt;use&gt;</code>, and CSS Media Queries to develop what I call <a href="https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/">adaptive SVGs</a>. Symbols let us define an element once and then <em>use</em> it again and again, making SVG animations easier to maintain, more efficient, and lightweight.</p>

<p>Since I wrote that explanation, I’ve designed and implemented new <a href="https://stuffandnonsense.co.uk/blog/say-hello-to-my-magnificent-7">Magnificent 7</a> animated graphics across <a href="https://stuffandnonsense.co.uk/">my website</a>. They play on the web design pioneer theme, featuring seven magnificent Old West characters.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://stuffandnonsense.co.uk/">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png"
			
			sizes="100vw"
			alt="Graphics featuring seven magnificent Old West characters"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View this animated SVG on <a href='https://stuffandnonsense.co.uk/'>my website</a>. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/1-graphics-old-west-characters.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><code>&lt;symbol&gt;</code> and <code>&lt;use&gt;</code> let me define a character design and reuse it across multiple SVGs and pages. First, I created my characters and put each into a <code>&lt;symbol&gt;</code> inside a hidden library SVG:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;!-- Symbols library --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" style="display:none;"&gt;
 &lt;symbol id="outlaw-1"&gt;[...]&lt;/symbol&gt;
 &lt;symbol id="outlaw-2"&gt;[...]&lt;/symbol&gt;
 &lt;symbol id="outlaw-3"&gt;[...]&lt;/symbol&gt;
 &lt;!-- etc. --&gt;
&lt;/svg&gt;
</code></pre>
</div>

<p>Then, I referenced those symbols in two other SVGs, one for large and the other for small screens:</p>

<pre><code class="language-svg">&lt;!-- Large screens --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" id="svg-large"&gt;
 &lt;use href="outlaw-1" /&gt;
 &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;!-- Small screens --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" id="svg-small"&gt;
 &lt;use href="outlaw-1" /&gt;
 &lt;!-- ... --&gt;
&lt;/svg&gt;
</code></pre>

<p>Elegant. But then came the infuriating. I could reuse the characters, but couldn’t animate or style them. I added CSS rules targeting elements within the symbols referenced by a <code>&lt;use&gt;</code>, but nothing happened. Colours stayed the same, and things that should move stayed static. It felt like I’d run into an invisible barrier, and I had.</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="understanding-the-shadow-dom-barrier">Understanding The Shadow DOM Barrier</h2>

<p>When you reference the contents of a <code>symbol</code> with <code>use</code>, a browser creates a copy of it in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">Shadow DOM</a>. Each <code>&lt;use&gt;</code> instance becomes its own encapsulated copy of the referenced <code>&lt;symbol&gt;</code>, meaning that CSS from outside can’t break through the barrier to style any elements directly. For example, in normal circumstances, this <code>tapping</code> value triggers a CSS animation:</p>

<pre><code class="language-svg">&lt;g class="outlaw-1-foot tapping"&gt;
 &lt;!-- ... --&gt;
&lt;/g&gt;
</code></pre>

<pre><code class="language-css">.tapping {
  animation: tapping 1s ease-in-out infinite;
}
</code></pre>

<p>But when the same animation is applied to a <code>&lt;use&gt;</code> instance of that same foot, nothing happens:</p>

<pre><code class="language-svg">&lt;symbol id="outlaw-1"&gt;
 &lt;g class="outlaw-1-foot"&gt;&lt;!-- ... --&gt;&lt;/g&gt;
&lt;/symbol&gt;

&lt;use href="#outlaw-1" class="tapping" /&gt;
</code></pre>

<pre><code class="language-css">.tapping {
  animation: tapping 1s ease-in-out infinite;
}
</code></pre>

<p>That’s because the <code>&lt;g&gt;</code> inside the <code>&lt;symbol&gt;</code> element is in a protected shadow tree, and the CSS Cascade stops dead at the <code>&lt;use&gt;</code> boundary. This behaviour can be frustrating, but it’s intentional as it ensures that reused symbol content stays consistent and predictable.</p>

<p>While learning how to develop adaptive SVGs, I found all kinds of attempts to work around this behaviour, but most of them sacrificed the reusability that makes SVG so elegant. I didn’t want to duplicate my characters just to make them blink at different times. I wanted a single <code>&lt;symbol&gt;</code> with instances that have their own timings and expressions.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.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-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png"
			
			sizes="100vw"
			alt="Several animated elements within a single SVG symbol"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Several animated elements within a single SVG symbol. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/2-animated-elements-single-svg-symbol.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="css-custom-properties-to-the-rescue">CSS Custom Properties To The Rescue</h2>

<p>While working on my pioneer animations, I learned that <strong>regular CSS values can’t cross the boundary into the Shadow DOM, but CSS Custom Properties can</strong>. And even though you can’t directly style elements inside a <code>&lt;symbol&gt;</code>, you can pass custom property values to them. So, when you insert custom properties into an inline style, a browser looks at the cascade, and those styles become available to elements inside the <code>&lt;symbol&gt;</code> being referenced.</p>

<p>I added <code>rotate</code> to an inline style applied to the <code>&lt;symbol&gt;</code> content:</p>

<pre><code class="language-svg">&lt;symbol id="outlaw-1"&gt;
  &lt;g class="outlaw-1-foot" style="
    transform-origin: bottom right; 
    transform-box: fill-box; 
    transform: rotate(var(--foot-rotate));"&gt;
    &lt;!-- ... --&gt;
  &lt;/g&gt;
&lt;/symbol&gt;
</code></pre>

<p>Then, defined the foot tapping animation and applied it to the element:</p>

<pre><code class="language-css">@keyframes tapping {
  0%, 60%, 100% { --foot-rotate: 0deg; }
  20% { --foot-rotate: -5deg; }
  40% { --foot-rotate: 2deg; }
}

use[data-outlaw="1"] {
  --foot-rotate: 0deg;
  animation: tapping 1s ease-in-out infinite;
}
</code></pre>

<h2 id="passing-multiple-values-to-a-symbol">Passing Multiple Values To A Symbol</h2>

<p>Once I’ve set up a symbol to use CSS Custom Properties, I can pass as many values as I want to any <code>&lt;use&gt;</code> instance. For example, I might define variables for <code>fill</code>, <code>opacity</code>, or <code>transform</code>. What’s elegant is that each <code>&lt;symbol&gt;</code> instance can then have its own set of values.</p>

<pre><code class="language-svg">&lt;g class="eyelids" style="
  fill: var(--eyelids-colour, #f7bea1);
  opacity: var(--eyelids-opacity, 1);
  transform: var(--eyelids-scale, 0);"
&gt;
  &lt;!-- etc. --&gt;
&lt;/g&gt;
</code></pre>

<pre><code class="language-css">use[data-outlaw="1"] {
  --eyelids-colour: #f7bea1; 
  --eyelids-opacity: 1;
}

use[data-outlaw="2"] {
  --eyelids-colour: #ba7e5e; 
  --eyelids-opacity: 0;
}
</code></pre>

<p>Support for passing CSS Custom Properties like this is solid, and every contemporary browser handles this behaviour correctly. Let me show you a few ways I’ve been using this technique, starting with a multi-coloured icon system.</p>

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

<h2 id="a-multi-coloured-icon-system">A Multi-Coloured Icon System</h2>

<p>When I need to maintain a set of icons, I can define an icon once inside a <code>&lt;symbol&gt;</code> and then use custom properties to apply colours and effects. Instead of needing to duplicate SVGs for every theme, each <code>use</code> can carry its own values.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="167"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png"
			
			sizes="100vw"
			alt="Custom properties for the fill colours in several Bluesky icons"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Custom properties for the fill colours in several Bluesky icons. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/3-custom-properties-colours.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For example, I applied an <code>--icon-fill</code> custom property for the default <code>fill</code> colour of the <code>&lt;path&gt;</code> in this Bluesky icon :</p>

<pre><code class="language-svg">&lt;symbol id="icon-bluesky"&gt;
  &lt;path fill="var(--icon-fill, currentColor)" d="..." /&gt;
&lt;/symbol&gt;
</code></pre>

<p>Then, whenever I need to vary how that icon looks &mdash; for example, in a <code>&lt;header&gt;</code> and <code>&lt;footer&gt;</code> &mdash; I can pass new <code>fill</code> colour values to each instance:</p>

<pre><code class="language-html">&lt;header&gt;
  &lt;svg xmlns="http://www.w3.org/2000/svg"&gt;
    &lt;use href="#icon-bluesky" style="--icon-fill: #2d373b;" /&gt;
  &lt;/svg&gt;
&lt;/header&gt;

&lt;footer&gt;
  &lt;svg xmlns="http://www.w3.org/2000/svg"&gt;
    &lt;use href="#icon-bluesky" style="--icon-fill: #590d1a;" /&gt;
  &lt;/svg&gt;
&lt;/footer&gt;
</code></pre>

<p>These icons are the same shape but look different thanks to their inline styles.</p>

<h2 id="data-visualisations-with-css-custom-properties">Data Visualisations With CSS Custom Properties</h2>

<p>We can use <code>&lt;symbol&gt;</code> and <code>&lt;use&gt;</code> in plenty more practical ways. They’re also helpful for creating lightweight data visualisations, so imagine an infographic about three famous <a href="https://en.wikipedia.org/wiki/American_frontier">Wild West</a> sheriffs: <a href="https://en.wikipedia.org/wiki/Wyatt_Earp">Wyatt Earp</a>, <a href="https://en.wikipedia.org/wiki/Pat_Garrett">Pat Garrett</a>, and <a href="https://en.wikipedia.org/wiki/Bat_Masterson">Bat Masterson</a>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="421"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png"
			
			sizes="100vw"
			alt="Data visualisations with CSS Custom Properties"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Data visualisations with CSS Custom Properties. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/4-data-visualisations-css-custom-properties.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Each sheriff’s profile uses the same set of SVG three symbols: one for a bar representing the length of a sheriff’s career, another to represent the number of arrests made, and one more for the number of kills. Passing custom property values to each <code>&lt;use&gt;</code> instance can vary the bar lengths, arrests scale, and kills colour without duplicating SVGs. I first created symbols for those items:</p>

<pre><code class="language-svg">&lt;svg xmlns="http://www.w3.org/2000/svg" style="display:none;"&gt;
  &lt;symbol id="career-bar"&gt;
    &lt;rect
      height="10"
      width="var(--career-length, 100)" 
      fill="var(--career-colour, #f7bea1)"
    /&gt;
  &lt;/symbol&gt;
  
  &lt;symbol id="arrests-badge"&gt;
    &lt;path 
      fill="var(--arrest-color, #d0985f)" 
      transform="scale(var(--arrest-scale, 1))"
    /&gt;
  &lt;/symbol&gt;
  
  &lt;symbol id="kills-icon"&gt;
    &lt;path fill="var(--kill-colour, #769099)" /&gt;
  &lt;/symbol&gt;
&lt;/svg&gt;
</code></pre>

<p>Each symbol accepts one or more values:</p>

<ul>
<li><strong><code>--career-length</code></strong> adjusts the <code>width</code> of the career bar.</li>
<li><strong><code>--career-colour</code></strong> changes the <code>fill</code> colour of that bar.</li>
<li><strong><code>--arrest-scale</code></strong> controls the arrest badge size.</li>
<li><strong><code>--kill-colour</code></strong> defines the <code>fill</code> colour of the kill icon.</li>
</ul>

<p>I can use these to develop a profile of each sheriff using <code>&lt;use&gt;</code> elements with different inline styles, starting with Wyatt Earp.</p>

<div class="break-out">
<pre><code class="language-svg">&lt;svg xmlns="http://www.w3.org/2000/svg"&gt;
  &lt;g id="wyatt-earp"&gt;
    &lt;use href="&#35;career-bar" style="--career-length: 400; --career-color: &#35;769099;"/&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;!-- ... --&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 1;" /&gt;
    &lt;use href="&#35;kills-icon" style="--kill-color: &#35;769099;" /&gt;
  &lt;/g&gt;

  &lt;g id="pat-garrett"&gt;
    &lt;use href="&#35;career-bar" style="--career-length: 300; --career-color: &#35;f7bea1;"/&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;!-- ... --&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 1;" /&gt;
    &lt;use href="&#35;kills-icon" style="--kill-color: &#35;f7bea1;" /&gt;
  &lt;/g&gt;

  &lt;g id="bat-masterson"&gt;
    &lt;use href="#career-bar" style="--career-length: 200; --career-color: &#35;c2d1d6;"/&gt;
    &lt;use href="#arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;!-- ... --&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 2;" /&gt;
    &lt;use href="&#35;arrests-badge" style="--arrest-scale: 1;" /&gt;
    &lt;use href="&#35;kills-icon" style="--kill-color: &#35;c2d1d6;" /&gt;
  &lt;/g&gt;
&lt;/svg&gt;
</code></pre>
</div>

<p>Each <code>&lt;use&gt;</code> shares the same symbol elements, but the inline variables change their colours and sizes. I can even animate those values to highlight their differences:</p>

<pre><code class="language-css">@keyframes pulse {
  0%, 100% { --arrest-scale: 1; }
  50% { --arrest-scale: 1.2; }
}

use[href="#arrests-badge"]:hover {
  animation: pulse 1s ease-in-out infinite;
}
</code></pre>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aCSS%20Custom%20Properties%20aren%e2%80%99t%20only%20helpful%20for%20styling;%20they%20can%20also%20channel%20data%20between%20HTML%20and%20SVG%e2%80%99s%20inner%20geometry,%20binding%20visual%20attributes%20like%20colour,%20length,%20and%20scale%20to%20semantics%20like%20arrest%20numbers,%20career%20length,%20and%20kills.%0a&url=https://smashingmagazine.com%2f2025%2f11%2fsmashing-animations-part-6-svgs-css-custom-properties%2f">
      
CSS Custom Properties aren’t only helpful for styling; they can also channel data between HTML and SVG’s inner geometry, binding visual attributes like colour, length, and scale to semantics like arrest numbers, career length, and kills.

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

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

<p>I started learning to animate elements within symbols while creating the animated graphics for my website’s Magnificent 7. To reduce complexity and make my code lighter and more maintainable, I needed to define each character once and reuse it across SVGs:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;!-- Symbols library --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" style="display:none;"&gt;
  &lt;symbol id="outlaw-1"&gt;[…]&lt;/symbol&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;!-- Large screens --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" id="svg-large"&gt;
  &lt;use href="outlaw-1" /&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;

&lt;!-- Small screens --&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" id="svg-small"&gt;
  &lt;use href="outlaw-1" /&gt;
  &lt;!-- ... --&gt;
&lt;/svg&gt;
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.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-6-svgs-css-custom-properties/5-characters.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png"
			
			sizes="100vw"
			alt="My website’s Magnificent 7 characters"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      My website’s Magnificent 7 characters. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/5-characters.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But I didn’t want those characters to stay static; I needed subtle movements that would bring them to life. I wanted their eyes to blink, their feet to tap, and their moustache whiskers to twitch. So, to animate these details, I pass animation data to elements inside those symbols using CSS Custom Properties, starting with the blinking.</p>

<p>I implemented the blinking effect by placing an SVG group over the outlaws’ eyes and then changing its <code>opacity</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="333"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png"
			
			sizes="100vw"
			alt="Blinking effect by animating eyelids’ opacity."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Blinking effect by animating eyelids’ opacity. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/6-blinking-effect.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To make this possible, I added an inline style with a CSS Custom Property to the group:</p>

<div class="break-out">
<pre><code class="language-svg">&lt;symbol id="outlaw-1" viewBox="0 0 712 2552"&gt;
 &lt;g class="eyelids" style="opacity: var(--eyelids-opacity, 1);"&gt;
    &lt;!-- ... --&gt;
  &lt;/g&gt;
&lt;/symbol&gt;
</code></pre>
</div>

<p>Then, I defined the blinking animation by changing <code>--eyelids-opacity</code>:</p>

<pre><code class="language-css">@keyframes blink {
  0%, 92% { --eyelids-opacity: 0; }
  93%, 94% { --eyelids-opacity: 1; }
  95%, 97% { --eyelids-opacity: 0.1; }
  98%, 100% { --eyelids-opacity: 0; }
}
</code></pre>

<p>…and applied it to every character:</p>

<div class="break-out">
<pre><code class="language-css">use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  animation: blink var(--blink-duration) infinite var(--blink-delay);
}
</code></pre>
</div>

<p>…so that each character wouldn’t blink at the same time, I set a different <code>--blink-delay</code> before they all start blinking, by passing another Custom Property:</p>

<pre><code class="language-css">use[data-outlaw="1"] { --blink-delay: 1s; }
use[data-outlaw="2"] { --blink-delay: 2s; }
<!-- ... -->
use[data-outlaw="7"] { --blink-delay: 3s; }
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png"
			
			sizes="100vw"
			alt="Foot tapping effect by animating the foot’s rotation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Foot tapping effect by animating the foot’s rotation. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/7-foot-tapping-effect.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Some of the characters tap their feet, so I added an inline style with a CSS Custom Property to those groups, too:</p>

<pre><code class="language-svg">&lt;symbol id="outlaw-1" viewBox="0 0 712 2552"&gt;
  &lt;g class="outlaw-1-foot" style="
    transform-origin: bottom right; 
    transform-box: fill-box; 
    transform: rotate(var(--foot-rotate));"&gt;
  &lt;/g&gt;
&lt;/symbol&gt;
</code></pre>

<p>Defining the foot-tapping animation:</p>

<pre><code class="language-css">@keyframes tapping {
  0%, 60%, 100% { --foot-rotate: 0deg; }
  20% { --foot-rotate: -5deg; }
  40% { --foot-rotate: 2deg; }
}
</code></pre>

<p>And adding those extra Custom Properties to the characters’ declaration:</p>

<pre><code class="language-css">use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  --foot-rotate: 0deg;
  animation: 
    blink var(--blink-duration) infinite var(--blink-delay),
    tapping 1s ease-in-out infinite;
}
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="333"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png"
			
			sizes="100vw"
			alt="Jiggling effect by animating the moustaches’ translation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Jiggling effect by animating the moustaches’ translation. (<a href='https://files.smashing.media/articles/smashing-animations-part-6-svgs-css-custom-properties/8-jiggling-effect.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>…before finally making the character’s whiskers jiggle via an inline style with a CSS Custom Property which describes how his moustache transforms:</p>

<pre><code class="language-svg">&lt;symbol id="outlaw-1" viewBox="0 0 712 2552"&gt;
  &lt;g class="outlaw-1-tashe" style="
    transform: translateX(var(--jiggle-x, 0px));"
  &gt;
    &lt;!-- ... --&gt;
  &lt;/g&gt;
&lt;/symbol&gt;
</code></pre>

<p>Defining the jiggle animation:</p>

<pre><code class="language-css">@keyframes jiggle {
  0%, 100% { --jiggle-x: 0px; }
  20% { --jiggle-x: -3px; }
  40% { --jiggle-x: 2px; }
  60% { --jiggle-x: -1px; }
  80% { --jiggle-x: 4px; }
}
</code></pre>

<p>And adding those properties to the characters’ declaration:</p>

<pre><code class="language-css">use[data-outlaw] {
  --blink-duration: 4s;
  --eyelids-opacity: 1;
  --foot-rotate: 0deg;
  --jiggle-x: 0px;
  animation: 
    blink var(--blink-duration) infinite var(--blink-delay),
    jiggle 1s ease-in-out infinite,
    tapping 1s ease-in-out infinite;
}
</code></pre>

<p>With these moving parts, the characters come to life, but my markup remains remarkably lean. By combining several animations into a single declaration, I can choreograph their movements without adding more elements to my SVG. Every outlaw shares the same base <code>&lt;symbol&gt;</code>, and their individuality comes entirely from CSS Custom Properties.</p>

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

<h2 id="pitfalls-and-solutions">Pitfalls And Solutions</h2>

<p>Even though this technique might seem bulletproof, there are a few traps it’s best to avoid:</p>

<ul>
<li><strong>CSS Custom Properties only work if they’re referenced with a <code>var()</code> inside a <code>&lt;symbol&gt;</code>.</strong> Forget that, and you’ll wonder why nothing updates. Also, properties that aren’t naturally inherited, like <code>fill</code> or <code>transform</code>, need to use <code>var()</code> in their value to benefit from the cascade.</li>
<li><strong>It’s always best to include a fallback value alongside a custom property</strong>, like <code>opacity: var(--eyelids-opacity, 1);</code> to ensure SVG elements render correctly even without custom property values applied.</li>
<li><strong>Inline styles set via the <code>style</code> attribute take precedence</strong>, so if you mix inline and external CSS, remember that Custom Properties follow normal cascade rules.</li>
<li><strong>You can always use DevTools to inspect custom property values.</strong> Select a <code>&lt;use&gt;</code> instance and check the Computed Styles panel to see which custom properties are active.</li>
</ul>

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

<p>The <code>&lt;symbol&gt;</code> and <code>&lt;use&gt;</code> elements are among the most elegant but sometimes frustrating aspects of SVG. The Shadow DOM barrier makes animating them trickier, but <strong>CSS Custom Properties act as a bridge</strong>. They let you pass colour, motion, and personality across that invisible boundary, resulting in cleaner, lighter, and, best of all, fun animations.</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 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">2026-02-09T03:03:08+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">2026-02-09T03:03:08+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>Victor Ayomipo</author><title>Integrating CSS Cascade Layers To An Existing Project</title><link>https://www.smashingmagazine.com/2025/09/integrating-css-cascade-layers-existing-project/</link><pubDate>Wed, 10 Sep 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/09/integrating-css-cascade-layers-existing-project/</guid><description>The idea behind this is to share a full, unfiltered look at integrating CSS Cascade Layers into an existing legacy codebase. In practice, it’s about refactoring existing CSS to use cascade layers without breaking anything.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/09/integrating-css-cascade-layers-existing-project/" />
              <title>Integrating CSS Cascade Layers To An Existing Project</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Integrating CSS Cascade Layers To An Existing Project</h1>
                  
                    
                    <address>Victor Ayomipo</address>
                  
                  <time datetime="2025-09-10T10:00:00&#43;00:00" class="op-published">2025-09-10T10:00:00+00:00</time>
                  <time datetime="2025-09-10T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>You can always get a fantastic overview of things in Stephenie Eckles’ article, “<a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">Getting Started With CSS Cascade Layers</a>”. But let’s talk about the experience of integrating cascade layers into real-world code, the good, the bad, and the spaghetti.</p>

<p>I could have created a sample project for a classic walkthrough, but nah, that’s not how things work in the real world. I want to get our hands dirty, like inheriting code with styles that work and no one knows why.</p>

<p>Finding projects without cascade layers was easy. The tricky part was finding one that was messy enough to have specificity and organisation issues, but broad enough to illustrate different parts of cascade layers integration.</p>

<p>Ladies and gentlemen, I present you with this <a href="https://github.com/Drix10/discord-bot-web">Discord bot website</a> by <a href="https://github.com/Drix10">Drishtant Ghosh</a>. I’m deeply grateful to Drishtant for allowing me to use his work as an example. This project is a typical landing page with a navigation bar, a hero section, a few buttons, and a mobile menu.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png"
			
			sizes="100vw"
			alt="Discord Bot landing page, including a circular logo centered above a heading, text blub, then a row of three buttons."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/integrating-css-cascade-layers-existing-project/1-discord-bot-landing-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You see how it looks perfect on the outside. Things get interesting, however, when we look at the CSS styles under the hood.</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="understanding-the-project">Understanding The Project</h2>

<p>Before we start throwing <code>@layers</code> around, let’s get a firm understanding of what we’re working with. I <a href="https://codepen.io/vayospot/pen/bNdoYdP">cloned</a> the GitHub repo, and since our focus is working with CSS Cascade Layers, I’ll focus only on the main page, which consists of three files: <code>index.html</code>, <code>index.css</code>, and <code>index.js</code>.</p>

<p><strong>Note</strong>: <em>I didn’t include other pages of this project as it’d make this tutorial too verbose. However, you can refactor the other pages as an experiment.</em></p>

<p>The <code>index.css</code> file is over 450 lines of code, and skimming through it, I can see some red flags right off the bat:</p>

<ul>
<li>There’s a lot of code repetition with the same selectors pointing to the same HTML element.</li>
<li>There are quite a few <code>#id</code> selectors, which one might argue shouldn’t be used in CSS (and I am one of those people).</li>
<li><code>#botLogo</code> is defined twice and over 70 lines apart.</li>
<li>The <code>!important</code> keyword is used liberally throughout the code.</li>
</ul>

<p>And yet the site works. There is nothing “technically” wrong here, which is another reason CSS is a big, beautiful monster &mdash; errors are silent!</p>

<h2 id="planning-the-layer-structure">Planning The Layer Structure</h2>

<p>Now, some might be thinking, <em>“Can’t we simply move all of the styles into a single layer, like <code>@layer legacy</code> and call it a day?”</em></p>

<p>You could… but I don’t think you should.</p>

<p>Think about it: If more layers are added after the <code>legacy</code> layer, they <em>should</em> override the styles contained in the <code>legacy</code> layer because the specificity of layers is organized by priority, where the layers declared later carry higher priority.</p>

<pre><code class="language-css">/&#42; new is more specific &#42;/
@layer legacy, new;

/&#42; legacy is more specific &#42;/
@layer new, legacy;
</code></pre>

<p>That said, we must remember that the site’s existing styles make liberal use of the <code>!important</code> keyword. And when that happens, the order of cascade layers gets reversed. So, even though the layers are outlined like this:</p>

<pre><code class="language-css">@layer legacy, new;
</code></pre>

<p>…any styles with an <code>!important</code> declaration suddenly shake things up. In this case, the priority order becomes:</p>

<ol>
<li><code>!important</code> styles in the <code>legacy</code> layer (most powerful),</li>
<li><code>!important</code> styles in the <code>new</code> layer,</li>
<li>Normal styles in the <code>new</code> layer,</li>
<li>Normal styles in the <code>legacy</code> layer (least powerful).</li>
</ol>

<p>I just wanted to clear that part up. Let’s continue.</p>

<p>We know that cascade layers handle specificity by creating an explicit order where each layer has a clear responsibility, and later layers always win.</p>

<p>So, I decided to split things up into five distinct layers:</p>

<ul>
<li><strong><code>reset</code></strong>: Browser default resets like <code>box-sizing</code>, margins, and paddings.</li>
<li><strong><code>base</code></strong>: Default styles of HTML elements, like <code>body</code>, <code>h1</code>, <code>p</code>, <code>a</code>, etc., including default typography and colours.</li>
<li><strong><code>layout</code></strong>: Major page structure stuff for controlling how elements are positioned.</li>
<li><strong><code>components</code></strong>: Reusable UI segments, like buttons, cards, and menus.</li>
<li><strong><code>utilities</code></strong>: Single helper modifiers that do just one thing and do it well.</li>
</ul>

<p>This is merely how I like to break things out and organize styles. Zell Liew, for example, <a href="https://css-tricks.com/composition-in-css/">has a different set of four buckets</a> that could be defined as layers.</p>

<p>There’s also the concept of dividing things up even further into <strong>sublayers</strong>:</p>

<pre><code class="language-css">@layer components {
  /&#42; sub-layers &#42;/
  @layer buttons, cards, menus;
}

/&#42; or this: &#42;/
@layer components.buttons, components.cards, components.menus;
</code></pre>

<p>That might come in handy, but I also don’t want to overly abstract things. That might be a better strategy for a project that’s scoped to a well-defined design system.</p>

<p>Another thing we could leverage is <strong>unlayered styles</strong> and the fact that any styles not contained in a cascade layer get the highest priority:</p>

<pre><code class="language-css">@layer legacy { a { color: red !important; } }
@layer reset { a { color: orange !important; } }
@layer base { a { color: yellow !important; } }

/&#42; unlayered &#42;/
a { color: green !important; } /&#42; highest priority &#42;/
</code></pre>

<p>But I like the idea of keeping all styles organized in explicit layers because it keeps things <strong>modular</strong> and <strong>maintainable</strong>, at least in this context.</p>

<p>Let’s move on to adding cascade layers to this project.</p>

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

<h2 id="integrating-cascade-layers">Integrating Cascade Layers</h2>

<p>We need to define the layer order at the top of the file:</p>

<pre><code class="language-css">@layer reset, base, layout, components, utilities;
</code></pre>

<p>This makes it easy to tell which layer takes precedence over which (they get more priority from left to right), and now we can think in terms of layer responsibility instead of selector weight. Moving forward, I’ll proceed through the stylesheet from top to bottom.</p>

<p>First, I noticed that the <a href="https://fonts.google.com/specimen/Poppins?query=poppins">Poppins font</a> was imported in both the HTML and CSS files, so I removed the CSS import and left the one in <code>index.html</code>, as that’s generally recommended for quickly loading fonts.</p>

<p>Next is the universal selector (<code>*</code>) styles, which include <a href="https://css-tricks.com/box-sizing/">classic reset styles</a> that are perfect for <code>@layer reset</code>:</p>

<pre><code class="language-css">@layer reset {
  &#42; {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}
</code></pre>

<p>With that out of the way, the <code>body</code> selector is next. I’m putting this into <code>@layer base</code> because it contains core styles for the project, like backgrounds and fonts:</p>

<div class="break-out">
<pre><code class="language-css">@layer base {
  body {
    background-image: url("bg.svg"); /&#42; Renamed to bg.svg for clarity &#42;/
    font-family: "Poppins", sans-serif;
    /&#42; ... other styles &#42;/
  }
}
</code></pre>
</div>

<p>The way I’m tackling this is that styles in the <code>base</code> layer should generally affect the whole document. So far, no page breaks or anything.</p>

<h3 id="swapping-ids-for-classes">Swapping IDs For Classes</h3>

<p>Following the <code>body</code> element selector is the page loader, which is defined as an ID selector, <code>#loader</code>.</p>

<blockquote>I’m a firm believer in using class selectors over ID selectors as much as possible. It keeps specificity low by default, which prevents specificity battles and <a href="https://css-tricks.com/the-difference-between-id-and-class/">makes the code a lot more maintainable</a>.</blockquote>

<p>So, I went into the <code>index.html</code> file and refactored elements with <code>id=&quot;loader&quot;</code> to <code>class=&quot;loader&quot;</code>. In the process, I saw another element with <code>id=&quot;page&quot;</code> and changed that at the same time.</p>

<p>While still in the <code>index.html</code> file, I noticed a few <code>div</code> elements missing closing tags. It is astounding how permissive browsers are with that. Anyways, I cleaned those up and moved the <code>&lt;script&gt;</code> tag out of the <code>.heading</code> element to be a direct child of <code>body</code>. Let’s not make it any tougher to load our scripts.</p>

<p>Now that we’ve levelled the specificity playing field by moving IDs to classes, we can drop them into the <code>components</code> layer since a loader is indeed a reusable component:</p>

<pre><code class="language-css">@layer components {
  .loader {
    width: 100%;
    height: 100vh;
    /&#42; ... &#42;/
  }
  .loader .loading {
    /&#42; ... &#42;/
  }
  .loader .loading span {
    /&#42; ... &#42;/
  }
  .loader .loading span:before {
    /&#42; ... &#42;/
  }
}
</code></pre>

<h3 id="animations">Animations</h3>

<p>Next are keyframes, and this was a bit tricky, but I eventually chose to isolate animations in their own new fifth layer and updated the layer order to include it:</p>

<div class="break-out">
<pre><code class="language-css">@layer reset, base, layout, components, utilities, animations;
</code></pre>
</div>

<p>But why place <code>animations</code> as the last layer? Because animations are generally the last to run and shouldn’t be affected by style conflicts.</p>

<p>I searched the project’s styles for <code>@keyframes</code> and dumped them into the new layer:</p>

<pre><code class="language-css">@layer animations {
  @keyframes loading {
    /&#42; ... &#42;/
  }
  @keyframes loading2 {
    /&#42; ... &#42;/
  }
  @keyframes pageShow {
    /&#42; ... &#42;/
  }
}
</code></pre>

<p>This gives a clear distinction of static styles from dynamic ones while also enforcing reusability.</p>

<h3 id="layouts">Layouts</h3>

<p>The <code>#page</code> selector also has the same issue as <code>#id</code>, and since we fixed it in the HTML earlier, we can modify it to <code>.page</code> and drop it in the <code>layout</code> layer, as its main purpose is to control the initial visibility of the content:</p>

<pre><code class="language-css">@layer layout {
  .page {
    display: none;
  }
}
</code></pre>

<h3 id="custom-scrollbars">Custom Scrollbars</h3>

<p>Where do we put these? Scrollbars are global elements that persist across the site. This might be a gray area, but I’d say it fits perfectly in <code>@layer base</code> since it’s a global, default feature.</p>

<pre><code class="language-css">@layer base {
  /&#42; ... &#42;/
  ::-webkit-scrollbar {
    width: 8px;
  }
  ::-webkit-scrollbar-track {
    background: &#35;0e0e0f;
  }
  ::-webkit-scrollbar-thumb {
    background: &#35;5865f2;
    border-radius: 100px;
  }
  ::-webkit-scrollbar-thumb:hover {
    background: &#35;202225;
  }
}
</code></pre>

<p>I also removed the <code>!important</code> keywords as I came across them.</p>

<h3 id="navigation">Navigation</h3>

<p>The <code>nav</code> element is pretty straightforward, as it is the main structure container that defines the position and dimensions of the navigation bar. It should definitely go in the <code>layout</code> layer:</p>

<pre><code class="language-css">@layer layout {
  /&#42; ... &#42;/
  nav {
    display: flex;
    height: 55px;
    width: 100%;
    padding: 0 50px; /&#42; Consistent horizontal padding &#42;/
    /&#42; ... &#42;/
  }
}
</code></pre>

<h3 id="logo">Logo</h3>

<p>We have three style blocks that are tied to the logo: <code>nav .logo</code>, <code>.logo img</code>, and <code>#botLogo</code>. These names are redundant and could benefit from inheritance component reusability.</p>

<p>Here’s how I’m approaching it:</p>

<ol>
<li>The <code>nav .logo</code> is overly specific since the logo can be reused in other places. I dropped the <code>nav</code> so that the selector is just <code>.logo</code>. There was also an <code>!important</code> keyword in there, so I removed it.</li>
<li>I updated <code>.logo</code> to be a Flexbox container to help position <code>.logo img</code>, which was previously set with less flexible absolute positioning.</li>
<li>The <code>#botLogo</code> ID is declared twice, so I merged the two rulesets into one and lowered its specificity by making it a <code>.botLogo</code> class. And, of course, I updated the HTML to replace the ID with the class.</li>
<li>The <code>.logo img</code> selector becomes <code>.botLogo</code>, making it the base class for styling all instances of the logo.</li>
</ol>

<p>Now, we’re left with this:</p>

<pre><code class="language-css">/&#42; initially .logo img &#42;/
.botLogo {
  border-radius: 50%;
  height: 40px;
  border: 2px solid &#35;5865f2;
}

/&#42; initially &#35;botLogo &#42;/
.botLogo {
  border-radius: 50%;
  width: 180px;
  /&#42; ... &#42;/
}
</code></pre>

<p>The difference is that one is used in the navigation and the other in the hero section heading. We can transform the second <code>.botLogo</code> by slightly increasing the specificity with a <code>.heading .botLogo</code> selector. We may as well clean up any duplicated styles as we go.</p>

<p>Let’s place the entire code in the <code>components</code> layer as we’ve successfully turned the logo into a reusable component:</p>

<div class="break-out">
<pre><code class="language-css">@layer components {
  /&#42; ... &#42;/
  .logo {
    font-size: 30px;
    font-weight: bold;
    color: &#35;fff;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .botLogo {
    aspect-ratio: 1; /&#42; maintains square dimensions with width &#42;/
    border-radius: 50%;
    width: 40px;
    border: 2px solid &#35;5865f2;
  }
  .heading .botLogo {
    width: 180px;
    height: 180px;
    background-color: &#35;5865f2;
    box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5);
    /&#42; ... &#42;/
  }
}
</code></pre>
</div>

<p>This was a bit of work! But now the logo is properly set up as a component that fits perfectly in the new layer architecture.</p>

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

<h3 id="navigation-list">Navigation List</h3>

<p>This is a typical navigation pattern. Take an unordered list (<code>&lt;ul&gt;</code>) and turn it into a flexible container that displays all of the list items horizontally on the same row (with wrapping allowed). It’s a type of navigation that can be reused, which belongs in the <code>components</code> layer. But there’s a little refactoring to do before we add it.</p>

<p>There’s already a <code>.mainMenu</code> class, so let’s lean into that. We’ll swap out any <code>nav ul</code> selectors with that class. Again, it keeps specificity low while making it clearer what that element does.</p>

<pre><code class="language-css">@layer components {
  /&#42; ... &#42;/
  .mainMenu {
    display: flex;
    flex-wrap: wrap;
    list-style: none;
  }
  .mainMenu li {
    margin: 0 4px;
  }
  .mainMenu li a {
    color: &#35;fff;
    text-decoration: none;
    font-size: 16px;
    /&#42; ... &#42;/
  }
  .mainMenu li a:where(.active, .hover) {
    color: &#35;fff;
    background: &#35;1d1e21;
  }
  .mainMenu li a.active:hover {
    background-color: &#35;5865f2;
  }
}
</code></pre>

<p>There are also two buttons in the code that are used to toggle the navigation between “open” and “closed” states when the navigation is collapsed on smaller screens. It’s tied specifically to the <code>.mainMenu</code> component, so we’ll keep everything together in the <code>components</code> layer. We can combine and simplify the selectors in the process for cleaner, more readable styles:</p>

<pre><code class="language-css">@layer components {
  /&#42; ... &#42;/
  nav:is(.openMenu, .closeMenu) {
    font-size: 25px;
    display: none;
    cursor: pointer;
    color: &#35;fff;
  }
}
</code></pre>

<p>I also noticed that several other selectors in the CSS were not used anywhere in the HTML. So, I removed those styles to keep things trim. There are <a href="https://css-tricks.com/how-do-you-remove-unused-css-from-a-site/">automated ways to go about this</a>, too.</p>

<h3 id="media-queries">Media Queries</h3>

<p>Should media queries have a dedicated layer (<code>@layer responsive</code>), or should they be in the same layer as their target elements? I really struggled with that question while refactoring the styles for this project. I did some research and testing, and my verdict is the latter, that <strong>media queries ought to be in the same layer as the elements they affect</strong>.</p>

<p>My reasoning is that keeping them together:</p>

<ul>
<li>Maintains responsive styles with their base element styles,</li>
<li>Makes overrides predictable, and</li>
<li>Flows well with component-based architecture common in modern web development.</li>
</ul>

<p>However, it also means <strong>responsive logic</strong> is scattered across layers. But it beats the one with a gap between the layer where elements are styled and the layer where their responsive behaviors are managed. That’s a deal-breaker for me because it’s way too easy to update styles in one layer and forget to update their corresponding responsive style in the responsive layer.</p>

<p>The other big point is that media queries in the same layer have <strong>the same priority</strong> as their elements. This is consistent with my overall goal of keeping the CSS Cascade simple and predictable, free of style conflicts.</p>

<p>Plus, the <a href="https://css-tricks.com/tag/nesting/">CSS nesting syntax</a> makes the relationship between media queries and elements super clear. Here’s an abbreviated example of how things look when we nest media queries in the <code>components</code> layer:</p>

<pre><code class="language-css">@layer components {
  .mainMenu {
    display: flex;
    flex-wrap: wrap;
    list-style: none;
  }
  @media (max-width: 900px) {
    .mainMenu {
      width: 100%;
      text-align: center;
      height: 100vh;
      display: none;
    }
  }
}
</code></pre>

<p>This also allows me to nest a component’s child element styles (e.g., <code>nav .openMenu</code> and <code>nav .closeMenu</code>).</p>

<pre><code class="language-css">@layer components {
  nav {
    &.openMenu {
      display: none;
      
      @media (max-width: 900px) {
        &.openMenu {
          display: block;
        }
      }
    }
  }
}
</code></pre>

<h3 id="typography-buttons">Typography &amp; Buttons</h3>

<p>The <code>.title</code> and <code>.subtitle</code> can be seen as typography components, so they and their responsive associates go into &mdash; you guessed it &mdash; the <code>components</code> layer:</p>

<pre><code class="language-css">@layer components {
  .title {
    font-size: 40px;
    font-weight: 700;
    /&#42; etc. &#42;/
  }
  .subtitle {
    color: rgba(255, 255, 255, 0.75);
    font-size: 15px;
    /&#42; etc.. &#42;/
  }
  @media (max-width: 420px) {
    .title {
      font-size: 30px;
    }
    .subtitle {
      font-size: 12px;
    }
  }
}
</code></pre>

<p>What about buttons? Like many website’s this one has a class, <code>.btn</code>, for that component, so we can chuck those in there as well:</p>

<pre><code class="language-css">@layer components {
  .btn {
    color: &#35;fff;
    background-color: #1d1e21;
    font-size: 18px;
    /&#42; etc. &#42;/
  }
  .btn-primary {
    background-color: &#35;5865f2;
  }
  .btn-secondary {
    transition: all 0.3s ease-in-out;
  }
  .btn-primary:hover {
    background-color: &#35;5865f2;
    box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5);
    /&#42; etc. &#42;/
  }
  .btn-secondary:hover {
    background-color: &#35;1d1e21;
    background-color: rgba(88, 101, 242, 0.7);
  }
  @media (max-width: 420px) {
    .btn {
      font-size: 14px;
      margin: 2px;
      padding: 8px 13px;
    }
  }
  @media (max-width: 335px) {
    .btn {
      display: flex;
      flex-direction: column;
    }
  }
}
</code></pre>

<h3 id="the-final-layer">The Final Layer</h3>

<p>We haven’t touched the <code>utilities</code> layer yet! I’ve reserved this layer for helper classes that are designed for specific purposes, like hiding content &mdash; or, in this case, there’s a <code>.noselect</code> class that fits right in. It has a single reusable purpose: to disable selection on an element.</p>

<p>So, that’s going to be the only style rule in our <code>utilities</code> layer:</p>

<pre><code class="language-css">@layer utilities {
  .noselect {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -webkit-user-drag: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
}
</code></pre>
 

<p>And that’s it! We’ve completely refactored the CSS of a real-world project to use CSS Cascade Layers. You can compare <a href="https://codepen.io/vayospot/pen/bNdoYdP">where we started</a> with the <a href="https://codepen.io/vayospot/pen/XJbeVdB">final code</a>.</p>

<h2 id="it-wasn-t-all-easy">It Wasn’t All Easy</h2>

<p>That’s not to say that working with Cascade Layers was challenging, but there were some sticky points in the process that forced me to pause and carefully think through what I was doing.</p>

<p>I kept some notes as I worked:</p>

<ul>
<li><strong>It’s tough to determine where to start with an existing project.</strong><br />
However, by defining the layers first and setting their priority levels, I had a framework for deciding how and where to move specific styles, even though I was not totally familiar with the existing CSS. That helped me avoid situations where I might second-guess myself or define extra, unnecessary layers.</li>
<li><strong>Browser support is still a thing!</strong><br />
I mean, Cascade Layers enjoy 94% support coverage as I’m writing this, but you might be one of those sites that needs to accommodate legacy browsers that are unable to support layered styles.</li>
<li><strong>It wasn’t clear where media queries fit into the process.</strong><br />
Media queries put me on the spot to find where they work best: nested in the same layers as their selectors, or in a completely separate layer? I went with the former, as you know.</li>
<li><strong>The <code>!important</code> keyword is a juggling act.</strong><br />
They invert the entire layering priority system, and this project was littered with instances. Once you start chipping away at those, the existing CSS architecture erodes and requires a balance between refactoring the code and fixing what’s already there to know exactly how styles cascade.</li>
</ul>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aOverall,%20refactoring%20a%20codebase%20for%20CSS%20Cascade%20Layers%20is%20a%20bit%20daunting%20at%20first%20glance.%20The%20important%20thing,%20though,%20is%20to%20acknowledge%20that%20it%20isn%e2%80%99t%20really%20the%20layers%20that%20complicate%20things,%20but%20the%20existing%20codebase.%0a&url=https://smashingmagazine.com%2f2025%2f09%2fintegrating-css-cascade-layers-existing-project%2f">
      
Overall, refactoring a codebase for CSS Cascade Layers is a bit daunting at first glance. The important thing, though, is to acknowledge that it isn’t really the layers that complicate things, but the existing codebase.

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

<p>It’s tough to completely overhaul someone’s existing approach for a new one, even if the new approach is elegant.</p>

<h2 id="where-cascade-layers-helped-and-didn-t">Where Cascade Layers Helped (And Didn’t)</h2>

<p>Establishing layers improved the code, no doubt. I’m sure there are some <strong>performance benchmarks</strong> in there since we were able to remove unused and conflicting styles, but the real win is in <strong>a more maintainable set of styles</strong>. It’s easier to find what you need, know what specific style rules are doing, and where to insert new styles moving forward.</p>

<p>At the same time, I wouldn’t say that Cascade Layers are a silver bullet solution. Remember, CSS is intrinsically tied to the HTML structure it queries. If the HTML you’re working with is unstructured and suffers from <code>div</code>-itus, then you can safely bet that the effort to untangle that mess is higher and involves rewriting markup at the same time.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aRefactoring%20CSS%20for%20cascade%20layers%20is%20most%20certainly%20worth%20the%20maintenance%20enhancements%20alone.%0a&url=https://smashingmagazine.com%2f2025%2f09%2fintegrating-css-cascade-layers-existing-project%2f">
      
Refactoring CSS for cascade layers is most certainly worth the maintenance enhancements alone.

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

<p>It may be “easier” to start from scratch and define layers as you work from the ground up because there’s less inherited overhead and technical debt to sort through. But if you have to start from an existing codebase, you might need to de-tangle the complexity of your styles first to determine exactly how much refactoring you’re looking at.</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>Gabriel Shoyombo</author><title>CSS Intelligence: Speculating On The Future Of A Smarter Language</title><link>https://www.smashingmagazine.com/2025/07/css-intelligence-speculating-future-smarter-language/</link><pubDate>Wed, 02 Jul 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/07/css-intelligence-speculating-future-smarter-language/</guid><description>CSS has evolved from a purely presentational language into one with growing logical powers — thanks to features like container queries, relational pseudo-classes, and the &lt;code>if()&lt;/code> function. Is it still just for styling, or is it becoming something more? Gabriel Shoyombo explores how smart CSS has become over the years, where it is heading, the challenges it addresses, whether it is becoming too complex, and how developers are reacting to this shift.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/07/css-intelligence-speculating-future-smarter-language/" />
              <title>CSS Intelligence: Speculating On The Future Of A Smarter Language</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>CSS Intelligence: Speculating On The Future Of A Smarter Language</h1>
                  
                    
                    <address>Gabriel Shoyombo</address>
                  
                  <time datetime="2025-07-02T13:00:00&#43;00:00" class="op-published">2025-07-02T13:00:00+00:00</time>
                  <time datetime="2025-07-02T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Once upon a time, CSS was purely presentational. It imperatively handled the fonts, colors, backgrounds, spacing, and layouts, among other styles, for markup languages. It was a <strong>language for looks</strong>, doing what it was asked to, never thinking or making decisions. At least, that was what it was made for when <a href="https://www.w3.org/People/howcome/">Håkon Wium Lie proposed CSS in 1994</a>, and the World Wide Web Consortium (W3C) adopted it two years later.</p>

<p>Fast-forward to today, a lot has changed with the addition of new features, and more are on the way that shift the style language to a more imperative paradigm. CSS now actively powers complex responsive and interactive user interfaces. With recent advancements like <a href="https://www.smashingmagazine.com/2021/05/complete-guide-css-container-queries/">container queries</a>, <a href="https://www.smashingmagazine.com/2021/06/has-native-css-parent-selector/">relational pseudo-classes</a>, and <a href="https://www.w3.org/TR/css-values-5/#if-notation">the <code>if()</code> function</a>, the language once within the <strong>domains of presentations</strong> has stepped foot into the <strong>territory of logic</strong>, reducing its reliance on the language that had handled its logical aspect to date, JavaScript.</p>

<p>This shift presents interesting questions about CSS and its future for developers. CSS has deliberately remained within the domains of styling alone for a while now, but is it time for that to change? Also, is CSS still a <strong>presentational language</strong> as it started, or is it becoming something more and bigger? This article explores how smart CSS has become over the years, where it is heading, the problems it is solving, whether it is getting too complex, and how developers are reacting to this shift.</p>

<h2 id="historical-context-css-s-intentional-simplicity">Historical Context: CSS’s Intentional Simplicity</h2>

<p>A glimpse into CSS history shows a language born to separate content from presentation, making web pages easier to manage and maintain. The first official version of CSS, <a href="https://www.w3.org/TR/CSS1/">CSS1</a>, was released in 1996, and it introduced basic styling capabilities like font properties, colors, box model (padding, margin, and border), sizes (width and height), a few simple displays (none, block, and inline), and basic selectors.</p>

<p>Two years later, <a href="https://www.w3.org/TR/CSS2/">CSS2 was launched</a> and expanded what CSS could style in HTML with features like positioning, <code>z-index</code>, enhanced selectors, table layouts, and media types for different devices. However, there were inconsistencies within the style language, an issue CSS2.1 resolved in 2011, becoming the standard for modern CSS. It simplified web authoring and site maintenance.</p>

<p>CSS was largely <strong>static</strong> and <strong>declarative</strong> during the years between CSS1 and CSS2.1. Developers experienced a mix of frustrations and breakthroughs for their projects. Due to the absence of intuitive layouts like Flexbox and CSS Grid, developers relied on hacky alternatives with table layouts, positioning, or floats to get around complex designs, even though <a href="https://www.w3.org/TR/CSS1/#floating-elements">floats were originally designed for text to wrap around an obstacle</a> on a webpage, usually a media object. As a result, developers faced issues with collapsing containers and unexpected wrapping behaviour. Notwithstanding, basic styling was intuitive. A newbie could easily pick up web development today and add basic styling the next day. CSS was separated from content and logic, and as a result, it was <strong>highly performant</strong> and <strong>lightweight</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="css3-the-first-step-toward-context-awareness">CSS3: The First Step Toward Context Awareness</h2>

<p>Things changed <a href="https://www.smashingmagazine.com/2012/07/learning-css3-useful-reference-guide/">when CSS3 rolled out</a>. Developers had expected a single monolithic update like the previous versions, but their expectations and the reality of the latest release were unmatched. The CSS3 red carpet revealed a <strong>modular system</strong> with powerful layout tools like Flexbox, CSS Grid, and media queries, defining for the first time how developers establish responsive designs. <a href="https://www.w3.org/Style/CSS/current-work">With over 20 modules</a>, CSS3 marked the inception of a <strong>“smarter CSS”</strong>.</p>

<p>Flexbox’s introduction around 2012 provided a flexible, one-dimensional layout system, while CSS Grid, launched in 2017, took layout a step further by offering a two-dimensional layout framework, making complex designs with minimal code possible. These advancements, as discussed by <a href="https://origin-blog.mediatemple.net/design-creative/five-huge-css-milestones/">Chris Coyier</a>, reduced reliance on hacks like floats.</p>

<p>It did not stop there. There’s <a href="https://www.w3.org/TR/mediaqueries-3/">media queries</a>, a prominent release of CSS3, that is one of the major contributors to this <em>smart CSS</em>. With media queries, CSS can react to different devices’ screens, adjusting its styles to fit the screen dimensions, aspect ratio, and orientation, a feat that earlier versions could not easily achieve. In the fifth level, it added <a href="https://www.w3.org/TR/mediaqueries-5/#mf-user-preferences">user preference media features</a> such as <code>prefers-color-scheme</code> and <code>prefers-reduced-motion</code>, making CSS more <strong>user-centric</strong> by adapting styles to user settings, <strong>enhancing accessibility</strong>.</p>

<p>CSS3 marked the beginning of a <strong>context-aware CSS</strong>.</p>

<blockquote>Context-awareness means the ability to understand and react to the situation around you or in your environment accordingly. It means systems and devices can sense critical information, like your location, time of day, and activity, and adjust accordingly.</blockquote>

<p>In web development, the term “context-awareness” has always been used with components, but what drives a context-aware component? If you mentioned anything other than the component’s styles, you would be wrong! For a component to be considered context-aware, <a href="https://www.lukeleber.com/blog/2024-07-25-context-aware-components">it needs to feel its environment’s presence</a> and know what happens in it. For instance, for your website to update its styles to accommodate a dark mode interface, it needs to be aware of the user’s preferences. Also, to change its layout, a website needs to know the device a user is accessing it on &mdash; and thanks to user preference media queries, that is possible.</p>

<p>Despite these features, CSS remained largely reactive. It responded to external factors like screen size (via media queries) or input states (like <code>:hover</code>, <code>:focus</code>, or <code>:checked</code>), but it never made decisions based on the changes in its environment. Developers typically turn to JavaScript for that level of interaction.</p>

<p>However, not anymore.</p>

<p>For example, with container queries and, more recently, <a href="https://www.smashingmagazine.com/2024/06/what-are-css-container-style-queries-good-for/">container <em>style</em> queries</a>, CSS now responds not only to layout constraints but to <strong>design intent</strong>. It can adjust based on a component’s environment and even its parent’s theme or state. And that’s not all. The recently specced <code>if()</code> function promises <strong>inline conditional logic</strong>, <a href="https://css-tricks.com/if-css-gets-inline-conditionals/">allowing styles to change based on conditions</a>, all of which can be achieved without scripting.</p>

<p>These developments suggest CSS is moving beyond presentation to handle behaviour, challenging its traditional role.</p>

<h2 id="new-css-features-driving-intelligence">New CSS Features Driving Intelligence</h2>

<p>Several features are currently pushing CSS towards a dynamic and adaptive edge, thereby making it smarter, but these two are worth mentioning: container style queries and the <code>if()</code> function.</p>

<h3 id="what-are-container-style-queries-and-why-do-they-matter">What Are Container Style Queries, And Why Do They Matter?</h3>

<p>To better understand what container style queries are, it makes sense to make a quick stop at a close cousin: container size queries introduced in the <a href="https://www.w3.org/TR/css-contain-3/">CSS Containment Module Level 3</a>.</p>

<p><a href="https://www.smashingmagazine.com/2021/05/complete-guide-css-container-queries/">Container size queries</a> allow developers to style elements based on the dimensions of their parent container. This is a huge win for component-based designs as it eliminates the need to shoehorn responsive styles into global media queries.</p>

<pre><code class="language-css">/&#42; Size-based container query &#42;/
@container (min-width: 500px) {
  .card {
    flex-direction: row;
  }
}
</code></pre>

<p><a href="https://css-tricks.com/css-container-queries/#aa-container-style-queries">Container style queries</a> take it a step further by allowing you to style elements based on custom properties (aka CSS variables) set on the container.</p>

<pre><code class="language-css">/&#42; Style-based container query &#42;/
@container style(--theme: dark) {
  .button {
    background: black;
    color: white;
  }
}
</code></pre>

<p>These features are a big deal in CSS because they unlock <strong>context-aware components</strong>. A button can change appearance based on a <code>--theme</code> property set by a parent without using JavaScript or hardcoded classes.</p>

<h3 id="the-if-function-a-glimpse-into-the-future">The <code>if()</code> Function: A Glimpse Into The Future</h3>

<p>The CSS <code>if()</code> function might just be the most radical shift yet. When implemented (Chrome is the only one to support it, <a href="https://developer.chrome.com/blog/new-in-chrome-137?hl=en#if">as of version 137</a>), it would allow developers to write inline conditional logic directly in property declarations. Think of the <em>ternary operator</em> in CSS.</p>

<pre><code class="language-css">padding: if(style(--theme: dark): 2rem; else: 3rem);
</code></pre>

<p>This hypothetical line or pseudo code, <em>not syntax</em>, sets the text color to white <em>if</em> the <code>--theme</code> variable equals <code>dark</code>, or black otherwise. Right now, the <code>if()</code> function is not supported in any browser, but it is on the radar of the CSS Working Group, and influential developers like <a href="https://lea.verou.me/blog/2024/css-conditionals/">Lea Verou</a> are already exploring its possibilities.</p>

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

<h2 id="the-new-css-is-the-boundary-between-css-and-javascript-blurring">The New CSS: Is The Boundary Between CSS And JavaScript Blurring?</h2>

<p>Traditionally, the separation of concerns concerning styling was thus: <a href="https://medium.com/@giosterr44/mastering-the-basics-why-html-css-javascript-are-still-essential-c0343ab485b4">CSS for how things look and JavaScript for how things behave</a>. However, features like container style queries and the specced <code>if()</code> function are starting to blur the line. CSS is beginning to <em>behave</em>, not in the sense of API calls or event listeners, but in the ability to conditionally apply styles based on logic or context.</p>

<p>As web development evolved, CSS started encroaching on JavaScript territory. CSS3 brought in animations and transitions, a powerful combination for interactive web development, which was impossible without JavaScript in the earlier days. Today, research proves that CSS has taken on several interactive tasks previously handled by JavaScript. For example, the <code>:hover</code> pseudo-class and <code>transition</code> property allow for visual feedback and smooth animations, as discussed in “<a href="https://www.smashingmagazine.com/2011/02/bringing-interactivity-to-your-website-with-web-standards/">Bringing Interactivity To Your Website With Web Standards</a>”.</p>

<p>That’s not all. Toggling accordions and modals existed within the domains of JavaScript before, but today, this is possible with new <a href="https://pagepro.co/blog/html-css-vs-javascript/">powerful CSS combos like the <code>&lt;details&gt;</code> and <code>&lt;summary&gt;</code> HTML tags for accordions or modals with the <code>:target</code> pseudo-class</a>. CSS can also handle tooltips using <code>aria-label</code> with <code>content: attr(aria-label)</code>, and star ratings with radio inputs and labels, as detailed in the same <a href="https://pagepro.co/blog/html-css-vs-javascript/">article</a>.</p>

<p>Another article, “<a href="https://blog.logrocket.com/5-things-you-can-do-with-css-instead-of-javascript/">5 things you can do with CSS instead of JavaScript</a>”, lists features like <code>scroll-behavior: smooth</code> for smooth scrolling and <code>@media (prefers-color-scheme: dark)</code> for dark mode, tasks that once required JavaScript. In the same article, you can also see that it’s possible to create a carousel without JavaScript by using the CSS scroll snapping functionality (and we’re not even talking about features designed specifically for creating carousels solely in CSS, recently <a href="https://developer.chrome.com/blog/carousels-with-css?hl=en">prototyped in Chrome</a>).</p>

<p>These extensions of CSS into the JavaScript domain have now left the latter with handling only complex, crucial interactions in a web application, such as user inputs, making API calls, and managing state. While the CSS pseudo-classes like <code>:valid</code> and <code>:invalid</code> can help as error or success indicators in input elements, you still need JavaScript for dynamic content updates, form validation, and real-time data fetching.</p>

<p>CSS now solves problems that many developers never knew existed. With JavaScript out of the way in many style scenarios, developers now have simplified codebases. The dependencies are fewer, the overheads are lower, and website performance is better, especially on mobile devices. In fact, this shift leans CSS towards a <strong>more accessible web</strong>, as CSS-driven designs are often easier for browsers and assistive technologies to process.</p>

<p>While the new features come with a lot of benefits, they also introduce complexities that did not exist before:</p>

<ul>
<li>What happens when logic is spread across both CSS and JavaScript?</li>
<li>How do we <strong>debug conditional styles</strong> without a clear view of what triggered them?</li>
<li>CSS only had to deal with basic styling like colors, fonts, layouts, and spacing, which were easier for new developers to onboard. How hard does the <strong>learning curve</strong> become as these new features require understanding concepts once exclusive to JavaScript?</li>
</ul>

<p>Developers are split. While some welcome the idea of a natural evolution of a smarter, more component-aware web, <a href="https://css-tricks.com/is-there-too-much-css-now/">others worry CSS is becoming too complex</a> &mdash; a language originally designed for formatting documents now juggling logic trees and style computation.</p>

<h2 id="divided-perspective-is-logic-in-css-helpful-or-harmful">Divided Perspective: Is Logic In CSS Helpful Or Harmful?</h2>

<p>While the evidence in the previous section leans towards boundary-blurring, there’s significant <strong>controversy among developers</strong>. Many modern developers argue that logic in CSS is long overdue. As web development grows more componentized, the limitations of declarative styling have become more apparent, causing proponents to see logic as a necessary evolution for a once purely styling language.</p>

<p>For instance, in frontend libraries like React, components often require conditional styles based on props or states. Developers have had to make do with JavaScript or CSS-in-JS solutions for such cases, but the truth remains that these solutions are not right. They introduce complexity and couple styles and logic. CSS and JavaScript are meant to have standalone concerns in web development, <a href="https://css-tricks.com/the-differing-perspectives-on-css-in-js/">but libraries like CSS-in-JS have ignored the rules and combined both</a>.</p>

<p>We have seen how preprocessors like SASS and LESS proved the usefulness of conditionals, loops, and variables in styling. Developers who do not accept the CSS in JavaScript approach have settled for these preprocessors. Nevertheless, like <a href="https://x.com/argyleink/status/1317304102460608512?t=rgyYyNApPOZt8iqh8NTEUQ&amp;s=19">Adam Argyle</a>, they voice their need for native CSS solutions. With native conditionals, developers could reduce JavaScript overhead and avoid runtime class toggling to achieve conditional presentation.</p>

<blockquote>“It never felt right to me to manipulate style settings in JavaScript when CSS is the right tool for the job. With CSS custom properties, we can send to CSS what needs to come from JavaScript.”<br /><br />&mdash; <a href="https://x.com/codepo8/status/1358082931122724864">Chris Heilmann</a></blockquote>

<p>Also, Bob Ziroll <a href="https://x.com/bobziroll/status/1819078139055595669">dislikes using JavaScript for what CSS is meant to handle</a> and finds it unnecessary. This reflects a preference for using CSS for styling tasks, even when JavaScript is involved. These developers embrace CSS’s new capabilities, seeing it as a way to reduce JavaScript dependency for performance reasons.</p>

<p>Others argue against it. Introducing logic into CSS is a slippery slope, and CSS could lose its core strengths &mdash; simplicity, readability, and accessibility &mdash; by becoming too much like a programming language. The fear is that developers run the risk of <a href="https://www.smashingmagazine.com/2024/02/web-development-getting-too-complex/">complicating the web more than it is supposed to be</a>.</p>

<blockquote>“I’m old-fashioned. I like my CSS separated from my HTML; my HTML separated from my JS; my JS separated from my CSS.”<br /><br />&mdash; <a href="https://x.com/SaraSoueidan/status/1273181281103351812">Sara Soueidan</a></blockquote>

<p>This view emphasises the traditional separation of concerns, arguing that mixing roles can complicate maintenance. Additionally, Brad Frost has also <a href="https://x.com/brad_frost/status/993189025132490755">expressed skepticism</a> when talking specifically about CSS-in-JS, stating that it, <em>“doesn’t scale to non-JS-framework environments, adds more noise to an already-noisy JS file, and the demos/examples I have seen haven’t embodied CSS best practices.”</em> This highlights concerns about scalability and best practices, suggesting that <strong>the blurred boundary might not always be beneficial</strong>.</p>

<p>Community discussions, such as on <a href="https://stackoverflow.com/questions/24012569/is-it-always-better-to-use-css-when-possible-instead-of-js">Stack Overflow</a>, also reflect this divide. A question like <em>“Is it always better to use CSS when possible instead of JS?”</em> receives answers favouring CSS for performance and simplicity, but others argue JavaScript is necessary for complex scenarios, illustrating the ongoing debate. Don’t be fooled. It might seem convenient to agree that CSS performs better than JavaScript in styling, <a href="https://css-tricks.com/myth-busting-css-animations-vs-javascript/">but that’s not always the case</a>.</p>

<h2 id="a-smarter-css-without-losing-its-soul">A Smarter CSS Without Losing Its Soul</h2>

<p>CSS has always stood apart from full-blown programming languages, like JavaScript, by being declarative, accessible, and purpose-driven.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIf%20CSS%20is%20to%20grow%20more%20intelligent,%20the%20challenge%20lies%20not%20in%20making%20it%20more%20powerful%20for%20its%20own%20sake%20but%20in%20evolving%20it%20without%20compromising%20its%20major%20concern.%0a&url=https://smashingmagazine.com%2f2025%2f07%2fcss-intelligence-speculating-future-smarter-language%2f">
      
If CSS is to grow more intelligent, the challenge lies not in making it more powerful for its own sake but in evolving it without compromising its major concern.

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

<p>So, what might a logically enriched but <em>still declarative</em> CSS look like? Let’s find out.</p>

<h3 id="conditional-rules-if-when-else-with-carefully-introduced-logic">Conditional Rules (<code>if</code>, <code>@when</code>…<code>@else</code>) With Carefully Introduced Logic</h3>

<p>A major frontier in CSS evolution is the introduction of native conditionals via the <a href="https://chromestatus.com/feature/6313805904347136"><code>if()</code></a> function and the <code>@when</code>…<code>@else</code> at-rules, which are part of the <a href="https://drafts.csswg.org/css-conditional-5/">CSS Conditional Rules Module Level 5</a> specification. While still in the early draft stages, this would allow developers to apply styles based on evaluated conditions without turning to JavaScript or a preprocessor. Unlike JavaScript’s imperative nature, these conditionals aim to keep logic ingrained in CSS’s existing flow, aligned with the cascade and specificity.</p>

<h3 id="more-powerful-intentional-selectors">More Powerful, Intentional Selectors</h3>

<p>Selectors have always been one of the major strengths of CSS, and expanding them in a targeted way would make it easier to express relationships and conditions declaratively without needing classes or scripts. Currently, <a href="https://css-tricks.com/the-css-has-selector/"><code>:has()</code></a> lets developers style a parent based on a child, and <code>:nth-child(An+B [of S]?)</code> (<a href="https://www.w3.org/TR/selectors-4/">in Selectors Level 4</a>) allows for more complex matching patterns. Together, they allow greater precision without altering CSS’s nature.</p>

<h3 id="scoped-styling-without-javascript">Scoped Styling Without JavaScript</h3>

<p>One of the challenges developers face in component-based frameworks like React or Vue is style scoping. Style scoping ensures styles apply only to specific elements or components and do not leak out. In the past, to achieve this, you needed to implement BEM naming conventions, CSS-in-JS, or build tools like CSS Modules. Native scoped styling in CSS, via the new experimental <a href="https://css-tricks.com/almanac/rules/s/scope/"><code>@scope</code></a> rule, allows developers to encapsulate styles in a specific context without extra tooling. This feature makes CSS more modular without tying it to JavaScript logic or complex class systems.</p>

<p>A fundamental design question now is whether we could empower CSS without making it like JavaScript. The truth is, to empower CSS with conditional logic, powerful selectors, and scoped rules, we don’t need it to mirror JavaScript’s syntax or complexity. The goal is declarative expressiveness, giving CSS more awareness and control while retaining its clear, readable nature, and we should focus on that. When done right, smarter CSS can amplify the language’s strengths rather than dilute them.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20real%20danger%20is%20not%20logic%20itself%20but%20unchecked%20complexity%20that%20obscures%20the%20simplicity%20with%20which%20CSS%20was%20built.%0a&url=https://smashingmagazine.com%2f2025%2f07%2fcss-intelligence-speculating-future-smarter-language%2f">
      
The real danger is not logic itself but unchecked complexity that obscures the simplicity with which CSS was built.

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

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

<h2 id="cautions-and-constraints-why-smart-isn-t-always-better">Cautions And Constraints: Why Smart Isn’t Always Better</h2>

<p>The push for a smarter CSS comes with significant trade-offs alongside control and flexibility. Over the years, <a href="https://www.quora.com/In-general-does-adding-features-to-a-programming-language-make-it-better">history has shown that adding a new feature to a language or framework, or library, most likely introduces complexity</a>, not just for newbies, but also for expert developers. The danger is not in CSS gaining power but in how that power is implemented, taught, and used.</p>

<p>One of CSS’s greatest strengths has always been its <strong>approachability</strong>. Designers and beginners could learn the basics quickly: selectors, properties, and values. With more logic, scoping, and advanced selectors being introduced, that learning curve steepens. The risk is a widening gap between “basic CSS” and “real-world CSS”, echoing what happened with JavaScript and its ecosystem.</p>

<p>As CSS becomes more powerful, developers increasingly lean on tooling to manage and abstract that power, like building systems (e.g., webpack, Vite), linters and formatters, and component libraries with strict styling conventions. This creates dependencies that are hard to escape. <strong>Tooling becomes a prerequisite, not an option</strong>, further complicating onboarding and increasing setup time for projects that used to work with a single stylesheet.</p>

<p>Also, more logic means more potential for <strong>unexpected outcomes</strong>. New issues might arise that are harder to spot and fix. Resources like DevTools will then need to evolve to visualise scope boundaries, conditional applications, and complex selector chains. Until then, debugging may remain a challenge. <a href="https://robkendal.co.uk/blog/why-is-css-in-js-a-bad-or-good-idea/">All of these are challenges experienced with CSS-in-JS</a>; how much more Native CSS?</p>

<p>We’ve seen this before. CSS history is filled with overcomplicated workarounds, like tables for the layout before Flexbox, relying on floats with clear fix hacks, and overly rigid grid systems before native CSS Grid. In each case, the hacky solution eventually became the problem. CSS got better not by mimicking other languages but by <em>standardising thoughtful, declarative solutions</em>. With the right power, <a href="https://rachelandrew.co.uk/archives/2020/04/07/making-things-better/">we can make CSS better</a> at the end of the day.</p>

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

<p>We just took a walk down the history lane of CSS, explored its presence, and peeked into what its future could be. We can all agree that CSS has come a long way from a simple, declarative language to a <strong>dynamic</strong>, <strong>context-aware</strong>, and, yes, <strong>smarter language</strong>. The evolution, of course, comes with tension: a smarter styling language with fewer dependencies on scripts and a complex one with a steeper learning curve.</p>

<p>This is what I conclude:</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20future%20of%20CSS%20shouldn%e2%80%99t%20be%20a%20race%20to%20add%20logic%20for%20its%20own%20sake.%20Instead,%20it%20should%20be%20a%20thoughtful%20expansion,%20power%20balanced%20by%20clarity%20and%20innovation%20grounded%20in%20accessibility.%0a&url=https://smashingmagazine.com%2f2025%2f07%2fcss-intelligence-speculating-future-smarter-language%2f">
      
The future of CSS shouldn’t be a race to add logic for its own sake. Instead, it should be a thoughtful expansion, power balanced by clarity and innovation grounded in accessibility.

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

<p>That means asking tough questions before shipping new features. It means ensuring that new capabilities help solve actual problems without introducing new barriers.</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>Myriam Frisano</author><title>Decoding The SVG &lt;code>path&lt;/code> Element: Curve And Arc Commands</title><link>https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-curve-arc-commands/</link><pubDate>Mon, 23 Jun 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-curve-arc-commands/</guid><description>On her quest to teach you how to code vectors by hand, Myriam Frisano’s second installment of a &lt;code>path&lt;/code> deep dive explores the most complex aspects of SVG’s most powerful element. She’ll help you understand the underlying rules and function of how curves and arcs are constructed. By the end of it, your toolkit is ready to tackle all types of tasks required to draw with code — even if some of the lines twist and turn.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-curve-arc-commands/" />
              <title>Decoding The SVG &lt;code&gt;path&lt;/code&gt; Element: Curve And Arc Commands</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Decoding The SVG &lt;code&gt;path&lt;/code&gt; Element: Curve And Arc Commands</h1>
                  
                    
                    <address>Myriam Frisano</address>
                  
                  <time datetime="2025-06-23T10:00:00&#43;00:00" class="op-published">2025-06-23T10:00:00+00:00</time>
                  <time datetime="2025-06-23T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>In the <a href="https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-line-commands/">first part of decoding the SVG <code>path</code> pair</a>, we mostly dealt with converting things from semantic tags (<code>line</code>, <code>polyline</code>, <code>polygon</code>) into the <code>path</code> command syntax, but the <code>path</code> element didn’t really offer us any new shape options. This will change in this article as we’re learning how to draw <strong>curves</strong> and <strong>arcs</strong>, which just refer to parts of an ellipse.</p>

<h2 id="tl-dr-on-previous-articles">TL;DR On Previous Articles</h2>

<p>If this is your first meeting with this series, I recommend you familiarize yourself with the <a href="https://www.smashingmagazine.com/2024/09/svg-coding-examples-recipes-writing-vectors-by-hand/">basics of hand-coding SVG</a>, as well as <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/marker">how the <code>&lt;marker&gt;</code> works</a> and have a <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/animate">basic understanding of animate</a>, as this guide doesn’t explain them. I also recommend knowing about the <code>M/m</code> command within the <code>&lt;path&gt;</code> <code>d</code> attribute (I wrote the aforementioned <a href="https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-line-commands/">article on path line commands</a> to help).</p>

<p><strong>Note</strong>: <em>This article will solely focus on the syntax of curve and arc commands and not offer an introduction to <code>path</code> as an element.</em></p>

<p>Before we get started, I want to do a quick recap of how I code SVG, which is by using JavaScript. I don’t like dealing with numbers and math, and reading SVG code that has numbers filled into every attribute makes me lose all understanding of it. By giving coordinates names and having all my math easy to parse and all written out, I have a much better time with this type of code, and I think you will, too.</p>

<p>As the goal of this article is about understanding <code>path</code> syntax and not about doing placement or how to leverage loops and other more basic things, I will not run you through the entire setup of each example. I’ll share some snippets of the code, but please note that it may be slightly adjusted from the CodePen or simplified to make the article easier to read. However, if there are specific questions about code not part of the text that’s in the CodePen demos &mdash; the comment section is open, as always.</p>

<p>To keep this all framework-agnostic, the code is written in vanilla JavaScript, though, in practice, TypeScript comes highly recommended when dealing with complex images.</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="drawing-bézier-curves">Drawing Bézier Curves</h2>

<p>Being able to draw lines, polygons, polylines, and compounded versions of them is all fun and nice, but <code>path</code> can also do more than just offer more cryptic implementations of basic semantic SVG tags.</p>

<p>One of those additional types is Bézier curves.</p>

<p>There are multiple different curve commands. And this is where the idea of points and control points comes in.</p>

<blockquote><strong>Bézier math plotting is out of scope for this article.</strong><br />But, there is a visually gorgeous video by Freya Holmér called <a href="https://youtu.be/aVwxzDHniEw?si=WB_3i88VVJlZS6jf">The Beauty of Bézier Curves</a> which gets into the construction of cubic and quadratic bézier curves that features beautiful animation and the math becomes a lot easier to digest.</blockquote>

<p>Luckily, SVG allows us to draw quadratic curves with one control point and cubic curves with two control points without having to do any additional math.</p>

<p>So, what is a control point? A control point is the position of the handle that controls the curve. It is not a point that is drawn.</p>

<p>I found the best way to understand these path commands is to render them like a GUI, like Affinity and Illustrator would. Then, draw the “handles” and draw a few random curves with different properties, and see how they affect the curve. Seeing that animation also really helps to see the mechanics of these commands.</p>

<p>This is what I’ll be using markers and animation for in the following visuals. You will notice that the markers I use are rectangles and circles, and since they are connected to lines, I can make use of <code>marker</code> and then save myself a lot of animation time because these additional elements are rigged to the system. (And animating a single <code>d</code> command instead of <code>x</code> and <code>y</code> attributes separately makes the SVG code also much shorter.)</p>

<h3 id="quadratic-bézier-curves-q-t-commands">Quadratic Bézier Curves: <code>Q</code> &amp; <code>T</code> Commands</h3>

<p>The <code>Q</code> command is used to draw quadratic béziers. It takes two arguments: the control point and the end point.</p>

<p>So, for a simple curve, we would start with <code>M</code> to move to the start point, then <code>Q</code> to draw the curve.</p>

<div class="break-out">
<pre><code class="language-javascript">const path = `M${start.x} ${start.y} Q${control.x} ${control.y} ${end.x} ${end.y}`;
</code></pre>
</div>

<p>Since we have the Control Point, the Start Point, and the End Point, it’s actually quite simple to render the singular handle path like a graphics program would.</p>

<p>Funny enough, you probably have never interacted with a quadratic Bézier curve like with a cubic one in most common GUIs! Most of the common programs will convert this curve to a cubic curve with two handles and control points as soon as you want to play with it.</p>

<p>For the drawing, I created a couple of markers, and I’m drawing the handle in red to make it stand out a bit better.</p>

<p>I also stroked the main <code>path</code> with a gradient and gave it a crosshatch pattern fill. (We looked at <code>pattern</code> in <a href="https://www.smashingmagazine.com/2025/06/decoding-svg-path-element-line-commands/">my first article</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient"><code>linearGradient</code></a> is fairly similar. They’re both <code>def</code> elements you can refer to via <code>id</code>.) I like seeing the fill, but if you find it distracting, you can modify the variable for it.</p>

<p>I encourage you to look at the example with and without the rendering of the handle to see some of the nuance that happens around the points as the control points get closer to them.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="LEVXLoJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Quadratic Bézier Curve Visual [forked]](https://codepen.io/smashingmag/pen/LEVXLoJ) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/LEVXLoJ">SVG Path Quadratic Bézier Curve Visual [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<blockquote><strong>Quadratic Béziers are the “less-bendy” ones.</strong><br />These curves always remain somewhat related to “u” or “n” shapes and can’t be manipulated to be contorted. They can be squished, though.</blockquote>

<p>Connected Bézier curves are called “Splines”. And there is an additional command when chaining multiple quadratic curves, which is the <code>T</code> command.</p>

<p>The <code>T</code> command is used to draw a curve that is connected to the previous curve, so it always has to follow a <code>Q</code> command (or another <code>T</code> command). It only takes one argument, which is the endpoint of the curve.</p>

<div class="break-out">
<pre><code class="language-javascript">const path = `M${p1.x} ${p1.y} Q${cP.x} ${cP.y} ${p2.x} ${p2.y} T${p3.x} ${p3.y}`
</code></pre>
</div>

<p>The <code>T</code> command will actually use information about our control Point <code>cP</code> within the <code>Q</code> command.</p>

<p>To see how I created the following example. Notice that the inferred handles are drawn in green, while our specified controls are still rendered in red.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="vEOQJBM"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Quadratic Curve T Command [forked]](https://codepen.io/smashingmag/pen/vEOQJBM) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vEOQJBM">SVG Path Quadratic Curve T Command [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<p>OK, so the top curve takes two <code>Q</code> commands, which means, in total, there are three control points. Using a separate control point to create the scallop makes sense, but the third control point is just a reflection of the second control point through the preceding point.</p>

<p>This is what the <code>T</code> command does. It infers control points by reflecting them through the end point of the preceding <code>Q</code> (or <code>T</code>) command. You can see how the system all links up in the animation below, where all I’ve manipulated is the position of the main points and the first control points. The inferred control points follow along.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="WbvYENx"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Quadratic Bézier Spline T Command Visual [forked]](https://codepen.io/smashingmag/pen/WbvYENx) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/WbvYENx">SVG Path Quadratic Bézier Spline T Command Visual [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<p>The <code>q</code> and <code>t</code> commands also exist, so they will use relative coordinates.</p>

<p>Before I go on, if you do want to interact with a cubic curve, <a href="https://yqnn.github.io/svg-path-editor/">SVG Path Editor</a> allows you to edit all path commands very nicely.</p>

<h3 id="cubic-bézier-curves-c-and-s">Cubic Bézier Curves: <code>C</code> And <code>S</code></h3>

<p>Cubic Bézier curves work basically like quadratic ones, but instead of having one control point, they have two. This is probably the curve you are most familiar with.</p>

<p>The order is that you start with the first control point, then the second, and then the end point.</p>

<div class="break-out">
<pre><code class="language-javascript">const path = `M${p1.x} ${p1.y} C${cP1.x} ${cP1.y} ${cP2.x} ${cP2.y} ${p2.x} ${p2.y}`;
</code></pre>
</div>

<p>Let’s look at a visual to see it in action.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="EajOvaL"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Cubic Bézier Curve Animation [forked]](https://codepen.io/smashingmag/pen/EajOvaL) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/EajOvaL">SVG Path Cubic Bézier Curve Animation [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<blockquote><strong>Cubic Bézier curves are contortionists.</strong><br />Unlike the quadratic curve, this one can curl up and form loops and take on completely different shapes than any other SVG element. It can split the filled area into two parts, while the quadratic curve can not.</blockquote>

<p>Just like with the <code>T</code> command, a reflecting command is available for cubic curves <code>S</code>.</p>

<p>When using it, we get the first control point through the reflection, while we can define the new end control point and then the end point. Like before, this requires a spline, so at least one preceding <code>C</code> (or <code>S</code>) command.</p>

<pre><code class="language-javascript">const path = `    
  M ${p0.x} ${p0.y}
  C ${c0.x} ${c0.y} ${c1.x} ${c1.y} ${p1.x} ${p1.y}
  S ${c2.x} ${c2.y} ${p2.x} ${p2.y}
`;
</code></pre>

<p>I created a living visual for that as well.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="RNPqZPz"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Cubic Bézier Spline S Command Visual [forked]](https://codepen.io/smashingmag/pen/RNPqZPz) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/RNPqZPz">SVG Path Cubic Bézier Spline S Command Visual [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<blockquote><strong>When to use <code>T</code> and <code>S</code>:</strong><br />The big advantage of using these chaining reflecting commands is if you want to draw waves or just absolutely ensure that your spline connection is smooth.</blockquote>

<p>If you can’t use a reflection but want to have a nice, smooth connection, make sure your control points form a straight line. If you have a kink in the handles, your spline will get one, too.</p>

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

<h2 id="arcs-a-command">Arcs: <code>A</code> Command</h2>

<p>Finally, the last type of <code>path</code> command is to create arcs. Arcs are sections of circles or ellipses.</p>

<p>It’s my least favorite command because there are so many elements to it. But it is the secret to drawing a proper donut chart, so I have a bit of time spent with it under my belt.</p>

<p>Let’s look at it.</p>

<p>Like with any other <code>path</code> command, lowercase implies relative coordinates. So, just as there is an <code>A</code> command, there’s also an <code>a</code>.</p>

<p>So, an arc path looks like this:</p>

<div class="break-out">
<pre><code class="language-javascript">const path = `M${start.x} ${start.y} A${radius.x} ${radius.y} ${xAxisRotation} ${largeArcFlag} ${sweepFlag} ${end.x} ${end.y}`;
</code></pre>
</div>

<p>And what the heck are <code>xAxisRotation</code>, <code>largeArcFlag</code>, and <code>sweepFlag</code> supposed to be? In short:</p>

<ul>
<li><code>xAxisRotation</code> is the rotation of the underlying ellipse’s axes in degrees.</li>
<li><code>largeArcFlag</code> is a boolean value that determines if the arc is greater than 180°.</li>
<li><code>sweepFlag</code> is also a boolean and determines the arc direction, so does it go clockwise or counter-clockwise?</li>
</ul>

<p>To better understand these concepts, I created this visual.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="GgJwvZR"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [SVG Path Arc Command Visuals [forked]](https://codepen.io/smashingmag/pen/GgJwvZR) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgJwvZR">SVG Path Arc Command Visuals [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<h3 id="radius-size">Radius Size</h3>

<p>You’ll notice in that CodePen that there are ellipses drawn for each command. In the top row, they are overlapping, while in the bottom row, they are stacked up. Both rows actually use the same <code>radius.x</code>  and <code>radius.y</code> values in their arc definitions, while the distance between the start and end points increases for the second row.</p>

<p>The reason why the stacking happens is that the radius size is only taken into consideration if the start and end points fit within the specified ellipse. That behavior surprised me, and thus, I dug into the specs and found the following information on how the arc works:</p>

<blockquote>“Arbitrary numerical values are permitted for all elliptical arc parameters (other than the boolean flags), but user agents must make the following adjustments for invalid values when rendering curves or calculating their geometry:<br /><br />If the endpoint (<strong>x</strong>, <strong>y</strong>) of the segment is identical to the current point (e.g., the endpoint of the previous segment), then this is equivalent to omitting the elliptical arc segment entirely.<br /><br />If either <strong>rx</strong> or <strong>ry</strong> is 0, then this arc is treated as a straight line segment (a “lineto”) joining the endpoints.<br /><br />If either <strong>rx</strong> or <strong>ry</strong> have negative signs, these are dropped; the absolute value is used instead.<br /><br />If <strong>rx</strong>, <strong>ry</strong> and <strong>x-axis-rotation</strong> are such that there is no solution (basically, the ellipse is not big enough to reach from the current point to the new endpoint) then the ellipse is scaled up uniformly until there is exactly one solution (until the ellipse is just big enough).<br /><br />See the appendix section <a href="https://svgwg.org/svg2-draft/implnote.html#ArcCorrectionOutOfRangeRadii">Correction of out-of-range radii</a> for the mathematical formula for this scaling operation.”<br /><br />&mdash; <a href="https://svgwg.org/svg2-draft/paths.html#ArcOutOfRangeParameters">9.5.1 Out-of-range elliptical arc parameters</a></blockquote>

<p>So, really, that stacking is just nice and graceful error-handling and not how it was intended. Because the top row is how arcs should be used.</p>

<blockquote>When plugging in logical values, the underlying ellipses and the two points give us four drawing options for how we could connect the two points along an elliptical path. That’s what the boolean values are for.</blockquote>

<h3 id="xaxisrotation"><code>xAxisRotation</code></h3>

<p>Before we get to the booleans, the crosshatch pattern shows the <code>xAxisrotation</code>. The ellipse is rotated around its center, with the degree value being in relation to the x-direction of the SVG.</p>

<p>So, if you work with a circular ellipse, the rotation won’t have any effect on the arc (except if you use it in a pattern like I did there).</p>

<h3 id="sweep-flag">Sweep Flag</h3>

<p>Notice the little arrow marker to show the arc drawing direction. If the value is 0, the arc is drawn clockwise. If the value is 1, the arc is drawn counterclockwise.</p>

<h3 id="large-arc-flag">Large Arc Flag</h3>

<p>The large Arc Flag tells the path if you want the smaller or the larger arc from the ellipse. If we have a scaled case, we get exactly 180° of our ellipse.</p>

<blockquote>Arcs usually require a lot more annoying circular number-wrangling than I am happy doing (As soon as radians come to play, I tend to spiral into rabbit holes where I have to relearn too much math I happily forget.)<br /><br />They are more reliant on values being related to each other for the outcome to be as expected and there’s just so much information going in.<br /><br />But &mdash; and that’s a bit but &mdash; arcs are wonderfully powerful!</blockquote>

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

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

<p>Alright, that was a lot! However, I do hope that you are starting to see how <code>path</code> commands can be helpful. I find them extremely useful to illustrate data.</p>

<p>Once you know how easy it is to set up stuff like grids, boxes, and curves, it doesn’t take many more steps to create visualizations that are a bit more unique than what the standard data visualization libraries offer.</p>

<blockquote>With everything you’ve learned in this series of articles, you’re basically fully equipped to render all different types of charts &mdash; or other types of visualizations.</blockquote>

<p>Like, how about visualizing the underlying cubic-bezier of something like <code>transition-timing-function: ease;</code> in CSS? That’s the thing I made to figure out how I could turn those transition-timing-functions into something an <code>&lt;animate&gt;</code> tag understands.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="gbpQxgp"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS Cubic Beziers as SVG Animations &amp; CSS Transition Comparisons [forked]](https://codepen.io/smashingmag/pen/gbpQxgp) by <a href="https://codepen.io/halfapx">Myriam</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/gbpQxgp">CSS Cubic Beziers as SVG Animations &amp; CSS Transition Comparisons [forked]</a> by <a href="https://codepen.io/halfapx">Myriam</a>.</figcaption>
</figure>

<p>SVG is fun and quirky, and the <code>path</code> element may be the holder of the most overwhelming string of symbols you’ve ever laid eyes on during code inspection. However, if you take the time to understand the underlying logic, it all transforms into one beautifully simple and extremely powerful syntax.</p>

<p>I hope with this pair of <code>path</code> decoding articles, I managed to expose the underlying mechanics of how path plots work. If you want even more resources that don’t require you to dive through specs, try the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths">MDN tutorial about paths</a>. It’s short and compact, and was the main resource for me to learn all of this.</p>

<p>However, since I wrote my deep dive on the topic, I stumbled into the beautiful <a href="https://svg-tutorial.com">svg-tutorial.com</a>, which does a wonderful job visualizing SVG coding as a whole but mostly features my favorite arc visual of them all in the <a href="https://svg-tutorial.com/editor/arc">Arc Editor</a>. And if you have a path that you’d like properly decoded without having to store all of the information in these two articles, there’s <a href="https://svg-path-visualizer.netlify.app/">SVG Path Visualizer</a>, which breaks down path information super nicely.</p>

<p>And now: Go forth and have fun playing in the matrix.</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></channel></rss>