<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Techniques on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/techniques/index.xml</link><description>Recent content in Techniques 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>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>Mat Marquis</author><title>JavaScript For Everyone: Iterators</title><link>https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/</link><pubDate>Mon, 27 Oct 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/</guid><description>Here is a lesson on Iterators: iterables implement the iterable iteration interface, and iterators implement the iterator iteration interface. Sounds confusing? Mat breaks it all down in the article.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/" />
              <title>JavaScript For Everyone: Iterators</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>JavaScript For Everyone: Iterators</h1>
                  
                    
                    <address>Mat Marquis</address>
                  
                  <time datetime="2025-10-27T13:00:00&#43;00:00" class="op-published">2025-10-27T13:00:00+00:00</time>
                  <time datetime="2025-10-27T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Hey, I’m Mat, but “Wilto” works too &mdash; I’m here to teach you JavaScript. Well, not <em>here</em>-here; technically, I’m over at <a href="https://piccalil.li/javascript-for-everyone">Piccalil.li’s <em>JavaScript for Everyone</em></a> course to teach you JavaScript. The following is an excerpt from the <strong>Iterables and Iterators</strong> module: the lesson on Iterators.</p>

<p>Iterators are one of JavaScript’s more linguistically confusing topics, sailing <em>easily</em> over what is already a pretty high bar. There are <em>iterables</em> &mdash; array, Set, Map, and string &mdash; all of which follow the <strong>iterable protocol</strong>. To follow said protocol, an object must implement the <strong>iterable interface</strong>. In practice, that means that the object needs to include a <code>[Symbol.iterator]()</code> method somewhere in its prototype chain. Iterable protocol is one of two <strong>iteration protocols</strong>. The other iteration protocol is the <strong>iterator protocol</strong>.</p>

<p>See what I mean about this being linguistically fraught? Iterables implement the iterable iteration interface, and iterators implement the iterator iteration interface! If you can say that five times fast, then you’ve pretty much got the gist of it; easy-peasy, right?</p>

<p>No, listen, by the time you reach the end of this lesson, I promise it won’t be half as confusing as it might sound, especially with the context you’ll have from the lessons that precede it.</p>

<p>An <strong>iterable</strong> object follows the iterable protocol, which just means that the object has a conventional method for making iterators. The elements that it contains can be looped over with <code>for</code>…<code>of</code>.</p>

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

<p>To <em>reiterate</em> &mdash; a play on words for which I do not forgive myself, nor expect you to forgive me &mdash; an <strong>iterator</strong> object follows iterator protocol, and the elements it contains can be accessed <em>sequentially</em>, one at a time. Iterator protocol defines a standard way to produce a sequence of values, and optionally <code>return</code> a value once all possible values have been generated.</p>

<p>In order to follow the iterator protocol, an object has to &mdash; you guessed it &mdash; implement the <strong>iterator interface</strong>. In practice, that once again means that a certain method has to be available somewhere on the object&rsquo;s prototype chain. In this case, it’s the <code>next()</code> method that advances through the elements it contains, one at a time, and returns an object each time that method is called.</p>

<p>In order to meet the iterator interface criteria, the returned object must contain two properties with specific keys: one with the key <code>value</code>, representing the value of the current element, and one with the key <code>done</code>, a Boolean value that tells us if the iterator has advanced beyond the final element in the data structure. That’s not an awkward phrasing the editorial team let slip through: the value of that <code>done</code> property is <code>true</code> only when a call to <code>next()</code> results in an attempt to access an element <em>beyond</em> the final element in the iterator, not upon accessing the final element in the iterator. Again, a lot in print, but it’ll make more sense when you see it in action.</p>

<p>You’ve seen an example of a built-in iterator before, albeit briefly:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "aKey", "A value." ] ]);

console.log( theMap.keys() );
// Result: Map Iterator { constructor: Iterator() }
</code></pre>

<p>That’s right: while a Map object itself is an iterable, Map’s built-in methods <code>keys()</code>, <code>values()</code>, and <code>entries()</code> all return Iterator objects. You’ll also remember that I looped through those using <code>forEach</code> (a relatively recent addition to the language). Used that way, an iterator is indistinguishable from an iterable:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys().forEach( thing =&gt; {
  console.log( thing );
});
// Result: key
</code></pre>

<p>All iterators are iterable; they all implement the iterable interface:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys()[ Symbol.iterator ];
// Result: function Symbol.iterator()
</code></pre>

<p>And if you’re angry about the increasing blurriness of the line between iterators and iterables, wait until you get a load of this “top ten anime betrayals” video candidate: I’m going to demonstrate how to interact with an iterator by using an array.</p>

<p>“BOO,” you surely cry, having been so betrayed by one of your oldest and most indexed friends. “Array is an itera<em>ble</em>, not an itera<em>tor</em>!” You are both right to yell at me in general, and right about array in specific &mdash; an array <em>is</em> an iterable, not an iterator. In fact, while all iterators are iterable, none of the built-in iterables are iterators.</p>

<p>However, when you call that <code>[ Symbol.iterator ]()</code> method &mdash; the one that defines an object as an iterable &mdash; it returns an iterator object created from an iterable data structure:</p>

<pre><code class="language-jsx">const theIterable = [ true, false ];
const theIterator = theIterable[ Symbol.iterator ]();

theIterable;
// Result: Array [ true, false ]

theIterator;
// Result: Array Iterator { constructor: Iterator() }
</code></pre>

<p>The same goes for Set, Map, and &mdash; yes &mdash; even strings:</p>

<pre><code class="language-jsx">const theIterable = "A string."
const theIterator = theIterable[ Symbol.iterator ]();

theIterator;
// Result: String Iterator { constructor: Iterator() }
</code></pre>

<p>What we’re doing here manually &mdash; creating an iterator from an iterable using <code>%Symbol.iterator%</code> &mdash; is precisely how iterable objects work internally, and why they have to implement <code>%Symbol.iterator%</code> in order to <em>be</em> iterables. Any time you loop through an array, you’re actually looping through an iterator created from that Array. All built-in iterators <em>are</em> iterable. All built-in iterables can be used to <em>create</em> iterators.</p>

<p>Alternately &mdash; <em>preferably</em>, even, since it doesn’t require you to graze up against <code>%Symbol.iterator%</code> directly &mdash; you can use the built-in <code>Iterator.from()</code> method to create an iterator object from any iterable:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ true, false ]);

theIterator;
// Result: Array Iterator { constructor: Iterator() }
</code></pre>

<p>You remember how I mentioned that an iterator has to provide a <code>next()</code> method (that returns a very specific Object)? Calling that <code>next()</code> method steps through the elements that the iterator contains one at a time, with each call returning an instance of that Object:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ 1, 2, 3 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: 3, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }
</code></pre>

<p>You can think of this as a more controlled form of traversal than the traditional “wind it up and watch it go” <code>for</code> loops you’re probably used to &mdash; a method of accessing elements one step at a time, as-needed. Granted, you don’t <em>have</em> to step through an iterator in this way, since they have their very own <code>Iterator.forEach</code> method, which works exactly like you would expect &mdash; to a point:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ true, false ]);

theIterator.forEach( element =&gt; console.log( element ) );
/&#42; Result:
true
false
&#42;/
</code></pre>

<p>But there’s another big difference between iterables and iterators that we haven’t touched on yet, and for my money, it actually goes a long way toward making <em>linguistic</em> sense of the two. You might need to humor me for a little bit here, though.</p>

<p>See, an iterable object is an object that is iterable. No, listen, stay with me: you can iterate over an Array, and when you’re done doing so, you can still iterate over that Array. It is, by definition, an object that can be iterated over; it is the essential nature of an iterable to be iterable:</p>

<pre><code class="language-jsx">const theIterable = [ 1, 2 ];

theIterable.forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
1
2
&#42;/

theIterable.forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
1
2
&#42;/
</code></pre>

<p>In a way, an iterator object represents the singular <em>act</em> of iteration. Internal to an iterable, it is the mechanism by which the iterable is iterated over, each time that iteration is performed. As a stand-alone iterator object &mdash; whether you step through it using the <code>next</code> method or loop over its elements using <code>forEach</code> &mdash; once iterated over, that iterator is <em>past tense</em>; it is <em>iterated</em>. Because they maintain an internal state, the essential nature of an iterator is to be iterated over, singular:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ 1, 2 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }

theIterator.forEach( el =&gt; console.log( el ) );
// Result: undefined
</code></pre>

<p>That makes for neat work when you&rsquo;re using the Iterator constructor’s built-in methods to, say, filter or extract part of an Iterator object:</p>

<div class="break-out">
<pre><code class="language-jsx">const theIterator = Iterator.from([ "First", "Second", "Third" ]);

// Take the first two values from `theIterator`:
theIterator.take( 2 ).forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
"First"
"Second"
&#42;/

// theIterator now only contains anything left over after the above operation is complete:
theIterator.next();
// Result: Object { value: "Third", done: false }
</code></pre>
</div>

<p>Once you reach the end of an iterator, the act of iterating over it is complete. Iterated. Past-tense.</p>

<p>And so too is your time in this lesson, you might be relieved to hear. I know this was kind of a rough one, but the good news is: this course is iterable, not an iterator. This step in your iteration through it &mdash; this lesson &mdash; may be over, but the essential nature of this course is that you can iterate through it again. Don’t worry about committing all of this to memory right now &mdash; you can come back and revisit this lesson anytime.</p>

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

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

<p>I stand by what I wrote there, unsurprising as that probably is: this lesson is a tricky one, but listen, <em>you got this</em>. <a href="https://piccalil.li/javascript-for-everyone">JavaScript for Everyone</a> is designed to take you inside JavaScript’s head. Once you’ve started seeing how the gears mesh &mdash; seen the fingerprints left behind by the people who built the language, and the good, bad, and sometimes baffling decisions that went into that &mdash; no <em>itera-</em>, whether <em>-ble</em> or <em>-tor</em> will be able to stand in your way.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://piccalil.li/javascript-for-everyone">
    
    <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/javascript-for-everyone-iterators/1-javascript-for-everyone.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png"
			
			sizes="100vw"
			alt="Javascript for everyone course announcement"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <a href='https://piccalil.li/javascript-for-everyone'>JavaScript for Everyone</a> is now available and the launch price runs until midnight, October 28. Save £60 off the full price of £249 and get it for £189! (<a href='https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>My goal is to teach you the <em>deep magic</em> &mdash; the <em>how</em> and the <em>why</em> of JavaScript, using the syntaxes you’re most likely to encounter in your day-to-day work, at your pace and on your terms. If you’re new to the language, you’ll walk away from this course with a foundational understanding of JavaScript worth hundreds of hours of trial-and-error. If you’re a junior developer, you’ll finish this course with a depth of knowledge to rival any senior.</p>

<p>I hope to see you there.</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>Paul Boag</author><title>Functional Personas With AI: A Lean, Practical Workflow</title><link>https://www.smashingmagazine.com/2025/09/functional-personas-ai-lean-practical-workflow/</link><pubDate>Tue, 16 Sep 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/09/functional-personas-ai-lean-practical-workflow/</guid><description>For too long, personas have been something that many of us just created, despite the considerable work that goes into them, only to find they have limited usefulness. Paul Boag shows how to breathe new life into this stale UX asset and demonstrates that it’s possible to create truly useful functional personas in a lightweight way.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/09/functional-personas-ai-lean-practical-workflow/" />
              <title>Functional Personas With AI: A Lean, Practical Workflow</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Functional Personas With AI: A Lean, Practical Workflow</h1>
                  
                    
                    <address>Paul Boag</address>
                  
                  <time datetime="2025-09-16T08:00:00&#43;00:00" class="op-published">2025-09-16T08:00:00+00:00</time>
                  <time datetime="2025-09-16T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Traditional personas suck for UX work. They obsess over marketing metrics like age, income, and job titles while missing what actually matters in design: what people are trying to accomplish.</p>

<p><a href="https://boagworld.com/usability/personas/">Functional personas</a>, on the other hand, focus on what people are trying to do, not who they are on paper. With a simple AI‑assisted workflow, you can build and maintain personas that actually guide design, content, and conversion decisions.</p>

<ul>
<li>Keep users front of mind with task‑driven personas,</li>
<li>Skip fragile demographics; center on goals, questions, and blockers,</li>
<li>Use AI to process your messy inputs fast and fill research gaps,</li>
<li>Validate lightly, ship confidently, and keep them updated.</li>
</ul>

<p>In this article, I want to breathe new life into a stale UX asset.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="483"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png"
			
			sizes="100vw"
			alt="Traditional demographic personas"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Traditional demographic personas look good but quickly get outdated, need constant updating, and rarely offer practical UX guidance. (<a href='https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/traditional-demographic-personas.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>For too long, personas have been something that many of us just created, despite the considerable work that goes into them, only to find they have limited usefulness.</p>

<p>I know that many of you may have given up on them entirely, but I am hoping in this post to encourage you that it is possible to create truly useful personas in a lightweight way.</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-personas-still-matter">Why Personas Still Matter</h2>

<p>Personas give you a shared lens. When everyone uses the same reference point, you cut debate and make better calls. For UX designers, developers, and digital teams, that shared lens keeps you from designing in silos and helps you prioritize work that genuinely improves the experience.</p>

<p>I use personas as a quick test: <em>Would this change help this user complete their task faster, with fewer doubts?</em> If the answer is no (or a shrug), it’s probably a sign the idea isn’t worth pursuing.</p>

<h2 id="from-demographics-to-function">From Demographics To Function</h2>

<p>Traditional personas tell you someone’s age, job title, or favorite brand. That makes a nice poster, but it rarely changes design or copy.</p>

<p><strong>Functional personas flip the script.</strong> They describe:</p>

<ul>
<li><strong>Goals &amp; tasks:</strong> What the person is here to achieve.</li>
<li><strong>Questions &amp; objections:</strong> What they need to know before they act.</li>
<li><strong>Touchpoints:</strong> How the person interacts with the organization.</li>
<li><strong>Service gaps:</strong> How the company might be letting this persona down.</li>
</ul>

<p>When you center on tasks and friction, you get direct lines from user needs to UI decisions, content, and conversion paths.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="2354"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png"
			
			sizes="100vw"
			alt="Persona templates"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Persona templates should be customized for each organization’s specific needs and contexts. (<a href='https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/persona-templates.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>But remember, this list isn’t set in stone &mdash; adapt it to what’s actually useful in your specific situation.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aOne%20of%20the%20biggest%20problems%20with%20traditional%20personas%20was%20following%20a%20rigid%20template%20regardless%20of%20whether%20it%20made%20sense%20for%20your%20project.%20We%20must%20not%20fall%20into%20that%20same%20mistake%20with%20functional%20personas.%0a&url=https://smashingmagazine.com%2f2025%2f09%2ffunctional-personas-ai-lean-practical-workflow%2f">
      
One of the biggest problems with traditional personas was following a rigid template regardless of whether it made sense for your project. We must not fall into that same mistake with functional personas.

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

<h2 id="the-benefits-of-functional-personas">The Benefits of Functional Personas</h2>

<p>For small startups, functional personas <strong>reduce wasted effort</strong>. For enterprise teams, they keep sprawling projects grounded in what matters most.</p>

<p>However, because of the way we are going to produce our personas, they provide certain benefits in either case:</p>

<ul>
<li><strong>Lighten the load:</strong> They’re easier to update without large research cycles.</li>
<li><strong>Stay current:</strong> Because they are easy to produce, we can update them more often.</li>
<li><strong>Tie to outcomes:</strong> Tasks, objections, and proof points map straight to funnels, flows, and product decisions.</li>
</ul>

<p>We can deliver these benefits because we are going to use AI to help us, rather than carrying out a lot of time-consuming new research.</p>

<h2 id="how-ai-helps-us-get-there">How AI Helps Us Get There</h2>

<p>Of course, doing fresh research is always preferable. But in many cases, it is not feasible due to time or budget constraints. I would argue that using AI to help us create personas based on existing assets is preferable to having no focus on user attention at all.</p>

<p>AI tools can chew through the inputs you already have (surveys, analytics, chat logs, reviews) and surface patterns you can act on. They also help you scan public conversations around your product category to fill gaps.</p>

<p>I therefore recommend using AI to:</p>

<ul>
<li><strong>Synthesize inputs:</strong> Turn scattered notes into clean themes.</li>
<li><strong>Spot segments by need:</strong> Group people by jobs‑to‑be‑done, not demographics.</li>
<li><strong>Draft quickly:</strong> Produce first‑pass personas and sample journeys in minutes.</li>
<li><strong>Iterate with stakeholders:</strong> Update on the fly as you get feedback.</li>
</ul>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aAI%20doesn%e2%80%99t%20remove%20the%20need%20for%20traditional%20research.%20Rather,%20it%20is%20a%20way%20of%20extracting%20more%20value%20from%20the%20scattered%20insights%20into%20users%20that%20already%20exist%20within%20an%20organization%20or%20online.%0a&url=https://smashingmagazine.com%2f2025%2f09%2ffunctional-personas-ai-lean-practical-workflow%2f">
      
AI doesn’t remove the need for traditional research. Rather, it is a way of extracting more value from the scattered insights into users that already exist within an organization or online.

    </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="the-workflow">The Workflow</h2>

<p>Here’s how to move from scattered inputs to usable personas. Each step builds on the last, so treat it as a cycle you can repeat as projects evolve.</p>

<h3 id="1-set-up-a-dedicated-workspace">1. Set Up A Dedicated Workspace</h3>

<p>Create a dedicated space within your AI tool for this work. Most AI platforms offer project management features that let you organize files and conversations:</p>

<ul>
<li>In ChatGPT and Claude, use “Projects” to store context and instructions.</li>
<li>In Perplexity, Gemini and CoPilot similar functionality is referred to as “Spaces.”</li>
</ul>

<p>This project space becomes your central repository where all uploaded documents, research data, and generated personas live together. The AI will maintain context between sessions, so you won’t have to re-upload materials each time you iterate. This structured approach makes your workflow more efficient and helps the AI deliver more consistent results.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="525"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png"
			
			sizes="100vw"
			alt="ChatGPT Project for persona development"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      ChatGPT Projects serve as an effective tool for gathering and analyzing user research data in ways that directly support persona development. (<a href='https://files.smashing.media/articles/functional-personas-ai-lean-practical-workflow/chatgpt-projects-persona-development.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="2-write-clear-instructions">2. Write Clear Instructions</h3>

<p>Next, you can brief your AI project so that it understands what it wants from you. For example:</p>

<blockquote>“Act as a user researcher. Create realistic, functional personas using the project files and public research. Segment by needs, tasks, questions, pain points, and goals. Show your reasoning.”</blockquote>

<p>Asking for a rationale gives you a paper trail you can defend to stakeholders.</p>

<h3 id="3-upload-what-you-ve-got-even-if-it-s-messy">3. Upload What You’ve Got (Even If It’s Messy)</h3>

<p>This is where things get really powerful.</p>

<p>Upload everything (and I mean everything) you can put your hands on relating to the user. Old surveys, past personas, analytics screenshots, FAQs, support tickets, review snippets; dump them all in. The more varied the sources, the stronger the triangulation.</p>

<h3 id="4-run-focused-external-research">4. Run Focused External Research</h3>

<p>Once you have done that, you can supplement that data by getting AI to carry out “deep research” about your brand. Have AI scan recent (I often focus on the last year) public conversations for your brand, product space, or competitors. Look for:</p>

<ul>
<li>Who’s talking and what they’re trying to do;</li>
<li>Common questions and blockers;</li>
<li>Phrases people use (great for copywriting).</li>
</ul>

<p>Save the report you get back into your project.</p>

<h3 id="5-propose-segments-by-need">5. Propose Segments By Need</h3>

<p>Once you have done that, ask AI to suggest segments based on tasks and friction points (not demographics). Push back until each segment is <strong>distinct, observable, and actionable</strong>. If two would behave the same way in your flow, merge them.</p>

<p>This takes a little bit of trial and error and is where your experience really comes into play.</p>

<h3 id="6-generate-draft-personas">6. Generate Draft Personas</h3>

<p>Now you have your segments, the next step is to draft your personas. Use a simple template so the document is read and used. If your personas become too complicated, people will not read them. Each persona should:</p>

<ul>
<li>State goals and tasks,</li>
<li>List objections and blockers,</li>
<li>Highlight pain points,</li>
<li>Show touchpoints,</li>
<li>Identify service gaps.</li>
</ul>

<p>Below is a sample template you can work with:</p>

<pre><code class="language-markdown">&#35; Persona Title: e.g. Savvy Shopper
- Person's Name: e.g. John Smith.
- Age: e.g. 24
- Job: e.g. Social Media Manager

"A quote that sums up the persona's general attitude"

&#35;&#35; Primary Goal
What they’re here to achieve (1–2 lines).

&#35;&#35; Key Tasks
• Task 1
• Task 2
• Task 3

&#35;&#35; Questions & Objections
• What do they need to know before they act?
• What might make them hesitate?

&#35;&#35; Pain Points
• Where do they get stuck?
• What feels risky, slow, or confusing?

&#35;&#35; Touchpoints
• What channels are they most commonly interacting with?

&#35;&#35; Service Gaps
• How is the organization currently failing this persona?
</code></pre>

<p>Remember, you should customize this to reflect what will prove useful within your organization.</p>

<h3 id="7-validate">7. Validate</h3>

<p>It is important to validate that what the AI has produced is realistic. Obviously, no persona is a true representation as it is a snapshot in time of a Hypothetical user. However, we do want it to be as accurate as possible.</p>

<p>Share your drafts with colleagues who interact regularly with real users &mdash; people in support cells or research teams. Where possible, test with a handful of users. Then cut anything that you can’t defend or correct any errors that are identified.</p>

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

<h2 id="troubleshooting-guardrails">Troubleshooting &amp; Guardrails</h2>

<p>As you work through the above process, you will encounter problems. Here are common pitfalls and how to avoid them:</p>

<ul>
<li><strong>Too many personas?</strong><br />
Merge until each one changes a design or copy decision. Three strong personas beat seven weak ones.</li>
<li><strong>Stakeholder wants demographics?</strong><br />
Only include details that affect behavior. Otherwise, leave them out. Suggest separate personas for other functions (such as marketing).</li>
<li><strong>AI hallucinations?</strong><br />
Always ask for a rationale or sources. Cross‑check with your own data and customer‑facing teams.</li>
<li><strong>Not enough data?</strong><br />
Mark assumptions clearly, then validate with quick interviews, surveys, or usability tests.</li>
</ul>

<h2 id="making-personas-useful-in-practice">Making Personas Useful In Practice</h2>

<p>The most important thing to remember is to actually use your personas once they’ve been created. They can easily become forgotten PDFs rather than active tools. Instead, personas should shape your work and be referenced regularly. Here are some ways you can put personas to work:</p>

<ul>
<li><strong>Navigation &amp; IA:</strong> Structure menus by top tasks.</li>
<li><strong>Content &amp; Proof:</strong> Map objections to FAQs, case studies, and microcopy.</li>
<li><strong>Flows &amp; UI:</strong> Streamline steps to match how people think.</li>
<li><strong>Conversion:</strong> Match CTAs to personas’ readiness, goals, and pain points.</li>
<li><strong>Measurement:</strong> Track KPIs that map to personas, not vanity metrics.</li>
</ul>

<p>With this approach, personas evolve from static deliverables into <strong>dynamic reference points</strong> your whole team can rely on.</p>

<h2 id="keep-them-alive">Keep Them Alive</h2>

<p>Treat personas as a <strong>living toolkit</strong>. Schedule a refresh every quarter or after major product changes. Rerun the research pass, regenerate summaries, and archive outdated assumptions. The goal isn’t perfection; it’s keeping them relevant enough to guide decisions.</p>

<h2 id="bottom-line">Bottom Line</h2>

<p>Functional personas are faster to build, easier to maintain, and better aligned with real user behavior. By combining AI’s speed with human judgment, you can create personas that don’t just sit in a slide deck; they actively shape better products, clearer interfaces, and smoother experiences.</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>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>Declan Chidlow</author><title>Optimizing PWAs For Different Display Modes</title><link>https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/</link><pubDate>Tue, 26 Aug 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/</guid><description>Progressive Web Apps (PWAs) are a great way to make apps built for the web feel native, but in moving away from a browser environment, we can introduce usability issues. Declan covers how we can modify our app depending on what display mode is applied to mitigate these issues.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/08/optimizing-pwas-different-display-modes/" />
              <title>Optimizing PWAs For Different Display Modes</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Optimizing PWAs For Different Display Modes</h1>
                  
                    
                    <address>Declan Chidlow</address>
                  
                  <time datetime="2025-08-26T08:00:00&#43;00:00" class="op-published">2025-08-26T08:00:00+00:00</time>
                  <time datetime="2025-08-26T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p><a href="https://www.smashingmagazine.com/2020/12/progressive-web-apps/">Progressive web apps</a> (PWA) are a fantastic way to turn web applications into native-like, standalone experiences. They bridge the gap between websites and native apps, but this transformation can be prone to introducing design challenges that require thoughtful consideration.</p>

<p>We define our PWAs <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest">with a manifest file</a>. In our PWA’s manifest, we can select from a collection of display modes, each offering different levels of browser interface visibility:</p>

<ul>
<li><code>fullscreen</code>: Hides all browser UI, using the entire display.</li>
<li><code>standalone</code>: Looks like a native app, hiding browser controls but keeping system UI.</li>
<li><code>minimal-ui</code>: Shows minimal browser UI elements.</li>
<li><code>browser</code>: Standard web browser experience with full browser interface.</li>
</ul>

<p>Oftentimes, we want our PWAs to feel like apps rather than a website in a browser, so we set the display manifest member to one of the options that hides the browser’s interface, such as <code>fullscreen</code> or <code>standalone</code>. This is fantastic for helping make our applications feel more at home, but it can introduce some issues we wouldn’t usually consider when building for the web.</p>

<p>It’s easy to forget just how much functionality the browser provides to us. Things like forward/back buttons, the ability to refresh a page, search within pages, or even manipulate, share, or copy a page’s URL are all browser-provided features that users can lose access to when the browser’s UI is hidden. There is also the case of things that we display on websites that don’t necessarily translate to app experiences.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.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/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png"
			
			sizes="100vw"
			alt="The different PWA display modes as seen on an Android phone running Chrome 138."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The different PWA display modes as seen on an Android phone running Chrome 138. (<a href='https://files.smashing.media/articles/optimizing-pwas-different-display-modes/progressive-web-app-examples.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Imagine a user deep into a form with no back button, trying to share a product page without the ability to copy a URL, or hitting a bug with no refresh button to bail them out!</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aMuch%20like%20how%20we%20make%20different%20considerations%20when%20designing%20for%20the%20web%20versus%20designing%20for%20print,%20we%20need%20to%20make%20considerations%20when%20designing%20for%20independent%20experiences%20rather%20than%20browser-based%20experiences%20by%20tailoring%20the%20content%20and%20user%20experience%20to%20the%20medium.%0a&url=https://smashingmagazine.com%2f2025%2f08%2foptimizing-pwas-different-display-modes%2f">
      
Much like how we make different considerations when designing for the web versus designing for print, we need to make considerations when designing for independent experiences rather than browser-based experiences by tailoring the content and user experience to the medium.

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

<p>Thankfully, we’re provided with plenty of ways to customise the web.</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><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="using-media-queries-to-target-display-modes">Using Media Queries To Target Display Modes</h2>

<p>We use media queries all the time when writing CSS. Whether it’s switching up styles for print or setting breakpoints for responsive design, they’re commonplace in the web developer’s toolkit. Each of the display modes discussed previously can be used as a media query to alter the appearance of documents depending.</p>

<p>Media queries such as <code>@media (min-width: 1000px)</code> tend to get the most use for setting breakpoints based on the viewport size, but they’re capable of so much more. They can handle <a href="https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/">print styles</a>, device orientation, contrast preferences, and a whole ton more. In our case, we’re interested in the <code>display-mode</code> media feature.</p>

<p>Display mode media queries correspond to the current display mode.</p>

<p><strong>Note</strong>: <em>While we may set display modes in our manifest, the actual display mode may differ depending on browser support.</em></p>

<p>These media queries directly reference the current mode:</p>

<ul>
<li><code>@media (display-mode: standalone)</code> will only apply to pages set to standalone mode.</li>
<li><code>@media (display-mode: fullscreen)</code> applies to fullscreen mode. It is worth noting that this also applies when using the Fullscreen API.</li>
<li><code>@media (display-mode: minimal-ui)</code> applies to minimal UI mode.</li>
<li><code>@media (display-mode: browser)</code> applies to standard browser mode.</li>
</ul>

<p>It is also worth keeping an eye out for the <code>window-controls-overlay</code> and <code>tabbed</code> display modes. At the time of writing, these two display modes are experimental and can be used with <code>display_override</code>. <code>display-override</code> is a member of our PWA’s manifest, like <code>display</code>, but provides some extra options and power.</p>

<p><code>display</code> has a predetermined fallback chain (<code>fullscreen</code> -&gt; <code>standalone</code> -&gt; <code>minimal-ui</code> -&gt; <code>browser</code>) that we can’t change, but <code>display-override</code> allows setting a fallback order of our choosing, like the following:</p>

<pre><code class="language-json">"display_override": ["fullscreen", "minimal-ui"]
</code></pre>

<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window_Controls_Overlay_API"><code>window-controls-overlay</code></a> can only apply to PWAs running on a desktop operating system. It makes the PWA take up the entire window, with window control buttons appearing as an overlay. Meanwhile, <code>tabbed</code> is relevant when there are multiple applications within a single window.</p>

<p>In addition to these, there is also the <code>picture-in-picture</code> display mode that applies to (you guessed it) picture-in-picture modes.</p>

<p>We use these media queries exactly as we would any other media query. To show an element with the class <code>.pwa-only</code> when the display mode is standalone, we could do this:</p>

<pre><code class="language-css">.pwa-only {
    display: none;
}

@media (display-mode: standalone) {
    .pwa-only {
        display: block;
    }
}
</code></pre>

<p>If we wanted to show the element when the display mode is standalone <em>or</em> <code>minimal-ui</code>, we could do this:</p>

<div class="break-out">
<pre><code class="language-css">@media (display-mode: standalone), (display-mode: minimal-ui) {
    .pwa-only {
        display: block;
    }
}
</code></pre>
</div>

<p>As great as it is, sometimes CSS isn’t enough. In those cases, we can also reference the display mode and make necessary adjustments with JavaScript:</p>

<div class="break-out">
<pre><code class="language-javascript">const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
// Listen for display mode changes
window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) =&gt; {
  if (e.matches) {
    // App is now in standalone mode
    console.log("Running as PWA");
  }
});
</code></pre>
</div>

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

<h2 id="practical-applications">Practical Applications</h2>

<p>Now that we know how to make display modifications depending on whether users are using our web app as a PWA or in a browser, we can have a look at how we might put these newly learnt skills to use.</p>

<h3 id="tailoring-content-for-pwa-users">Tailoring Content For PWA Users</h3>

<p>Users who have an app installed as a PWA are already converted, so you can tweak your app to tone down the marketing speak and focus on the user experience. Since these users have demonstrated commitment by installing your app, they likely don’t need promotional content or installation prompts.</p>

<h3 id="display-more-options-and-features">Display More Options And Features</h3>

<p>You might need to directly expose more things in PWA mode, as people won’t be able to access the browser’s settings as easily when the browser UI is hidden. Features like changing font sizing, switching between light and dark mode, bookmarks, sharing, tabs, etc., might need an in-app alternative.</p>

<h3 id="platform-appropriate-features">Platform-Appropriate Features</h3>

<p>There are features you might not want on your web app because they feel out of place, but that you might want on your PWA. A good example is the bottom navigation bar, which is common in native mobile apps thanks to the easier reachability it provides, but uncommon on websites.</p>

<p>People sometimes print websites, but they very rarely print apps. Consider whether features like print buttons should be hidden in PWA mode.</p>

<h3 id="install-prompts">Install Prompts</h3>

<p>A common annoyance is a prompt to install a site as a PWA appearing when the user has already installed the site. Ideally, the browser will provide an install prompt of its own if our PWA is configured correctly, but not all browsers do, and it can be finicky. MDN has a fantastic guide on <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt">creating a custom button to trigger the installation of a PWA</a>, but it might not fit our needs.</p>

<p>We can improve things by hiding install prompts with our media query or detecting the current display mode with JavaScript and forgoing triggering popups in the first place.</p>

<p>We could even set this up as a reusable utility class so that anything we don’t want to be displayed when the app is installed as a PWA can be hidden with ease.</p>

<pre><code class="language-css">/&#42; Utility class to hide elements in PWA mode &#42;/
.hide-in-pwa {
  display: block;
}

@media (display-mode: standalone), (display-mode: minimal-ui) {
  .hide-in-pwa {
    display: none !important;
  }
}
</code></pre>

<p>Then in your HTML:</p>

<pre><code class="language-html">&lt;div class="install-prompt hide-in-pwa"&gt;
  &lt;button&gt;Install Our App&lt;/button&gt;
&lt;/div&gt;

&lt;div class="browser-notice hide-in-pwa"&gt;
  &lt;p&gt;For the best experience, install this as an app!&lt;/p&gt;
&lt;/div&gt;
</code></pre>

<p>We could also do the opposite and create a utility class to make elements only show when in a PWA, as we discussed earlier.</p>

<h3 id="strategic-use-of-scope-and-start-url">Strategic Use Of Scope And Start URL</h3>

<p>Another way to hide content from your site is to set the <code>scope</code> and <code>start_url</code> properties. These aren’t using media queries as we’ve discussed, but should be considered as ways to present different content depending on whether a site is installed as a PWA.</p>

<p>Here is an example of a manifest using these properties:</p>

<pre><code class="language-css">{
    "name": "Example PWA",</code>
    <code style="font-weight: bold;">"scope": "/dashboard/",</code>
    <code style="font-weight: bold;">"start_url": "/dashboard/index.html",</code>
    <code class="language-css">"display": "standalone",
    "icons": [
        {
            "src": "icon.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ]
}
</code></pre>

<p><code>scope</code> here defines the top level of the PWA. When users leave the scope of your PWA, they’ll still have an app-like interface but gain access to browser UI elements. This can be useful if you’ve got certain parts of your app that you still want to be part of the PWA but which aren’t necessarily optimised or making the necessary considerations.</p>

<p><code>start_url</code> defines the URL a user will be presented with when they open the application. This is useful if, for example, your app has marketing content at <code>example.com</code> and a dashboard at <code>example.com/dashboard/index.html</code>. It is likely that people who have installed the app as a PWA don’t need the marketing content, so you can set the <code>start_url</code> to <code>/dashboard/index.html</code> so the app starts on that page when they open the PWA.</p>

<h3 id="enhanced-transitions">Enhanced Transitions</h3>

<p><a href="https://www.smashingmagazine.com/2023/12/view-transitions-api-ui-animations-part1/">View transitions</a> can feel unfamiliar, out of place, and a tad gaudy on the web, but are a common feature of native applications. We can set up PWA-only view transitions by wrapping the relevant CSS appropriately:</p>

<pre><code class="language-css">@media (display-mode: standalone) {
  @view-transition {
    navigation: auto;
  }
}
</code></pre>

<p>If you’re <em>really</em> ambitious, you could also tweak the design of a site entirely to fit more closely with native design systems when running as a PWA by pairing a check for the display mode with a check for the device and/or browser in use as needed.</p>

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

<h2 id="browser-support-and-testing">Browser Support And Testing</h2>

<p>Browser support for display mode media queries is <a href="https://caniuse.com/mdn-css_at-rules_media_display-mode">good and extensive</a>. However, it’s worth noting that <strong>Firefox doesn’t have PWA support</strong>, and Firefox for Android only displays PWAs in <code>browser</code> mode, so you should make the necessary considerations. Thankfully, <a href="https://www.smashingmagazine.com/2013/09/progressive-enhancement-is-faster/">progressive enhancement</a> is on our side. If we’re dealing with a browser lacking support for PWAs or these media queries, we’ll be treated to <strong>graceful degradation</strong>.</p>

<p>Testing PWAs can be challenging because every device and browser handles them differently. Each display mode behaves slightly differently in every browser and OS combination.</p>

<p>Unfortunately, I don’t have a silver bullet to offer you with regard to this. Browsers don’t have a convenient way to simulate display modes for testing, so you’ll have to test out your PWA on different devices, browsers, and operating systems to be sure everything works everywhere it should, as it should.</p>

<h2 id="recap">Recap</h2>

<p>Using a PWA is a fundamentally different experience from using a web app in the browser, so considerations should be made. <code>display-mode</code> media queries provide a powerful way to create truly adaptive Progressive Web Apps that respond intelligently to their installation and display context. By leveraging these queries, we can do the following:</p>

<ul>
<li><strong>Hide redundant installation prompts</strong> for users who have already installed the app,</li>
<li><strong>Provide appropriate navigation aids</strong> when making browser controls unavailable,</li>
<li><strong>Tailor content and functionality</strong> to match user expectations in different contexts,</li>
<li><strong>Create more native-feeling experiences</strong> that respect platform conventions, and</li>
<li><strong>Progressively enhance the experience</strong> for committed users.</li>
</ul>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20key%20is%20remembering%20that%20PWA%20users%20in%20standalone%20mode%20have%20different%20needs%20and%20expectations%20than%20standard%20website%20visitors.%20By%20detecting%20and%20responding%20to%20display%20modes,%20we%20can%20create%20experiences%20that%20feel%20more%20polished,%20purposeful,%20and%20genuinely%20app-like.%0a&url=https://smashingmagazine.com%2f2025%2f08%2foptimizing-pwas-different-display-modes%2f">
      
The key is remembering that PWA users in standalone mode have different needs and expectations than standard website visitors. By detecting and responding to display modes, we can create experiences that feel more polished, purposeful, and genuinely app-like.

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

<p>As PWAs continue to mature, thoughtful implementations and tailoring will become increasingly important for creating truly compelling app experiences on the web. If you’re itching for even more information and PWA tips and tricks, check out Ankita Masand’s “<a href="https://www.smashingmagazine.com/2018/11/guide-pwa-progressive-web-applications/">Extensive Guide To Progressive Web Applications</a>”.</p>

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

<ul>
<li>“<a href="https://www.smashingmagazine.com/2021/11/magento-pwa-customizing-themes-coding/">Creating A Magento PWA: Customizing Themes vs. Coding From Scratch</a>”, Alex Husar</li>
<li>“<a href="https://www.smashingmagazine.com/2020/12/progressive-web-apps/">How To Optimize Progressive Web Apps: Going Beyond The Basics</a>”, Gert Svaiko</li>
<li>“<a href="https://www.smashingmagazine.com/2020/01/mobile-pwa-sticky-bars-elements/">How To Decide Which PWA Elements Should Stick</a>”, Suzanne Scacca</li>
<li>“<a href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/">Uniting Web And Native Apps With 4 Unknown JavaScript APIs</a>”, Juan Diego Rodríguez</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>Ilia Kanazin &amp; Marina Chernyshova</author><title>Designing With AI, Not Around It: Practical Advanced Techniques For Product Design Use Cases</title><link>https://www.smashingmagazine.com/2025/08/designing-with-ai-practical-techniques-product-design/</link><pubDate>Mon, 11 Aug 2025 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/08/designing-with-ai-practical-techniques-product-design/</guid><description>Prompting isn’t just about writing better instructions, but about designing better thinking. Ilia and Marina explore how advanced prompting can empower different product &amp;amp; design use cases, speeding up your workflow and improving results, from research and brainstorming to testing and beyond. Let’s dive in.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/08/designing-with-ai-practical-techniques-product-design/" />
              <title>Designing With AI, Not Around It: Practical Advanced Techniques For Product Design Use Cases</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Designing With AI, Not Around It: Practical Advanced Techniques For Product Design Use Cases</h1>
                  
                    
                    <address>Ilia Kanazin &amp; Marina Chernyshova</address>
                  
                  <time datetime="2025-08-11T08:00:00&#43;00:00" class="op-published">2025-08-11T08:00:00+00:00</time>
                  <time datetime="2025-08-11T08:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>AI is almost everywhere &mdash; it writes text, makes music, generates code, draws pictures, runs research, chats with you &mdash; and apparently even <a href="https://hbr.org/2025/04/how-people-are-really-using-gen-ai-in-2025">understands people better than they understand themselves</a>?!</p>

<p>It’s a lot to take in. The pace is wild, and new tools pop up faster than anyone has time to try them. Amid the chaos, one thing is clear: this isn’t hype, but it’s structural change.</p>

<p>According to the <a href="https://www.weforum.org/publications/the-future-of-jobs-report-2025/"><em>Future of Jobs Report 2025</em></a> by the World Economic Forum, one of the fastest-growing, most in-demand skills for the next five years is the <strong>ability to work with AI and Big Data</strong>. That applies to almost every role &mdash; including product design.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="673"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png"
			
			sizes="100vw"
			alt="A figure showing skills on the rise in 2025-2030, which places AI and big data on the first place"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/1-skills-on-the-rise-2025.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>What do companies want most from their teams? Right, efficiency. And AI can make people way more efficient. We’d easily spend 3x more time on tasks like replying to our managers without AI helping out. We’re learning to work with it, but many of us are still figuring out how to meet the rising bar.</p>

<p>That’s especially important for designers, whose work is all about empathy, creativity, critical thinking, and working across disciplines. It’s a uniquely human mix. At least, that’s what we tell ourselves.</p>

<p>Even as debates rage about AI’s limitations, tools today (June 2025 &mdash; timestamp matters in this fast-moving space) already assist with research, ideation, and testing, sometimes better than expected.</p>

<p>Of course, not everyone agrees. AI hallucinates, loses context, and makes things up. So how can both views exist at the same time? Very simple. It’s because both are true: AI is deeply flawed and surprisingly useful. The trick is knowing how to work with its strengths while managing its weaknesses. The real question isn’t whether AI is good or bad &mdash; it’s how we, as designers, stay sharp, stay valuable, and stay in the loop.</p>

<h2 id="why-prompting-matters">Why Prompting Matters</h2>

<p>Prompting matters more than most people realize because even small tweaks in how you ask can lead to radically different outputs. To see how this works in practice, let’s look at a simple example.</p>

<p>Imagine you want to improve the onboarding experience in your product. On the left, you have the prompt you send to AI. On the right, the response you get back.</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Input</th>
            <th>Output</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>How to improve onboarding in a SaaS product?</td>
            <td>👉 Broad suggestions: checklists, empty states, welcome modals…</td>
        </tr>
        <tr>
            <td>How to improve onboarding in Product A’s workspace setup flow?</td>
            <td>👉 Suggestions focused on workspace setup…</td>
        </tr>
        <tr>
            <td>How to improve onboarding in Product A’s workspace setup step to address user confusion?</td>
            <td>👉 ~10 common pain points with targeted UX fixes for each…</td>
        </tr>
    <tr>
            <td>How to improve onboarding in Product A by redesigning the workspace setup screen to reduce drop-off, with detailed reasoning?</td>
            <td>👉 ~10 paragraphs covering a specific UI change, rationale, and expected impact…</td>
        </tr>
    </tbody>
</table>

<p>This side-by-side shows just how much even the smallest prompt details can change what AI gives you.</p>

<p>Talking to an AI model isn’t that different from talking to a person. If you explain your thoughts clearly, you get a better understanding and communication overall.</p>

<blockquote>Advanced prompting is about moving beyond one-shot, throwaway prompts. It’s an iterative, structured process of refining your inputs using different techniques so you can guide the AI toward more useful results. It focuses on being intentional with every word you put in, giving the AI not just the task but also the path to approach it step by step, so it can actually do the job.</blockquote>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.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/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png"
			
			sizes="100vw"
			alt="Advanced prompting vs basic promting"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/designing-with-ai-practical-techniques-product-design/2-advanced-prompting.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Where basic prompting throws your question at the model and hopes for a quick answer, advanced prompting helps you <strong>explore options</strong>, <strong>evaluate branches of reasoning</strong>, and <strong>converge on clear, actionable outputs</strong>.</p>

<p>But that doesn’t mean simple prompts are useless. On the contrary, short, focused prompts work well when the task is narrow, factual, or time-sensitive. They’re great for idea generation, quick clarifications, or anything where deep reasoning isn’t required. <strong>Think of prompting as a scale, not a binary.</strong> The simpler the task, the faster a lightweight prompt can get the job done. The more complex the task, the more structure it needs.</p>

<p>In this article, we’ll dive into how advanced prompting can empower different product &amp; design use cases, speeding up your workflow and improving your results &mdash; whether you’re researching, brainstorming, testing, or beyond. Let’s dive in.</p>

<h2 id="practical-cases">Practical Cases</h2>

<p>In the next section, we’ll explore six practical prompting techniques that we’ve found most useful in real product design work. These aren’t abstract theories &mdash; each one is grounded in hands-on experience, tested across research, ideation, and evaluation tasks. Think of them as modular tools: you can mix, match, and adapt them depending on your use case. For each, we’ll explain the thinking behind it and walk through a sample prompt.</p>

<p><strong>Important note:</strong> The prompts you’ll see are not copy-paste recipes. Some are structured templates you can reuse with small tweaks; others are more specific, meant to spark your thinking. Use them as scaffolds, not scripts.</p>

<h3 id="1-task-decomposition-by-jtbd">1. Task Decomposition By JTBD</h3>

<p><em>Technique: Role, Context, Instructions template + Checkpoints (with self-reflection)</em></p>

<p>Before solving any problem, there’s a critical step we often overlook: breaking the problem down into clear, actionable parts.</p>

<p>Jumping straight into execution feels fast, but it’s risky. We might end up solving the wrong thing, or solving it the wrong way. That’s where GPT can help: not just by generating ideas, but by helping us think more clearly about the structure of the problem itself.</p>

<p>There are many ways to break down a task. One of the most useful in product work is the <strong>Jobs To Be Done (JTBD) framework</strong>. Let’s see how we can use advanced prompting to apply JTBD decomposition to any task.</p>

<p>Good design starts with understanding the user, the problem, and the context. Good prompting? Pretty much the same. That’s why most solid prompts include three key parts: Role, Context, and Instructions. If needed, you can also add the expected format and any constraints.</p>

<p>In this example, we’re going to break down a task into smaller jobs and add self-checkpoints to the prompt, so the AI can pause, reflect, and self-verify along the way.</p>

<blockquote><strong>Role</strong><br />Act as a senior product strategist and UX designer with deep expertise in Jobs To Be Done (JTBD) methodology and user-centered design. You think in terms of user goals, progress-making moments, and unmet needs &mdash; similar to approaches used at companies like Intercom, Basecamp, or IDEO.<br /><br /><strong>Context</strong><br />You are helping a product team break down a broad user or business problem into a structured map of Jobs To Be Done. This decomposition will guide discovery, prioritization, and solution design.<br /><br /><strong>Task & Instructions</strong><br />[👉 DESCRIBE THE USER TASK OR PROBLEM 👈🏼]<br />Use JTBD thinking to uncover:<ul><li>The main functional job the user is trying to get done;</li><li>Related emotional or social jobs;</li><li>Sub-jobs or tasks users must complete along the way;</li><li>Forces of progress and barriers that influence behavior.</li></ul><br /><strong>Checkpoints</strong><br />Before finalizing, check yourself:<ul><li>Are the jobs clearly goal-oriented and not solution-oriented?</li><li>Are sub-jobs specific steps toward the main job?</li><li>Are emotional/social jobs captured?</li><li>Are user struggles or unmet needs listed?</li></ul><br />If anything’s missing or unclear, revise and explain what was added or changed.</blockquote>

<p>With a simple one-sentence prompt, you’ll likely get a high-level list of user needs or feature ideas. An advanced approach can produce a structured JTBD breakdown of a specific user problem, which may include:</p>

<ul>
<li><strong>Main Functional Job</strong>: A clear, goal-oriented statement describing the primary outcome the user wants to achieve.</li>
<li><strong>Emotional &amp; Social Jobs</strong>: Supporting jobs related to how the user wants to feel or be perceived during their progress.</li>
<li><strong>Sub-Jobs</strong>: Step-by-step tasks or milestones the user must complete to fulfill the main job.</li>
<li><strong>Forces of Progress</strong>: A breakdown of motivations (push/pull) and barriers (habits/anxieties) that influence user behavior.</li>
</ul>

<p>But these prompts are most powerful when used with real context. Try it now with your product. Even a quick test can reveal unexpected insights.</p>

<h3 id="2-competitive-ux-audit">2. Competitive UX Audit</h3>

<p><em>Technique: Attachments + Reasoning Before Understanding + Tree of Thought (ToT)</em></p>

<p>Sometimes, you don’t need to design something new &mdash; you need to understand what already exists.</p>

<p>Whether you’re doing a competitive analysis, learning from rivals, or benchmarking features, the first challenge is making sense of someone else’s design choices. What’s the feature really for? Who’s it helping? Why was it built this way?</p>

<p>Instead of rushing into critique, we can use GPT to reverse-engineer the thinking behind a product &mdash; before judging it. In this case, start by:</p>

<ol>
<li>Grabbing the competitor’s documentation for the feature you want to analyze.</li>
<li>Save it as a PDF. Then head over to ChatGPT (or other models).</li>
<li>Before jumping into the audit, ask it to first make sense of the documentation. This technique is called <strong>Reasoning Before Understanding (RBU)</strong>. That means before you ask for critique, you ask for <strong>interpretation</strong>. This helps AI build a more accurate mental model &mdash; and avoids jumping to conclusions.</li>
</ol>

<blockquote><strong>Role</strong><br />You are a senior UX strategist and cognitive design analyst. Your expertise lies in interpreting digital product features based on minimal initial context, inferring purpose, user intent, and mental models behind design decisions before conducting any evaluative critique.<br /><br /><strong>Context</strong><br />You’ve been given internal documentation and screenshots of a feature. The goal is not to evaluate it yet, but to understand what it’s doing, for whom, and why.<br /><br /><strong>Task & Instructions</strong><br />Review the materials and answer:<ul><li>What is this feature for?</li><li>Who is the intended user?</li><li>What tasks or scenarios does it support?</li><li>What assumptions does it make about the user?</li><li>What does its structure suggest about priorities or constraints?</li></ul></blockquote>

<p>Once you get the first reply, take a moment to respond: clarify, correct, or add nuance to GPT’s conclusions. This helps align the model’s mental frame with your own.</p>

<p>For the audit part, we’ll use something called the Tree of Thought (ToT) approach.</p>

<p><strong>Tree of Thought (ToT)</strong> is a prompting strategy that asks the AI to “think in branches.” Instead of jumping to a single answer, the model explores multiple reasoning paths, compares outcomes, and revises logic before concluding &mdash; like tracing different routes through a decision tree. This makes it perfect for handling more complex UX tasks.</p>

<blockquote>You are now performing a UX audit based on your understanding of the feature. You’ll identify potential problems, alternative design paths, and trade-offs using a Tree of Thought approach, i.e., thinking in branches, comparing different reasoning paths before concluding.</blockquote>

<p>or</p>

<blockquote>Convert your understanding of the feature into a set of Jobs-To-Be-Done statements from the user’s perspective using a Tree of Thought approach.</blockquote>

<blockquote>List implicit assumptions this feature makes about the user's behavior, workflow, or context using a Tree of Thought approach.</blockquote>

<blockquote>Propose alternative versions of this feature that solve the same job using different interaction or flow mechanics using a Tree of Thought approach.</blockquote>

<h3 id="3-ideation-with-an-intellectual-opponent">3. Ideation With An Intellectual Opponent</h3>

<p><em>Technique: Role Conditioning + Memory Update</em></p>

<p>When you’re working on creative or strategic problems, there’s a common trap: AI often just agrees with you or tries to please your way of thinking. It treats your ideas like gospel and tells you they’re great &mdash; even when they’re not.</p>

<p>So how do you avoid this? How do you get GPT to challenge your assumptions and act more like a <strong>critical thinking partner</strong>? Simple: tell it to and ask to remember.</p>

<blockquote><strong>Instructions</strong><br />From now on, remember to follow this mode unless I explicitly say otherwise.<br /><br />Do not take my conclusions at face value. Your role is not to agree or assist blindly, but to serve as a sharp, respectful intellectual opponent.<br /><br />Every time I present an idea, do the following:<ul><li>Interrogate my assumptions: What am I taking for granted?</li><li>Present counter-arguments: Where could I be wrong, misled, or overly confident?</li><li>Test my logic: Is the reasoning sound, or are there gaps, fallacies, or biases?</li><li>Offer alternatives: Not for the sake of disagreement, but to expand perspective.</li><li>Prioritize truth and clarity over consensus: Even when it’s uncomfortable.</li></ul>Maintain a constructive, rigorous, truth-seeking tone. Don’t argue for the sake of it. Argue to sharpen thought, expose blind spots, and help me reach clearer, stronger conclusions.<br /><br />This isn’t a debate. It’s a collaboration aimed at insight.</blockquote>

<h3 id="4-requirements-for-concepting">4. Requirements For Concepting</h3>

<p><em>Technique: Requirement-Oriented + Meta prompting</em></p>

<p>This one deserves a whole article on its own, but let’s lay the groundwork here.</p>

<p>When you’re building quick prototypes or UI screens using tools like v0, Bolt, Lovable, UX Pilot, etc., your prompt needs to be better than most PRDs you’ve worked with. Why? Because the output depends entirely on how clearly and specifically you describe the goal.</p>

<p>The catch? Writing that kind of prompt is hard. So instead of jumping straight to the design prompt, try writing a <strong>meta-prompt first</strong>. That is a prompt that asks GPT to help you write a better prompt. Prompting about prompting, prompt-ception, if you will.</p>

<p>Here’s how to make that work: Feed GPT what you already know about the app or the screen. Then ask it to treat things like information architecture, layout, and user flow as variables it can play with. That way, you don’t just get one rigid idea &mdash; you get multiple concept directions to explore.</p>

<blockquote><strong>Role</strong><br />You are a product design strategist working with AI to explore early-stage design concepts.<br /><br /><strong>Goal</strong><br />Generate 3 distinct prompt variations for designing a Daily Wellness Summary single screen in a mobile wellness tracking app for Lovable/Bolt/v0.<br /><br />Each variation should experiment with a different Information Architecture and Layout Strategy. You don’t need to fully specify the IA or layout &mdash; just take a different angle in each prompt. For example, one may prioritize user state, another may prioritize habits or recommendations, and one may use a card layout while another uses a scroll feed.<br /><br /><strong>User context</strong><br />The target user is a busy professional who checks this screen once or twice a day (morning/evening) to log their mood, energy, and sleep quality, and to receive small nudges or summaries from the app.<br /><br /><strong>Visual style</strong><br />Keep the tone calm and approachable.<br /><br /><strong>Format</strong><br />Each of the 3 prompt variations should be structured clearly and independently.<br /><br />Remember: The key difference between the three prompts should be the underlying IA and layout logic. You don’t need to over-explain &mdash; just guide the design generator toward different interpretations of the same user need.</blockquote>

<h3 id="5-from-cognitive-walkthrough-to-testing-hypothesis">5. From Cognitive Walkthrough To Testing Hypothesis</h3>

<p><em>Technique: Casual Tree of Though + Casual Reasoning + Multi-Roles + Self-Reflection</em></p>

<p>Cognitive walkthrough is a powerful way to break down a user action and check whether the steps are intuitive.</p>

<p><strong>Example</strong>: “User wants to add a task” → Do they know where to click? What to do next? Do they know it worked?</p>

<p>We’ve found this technique super useful for reviewing our own designs. Sometimes there’s already a mockup; other times we’re still arguing with a PM about what should go where. Either way, GPT can help.</p>

<p>Here’s an advanced way to run that process:</p>

<blockquote><strong>Context</strong><br />You’ve been given a screenshot of a screen where users can create new tasks in a project management app. The main action the user wants to perform is “add a task”. Simulate behavior from two user types: a beginner with no prior experience and a returning user familiar with similar tools.<br /><br /><strong>Task & Instructions</strong><br />Go through the UI step by step and evaluate:<ol><li>Will the user know what to do at each step?</li><li>Will they understand how to perform the action?</li><li>Will they know they’ve succeeded?</li></ol>For each step, consider alternative user paths (if multiple interpretations of the UI exist). Use a casual Tree-of-Thought method.<br /><br />At each step, reflect: what assumptions is the user making here? What visual feedback would help reduce uncertainty?<br /><br /><strong>Format</strong><br />Use a numbered list for each step. For each, add observations, possible confusions, and UX suggestions.<br /><br /><strong>Limits</strong><br />Don’t assume prior knowledge unless it’s visually implied.<br />Do not limit analysis to a single user type.</blockquote>

<p>Cognitive walkthroughs are great, but they get even more useful when they lead to testable hypotheses.</p>

<p>After running the walkthrough, you’ll usually uncover moments that might confuse users. Instead of leaving that as a guess, turn those into concrete UX testing hypotheses.</p>

<p>We ask GPT to not only flag potential friction points, but to help define how we’d validate them with real users: using a task, a question, or observable behavior.</p>

<blockquote><strong>Task & Instructions</strong><br />Based on your previous cognitive walkthrough:<ol><li>Extract all potential usability hypotheses from the walkthrough.</li><li>For each hypothesis:<ul><li>Assess whether it can be tested through moderated or unmoderated usability testing.</li><li>Explain what specific UX decision or design element may cause this issue. Use causal reasoning.</li><li>For testable hypotheses:<ul><li>Propose a specific usability task or question.</li><li>Define a clear validation criterion (how you’ll know if the hypothesis is confirmed or disproved).</li><li>Evaluate feasibility and signal strength of the test (e.g., how easy it is to test, and how confidently it can validate the hypothesis).</li><li>Assign a priority score based on Impact, Confidence, and Ease (ICE).</li></ul></li></ul></li></ol><strong>Limits</strong><br />Don’t invent hypotheses not rooted in your walkthrough output. Only propose tests where user behavior or responses can provide meaningful validation. Skip purely technical or backend concerns.</blockquote>

<h3 id="6-cross-functional-feedback">6. Cross-Functional Feedback</h3>

<p><em>Technique: Multi-Roles</em></p>

<p>Good design is co-created. And good designers are used to working with cross-functional teams: PMs, engineers, analysts, QAs, you name it. Part of the job is turning scattered feedback into clear action items.</p>

<p>Earlier, we talked about how giving AI a “role” helps sharpen its responses. Now let’s level that up: what if we give it <strong>multiple roles at once</strong>? This is called <strong>multi-role prompting</strong>. It’s a great way to simulate a design review with input from different perspectives. You get quick insights and a more well-rounded critique of your design.</p>

<blockquote><strong>Role</strong><br />You are a cross-functional team of experts evaluating a new dashboard design:<ul><li>PM (focus: user value & prioritization)</li><li>Engineer (focus: feasibility & edge cases)</li><li>QA tester (focus: clarity & testability)</li><li>Data analyst (focus: metrics & clarity of reporting)</li><li>Designer (focus: consistency & usability)</li></ul><strong>Context</strong><br />The team is reviewing a mockup for a new analytics dashboard for internal use.<br /><br /><strong>Task & Instructions</strong><br />For each role:<ol><li>What stands out immediately?</li><li>What concerns might this role have?</li><li>What feedback or suggestions would they give?</li></ol></blockquote>

<h2 id="designing-with-ai-is-a-skill-not-a-shortcut">Designing With AI Is A Skill, Not A Shortcut</h2>

<p>By now, you’ve seen that prompting isn’t just about typing better instructions. It’s about <strong>designing better thinking</strong>.</p>

<p>We’ve explored several techniques, and each is useful in different contexts:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Technique</th>
            <th>When to use It</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Role + Context + Instructions + Constraints</td>
            <td>Anytime you want consistent, focused responses (especially in research, decomposition, and analysis).</td>
        </tr>
        <tr>
            <td>Checkpoints / Self-verification</td>
            <td>When accuracy, structure, or layered reasoning matters. Great for complex planning or JTBD breakdowns.</td>
        </tr>
        <tr>
            <td>Reasoning Before Understanding (RBU)</td>
            <td>When input materials are large or ambiguous (like docs or screenshots). Helps reduce misinterpretation.</td>
        </tr>
    <tr>
            <td>Tree of Thought (ToT)</td>
            <td>When you want the model to explore options, backtrack, compare. Ideal for audits, evaluations, or divergent thinking.</td>
        </tr>
    <tr>
            <td>Meta-prompting</td>
            <td>When you're not sure how to even ask the right question. Use it early in fuzzy or creative concepting.</td>
        </tr>
    <tr>
            <td>Multi-role prompting</td>
            <td>When you need well-rounded, cross-functional critique or to simulate team feedback.</td>
        </tr>
     <tr>
            <td>Memory-updated “opponent” prompting</td>
            <td>When you want to challenge your own logic, uncover blind spots, or push beyond echo chambers.</td>
        </tr>
    </tbody>
</table>

<p>But even the best techniques won’t matter if you use them blindly, so ask yourself:</p>

<ul>
<li>Do I need precision or perspective right now?

<ul>
<li><em>Precision?</em> Try <strong>Role + Checkpoints</strong> for clarity and control.</li>
<li><em>Perspective?</em> Use <strong>Multi-Role</strong> or <strong>Tree of Thought</strong> to explore alternatives.</li>
</ul></li>
<li>Should the model reflect my framing, or break it?

<ul>
<li><em>Reflect it?</em> Use <strong>Role + Context + Instructions</strong>.</li>
<li><em>Break it?</em> Try <strong>Opponent prompting</strong> to challenge assumptions.</li>
</ul></li>
<li>Am I trying to reduce ambiguity, or surface complexity?

<ul>
<li><em>Reduce ambiguity?</em> Use <strong>Meta-prompting</strong> to clarify your ask.</li>
<li><em>Surface complexity?</em> Go with <strong>ToT</strong> or <strong>RBU</strong> to expose hidden layers.</li>
</ul></li>
<li>Is this task about alignment, or exploration?

<ul>
<li><em>Alignment?</em> Use <strong>Multi-Roles prompting</strong> to simulate consensus.</li>
<li><em>Exploration?</em> Use <strong>Cognitive Walkthrough</strong> to push deeper.</li>
</ul></li>
</ul>

<p>Remember, you don’t need a long prompt every time. Use detail when the task demands it, not out of habit. AI can do a lot, but it reflects the shape of your thinking. And prompting is how you shape it. So don’t just prompt better. Think better. And design with AI &mdash; not around it.</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>Joas Pambou</author><title>Automating Design Systems: Tips And Resources For Getting Started</title><link>https://www.smashingmagazine.com/2025/08/automating-design-systems-tips-resources/</link><pubDate>Wed, 06 Aug 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/08/automating-design-systems-tips-resources/</guid><description>Design systems are more than style guides: they’re made up of workflows, tokens, components, and documentation &amp;mdash; all the stuff teams rely on to build consistent products. As projects grow, keeping everything in sync gets tricky fast. In this article, we’ll look at how smart tooling, combined with automation where it makes sense, can speed things up, reduce errors, and help your team focus on design over maintenance.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/08/automating-design-systems-tips-resources/" />
              <title>Automating Design Systems: Tips And Resources For Getting Started</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Automating Design Systems: Tips And Resources For Getting Started</h1>
                  
                    
                    <address>Joas Pambou</address>
                  
                  <time datetime="2025-08-06T10:00:00&#43;00:00" class="op-published">2025-08-06T10:00:00+00:00</time>
                  <time datetime="2025-08-06T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>A design system is more than just a set of colors and buttons. It’s a shared language that helps designers and developers build good products together. At its core, a design system includes <a href="https://www.smashingmagazine.com/2024/05/naming-best-practices/">tokens</a> (like colors, spacing, fonts), <a href="https://www.smashingmagazine.com/2022/12/anatomy-themed-design-system-components/">components</a> (such as buttons, forms, navigation), plus the <a href="https://www.smashingmagazine.com/2023/11/designing-web-design-documentation/">rules and documentation</a> that tie all together across projects.</p>

<p>If you’ve ever used systems like <a href="https://m3.material.io/">Google Material Design</a> or <a href="https://polaris-react.shopify.com/">Shopify Polaris</a>, for example, then you’ve seen how design systems set <strong>clear expectations for structure and behavior</strong>, making teamwork smoother and faster. But while design systems promote consistency, keeping everything in sync is the hard part. Update a token in Figma, like a color or spacing value, and that change has to show up in the code, the documentation, and everywhere else it’s used.</p>

<p>The same thing goes for components: when a button’s behavior changes, it needs to update across the whole system. That’s where the right tools and a bit of automation can make the difference. They help reduce repetitive work and keep the system easier to manage as it grows.</p>

<p>In this article, we’ll cover a variety of <strong>tools and techniques for syncing tokens, updating components, and keeping docs up to date</strong>, showing how automation can make all of it easier.</p>

<h2 id="the-building-blocks-of-automation">The Building Blocks Of Automation</h2>

<p>Let’s start with the basics. Color, typography, spacing, radii, shadows, and all the tiny values that make up your visual language are known as <strong>design tokens</strong>, and they’re meant to be the single source of truth for the UI. You’ll see them in design software like Figma, in code, in style guides, and in documentation. <a href="https://www.smashingmagazine.com/2024/05/naming-best-practices/">Smashing Magazine has covered them</a> before in great detail.</p>

<p>The problem is that they <strong>often go out of sync</strong>, such as when a color or component changes in design but doesn’t get updated in the code. The more your team grows or changes, the more these mismatches show up; not because people aren’t paying attention, but because <strong>manual syncing just doesn’t scale</strong>. That’s why <strong>automating tokens</strong> is usually the first thing teams should consider doing when they start building a design system. That way, instead of writing the same color value in Figma and then again in a configuration file, you pull from a shared token source and let that drive both design and development.</p>

<p>There are a few tools that are designed to help make this easier.</p>

<h3 id="token-studio">Token Studio</h3>

<p><a href="https://tokens.studio/">Token Studio</a> is a Figma plugin that lets you manage design tokens directly in your file, export them to different formats, and sync them to code.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="503"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png"
			
			sizes="100vw"
			alt="Token Studio"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/1-token-studio-figma.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="specify">Specify</h3>

<p><a href="https://specifyapp.com/">Specify</a> lets you collect tokens from Figma and push them to different targets, including GitHub repositories, continuous integration pipelines, documentation, and more.</p>


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

<h3 id="design-tokens-dev">Design-tokens.dev</h3>

<p><a href="https://design-tokens.dev/">Design-tokens.dev</a> is a helpful reference if you want tips for things like how to structure tokens, format them (e.g., JSON, YAML, and so on), and think about token types.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="370"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png"
			
			sizes="100vw"
			alt="Design-tokens.dev screen showing the output of named design tokens generated by the system."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/2-design-tokens-dev-screen.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="namedesigntokens-guide">NameDesignTokens.guide</h3>

<p><a href="https://namedesigntokens.guide/">NamedDesignTokens.guide</a> helps with naming conventions, which is honestly a common pain point, especially when you’re working with a large number of tokens.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="644"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png"
			
			sizes="100vw"
			alt="Token configuration"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/3-token-configuration.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once your tokens are set and connected, you’ll spend way less time fixing inconsistencies. It also gives you a solid base to scale, whether that’s adding themes, switching brands, or even building systems for multiple products.</p>

<p>That’s also when naming really starts to count. If your tokens or components aren’t clearly named, things can get confusing quickly.</p>

<p><strong>Note</strong>: <em>Vitaly Friedman’s “<a href="https://www.linkedin.com/posts/vitalyfriedman_how-to-name-things-httpslnkdineirqgv9a-activity-7338149568607363073-j0">How to Name Things</a>” is worth checking out if you’re working with larger systems.</em></p>

<p>From there, it’s all about components. Tokens define the values, but components are what people actually use, e.g., buttons, inputs, cards, dropdowns &mdash; you name it. In a perfect setup, you build a component once and reuse it everywhere. But without structure, it’s easy for things to “drift” out of scope. It’s easy to end up with five versions of the same button, and what’s in code doesn’t match what’s in Figma, for example.</p>

<blockquote>Automation doesn’t replace design, but rather, it connects everything to one source.</blockquote>

<p>The Figma component matches the one in production, the documentation updates when the component changes, and the whole team is pulling from the same library instead of rebuilding their own version. This is where real collaboration happens.</p>

<p>Here are a few tools that help make that happen:</p>

<table class="tablesaw break-out">
    <thead>
        <tr>
            <th>Tool</th>
            <th>What It Does</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><a href="https://www.uxpin.com/merge">UXPin Merge</a></td>
            <td>Lets you design using real code components. What you prototype is what gets built.</td>
        </tr>
        <tr>
            <td><a href="https://www.supernova.io/">Supernova</a></td>
            <td>Helps you publish a design system, sync design and code sources, and keep documentation up-to-date.</td>
        </tr>
        <tr>
            <td><a href="https://zeroheight.com/">Zeroheight</a></td>
            <td>Turns your Figma components into a central, browsable, and documented system for your whole team.</td>
        </tr>
    </tbody>
</table>

<h2 id="how-does-everything-connect">How Does Everything Connect?</h2>

<p>A lot of the work starts right inside your design application. Once your tokens and components are in place, tools like Supernova help you take it further by extracting design data, syncing it across platforms, and generating production-ready code. You don’t need to write custom scripts or use the Figma API to get value from automation; these tools handle most of it for you.</p>

<p>But for teams that want full control, <a href="https://www.figma.com/developers/api">Figma does offer an API</a>. It lets you do things like the following:</p>

<ul>
<li>Pull token values (like colors, spacing, typography) directly from Figma files,</li>
<li>Track changes to components and variants,</li>
<li>Tead metadata (like style names, structure, or usage patterns), and</li>
<li>Map which components are used where in the design.</li>
</ul>

<p>The Figma API is <strong>REST-based</strong>, so it works well with custom scripts and automations. You don’t need a huge setup, just the right pieces. On the development side, teams usually use Node.js or Python to handle automation. For example:</p>

<ul>
<li>Fetch styles from Figma.</li>
<li>Convert them into JSON.</li>
<li>Push the values to a design token repo or directly into the codebase.</li>
</ul>

<p>You won’t need that level of setup for most use cases, but it’s helpful to know it’s there if your team outgrows no-code tools.</p>

<ul>
<li>Where do your tokens and components come from?</li>
<li>How do updates happen?</li>
<li>What tools keep everything connected?</li>
</ul>

<p>The workflow becomes easier to manage once that’s clear, and you spend less time trying to fix changes or mismatches. When tokens, components, and documentation stay in sync, your team moves faster and spends less time fixing the same issues.</p>

<h2 id="extracting-design-data">Extracting Design Data</h2>

<p><strong>Figma</strong> is a collaborative design tool used to create UIs: buttons, layouts, styles, components, everything that makes up the visual language of the product. It’s also where all your design data lives, which includes the tokens we talked about earlier. This data is what we’ll extract and eventually connect to your codebase. But first, you’ll need a setup.</p>

<p>To follow along:</p>

<ol>
<li>Go to <a href="https://figma.com">figma.com</a> and create a free account.</li>
<li>Download the Figma desktop app if you prefer working locally, but keep an eye on system requirements if you’re on an older device.</li>
</ol>

<p>Once you’re in, you’ll see a home screen that looks something like the following:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.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/automating-design-systems-tips-resources/4-figma-dashboard.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png"
			
			sizes="100vw"
			alt="Figma dashboard showing a left sidebar navigation for exploring design files and a grid of thumbnail images on the right for previewing specific files."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/4-figma-dashboard.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>From here, it’s time to set up your design tokens. You can either create everything from scratch or <a href="https://www.figma.com/templates/">use a template from the Figma community</a> to save time. Templates are a great option if you don’t want to build everything yourself. But if you prefer full control, creating your setup totally works too.</p>

<p>There are other ways to get tokens as well. For example, a site like <a href="https://namedesigntokens.guide/">namedesigntokens.guide</a> lets you generate and download tokens in formats like JSON. The only catch is that Figma doesn’t let you import JSON directly, so if you go that route, you’ll need to bring in a middle tool like Specify to bridge that gap. It helps sync tokens between Figma, GitHub, and other places.</p>

<p>For this article, though, we’ll keep it simple and stick with Figma. Pick any design system template from the Figma community to get started; there are plenty to choose from.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="480"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png"
			
			sizes="100vw"
			alt="Showing a collection of Figma templates contributed by community members."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/5-collection-figma-templates.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Depending on the template you choose, you’ll get a pre-defined set of tokens that includes colors, typography, spacing, components, and more. These templates come in all types: website, e-commerce, portfolio, app UI kits, you name it. For this article, we’ll be using the <a href="https://www.figma.com/community/file/1055785285964148921"><strong>/Design-System-Template&ndash;Community</strong></a> because it includes most of the tokens you’ll need right out of the box. But feel free to pick a different one if you want to try something else.</p>

<p>Once you’ve picked your template, it’s time to download the tokens. We’ll use <strong>Supernova</strong>, a tool that connects directly to your Figma file and pulls out design tokens, styles, and components. It makes the design-to-code process a lot smoother.</p>

<h3 id="step-1-sign-up-on-supernova">Step 1: Sign Up on Supernova</h3>

<p>Go to <a href="https://supernova.io">supernova.io</a> and create an account. Once you’re in, you’ll land on a dashboard that looks like this:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="373"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png"
			
			sizes="100vw"
			alt="Supernova dashboard in an empty state. There is navigation in the left sidebar and a summary of activity in the main content showing no design tokens, components, assets, or documentation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/6-supernova-dashboard.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="step-2-connect-your-figma-file">Step 2: Connect Your Figma File</h3>

<p>To pull in the tokens, head over to the <strong>Data Sources</strong> section in Supernova and choose <strong>Figma</strong> from the list of available sources. (You’ll also see other options like Storybook or Figma variables, but we’re focusing on Figma.) Next, click on <strong>Connect a new file,</strong> paste the link to your Figma template, and click <strong>Import</strong>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="384"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png"
			
			sizes="100vw"
			alt="Supernova dashboard to connect Figma files"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/7-supernova-figma.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Supernova will load the full design system from your template. From your dashboard, you’ll now be able to see all the tokens.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="380"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png"
			
			sizes="100vw"
			alt="Supernova dashboard with tokens"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/8-supernova-figma.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="turning-tokens-into-code">Turning Tokens Into Code</h2>

<p>Design tokens are great inside Figma, but the real value shows when you turn them into code. That’s how the developers on your team actually get to use them.</p>

<p><strong>Here’s the problem</strong>: Many teams default to copying values manually for things like color, spacing, and typography. But when you make a change to them in Figma, the code is instantly out of sync. That’s why automating this process is such a big win.</p>

<p>Instead of rewriting the same theme setup for every project, you generate it, constantly translating designs into dev-ready assets, and keep everything in sync from one source of truth.</p>

<p>Now that we’ve got all our tokens in Supernova, let’s turn them into code. First, go to the <strong>Code Automation</strong> tab, then click <strong>New Pipeline</strong>. You’ll see different options depending on what you want to generate: React Native, CSS-in-JS, Flutter, Godot, and a few others.</p>

<p>Let’s go with the <strong>CSS-in-JS</strong> option for the sake of demonstration:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="383"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png"
			
			sizes="100vw"
			alt="Supernova Code Automation screen showing options for creating a new pipeline that pulls information from other services to produce code documentation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/9-supernova-code-automation-screen.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>After that, you’ll land on a setup screen with three sections: <strong>Data</strong>, <strong>Configuration</strong>, and <strong>Delivery</strong>.</p>

<h3 id="data">Data</h3>

<p>Here, you can pick a theme. At first, it might only give you “Black” as the option; you can select that or leave it empty. It really doesn’t matter for the time being.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="377"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png"
			
			sizes="100vw"
			alt="Supernova Code Automation screen"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/10-supernova-code-automation-screen.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="configuration">Configuration</h3>

<p>This is where you control how the code is structured. I picked <strong>PascalCase</strong> for how token names are formatted. You can also update how things like spacing, colors, or font styles are grouped and saved.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="380"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png"
			
			sizes="100vw"
			alt="Supernova Code Automation screen showing configuration of tokens"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/11-supernova-code-automation-screen-configuration.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="delivery">Delivery</h3>

<p>This is where you choose how you want the output delivered. I chose <strong>“Build Only”</strong>, which builds the code for you to download.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="375"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png"
			
			sizes="100vw"
			alt="Supernova Code Automation screen where you choose how you want the output delivered"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/12-supernova-code-automation-screen-delivery.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once you’re done, click <strong>Save</strong>. The pipeline is created, and you’ll see it listed in your dashboard. From here, you can download your token code, which is already generated.</p>

<h2 id="automating-documentation">Automating Documentation</h2>

<p>So, what’s the point of documentation in a design system?</p>

<p>You can think of it as the <strong>instruction manual</strong> for your team. It explains <em>what</em> each token or component is, <em>why</em> it exists, and <em>how</em> to use it. Designers, developers, and anyone else on your team can stay on the same page &mdash; no guessing, no back-and-forth. Just clear context.</p>

<p>Let’s continue from where we stopped. Supernova is capable of handling your documentation. Head over to the <strong>Documentation</strong> tab. This is where you can start editing everything about your design system docs, all from the same place.</p>

<p>You can:</p>

<ul>
<li>Add descriptions to your tokens,</li>
<li>Define what each base token is for (as well as what it’s <em>not</em> for),</li>
<li>Organize sections by colors, typography, spacing, or components, and</li>
<li>Drop in images, code snippets, or examples.</li>
</ul>

<p>You’re building the documentation inside the same tool where your tokens live. In other words, there’s no jumping between tools and no additional setup. That’s where the automation kicks in. You edit once, and your docs stay synced with your design source. It all stays in one environment.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.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/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png"
			
			sizes="100vw"
			alt="Supernova Code Automation screen where you automate documentation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/automating-design-systems-tips-resources/13-supernova-code-automation-screen-documentation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once you’re done, click <strong>Publish</strong> and you will be presented with a new window asking you to sign in. After that, you’re able to access your live documentation site.</p>

<h2 id="practical-tips-for-automations">Practical Tips For Automations</h2>

<p>Automation is great. It saves hours of manual work and keeps your design system tight across design and code. The trick is knowing when to automate and how to make sure it keeps working over time. You don’t need to automate everything right away. But if you’re doing the same thing over and over again, that’s a kind of red flag.</p>

<p>A few signs that it’s time to consider using automation:</p>

<ul>
<li>You’re using <strong>the same styles across multiple platforms</strong> (like web and mobile).</li>
<li>You have a <strong>shared design system</strong> used by more than one team.</li>
<li><strong>Design tokens change often</strong>, and you want updates to flow into code automatically.</li>
<li>You’re <strong>tired of manual updates</strong> every time the brand team tweaks a color.</li>
</ul>

<p>There are three steps you need to consider. Let’s look at each one.</p>

<h3 id="step-1-keep-an-eye-on-tools-and-api-updates">Step 1: Keep An Eye On Tools And API Updates</h3>

<p>If your pipeline depends on design tools, like Figma, or platforms, like Supernova, you’ll want to know when changes are made and evaluate how they impact your work, because even small updates can quietly affect your exports.</p>

<p>It’s a good idea to check <a href="https://www.figma.com/developers/api#changelog">Figma’s API changelog</a> now and then, especially if something feels off with your token syncing. They often update how variables and components are structured, and that can impact your pipeline. There’s also an <a href="https://www.figma.com/release-notes/">RSS feed for product updates</a>.</p>

<p>The same goes for <a href="https://updates.supernova.io">Supernova’s product updates</a>. They regularly roll out improvements that might tweak how your tokens are handled or exported. If you’re using open-source tools like <a href="https://v4.styledictionary.com">Style Dictionary</a>, keeping an eye on the GitHub repo (particularly the Issues tab) can save you from debugging weird token name changes later.</p>

<p>All of this isn’t about staying glued to release notes, but having a system to check if something suddenly stops working. That way, you’ll catch things before they reach production.</p>

<h3 id="step-2-break-your-pipeline-into-smaller-steps">Step 2: Break Your Pipeline Into Smaller Steps</h3>

<p>A common trap teams fall into is trying to automate <em>everything</em> in one big run: colors, spacing, themes, components, and docs, all processed in a single click. It sounds convenient, but it’s hard to maintain, and even harder to debug.</p>

<p>It’s much more manageable to split your automation into pieces. For example, having a single workflow that handles your core design tokens (e.g., colors, spacing, and font sizes), another for theme variations (e.g., light and dark themes), and one more for component mapping (e.g., buttons, inputs, and cards). This way, if your team changes how spacing tokens are named in Figma, you only need to update one part of the workflow, not the entire system. It’s also <strong>easier to test and reuse smaller steps</strong>.</p>

<h3 id="step-3-test-the-output-every-time">Step 3: Test The Output Every Time</h3>

<p>Even if everything runs fine, always take a moment to check the exported output. It doesn’t need to be complicated. A few key things:</p>

<ul>
<li><strong>Are the token names clean and readable?</strong><br />
If you see something like <code>PrimaryColorColorText</code>, that’s a red flag.</li>
<li><strong>Did anything disappear or get renamed unexpectedly?</strong><br />
It happens more often than you think, especially with typography or spacing tokens after design changes.</li>
<li><strong>Does the UI still work?</strong><br />
If you’re using something like Tailwind, CSS variables, or custom themes, double-check that the new token values aren’t breaking anything in the design or build process.</li>
</ul>

<p>To catch issues early, it helps to run tools like <a href="https://eslint.org">ESLint</a> or <a href="https://stylelint.io">Stylelint</a> right after the pipeline completes. They’ll flag odd syntax or naming problems before things get shipped.</p>

<h2 id="how-ai-can-help">How AI Can Help</h2>

<p>Once your automation is stable, there’s a next layer that can boost your workflow: AI. It’s not just for writing code or generating mockups, but for helping with the small, repetitive things that eat up time in design systems. When used right, AI can assist without replacing your control over the system.</p>

<p>Here’s where it might fit into your workflow:</p>

<h3 id="naming-suggestions">Naming Suggestions</h3>

<p>When you’re dealing with hundreds of tokens, naming them clearly and consistently is a real challenge. Some AI tools can help by suggesting clean, readable names for your tokens or components based on patterns in your design. It’s not perfect, but it’s a good way to kickstart naming, especially for large teams.</p>

<h3 id="pattern-recognition">Pattern Recognition</h3>

<p>AI can also spot repeated styles or usage patterns across your design files. If multiple buttons or cards share similar spacing, shadows, or typography, tools powered by AI can group or suggest components for systemization even before a human notices.</p>

<h3 id="automated-documentation">Automated Documentation</h3>

<p>Instead of writing everything from scratch, AI can generate first drafts of documentation based on your tokens, styles, and usage. You still need to review and refine, but it takes away the blank-page problem and saves hours.</p>

<p>Here are a few tools that already bring AI into the design and development space in practical ways:</p>

<ul>
<li><a href="https://uizard.io/"><strong>Uizard</strong></a>: Uizard uses AI to turn wireframes into designs automatically. You can sketch something by hand, and it transforms that into a usable mockup.</li>
<li><a href="https://www.animaapp.com/"><strong>Anima</strong></a>: Anima can convert Figma designs into responsive React code. It also helps fill in real content or layout structures, making it a powerful bridge between design and development, with some AI assistance under the hood.</li>
<li><a href="https://www.builder.io/"><strong>Builder.io</strong></a>: Builder uses AI to help generate and edit components visually. It&rsquo;s especially useful for marketers or non-developers who need to build pages fast. AI helps streamline layout, content blocks, and design rules.</li>
</ul>

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

<p>This article is not about achieving complete automation in the technical sense, but more about using <strong>smart tools to streamline the menial and manual aspects of working with design systems</strong>. Exporting tokens, generating docs, and syncing design with code can be automated, making your process quicker and more reliable with the right setup.</p>

<p>Instead of rebuilding everything from scratch every time, you now have a way to keep things consistent, stay organized, and save time.</p>

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

<ul>
<li>“<a href="https://thedesignsystem.guide/">Design System Guide</a>” by Romina Kavcic</li>
<li>“<a href="https://www.smashingmagazine.com/2025/05/design-system-in-90-days/">Design System In 90 Days</a>” by Vitaly Friedman</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>Amejimaobari Ollornwi</author><title>Handling JavaScript Event Listeners With Parameters</title><link>https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/</link><pubDate>Mon, 21 Jul 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/</guid><description>Event listeners are essential for interactivity in JavaScript, but they can quietly cause memory leaks if not removed properly. And what if your event listener needs parameters? That’s where things get interesting. Amejimaobari Ollornwi shares which JavaScript features make handling parameters with event handlers both possible and well-supported.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/" />
              <title>Handling JavaScript Event Listeners With Parameters</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Handling JavaScript Event Listeners With Parameters</h1>
                  
                    
                    <address>Amejimaobari Ollornwi</address>
                  
                  <time datetime="2025-07-21T10:00:00&#43;00:00" class="op-published">2025-07-21T10:00:00+00:00</time>
                  <time datetime="2025-07-21T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>JavaScript event listeners are very important, as they exist in almost every web application that requires interactivity. As common as they are, it is also essential for them to be managed properly. Improperly managed event listeners can lead to memory leaks and can sometimes cause performance issues in extreme cases.</p>

<p>Here’s the real problem: <strong>JavaScript event listeners are often not removed after they are added.</strong> And when they are added, they do not require parameters most of the time &mdash; except in rare cases, which makes them a little trickier to handle.</p>

<p>A common scenario where you may need to use parameters with event handlers is when you have a dynamic list of tasks, where each task in the list has a “Delete” button attached to an event handler that uses the task’s ID as a parameter to remove the task. In a situation like this, it is a good idea to remove the event listener once the task has been completed to ensure that the deleted element can be successfully cleaned up, a process known as <a href="https://javascript.info/garbage-collection">garbage collecti</a><a href="https://javascript.info/garbage-collection">on</a>.</p>

<h2 id="a-common-mistake-when-adding-event-listeners">A Common Mistake When Adding Event Listeners</h2>

<p>A very common mistake when adding parameters to event handlers is calling the function with its parameters inside the <code>addEventListener()</code> method. This is what I mean:</p>

<pre><code class="language-javascript">button.addEventListener('click', myFunction(param1, param2));
</code></pre>

<p>The browser responds to this line by immediately calling the function, irrespective of whether or not the click event has happened. In other words, the function is invoked right away instead of being deferred, so it never fires when the click event actually occurs.</p>

<p>You may also receive the following console error in some cases:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="75"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png"
			
			sizes="100vw"
			alt="Uncaught TypeError"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Uncaught TypeError: Failed to execute. <code>addEventListener</code> on <code>EventTarget</code>: parameter is not of type <code>Object</code>. (<a href='https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This error makes sense because the second parameter of the <code>addEventListener</code> method <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#listener">can only accept</a> a JavaScript function, an object with a <code>handleEvent()</code> method, or simply <code>null</code>. A quick and easy way to avoid this error is by changing the second parameter of the <code>addEventListener</code> method to an arrow or anonymous function.</p>

<pre><code class="language-javascript">button.addEventListener('click', (event) =&gt; {
  myFunction(event, param1, param2); // Runs on click
});
</code></pre>

<p>The only hiccup with using arrow and anonymous functions is that they cannot be removed with the traditional <code>removeEventListener()</code> method; you will have to make use of <code>AbortController</code>, which may be overkill for simple cases. <code>AbortController</code> shines when you have multiple event listeners to remove at once.</p>

<p>For simple cases where you have just one or two event listeners to remove, the <code>removeEventListener()</code> method still proves useful. However, in order to make use of it, you’ll need to store your function as a reference to the listener.</p>

<h2 id="using-parameters-with-event-handlers">Using Parameters With Event Handlers</h2>

<p>There are several ways to include parameters with event handlers. However, for the purpose of this demonstration, we are going to constrain our focus to the following two:</p>

<h3 id="option-1-arrow-and-anonymous-functions">Option 1: Arrow And Anonymous Functions</h3>

<p>Using arrow and anonymous functions is the fastest and easiest way to get the job done.</p>

<p>To add an event handler with parameters using arrow and anonymous functions, we’ll first need to call the function we’re going to create inside the arrow function attached to the event listener:</p>

<pre><code class="language-javascript">const button = document.querySelector("#myButton");

button.addEventListener("click", (event) =&gt; {
  handleClick(event, "hello", "world");
});
</code></pre>

<p>After that, we can create the function with parameters:</p>

<pre><code class="language-javascript">function handleClick(event, param1, param2) {
  console.log(param1, param2, event.type, event.target);
}
</code></pre>

<p>Note that with this method, removing the event listener requires the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController"><code>AbortController</code></a>. To remove the event listener, we create a new <code>AbortController</code> object and then retrieve the <code>AbortSignal</code> object from it:</p>

<pre><code class="language-javascript">const controller = new AbortController();
const { signal } = controller;
</code></pre>

<p>Next, we can pass the <code>signal</code> from the <code>controller</code> as an option in the <code>removeEventListener()</code> method:</p>

<pre><code class="language-javascript">button.addEventListener("click", (event) =&gt; {
  handleClick(event, "hello", "world");
}, { signal });
</code></pre>

<p>Now we can remove the event listener by calling <code>AbortController.abort()</code>:</p>

<pre><code class="language-javascript">controller.abort()
</code></pre>

<h3 id="option-2-closures">Option 2: Closures</h3>

<p>Closures in JavaScript are another feature that can help us with event handlers. Remember the mistake that produced a type error? That mistake can also be corrected with closures. Specifically, with closures, a function can access variables from its outer scope.</p>

<p>In other words, we can access the parameters we need in the event handler from the outer function:</p>

<div class="break-out">
<pre><code class="language-javascript">function createHandler(message, number) {
  // Event handler
  return function (event) {
  console.log(`${message} ${number} - Clicked element:`, event.target);
    };
  }

  const button = document.querySelector("&#35;myButton");
  button.addEventListener("click", createHandler("Hello, world!", 1));
}
</code></pre>
</div>

<p>This establishes a function that returns another function. The function that is created is then called as the second parameter in the <code>addEventListener()</code> method so that the inner function is returned as the event handler. And with the power of closures, the parameters from the outer function will be made available for use in the inner function.</p>

<p>Notice how the <code>event</code> object is made available to the inner function. This is because the inner function is what is being attached as the event handler. The event object is passed to the function automatically because it’s the event handler.</p>

<p>To remove the event listener, we can use the <code>AbortController</code> like we did before. However, this time, let’s see how we can do that using the <code>removeEventListener()</code> method instead.</p>

<p>In order for the <code>removeEventListener</code> method to work, a reference to the <code>createHandler</code> function needs to be stored and used in the <code>addEventListener</code> method:</p>

<div class="break-out">
<pre><code class="language-javascript">function createHandler(message, number) {
  return function (event) {
    console.log(`${message} ${number} - Clicked element:`, event.target);
  };
}
const handler = createHandler("Hello, world!", 1);
button.addEventListener("click", handler);
</code></pre>
</div>

<p>Now, the event listener can be removed like this:</p>

<pre><code class="language-javascript">button.removeEventListener("click", handler);
</code></pre>

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

<p>It is good practice to always remove event listeners whenever they are no longer needed to prevent memory leaks. Most times, event handlers do not require parameters; however, in rare cases, they do. Using JavaScript features like closures, <code>AbortController</code>, and <code>removeEventListener</code>, handling parameters with event handlers is both possible and well-supported.</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></channel></rss>