<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tools on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/tools/index.xml</link><description>Recent content in Tools on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 12 May 2026 16:11:33 +0000</lastBuildDate><item><author>Kyrylo Levashov</author><title>Rethinking The Experience Of System Tools</title><link>https://www.smashingmagazine.com/2026/05/rethinking-experience-system-tools/</link><pubDate>Tue, 05 May 2026 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/05/rethinking-experience-system-tools/</guid><description>Design always starts with function &amp;mdash; function shapes form. But if that function can’t be made completely invisible and people still have to interact with it, it inevitably becomes part of their experience. In this article, Kyrylo Levashov explains why the question has shifted from, “Should your utility software feel better to use?” to “Can your utility software afford not to?“</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/05/rethinking-experience-system-tools/" />
              <title>Rethinking The Experience Of System Tools</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Rethinking The Experience Of System Tools</h1>
                  
                    
                    <address>Kyrylo Levashov</address>
                  
                  <time datetime="2026-05-05T08:00:00&#43;00:00" class="op-published">2026-05-05T08:00:00+00:00</time>
                  <time datetime="2026-05-05T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>MacPaw</b></p>
                

<p>Your grandmother’s vacuum was a trusty but ugly workhorse hidden in a dark closet. Dyson turned that practical tool into an aspirational product, one you love leaving out even when guests come over. Dish soap was just dish soap until Method put it in a glass container, and it became an addition to, not a distraction from, the aesthetics of your kitchen. Physical product brands spent the last two decades transforming mundane, practical items like soap and vacuums into must-have experiences.</p>

<p>But utility software &mdash; especially maintenance tools, a type of system software designed to analyze, configure, optimize, and maintain a computer &mdash; hasn’t made that leap from something you open as a chore to an experience you choose with excitement. And that means those brands are missing an interesting design opportunity: these tools are well overdue for a more intelligent, more human, and less emotionally flat approach.</p>

<h2 id="the-most-underexplored-frontier-in-ux-is-the-maintenance-layer">“The Most Underexplored Frontier In UX Is The Maintenance Layer.”</h2>

<p>Utility software still feels like a chore. Using it has all the excitement of pulling out that dusty old vacuum from the back of the closet. These four common software design assumptions illustrate why the category hasn’t yet transcended its chore status.</p>

<ul>
<li><strong>Assuming the user already resents the task</strong>: they’re here because something is wrong, not because they chose to open this tool. Designing accordingly means assuming they want the software to be fast, clinical, invisible, and something to get out of the way, not get into. But a design built for resentment produces tools that deserve it. If you expect your users to want to get out of the product as fast as possible, they’ll feel it in the design.</li>
<li><strong>Assuming function is enough and feelings are for consumer apps</strong>: emotion in interface design is decoration. The maintenance layer is infrastructure, and nobody decorates infrastructure. But nobody decorated dish soap either, until Method. They didn’t change the product, just the user’s relationship to the tool they use to accomplish a task.</li>
<li><strong>Assuming your users are not your fans because nobody cares about maintenance tools</strong>: utility tools don’t build communities, and nobody posts about running a disk cleanup. But people care deeply about tools that respect their time and make complex things simple for them to use. The MacPaw team listens to our community and implements many of the features they ask for, because we know users can be fans too, and they should shape how our products work.</li>
<li><strong>Assuming that designers shouldn’t waste pixels on personality</strong>: you need to hide complexity and show minimal UI. Utility software should look neutral, technical, and forgettable.</li>
</ul>

<p>But when software hides the system, people lose trust in it.</p>

<p>Design always starts with function &mdash; function shapes form. But if that function can’t be made completely invisible and people still have to interact with it, it inevitably becomes part of their experience. In that case, people expect it not just to work, but to match their environment, influence their mood, and contribute to their overall experience.</p>

<p>A good example is a watch. Its core function is simple: show the time. But because a watch occupies physical space in a person’s world, you want more from it than just functionality. It needs to play an aesthetic role and complement the environment.</p>

<h2 id="the-maintenance-layer-is-a-behavioral-problem-not-just-a-ux-one">“The Maintenance Layer Is A Behavioral Problem, Not Just A UX One.”</h2>

<p>The user experience in utility software matters more than the industry tends to admit. In utility software, experience is not something added on top of function. It emerges from how the function is structured, explained, and interacted with. If you think you can design the most functional app on the market without considering how users understand and experience the process, you’re missing an opportunity to build a relationship with that user.</p>

<p>Part of that ignored UX element is a behavioral problem: users don’t avoid utility software because using it is hard, but instead because it produces <a href="https://research.macpaw.com/publications/emotional-assesments-ux">no positive emotional signal</a> at any point. The problem is rarely complex. It’s the absence of meaningful interaction during the process of using the app.</p>

<p>Another issue is focusing solely on function. <a href="https://uxmag.com/articles/the-aesthetic-usability-effect-why-beautiful-looking-products-are-preferred-over-usable-but-not-beautiful-ones">The aesthetic-usability effect</a> shows us clearly that if something looks better, it feels better &mdash; ATM screens in a 1995 study were judged easier to use if the screen layout was more attractive. Even something as purely functional as an ATM screen display needs attention to how the function is structured, presented, and perceived.</p>

<p>And then there’s the memory problem. People remember <a href="https://www.researchgate.net/publication/5246508_Evaluations_of_Pleasurable_Experiences_The_Peak-End_Rule">the emotional peak and the ending of an experience</a>, not the average. A completed process that ends with a clear “done” is remembered more positively than one that just fades out, even if the end task is completed successfully in both cases. System tools rarely intentionally design the ending of an interaction &mdash; they just stop running.</p>

<h2 id="thoughtful-system-design-can-transform-maintenance-from-a-technical-chore-into-a-seamless-user-experience">“Thoughtful System Design Can Transform Maintenance From A Technical Chore Into A Seamless User Experience.”</h2>

<p>What does emotional design actually mean, then, in utility UX? Here are three principles the MacPaw team follows to design its products against the category norm.</p>

<h3 id="translating-system-complexity-into-human-language">Translating system complexity into human language</h3>

<p>Maintenance tools deal with storage, task management, and background processes. Good design explains what’s happening, avoids system jargon, and communicates outcomes clearly.</p>

<p><a href="https://www.figma.com/blog/the-linear-method-opinionated-software/">Linear’s game-changing move</a> that illustrates this principle was agreeing on straightforward units of work, like projects and teams, that any new user can immediately understand. That helps them spend less time ramping up and more time building.</p>

<h3 id="make-the-process-clear-and-show-progress">Make the process clear and show progress</h3>

<p>System tools run complex processes. Design should show progress, impact, and system change to create trust and control.</p>

<p>Vercel’s deployment infrastructure is an excellent example here. When you trigger a build, the browser tab favicon changes &mdash; a spinner while building, a green checkmark when done, a red X if it fails. It’s ruthlessly functional, not visual or warm, but it’s emotionally intelligent: it exists purely to reduce the low-level anxiety of waiting for a build to finish.</p>

<h3 id="design-the-moment-of-completion">Design the moment of completion</h3>

<p>Maintenance tasks often end quietly. But completion is the emotional payoff. Design should emphasize clarity of results, a sense of resolution, and visible improvement so users remember a positive and distinct ending.</p>

<p>Take the new CleanMyMac by MacPaw <a href="https://macpaw.com/news/introducing-new-cleanmymac">after its 2024 major update</a>. Unlike the maintenance utility category norm, CleanMyMac uses visual language, including color, depth, motion, icons, and 3D illustrations, to shift the focus from diagnosing problems to showing progress: space cleared, threats removed, time saved. Instead of confronting the user with what&rsquo;s wrong, the interface closes with a picture of a machine that&rsquo;s already working better.</p>

<p>The task is the same, but the ending tells a different story, giving the user a picture of a machine that&rsquo;s already working better.</p>

<h2 id="even-if-you-don-t-care-about-emotional-design-as-a-principle-the-change-is-coming-anyway">“Even if you don’t care about emotional design as a principle, the change is coming anyway.”</h2>

<p>The market is forcing this issue even for those who don’t find the argument I’ve made here compelling.</p>

<p>That’s partly generational &mdash; designers and users who grew up with Linear, Figma, and Notion have a completely different baseline for the tools they use. Good software is not a happy accident for them, but a given. That generation is now the primary audience for maintenance software, and so the old “it’s fine, it’s just a utility” excuse doesn’t work philosophically or commercially. Just like Dyson and Method changed how entire product categories approached design, the current state of utility software is shifting for good.</p>

<p>And digital fatigue is the current cultural state. The resurgence of vinyl records, film cameras, and dumbphones is not merely nostalgia, but a signal that the emotional relationship between people and their tools is changing.</p>

<p>The question has shifted from whether your utility software should feel better to use to whether it can afford not to.</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>(il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Ruben Ferreira Duarte</author><title>Testing Font Scaling For Accessibility With Figma Variables</title><link>https://www.smashingmagazine.com/2026/03/testing-font-scaling-accessibility-figma-variables/</link><pubDate>Tue, 24 Mar 2026 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/03/testing-font-scaling-accessibility-figma-variables/</guid><description>Accessibility works best when it blends into everyday design workflows. The goal isn’t a big transformation, but simple work processes that fit naturally into a team’s routine. With Figma variables, testing font size increases becomes part of the design flow itself, making accessibility feel almost inevitable rather than optional.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/03/testing-font-scaling-accessibility-figma-variables/" />
              <title>Testing Font Scaling For Accessibility With Figma Variables</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Testing Font Scaling For Accessibility With Figma Variables</h1>
                  
                    
                    <address>Ruben Ferreira Duarte</address>
                  
                  <time datetime="2026-03-24T13:00:00&#43;00:00" class="op-published">2026-03-24T13:00:00+00:00</time>
                  <time datetime="2026-03-24T13:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Building a true culture of digital accessibility in a company is a mission of resilience and perseverance. It’s not difficult for the discourse on accessibility to fall into the usual clichés. Accessibility is very important for people. The accessibility of digital products and services promotes inclusion. Or even, all professionals on the teams should be involved in accessibility work. Of course. No one in their right mind will dispute any of these statements (I hope).</p>

<p>However, the second part of this conversation, which very few companies reach, is <em>“how?”</em> How do we make this happen in the midst of the day-to-day work of digital transformation teams, which, as we all know, are immersed in demanding scripts, often with a very limited number of people available? Most of the time, the choice ends up being between <em>“we do this”</em> and <em>“that.”</em> And it shouldn’t, because, in these cases, I never saw accessibility winning in this equation.</p>

<p>It shouldn’t be this way. You don’t need to be this way. First of all, because choosing between accessibility and anything else isn’t the right choice. Accessibility is no longer just another feature to be added to the others. It’s an added value for the business and, currently, a legal obligation that can have serious consequences for companies. On the other hand, there are intelligent, optimized, and impactful ways to incorporate accessibility principles into the natural dynamics of teams. It’s possible to work on accessibility without turning team operations upside down. In essence, that’s what AccessibilityOps does. <strong>Empowering people</strong> and <strong>providing teams with simple processes</strong> so they can  <strong>integrate accessibility work into their daily routines</strong> without disproportionate effort.</p>

<h2 id="accessibility-and-design">Accessibility And Design</h2>

<p>Working on digital accessibility in design can involve several actions. It’s clear that we need to pay particular attention to color and how it’s used to convey meaning. Of course, the interaction sizes of elements must be comfortable. But, most importantly, we must <strong>think about design from a versatile perspective</strong>. An interface isn’t a poster. We can control many aspects of that design, but how users interact with the interface is subject to an endless number of variables. The type of device, context, purpose, network quality, etc. All of this greatly affects each person’s experience and interaction. Along with all this, when digital accessibility concerns are brought into the design process, it adds even more variables.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.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/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png"
			
			sizes="100vw"
			alt="Assistive technologies"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/01-assistive-technologies.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>People often use what are called <strong>assistive technologies and strategies</strong>. Basically, these are technological tools or, at the very least, “tricks” that people resort to in order to find more comfortable usage models. The famous screen readers, commonly associated with the use of blind people (but which are not only useful to them), for example, are an assistive technology. Changing colors or color contrasts between different elements is also an assistive technology. Increasing the font size (which we discussed in this text) is another example. There are countless assistive technologies and strategies. Almost as many as the different contexts of use for each person.</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><strong>Web forms</strong> are at the center of every meaningful interaction. Meet Adam Silver&rsquo;s <strong><a href="https://www.smashingmagazine.com/printed-books/form-design-patterns/">Form Design Patterns</a></strong>, a practical guide to <strong>designing and building forms</strong> for the web.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/form-design-patterns/" 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://www.smashingmagazine.com/printed-books/form-design-patterns/" 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/64e57b41-b7f1-4ae3-886a-806cce580ef9/form-design-patterns-shop-image-1-1.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/51e0f837-d85d-4b28-bfab-1c9a47f0ce33/form-design-patterns-shop-image.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="we-don-t-control-everything">We Don’t Control Everything</h2>

<p>In other words (and this is the “bad news” for us designers), “our design” is subject, from the users’ perspective, to transformations that we don’t control. It will be “transformed” by the user, ensuring that they can interact with the application and everything it offers in the most comfortable way possible. And that’s a good thing. If this happens and everything goes well, we will have surely done our accessibility work very well, and we all deserve congratulations. If the user applies any of these support technologies and strategies and still cannot use the digital application, it’s a sign that something is not working as it should.</p>

<p>Oh, and speaking of which. Don’t even think about blocking the use of these technologies or support strategies. They may be “destroying” your beautiful design, but they are allowing more and more people to actually use the app. In the end, wasn’t that exactly what we promised we wanted to do? Design for (all) people. Without exception?</p>

<h2 id="increase-font-size">Increase Font Size</h2>

<p>How many times have we heard someone &mdash; friends, family, or even colleagues &mdash; complaining that this or that text is too small? Text plays a very important role in the digital experience. Much information is conveyed through text: instructions for use, button captions, or interactive elements. All of this uses text as a communication tool. If reading all these elements is difficult, naturally, the experience is severely impaired.</p>

<p>Comfortable text reading, regardless of its function, is a non-negotiable principle. This reading can be facilitated by using comfortable sizes in the design. However, supporting technologies and strategies, through the functionality of increasing font size, can also help improve readability. According to <a href="https://appt.org/en/stats/font-size">APPT</a> data, <strong>26% of Android and iOS mobile device users</strong> increase the default font size (data from February 2026). One in four users increases the font size on their smartphone. This is a very significant sample of people, making this functionality unavoidable in design processes.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.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/testing-font-scaling-accessibility-figma-variables/02-chart.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png"
			
			sizes="100vw"
			alt="Chart with font sizes where 26% of users use large font-size."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/02-chart.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="compliance-with-guidelines">Compliance With Guidelines</h2>

<p>Increasing font size in interfaces can represent a huge design challenge. It’s important to understand that, suddenly, some text elements, due to user actions, can double in size from their initial size.</p>

<blockquote>“With the exception of captions and text images, text can be resized without assistive technology up to 200% without loss of content or functionality.”<br /><br />&mdash; <a href="https://acessibilidade.gov.pt/wcag/#resize-text">Success criterion 1.4.4, “Resizing Text”</a> of the Web Content Accessibility Guidelines (WCAG), version 2.2</blockquote>

<p>This success criterion is at the AA compliance level, meaning this is an absolutely mandatory feature according to any legal framework.</p>

<p>It’s easy to understand the 200% in this success criterion. If we assume we design the interfaces at a 100% scale, meaning the element size is the initial size, then increasing the text by up to 200% will correspond to doubling the initial size. Other enlargement scales can also be used, such as 120%, 140%, and so on. In other words, we have to ensure that users can increase the text to double its initial size through supporting technologies or strategies (and this is not a minor detail).</p>

<p>To comply with this standard, we don’t need to provide text size increase tools in the interfaces. In practice, these features are nothing more than redundancy. Devices already allow this to be done in a standardized way. Users who really need this setting know it (because, without it, their lives would be much more difficult). Well, they already have this setting applied across their device. And that means we can eliminate these additional interface elements, simplifying the experience.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.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/testing-font-scaling-accessibility-figma-variables/03-feature.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png"
			
			sizes="100vw"
			alt="Text size increase tool in the interface"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/03-feature.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="standardized-access">Standardized Access</h2>

<p>An important concept to remember about assistive technologies, particularly in this case regarding increasing font size, is that most devices already have many of these tools installed by default. In other words, in many cases, users don’t need to purchase their own software or buy a specific type of device just to have this functionality.</p>

<p>Whether on mobile devices or even in web browsers, in the vast majority of cases, it’s easy to find installed features that allow you to increase the default font size we’re using throughout the interface. This principle of increasing font size can be applied to digital products, such as apps, or even to any type of website running on the standard web browsers used today.</p>

<h3 id="iphones">iPhones</h3>

<p>On iPhone devices, the font size increase feature is integrated by default. To use this feature, simply access the “Settings” panel, select “Accessibility,” and within the “Vision” options group, access the “Text Size and Display” feature and configure the desired font size increase on that screen.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.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/testing-font-scaling-accessibility-figma-variables/04-iphones.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png"
			
			sizes="100vw"
			alt="iPhone screens with settings on accessibility"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/04-iphones.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="google-chrome">Google Chrome</h3>

<p>Web browsers also offer, by default, the functionality to increase font size. For example, in Google Chrome, this feature is available in the “Options” panel, specifically in the “Appearance” area. In the list of options that appear in this group, simply select the “Font size” option. Normally, the “Medium &mdash; Recommended” option will be selected. You can change this setting to any other available font size. Try, for example, the “Very large” option.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.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/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png"
			
			sizes="100vw"
			alt="Google Chrome settings on accessibility"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/05-google-chrome.png'>Large preview</a>)
    </figcaption>
  
</figure>

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

<h2 id="test-in-figma">Test In Figma</h2>

<p>To ensure that digital accessibility work becomes effective in the daily lives of teams, it is essential to find <strong>simple work processes</strong>. Actions or initiatives that can be integrated into the team’s routine, that address accessibility in an integrated way, and do not require a dramatic transformation of the current reality. If that were necessary, he believes, it wouldn’t happen most of the time. Therefore, designing simple work processes is half the battle for accessibility to truly happen, in this case, also within a design team.</p>

<p>Regarding testing font size increases in design, we have extraordinary tools at our disposal today. Those who remember the days of designing complex interfaces in <a href="https://www.adobe.com/pt/products/photoshop.html">Adobe Photoshop</a> will recognize the differences in the tools we have today (and thankfully so). It’s now possible, through tools like <a href="https://www.figma.com/design/">Figma</a>, to create such dynamism in design that testing font size increases for accessibility becomes almost unavoidable for the team.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.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/testing-font-scaling-accessibility-figma-variables/06-hands-on.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png"
			
			sizes="100vw"
			alt="Visualization on font sizes"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/06-hands-on.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: To take this test, you need to have a strong grasp of Figma’s <a href="https://help.figma.com/hc/en-us/articles/360039957034-Create-and-apply-text-styles">text styles</a>, <a href="https://help.figma.com/hc/en-us/articles/360040451373-Guide-to-auto-layout">auto layouts</a>, and <a href="https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma">variables</a>. These three are fundamental tools for success without much extra effort. If you haven’t yet mastered these features, it’s highly recommended that you start there. Don’t skip steps. Learning is a gradual process that must be followed in a structured, step-by-step manner.</p>

<h3 id="where-do-we-want-to-go">Where Do We Want To Go?</h3>

<p>The font size increase test in Figma that we want to perform is simple. We want to have a set of variables available for all the text styles we use in the interface, allowing us to choose whether we want to see the interface with the text at a scale of 100%, 120%, 140%, 160%, 180%, or 200%. As we apply this set of variables (much like applying variables for light and dark mode), we observe the transformations of the text in the interface and understand to what extent adaptations are needed in each version of the interface with different typographic scales.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.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/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png"
			
			sizes="100vw"
			alt="Font scaling"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/07-font-scaling.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="how-do-we-make-this-happen">How Do We Make This Happen?</h3>

<p>For this test to go so smoothly, you need to do some groundwork. <a href="https://dxd.pt/design-systems/">Design systems</a> can greatly help optimize much of this initial work. But I won’t lie to you. For the test to work well, your design needs to have a very serious level of organization and systematization.</p>

<p>This isn’t really a guide, because each team will have its own work model, and these recommendations can be applied in different ways (and that’s okay). However, for this test to work, it’s important to ensure certain assumptions in the design. To help you phase the implementation of this test model, here are some steps to follow. Step-by-step instructions to guide you in organizing your files and ensuring you can fully execute this test in the simplest and most practical way possible.</p>

<h3 id="1-designing-the-interfaces">1. Designing The Interfaces</h3>

<p>It all starts with the design. Before any testing, the focus should, as it should, be on the design of each interface that we will want to test later. At this stage, there is still no specific concern with the font size increase test that we will perform later. Naturally, all interface design should, from the outset, follow the most basic <a href="https://www.a11yproject.com/checklist/">accessibility recommendations</a> applied to design.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.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/testing-font-scaling-accessibility-figma-variables/08-design-screens.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png"
			
			sizes="100vw"
			alt="Design screens"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/08-design-screens.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="2-apply-auto-layouts-to-all-elements">2. Apply Auto Layouts To All Elements</h3>

<p>In every screen design you create, you’ll need to ensure you apply auto layouts perfectly. This is a very important step. It’s this consistent application of auto layouts to the entire structure and design elements that will later guarantee the scalability of the interface when we start testing font size increases. You really can’t underestimate this step. If you don’t pay it the attention it deserves, you’ll see when we test typographic scaling in the interfaces, everything breaking down like an elephant in a china shop.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.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/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png"
			
			sizes="100vw"
			alt="Auto layout"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/09-auto-layout.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="3-structuring-and-applying-text-styles">3. Structuring And Applying Text Styles</h3>

<p>To perform our font size increase test, we’ll also need you to have applied text styles to each interface design. You probably even started creating them as you were drawing. Great. If you haven’t done so, it’s important that you do it now. For the test to work perfectly, we really need this. Don’t leave any text element in the design without a text style applied.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.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/testing-font-scaling-accessibility-figma-variables/10-text-styles.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png"
			
			sizes="100vw"
			alt="Text styles"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/10-text-styles.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="4-define-the-set-of-variables-100">4. Define The Set Of Variables 100%</h3>

<p>This test forces a fairly high degree of optimization. In practice, this means we will have to use Figma variables for all the characteristics of the text styles we have in the interface. At this stage, you must define Figma “number” variables for at least the font-size and line-height of the text styles you applied to the drawing. With this step, you are defining the font size increase scale values for a 100% visualization model, that is, the initial and reference version of the drawing. It is important that you structure these variables for each text style in the drawing because, subsequently, we will have to consider the enlargement scale of each of these text elements.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.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/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png"
			
			sizes="100vw"
			alt="Defining the set of variables 100%"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/11-variables-100-percent.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="5-apply-the-variables-to-the-text-styles">5. Apply The Variables To The Text Styles</h3>

<p>Having defined the variables for the 100% scale text styles, you must now apply them to the elements of the text styles already created. Don’t forget to apply variables at least to the font-size and line-height characteristics. If you have more typographical variables, that’s fine. But you should at least have variables applied to font-size and line-height. This is really very important.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.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/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png"
			
			sizes="100vw"
			alt="Applying variables to the text styles"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/12-text-styles-variables.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="6-define-the-variables-for-increasing-the-text-size">6. Define The Variables For Increasing The Text Size</h3>

<p>Now that you have the variables applied to the 100% scale text styles, the next step is to create the variables for the other font size increase scales. In practice, you have to create the variables that will tell the system what font size each text style will grow to when the increase scale is 120%, 140%, 160%, etc.</p>

<p>To define the font-size and line-height values, simply multiply the initial value by the scale percentage. For example, if a text style has a font-size of 16px, the size for the 120% scale will be 16 multiplied by 1.2, which gives a result of 19.2. Repeat this calculation for all font-size and line-height values of the font size increase scale percentages you choose.</p>

<p>You can also choose whether or not to apply rounding to the final values. This is an approximate test, and therefore any differences that may arise from rounding will not affect the final perception of the test result.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.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/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png"
			
			sizes="100vw"
			alt="Font scalling variables"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/13-font-scaling-variables.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="7-apply-variables-to-different-scale-versions">7. Apply Variables To Different Scale Versions</h3>

<p>The moment of truth has arrived. The next step is to understand if we have everything working so that the test runs perfectly. Therefore, you should copy the original interface and apply the set of variables for each of the font size increase rates that make sense to you. Repeat this process for all the font size increase percentages you have defined.</p>

<p>As a suggestion, you can use the 120%, 140%, 160%, 180%, and 200% increase percentages as a reference. If you want to simplify, you can reduce the number of scaling percentages you are working with. Regardless of the number of percentages you are working with, you should always work with the minimum of 100% and 200% scales.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.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/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png"
			
			sizes="100vw"
			alt="Applied variables to different scale versions"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/14-apply-different-scales.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="8-identify-areas-for-improvement">8. Identify Areas For Improvement</h3>

<p>By applying different font size increase scales to the same screen, it’s easy to understand where improvements might be needed. This is where the real test of increasing font size in interface design and the most interesting accessibility work begins.</p>

<p>In your analysis of the various screens, keep some important aspects in mind:</p>

<ul>
<li>The fact that the text appears gigantic isn’t a problem and doesn’t “ruin” the design. Remember that this can mean the difference between someone being able to use a particular product or service or not.</li>
<li>An accessibility problem exists when increasing the font size makes it impossible for the user to read certain texts or to activate certain controls.</li>
<li>For text elements that are already very large, increasing the font size might not make sense. Doing so could make those elements disproportionate, which wouldn’t improve readability (since they are already a good size) and would occupy completely unnecessary space.</li>
<li>If there are elements that appear to be popping out of the screen, the first step is to confirm how you are applying <strong>auto layout</strong>. Many design aspects can be easily resolved with the proper use of auto layout.</li>
<li>Regardless of the scale of font size increase, it is essential to maintain the <strong>visual hierarchy of the typography</strong>, as this readability is important for perceiving the different levels of information present on the screen.</li>
<li>This test can help identify elements that may need adjustments directly in the code to function well at a given scale of increase. Not everything can be solved through design alone, and that’s perfectly fine. Accessibility is essentially a team effort.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.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/testing-font-scaling-accessibility-figma-variables/15-critical-points.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png"
			
			sizes="100vw"
			alt="Critical points for improvement"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/15-critical-points.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="9-make-corrections-and-adjustments-to-the-design">9. Make Corrections And Adjustments To The Design</h3>

<p>Finally, based on the various screens with different text enlargement scales applied, you can make the design changes that make sense. Some of these adjustments may only be necessary in code. In these cases, you document all these suggestions and pass them on to the development team. It is also crucial to reinforce (again) that some of the problems you may encounter in the design can be quickly resolved in the design process, with the simple and correct application of auto-layout properties.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.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/testing-font-scaling-accessibility-figma-variables/16-make-changes.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png"
			
			sizes="100vw"
			alt="Design changes to those critical points"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/16-make-changes.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="10-go-back-to-the-beginning-and-repeat-the-process">10. Go Back To The Beginning And Repeat The Process</h3>

<p>This is a cyclical approach. This means you should repeat these steps, or variations thereof, as many times as necessary throughout the project. It’s natural that, over time and with process optimization, some of these steps will cease to make sense. That’s absolutely not a problem. But the most important thing to realize here is that accessibility and this process of testing font size increases shouldn’t be done just once, and that’s it. It’s a test to be done many, many times throughout the day-to-day work of each project and team.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.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/testing-font-scaling-accessibility-figma-variables/17-staring-point.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png"
			
			sizes="100vw"
			alt="Starting point"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/17-staring-point.png'>Large preview</a>)
    </figcaption>
  
</figure>

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

<h2 id="the-role-of-design-systems">The Role Of Design Systems</h2>

<p>At first glance, this list of steps might seem like a complex exercise. But it’s not. This is because the vast majority, if not all, of these steps are easy to execute in any context where a design system exists. In fact, design systems have become an <strong>unavoidable standard</strong> in the Product Design industry. We can discuss what each team calls a design system, but the truth is that it’s very difficult today to find a Product Design team that doesn’t have, at the very least, a minimally structured library of components and styles.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.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/testing-font-scaling-accessibility-figma-variables/18-design-systems.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png"
			
			sizes="100vw"
			alt="Visualization on design systems"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/18-design-systems.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>With this foundation, whether more or less documented, it’s very easy to apply this type of font size increase test using Figma variables. Furthermore, if your design system already has, for example, structured variables for light and dark mode, it means you’re already applying the exact same principles we used to perform this test. So, nothing new.</p>

<p>Working with design systems involves a level of <strong>structuring and organization</strong> that is also very useful for creating this type of test. There’s a myth that design systems limit creativity. This is not true. Design systems help solve the “bureaucratic” part of design, so we can actually have more time for what matters: in this case, testing accessibility and building more and more products and services that are truly accessible to the greatest number of people.</p>

<h2 id="example-file">Example File</h2>

<p>It’s always easier to see an example than just read a description of a process. If this is true in many disciplines of knowledge, in design, this premise makes even more sense. Therefore, in this <a href="https://www.figma.com/community/file/1600134823556764105">Figma file</a>, freely published and openly available to the community, you’ll find a <strong>practical example of the entire testing process</strong> described here. Remember that this is just an example. There may be countless ways to perform this type of test within the context of a Figma file.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.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/testing-font-scaling-accessibility-figma-variables/19-community.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png"
			
			sizes="100vw"
			alt="Visualization for the Figma file on testing font scaling"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/testing-font-scaling-accessibility-figma-variables/19-community.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Be sure to look at this approach with a critical eye. It’s a suggestion for testing font size increases that follows a specific process. Despite this, the approach should be adapted to your team’s specific reality, processes, and level of maturity. Simply copying formulas from other teams without understanding if they make sense in our own context is a sure way to make accessibility efforts disproportionate. Every situation is unique. This approach attempts to simplify accessibility work as much as possible in this specific context. And remember: if something happens, however small, it’s a step forward, not a step backward. And that should be celebrated by everyone on the team.</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>Godstime Aburu</author><title>Getting Started With The Popover API</title><link>https://www.smashingmagazine.com/2026/03/getting-started-popover-api/</link><pubDate>Mon, 02 Mar 2026 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/03/getting-started-popover-api/</guid><description>What happens if you rebuild a single tooltip using the browser’s native model without the aid of a library? The Popover API turns tooltips from something you simulate into something the browser actually understands. Opening and closing, keyboard interaction, Escape handling, and much of the accessibility now come from the platform itself, not from ad-hoc JavaScript.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/03/getting-started-popover-api/" />
              <title>Getting Started With The Popover API</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Getting Started With The Popover API</h1>
                  
                    
                    <address>Godstime Aburu</address>
                  
                  <time datetime="2026-03-02T10:00:00&#43;00:00" class="op-published">2026-03-02T10:00:00+00:00</time>
                  <time datetime="2026-03-02T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Tooltips feel like the smallest UI problem you can have. They’re tiny and usually hidden. When someone asks how to build one, the traditional answer almost always comes back using some JavaScript library. And for a long time, that was the sensible advice.</p>

<p>I followed it, too.</p>

<p>On the surface, a tooltip is simple. Hover or focus on an element, show a little box with some text, then hide it when the user moves away. But once you ship one to real users, the edges start to show. Keyboard users <code>Tab</code> into the trigger, but never see the tooltip. Screen readers announce it twice, or not at all. The tooltip flickers when you move the mouse too quickly. It overlaps content on smaller screens. Pressing <code>Esc</code> does not close it. Focus gets lost.</p>

<p>Over time, my tooltip code grew into something I didn’t really want to own anymore. Event listeners piled up. Hover and focus had to be handled separately. Outside clicks needed special cases. ARIA attributes had to be kept in sync by hand. Every small fix added another layer of logic.</p>

<p>Libraries helped, but they were also more like black boxes I worked around instead of fully understanding what was happening behind the scenes.</p>

<p>That was what pushed me to look at the newer <a href="https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute">Popover API</a>. I wanted to see what would happen if I rebuilt a single tooltip using the browser’s native model without the aid of a library.</p>

<p>As we start, it’s worth noting that, as with any new feature, there are some things with it that are still being ironed out. That said, it currently enjoys great browser support, although there are several pieces to the overall API that are in flux. It’s worth keeping an eye on <a href="https://caniuse.com/?search=popover+api">Caniuse</a> in the meantime.</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-old-tooltip">The “Old” Tooltip</h2>

<p>Before the Popover API, using a tooltip library was not a shortcut. It was the default. Browsers didn’t have a native concept of a tooltip that worked across mouse, keyboard, and assistive technology. If you cared about correctness, your only option was to use a library, and that is exactly what I did.</p>

<p>At a high level, the pattern was always the same: a trigger element, a hidden tooltip element, and JavaScript to coordinate the two.</p>

<div class="break-out">
<pre><code class="language-html">&lt;button class="info"&gt;?&lt;/button&gt;
&lt;div class="tooltip" role="tooltip"&gt;Helpful text&lt;/div&gt;
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="257"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png"
			
			sizes="100vw"
			alt="The old approach with ~60 lines of JavaScript with five event listeners vs the new approach is about 10 lines of declarative HTML with zero event listeners."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      The old approach required ~60 lines of JavaScript with five event listeners and manual state management. The new approach is about 10 lines of declarative HTML with zero event listeners. (<a href='https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The library handled the wiring that allowed the element to show on hover or focus, hide on blur or mouse leave, and reposition/resize on scroll.</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/1168681775"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<p>None of it was accidental. It was merely compensating for gaps in web platform features.</p>

<h2 id="why-i-used-a-library">Why I Used A Library</h2>

<p>The library was doing real work for me: positioning, flipping at viewport edges, event coordination across input types, and scroll awareness inside complex layouts. Positioning alone justified the dependency. Handling scroll containers, transforms, and responsive layouts correctly is not simple.</p>

<p>The real issues showed up in <strong>accessibility behavior</strong>, not visuals. The tooltip worked, but not all the time. Here’s where things started to fray at the seams:</p>

<ul>
<li>Tooltips sometimes appeared late or not at all.</li>
<li>Tabbing quickly could skip them entirely.</li>
<li>Escape dismissal was not reliable.</li>
</ul>


<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/1168682121"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Keyboard navigation with the old implementation: Tabbing quickly causes tooltips to be skipped entirely, and Escape dismissal is unreliable.</figcaption>
	
</figure>

<p>I also ran into issues trying to sync hover and focus behavior:</p>

<ul>
<li>Mouse users expect immediacy.</li>
<li>Keyboard users expect predictability.</li>
<li>Supporting both meant delays and edge cases.</li>
</ul>


<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/1168682494"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>This timing mismatch creates an inconsistent experience across input methods.</figcaption>
	
</figure>

<p>Not to mention, there were issues with <strong>assistive technologies</strong>, particularly screen readers: Sometimes the tooltip was announced, sometimes it wasn’t, and sometimes it was announced twice.</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/1168682680"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Screen reader behavior with custom tooltips.</figcaption>
	
</figure>

<p>Keeping ARIA attributes in sync required manual updates. Miss one state change, and the tooltip became confusing or invisible to the accessibility tree.</p>

<h2 id="this-was-not-bad-code">This Was Not Bad Code</h2>

<p>The implementation was tested, the library was solid, and the behavior was reasonable given the tools available at the time.</p>

<blockquote>The core problem was not the code. It was that the web platform lacked proper affordances.</blockquote>

<p>For example, the browser has no real way of knowing that the element was a tooltip. Everything was built from conventions: generic elements, event listeners, manually-managed ARIA, and custom dismissal logic.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.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/getting-started-popover-api/2-before-after-popover-api.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png"
			
			sizes="100vw"
			alt="Event flow: before and after Popover API."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Before: A tangled web of event listeners, state management, and manual ARIA updates. After: The browser understands the relationship declaratively. (<a href='https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Over time, the tooltip could become fragile. Small changes carried risk. Minor fixes caused regressions. Worse, adding new tooltips inherited the same complexity. Things technically worked, but never felt settled or complete.</p>

<p>That was the state of things when I decided to rebuild the tooltip using the browser’s native <a href="https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute">Popover API</a>.</p>

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

<h2 id="the-moment-i-tried-the-popover-api">The Moment I Tried The Popover API</h2>

<p>I didn’t switch to using the Popover API because I wanted to experiment with something new. I switched because I was tired of maintaining tooltip behavior that I believed the browser should have already understood.</p>

<p>I was skeptical at first. Most new web APIs promise simplicity, but still require glue, edge-case handling, or fallback logic that quietly recreates the same complexity that you were trying to escape.</p>

<p>So, I tried the Popover API in the smallest way possible. Here’s what that looked like:</p>

<div class="break-out">
<pre><code class="language-html">&lt;!-- popovertarget creates the connection to id="tip-1" --&gt;
&lt;button popovertarget="tip-1"&gt;?&lt;/button&gt;

&lt;!-- popover="manual": browser manages this as a popover --&gt;
&lt;!-- role="tooltip": tells assistive technology what this is --&gt;
&lt;div id="tip-1" popover="manual" role="tooltip"&gt;
  This button triggers a helpful tip.
&lt;/div&gt;
</code></pre>
</div>


<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/1168683587"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>The complete tooltip implementation using the Popover API</figcaption>
	
</figure>

<p>No event listeners. No state tracking. No ARIA updates handled in JavaScript. I focused the button, and the tooltip appeared. I pressed the <code>Esc</code> key, and it disappeared.</p>

<h2 id="what-immediately-stood-out">What Immediately Stood Out</h2>

<p>A few things became obvious within minutes:</p>

<h3 id="i-didn-t-write-any-javascript-to-open-or-close-it">I Didn’t Write Any JavaScript To Open Or Close It</h3>

<p>The browser handled invocation entirely through HTML. The relationship between trigger and tooltip was explicit.</p>

<h3 id="the-esc-key-just-worked">The <code>Esc</code> Key Just Worked</h3>

<p>I didn’t add a key listener. Pressing the <code>Esc</code> key properly closed the tooltip because the browser understands that popovers should be dismissible.</p>

<h3 id="aria-state-automatically-synced">ARIA State Automatically Synced</h3>

<p>The <code>aria-expanded</code> attribute updated on its own when the popover opened and closed. There was no need for manual bookkeeping and no risk of stale state.</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/1168684067"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>The browser’s DevTools showing <code>aria-expanded</code> automatically updating from <code>false</code> to <code>true</code> as the popover opens.</figcaption>
	
</figure>

<p>This was the moment that the Popover API stopped feeling like a convenience and more like true bona fide platform behavior.</p>

<p>What surprised me most was not the reduced code but the <strong>change in responsibility</strong>. Before, the tooltip existed because my JavaScript said so. Now, it exists because the browser understands what it is supposed to be and its role in the markup. The tooltip is no longer simply a box positioned near a button anymore, but participating in the browser’s focus model, the accessibility tree, and native dismissal rules.</p>

<p>That’s when my migration to the Popover API started.</p>

<h3 id="understanding-invoker-commands">Understanding Invoker Commands</h3>

<p>The <code>popovertarget</code> and <code>popovertargetaction</code> attributes are part of HTML’s invoker commands, a declarative way to control interactive elements without JavaScript.</p>

<ul>
<li><code>popovertarget=&quot;id&quot;</code>: Connects the button to a popover element.</li>
<li><code>popovertargetaction</code>: Specifies what should happen:

<ul>
<li><code>show</code>: Only opens the popover.</li>
<li><code>hide</code>: Only closes the popover.</li>
<li><code>toggle</code>(default): Opens the popover if closed and closes it if it’s open.</li>
</ul></li>
</ul>

<p>This means you can have multiple triggers for the same tooltip:</p>

<div class="break-out">
<pre><code class="language-html">&lt;button popovertarget="help-tip" popovertargetaction="show"&gt;
  Show Help
&lt;/button&gt;

&lt;button popovertarget="help-tip" popovertargetaction="hide"&gt;
  Close Help
&lt;/button&gt;

&lt;div id="help-tip" popover="manual" role="tooltip"&gt;
  Help content
&lt;/div&gt;
</code></pre>
</div>

<p>The browser coordinates everything with no JavaScript needed for the basic interaction.</p>

<h2 id="free-accessibility-wins">Free Accessibility Wins</h2>

<p>This is the part that made me switch completely. I expected the Popover API to reduce code. I didn’t expect it to remove entire categories of accessibility bugs I had been chasing for years. Before the migration, my tooltip system looked fine at the very least. Keyboard support existed, ARIA attributes were present, and screen readers usually behaved accordingly. But “usually” did a lot of heavy lifting.</p>

<p>Once I swapped in native popovers, three things changed immediately.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.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/getting-started-popover-api/3-accessibility-tree-comparison.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png"
			
			sizes="100vw"
			alt="Accessibility tree comparison: Custom vs Native Popover API."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Custom implementations use fragile JavaScript to connect triggers and tooltips. The Popover API creates a native browser connection that assistive technology can trust. (<a href='https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="1-the-keyboard-just-works">1. The Keyboard “Just Works”</h3>

<p>Keyboard support depended on multiple layers lining up correctly: focus had to trigger the tooltip, blur had to hide it, <code>Esc</code> had to be wired manually, and timing mattered. If you missed one edge case, the tooltip would either stay open too long or disappear before it could be read.</p>

<p>With the <code>popover</code> attribute set to <code>auto</code> or <code>manual</code>, the browser takes over the basics: <code>Tab</code> and <code>Shift</code>+<code>Tab</code> behave normally, <code>Esc</code> closes the tooltip every time, and no extra listeners are required.</p>

<pre><code class="language-html">&lt;div popover="manual"&gt;
  Helpful explanation
&lt;/div&gt;
</code></pre>

<p>What disappeared from my codebase were global keydown handlers, <code>Esc</code>-specific cleanup logic, and state checks during keyboard navigation. The keyboard experience stopped being something I had to maintain, and it became a browser guarantee.</p>

<h3 id="2-screenreader-predictability">2. Screenreader Predictability</h3>

<p>This was the biggest improvement. Even with careful ARIA work, the behavior varied, as I outlined earlier. Every small change felt risky. Using a popover with a proper role looks and feels a lot more stable and predictable as far as what’s going to happen:</p>

<pre><code class="language-html">&lt;div popover="manual" role="tooltip"&gt;
  Helpful explanation
&lt;/div&gt;
</code></pre>

<p>And here’s another win: After the switch, <a href="https://www.smashingmagazine.com/2024/11/why-optimizing-lighthouse-score-not-enough-fast-website/">Lighthouse</a> stopped flagging incorrect ARIA state warnings for the interaction, largely because there are no longer custom ARIA states for me to accidentally get wrong.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.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/getting-started-popover-api/4-manual-aria-popover-api.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png"
			
			sizes="100vw"
			alt="ARIA state warnings before migration, and 100% audit score after switching to the Popover API."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Before the migration, Lighthouse flagged accessibility warnings about incorrect ARIA state management. After switching to the Popover API, the audit score improved. (<a href='https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="3-focus-management">3. Focus Management</h3>

<p>Focus used to be fragile. Before, I had rules like: let focus trigger show tooltip, move focus into tooltip and don’t close, blur trigger when it’s too close, and close tooltip and restore focus manually. This worked until it didn’t.</p>

<p>With the Popover API, the browser enforces a simpler model where focus can more naturally move into the popover. Closing the popover returns focus to the trigger, and there are no invisible focus traps or lost focus moments. And I didn’t add focus restoration code; I removed it.</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/1168686318"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Tab to focus the trigger, the tooltip appears, press <code>Escape</code> to dismiss, and focus automatically returns to the trigger.</figcaption>
	
</figure>

<h2 id="where-the-popover-api-maybe-still-isn-t-enough">Where The Popover API Maybe Still Isn’t Enough</h2>

<p>As much as the Popover API has simplified my code and improved semantics, it still has not completely eliminated JavaScript. That’s not totally a bad thing because what’s changed is that JavaScript is no longer a key dependency. I am no longer compensating for missing platform behavior anymore. I am much more focused on <em>intent</em>.</p>

<p>Here are a few places where I could see the API continue to improve.</p>

<h3 id="tooltip-timing-still-matters">Tooltip Timing Still Matters</h3>

<p>Native popovers open and close immediately. That is usually the expected behavior, but not always ideal for what we consider to be tooltips. In those cases, instant dismissal can feel unstable when you move your mouse a few pixels too quickly or accidentally brush past the trigger &mdash; the tooltip will flash, then disappear, which can be jarring.</p>

<p>I want to be able to control that timing and apply delays between hover or focus and opening the tooltip. So I still add small delays. What changed was how much of the interaction logic I actually needed to own. Before, even basic open and close behavior required JavaScript. With the Popover API, and especially with HTML invoker commands, that responsibility shifts back to the browser.</p>

<pre><code class="language-html">&lt;button
  popovertarget="help-tip"
  popovertargetaction="show"&gt;
  ?
&lt;/button&gt;

&lt;div id="help-tip" popover="manual" role="tooltip"&gt;
  This button triggers a helpful tip.
&lt;/div&gt;
</code></pre>

<p>At this point, the browser handles invocation, dismissal, and ARIA state on its own. There’s no JavaScript involved just to make the tooltip appear or disappear.</p>

<p>JavaScript only comes back in when I want intentional behavior. In this case, a short delay before hiding the tooltip, and cancelling if the pointer moves into it. This isn’t about accessibility fixes. It’s about human behavior.</p>

<p>It’s worth noting that CSS is beginning to explore this space as well. The emerging interest/invoker work introduces <a href="https://css-tricks.com/a-first-look-at-the-interest-invoker-api-for-hover-triggered-popovers/#aa-interest-delay-and-the-css-of-it-all">ways to express entry and exit delays directly in CSS</a>, which could remove this small bit of JavaScript entirely. For now, I still handle it imperatively, but the direction of the platform is clear.</p>

<pre><code class="language-javascript">let hideTimeout;

const show = () =&gt; {
  clearTimeout(hideTimeout);
  tooltip.showPopover();
};

const hide = () =&gt; {
  hideTimeout = setTimeout(() =&gt; {
    tooltip.hidePopover();
  }, 200);
};
</code></pre>

<p>The difference is that this logic stays small and local. It no longer defines how the tooltip works. It simply refines how it feels.</p>

<h3 id="hover-intent-with-invoker-commands">Hover Intent With Invoker Commands</h3>

<p>The browser does not know why someone hovers over an element or focuses on it. Was it intentional, or was the pointer just passing through? That part has always required some judgment.</p>

<p>What changed is where that logic lives. With invoker commands handling the core open and close behavior, JavaScript no longer owns the interaction model. It only adds intent on top of it.</p>

<pre><code class="language-html">&lt;button
 popovertarget="help-tip"
 popovertargetaction="show"&gt;
  ?
&lt;/button&gt;
</code></pre>

<p>The platform manages invocation, dismissal, and ARIA state. JavaScript is only needed when we want behavior that the browser cannot infer, such as a short delay before hiding or cancelling dismissal if the pointer moves into the tooltip.</p>

<pre><code class="language-javascript">let hideTimeout;

const show = () =&gt; {
clearTimeout(hideTimeout);
  tooltip.showPopover();
};

const hide = () =&gt; {
  hideTimeout = setTimeout(() =&gt; {
    tooltip.hidePopover();
  }, 200);
};
</code></pre>

<p>And again, CSS is beginning to explore this space with new interaction primitives, which may reduce the need for custom hover intent code even further.</p>

<h3 id="manual-popovers-and-focus">Manual Popovers And Focus</h3>

<p>For <code>popover=&quot;manual&quot;</code>, the browser does not restore focus automatically the way it can for auto popovers. That responsibility remains explicit. When a tooltip opens on focus and closes on blur, I return focus deliberately to the trigger:</p>

<pre><code class="language-javascript">tooltip.hidePopover();
trigger.focus();
</code></pre>

<p>This is not a limitation but a clear boundary between platform behavior and person intent.</p>

<h3 id="the-honest-take">The Honest Take</h3>

<p>The Popover API does not magically solve tooltips. It stopped forcing me to rebuild fragile infrastructure. I still write JavaScript and think about edge cases, but now I am solving product problems instead of recreating UI primitives the browser should already understand.</p>

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

<h2 id="when-i-would-still-reach-for-a-tooltip-library">When I would Still Reach For A Tooltip Library</h2>

<p>Even after migrating my tooltips to the Popover API, I did not walk away thinking libraries were old and obsolete. They have earned their place, just in more specific situations.</p>

<h3 id="1-large-or-mature-design-systems">1. Large Or Mature Design Systems</h3>

<p>If you are maintaining a large design system used across multiple teams, a tooltip library can still make sense because centralized behavior, documented patterns, and consistent defaults across products. In those environments, changing the underlying interaction model is not just a technical decision; it is an organizational one. A well-maintained library gives teams guardrails, especially when not everyone is deeply familiar with accessibility nuances.</p>

<h3 id="2-complex-positioning-requirements">2. Complex Positioning Requirements</h3>

<p>For most tooltips, native positioning is enough, but if you need collision detection across nested scroll containers, custom flipping logic, or fine-grained control over offsets and boundaries, libraries like <a href="https://floating-ui.com">Floating UI</a> still shine. They are optimized for geometry problems that the platform is only beginning to address.</p>

<p>It is also worth mentioning <a href="https://css-tricks.com/css-anchor-positioning-guide/">CSS anchor positioning</a>, which is starting to cover many of the problems that tooltip libraries historically solved. Anchors allow a popover to be positioned relative to a trigger using pure CSS, including viewport-aware placement and edge flipping. This moves even more responsibility back to the platform instead of JavaScript.</p>

<p>That said, anchor positioning is still new and <a href="https://css-tricks.com/css-anchor-positioning-guide/#aa-known-bugs">there are known issues,</a> although the good news is that they are part of Interop, meaning <a href="https://webstatus.dev/features/popover?q=baseline_date%3A2025-01-01..2025-12-31&amp;start=25">we can look forward to full and consistent browser support</a>. For teams that need consistent cross-browser behavior today, libraries remain the practical choice. The direction is clear that the platform is steadily absorbing work that once required dedicated positioning engines.</p>

<h3 id="3-teams-without-accessibility-experience">3. Teams Without Accessibility Experience</h3>

<p>This one matters. If a team does not have strong accessibility knowledge, a good library can act as a safety net, though it will not guarantee perfect accessibility. It can, however, prevent the many common mistakes. The Popover API gives you better defaults, but it still assumes you know when to add roles, labels, focus management, and testing. Without that understanding, even native tools can be misused.</p>

<h2 id="the-decision-line">The Decision Line</h2>

<p>For me, the choice now looks like this:</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aUse%20the%20Popover%20API%20for%20simplicity,%20clarity,%20and%20platform-aligned%20behavior.%20Use%20a%20library%20when%20scale,%20customization,%20or%20constraints%20demand%20it.%20It%e2%80%99s%20not%20about%20purity.%20It%e2%80%99s%20about%20choosing%20the%20right%20level%20of%20abstraction%20for%20the%20problem%20in%20front%20of%20you.%0a&url=https://smashingmagazine.com%2f2026%2f03%2fgetting-started-popover-api%2f">
      
Use the Popover API for simplicity, clarity, and platform-aligned behavior. Use a library when scale, customization, or constraints demand it. It’s not about purity. It’s about choosing the right level of abstraction for the problem in front of you.

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

<p>And sometimes the right tool is still a library &mdash; just no longer by default.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="800"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png"
			
			sizes="100vw"
			alt=""
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png'>Large preview</a>)
    </figcaption>
  
</figure>

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

<p>The Popover API means that tooltips are no longer something you simulate. They’re something the browser understands. Opening, closing, keyboard behavior, Escape handling, and a big chunk of accessibility now come from the platform itself, not from ad-hoc JavaScript.</p>

<p>That does not mean tooltip libraries are obsolete because they still make sense for complex design systems, heavy customization, or legacy constraints, but <strong>the default has shifted</strong>. For the first time, the simplest tooltip can also be the most correct one. If you are curious, try this experiment: Simply replace just one tooltip in your product with the Popover API, do not rewrite everything, do not migrate a whole system, and just pick one and see what disappears from your code.</p>

<p>When the platform gives you a better primitive, the win is not just fewer lines of JavaScript, but it is fewer things you have to worry about at all.</p>

<p>Check out the full source code in <a href="https://github.com/BboyGT/PopOver-API">my GitHub repo</a>.</p>

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

<p>For deeper dives into popovers and related APIs:</p>

<ul>
<li>“<a href="https://css-tricks.com/poppin-in/">Poppin’ In</a>”, Geoff Graham</li>
<li>“<a href="https://css-tricks.com/clarifying-the-relationship-between-popovers-and-dialogs/">Clarifying the Relationship Between Popovers and Dialogs</a>”, Zell Liew</li>
<li>“<a href="https://una.im/popover-hint/">What is popover=hint?</a>”, Una Kravets</li>
<li>“<a href="https://css-tricks.com/invoker-commands-additional-ways-to-work-with-dialog-popover-and-more/">Invoker Commands</a>”, Daniel Schwarz</li>
<li>“<a href="https://css-tricks.com/creating-an-auto-closing-notification-with-an-html-popover/">Creating an Auto-Closing Notification with an HTML Popover</a>”, Preethi</li>
<li><a href="https://open-ui.org/components/popover.research.explainer/">Open UI Popover API Explainer</a></li>
<li>“<a href="https://css-tricks.com/popover-the-balloons/">Pop(over) the Balloons</a>”, John Rhea</li>
<li>“<a href="https://css-tricks.com/css-anchor-positioning-guide/">CSS Anchor Positioning</a>”, Juan Diego Rodríguez</li>
</ul>

<p>MDN also <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">offers comprehensive technical documentation</a> for the Popover API.</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>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-05-12T16:11:33+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>Stefan Kaltenegger</author><title>Practical Use Of AI Coding Tools For The Responsible Developer</title><link>https://www.smashingmagazine.com/2026/01/practical-use-ai-coding-tools-responsible-developer/</link><pubDate>Fri, 30 Jan 2026 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/01/practical-use-ai-coding-tools-responsible-developer/</guid><description>AI coding tools like agents can be valuable allies in everyday development work. They help handle time-consuming grunt work, guide you through large legacy codebases, and offer low-risk ways to implement features in previously unfamiliar programming languages. Here are practical, easy-to-apply techniques to help you use these tools to improve your workflow.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/01/practical-use-ai-coding-tools-responsible-developer/" />
              <title>Practical Use Of AI Coding Tools For The Responsible Developer</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Practical Use Of AI Coding Tools For The Responsible Developer</h1>
                  
                    
                    <address>Stefan Kaltenegger</address>
                  
                  <time datetime="2026-01-30T13:00:00&#43;00:00" class="op-published">2026-01-30T13:00:00+00:00</time>
                  <time datetime="2026-01-30T13:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>Over the last two years, my team at <a href="https://work.co/">Work &amp; Co</a> and I have been testing out and gradually integrating AI coding tools like Copilot, Cursor, Claude, and ChatGPT to help us ship web experiences that are used by the masses. Admittedly, after some initial skepticism and a few aha moments, various AI tools have found their way into my daily use. Over time, the list of applications where we found it made sense to let AI take over started to grow, so I decided to share some <strong>practical use cases</strong> for AI tools for what I call the “responsible developer”.</p>

<p>What do I mean by a <strong>responsible developer</strong>?</p>

<p>We have to make sure that we deliver quality code as expected by our stakeholders and clients. Our contributions (i.e., pull requests) should not become a burden on our colleagues who will have to review and test our work. Also, in case you work for a company: The tools we use need to be approved by our employer. Sensitive aspects like security and privacy need to be handled properly: Don’t paste secrets, customer data (PII), or proprietary code into tools without policy approval. Treat it like code from a stranger on the internet. Always test and verify.</p>

<p><strong>Note</strong>: <em>This article assumes some very basic familiarity with AI coding tools like Copilot inside VSCode or Cursor. If all of this sounds totally new and unfamiliar to you, the <a href="https://github.com/features/copilot/tutorials">Github Copilot video tutorials</a> can be a fantastic starting point for you.</em></p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="518"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png"
			
			sizes="100vw"
			alt="Screenshot of of VSCode with Copilot Chat open in the right panel"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      View of VSCode with Copilot Chat open in the right panel. (<a href='https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/01-vscode-copilot.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="helpful-applications-of-ai-coding-tools">Helpful Applications Of AI Coding Tools</h2>

<p><strong>Note</strong>: The following examples will mainly focus on working in JavaScript-based web applications like React, Vue, Svelte, or Angular.</p>

<h3 id="getting-an-understanding-of-an-unfamiliar-codebase">Getting An Understanding Of An Unfamiliar Codebase</h3>

<p>It’s not uncommon to work on established codebases, and joining a large legacy codebase can be intimidating. Simply open your project and your AI agent (in my case, Copilot Chat in VSCode) and start asking questions just like you would ask a colleague. In general, I like to talk to any AI agent just as I would to a fellow human.</p>

<p>Here is a more refined example prompt:</p>

<blockquote>“Give me a high-level architecture overview: entrypoints, routing, auth, data layer, build tooling. Then list 5 files to read in order. Treat explanations as hypotheses and confirm by jumping to referenced files.”</blockquote>

<p>You can keep asking follow-up questions like <em>“How does the routing work in detail?”</em> or <em>“Talk me through the authentication process and methods”</em> and it will lead you to helpful directions to shine some light into the dark of an unfamiliar codebase.</p>

<h3 id="triaging-breaking-changes-when-upgrading-dependencies">Triaging Breaking Changes When Upgrading Dependencies</h3>

<p>Updating npm packages, especially when they come with breaking changes, can be tedious and time-consuming work, and make you debug a fair amount of regressions. I recently had to upgrade the data visualization library <a href="plotly.js">plotly.js</a> up one major release version from version 2 to 3, and as a result of that, the axis labeling in some of the graphs stopped working.</p>

<p>I went on to ask ChatGPT:</p>

<blockquote>“I updated my Angular project that uses Plotly. I updated the plotly.js &mdash; dist package from version 2.35.2 to 3.1.0 &mdash; and now the labels on the x and y axis are gone. What happened?”</blockquote>

<p>The agent came back with a solution promptly (see for yourself below).</p>

<p><strong>Note</strong>: <em>I still verified the explanation against the official migration guide before shipping the fix.</em></p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="647"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png"
			
			sizes="100vw"
			alt="Screenshot of the response from ChatGPT when prompted “I updated my Angular project that uses plotly. I updated the plotly package from plotly.js-dist: ^2.35.2 to plotly.js-dist: ^3.1.0, - and now the labels on the x and y axis are gone - what happened? Thought for 19s. Short answer: Plotly.js v3 removed the string shorthand for titles. You must use the new object form.”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/02-chatgpt-plotly.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="replicating-refactors-safely-across-files">Replicating Refactors Safely Across Files</h3>

<p>Growing codebases most certainly unveil opportunities for code consolidation. For example, you notice code duplication across files that can be extracted into a single function or component. As a result, you decide to create a shared component that can be included instead and perform that refactor in one file. Now, instead of manually carrying out those changes to your remaining files, you ask your agent to roll out the refactor for you.</p>

<p>Agents let you select multiple files as context. Once the refactor for one file is done, I can add both the refactored and untouched files into context and prompt the agent to roll out the changes to other files like this: <em>“Replicate the changes I made in file A to file B as well”</em>.</p>

<h3 id="implementing-features-in-unfamiliar-technologies">Implementing Features In Unfamiliar Technologies</h3>

<p>One of my favorite aha-moments using AI coding tools was when it helped me create a quite complex animated gradient animation in GLSL, a language I have been fairly unfamiliar with. On a recent project, our designers came up with an animated gradient as a loading state on a 3D object. I really liked the concept and wanted to deliver something unique and exciting to our clients. The problem: I only had two days to implement it, and GLSL has quite the steep learning curve.</p>

<p>Again, an AI tool (in this case, ChatGPT) came in handy, and I started quite simply prompting it to create a standalone HTML file for me that renders a canvas and a very simple animated color gradient. Step after step, I prompted the AI to add more finesse to it until I arrived at a decent result so I could start integrating the shader into my actual codebase.</p>

<p>The end result: Our clients were super happy, and we delivered a complex feature in a small amount of time thanks to AI.</p>

<h3 id="writing-tests">Writing Tests</h3>

<p>In my experience, there’s rarely enough time on projects to continuously write and maintain a proper suite of unit and integration tests, and on top of that, many developers don’t really enjoy the task of writing tests. Prompting your AI helper to set up and write tests for you is entirely possible and can be done in a small amount of time. Of course, you, as a developer, should still make sure that your tests actually take a look at the critical parts of your application and follow sensible testing principles, but you can “outsource” the writing of the tests to our AI helper.</p>

<p>Example prompt:</p>

<blockquote>“Write unit tests for this function using Jest. Cover happy path, edge cases, and failure modes. Explain why each test exists.”</blockquote>

<p>You can even pass along testing guru Kent C. Dodds’ testing best practices as guidelines to your agent, like below:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://x.com/kentcdodds/status/2008940437747409406">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="795"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png"
			
			sizes="100vw"
			alt="A post on X by @kentcdodds that reads “When telling AI to write tests, I find those tests improve a lot when I send: Could you apply these principles to the tests? https://kentcdodds.com/blog/avoid-nesting-when-youre-testing and https://epicweb.dev/better-test-setup-with-disposable-objects. Just did it and the tests got much more clear and terse.”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://x.com/kentcdodds/status/2008940437747409406'>x.com/kentcdodds</a>. (<a href='https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/03-post-testing.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="internal-tooling">Internal Tooling</h3>

<p>Somewhat similar to the shader example mentioned earlier, I was recently tasked to analyze code duplication in a codebase and compare before and after a refactor. Certainly not a trivial task if you don’t want to go the time-consuming route of comparing files manually. With the help of Copilot, I created a script that analyzed code duplication for me, arranged and ordered the output in a table, and exported it to Excel. Then I took it a step further. When our code refactor was done, I prompted the agent to take my existing Excel sheet as the baseline, add in the current state of duplication in separate columns, and calculate the delta.</p>

<h3 id="updating-code-written-a-long-time-ago">Updating Code Written A Long Time Ago</h3>

<p>Recently, an old client of mine hit me up, as over time, a few features weren’t working properly on his website anymore.</p>

<p>The catch: The website was built almost ten years ago, and the JavaScript and SCSS were using rather old compile tools like requireJS, and the setup required an older version of Node.js that wouldn’t even run on my 2025 MacBook.</p>

<p>Updating the whole build process by hand would have taken me days, so I decided to prompt the AI agent, <em>“Can you update the JS and SCSS build process to a lean 2025 stack like Vite?”</em> It sure did, and after around an hour of refining with the agent, I had my SCSS and JS build switched to Vite, and I was able to focus on actual bugfixing. Just make sure to properly validate the output and compiled files when doing such integral changes to your build process.</p>

<h3 id="summarizing-and-drafting">Summarizing And Drafting</h3>

<p>Would you like to summarize all your recent code changes in one sentence for a commit message, or have a long list of commits and would like to sum them up in three bullet points? No problem, let the AI take care of it, but please make sure to proofread it.</p>

<p>An example prompt is as simple as messaging a fellow human: <em>“Please sum up my recent changes in concise bullet points”</em>.</p>

<p>My advice here would be to use GPT for writing with caution, and as with code, please check the output before sending or submitting.</p>

<h2 id="recommendations-and-best-practices">Recommendations And Best Practices</h2>

<h3 id="prompting">Prompting</h3>

<p>One of the not-so-obvious benefits of using AI is that the more specific and tailored your prompts are, the better the output. The process of prompting an AI agent forces us to <strong>formulate our requirements as specifically as possible</strong> before we write and code. This is why, as a general rule, I highly recommend being as specific as possible with your prompting.</p>

<p>Ryan Florence, co-author of Remix, suggests a simple yet powerful way to improve this process by finishing your initial prompt with the sentence:</p>

<blockquote>“Before we start, do you have any questions for me?”</blockquote>

<p>At this point, the AI usually comes back with helpful questions where you can clarify your specific intent, guiding the agent to provide you with a <strong>more tailored approach</strong> for your task.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://x.com/ryanflorence/status/1959407056286568828">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="690"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png"
			
			sizes="100vw"
			alt="A post on X by @ryanflorence that reads: “I always ask Cursor: Do you have any questions before you begin to implement this? Usually, some good questions arise, I answer them, and have the model update the feature explanation doc and spec doc. Eventually, it has no more questions and often nails the sessions.”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://x.com/ryanflorence/status/1959407056286568828'>x.com/ryanflorence</a>. (<a href='https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/04-post-cursor.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="use-version-control-and-work-in-digestible-chunks">Use Version Control And Work In Digestible Chunks</h3>

<p>Using version control like git not only comes in handy when collaborating as a team on a single codebase but also to provide you as an individual contributor with stable points to roll back to in case of an emergency. Due to its non-deterministic nature, AI can sometimes go rogue and make changes that are simply not helpful for what you are trying to achieve and eventually break things irreparably.</p>

<p>Splitting up your work into <strong>multiple commits</strong> will help you create stable points that you can revert to in case things go sideways. And your teammates will thank you as well, as they will have an easier time reviewing your code when it is split up into semantically well-structured chunks.</p>

<h3 id="review-thoroughly">Review Thoroughly</h3>

<p>This is more of a general best practice, but in my opinion, it becomes even more important when using AI tools for development work: <strong>Be the first critical reviewer of your code</strong>. Make sure to take some time to go over your changes line by line, just like you would review someone else’s code, and only submit your work once it passes your own self-review.</p>

<blockquote>“Two things are both true to me right now: AI agents are amazing and a huge productivity boost. They are also massive slop machines if you turn off your brain and let go completely.”<br /><br />&mdash; Armin Ronacher in his blog post <a href="https://lucumr.pocoo.org/2026/1/18/agent-psychosis/">Agent Psychosis: Are We Going Insane?</a></blockquote>

<h2 id="conclusion-and-critical-thoughts">Conclusion And Critical Thoughts</h2>

<p>In my opinion, AI coding tools can improve our productivity as developers on a daily basis and free up mental capacity for more planning and high-level thinking. They force us to articulate our desired outcome with meticulous detail.</p>

<p>Any AI can, at times, hallucinate, which basically means it lies in a confident tone. So please make sure to check and test, especially when you are in doubt. AI is not a silver bullet, and I believe, excellence and the ability to solve problems as a developer will never go out of fashion.</p>

<p>For developers who are just starting out in their career these tools can be highly tempting to do the majority of the work for them. What may get lost here is the often draining and painful work through bugs and issues that are tricky to debug and solve, aka “the grind”. Even Cursor AI’s very own Lee Robinson questions this in one of his posts:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://x.com/leerob/status/1996281383535382909">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="1093"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png"
			
			sizes="100vw"
			alt="A post on X by @leerob that reads: “My biggest worries about coding with AI: 1. Beginners not actually learning 2. Atrophy of skills I’m seeing #1 happen, and I don’t have a good answer yet. Leveling up as an engineer requires grinding, and it’s not always fun. If AI can solve most of the problems for you, when do you lean into the healthy friction? When do you embrace the suck? Coupled with fewer opportunities for pair programming, it’s definitely tougher for those starting their engineering career.”"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Image source: <a href='https://x.com/leerob/status/1996281383535382909'>x.com/leerob</a>. (<a href='https://files.smashing.media/articles/practical-use-ai-coding-tools-responsible-developer/05-post-leerob.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>AI coding tools are evolving at a fast pace, and I am excited for what will come next. I hope you found this article and its tips helpful and are excited to try out some of these for yourself.</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>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-05-12T16:11:33+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>Daniel Schwarz</author><title>Penpot Is Experimenting With MCP Servers For AI-Powered Design Workflows</title><link>https://www.smashingmagazine.com/2026/01/penpot-experimenting-mcp-servers-ai-powered-design-workflows/</link><pubDate>Thu, 08 Jan 2026 08:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2026/01/penpot-experimenting-mcp-servers-ai-powered-design-workflows/</guid><description>&lt;a href="https://penpot.app/?utm_source=SmashingMagazine&amp;amp;utm_medium=Article&amp;amp;utm_campaign=MCPserver">Penpot&lt;/a> is experimenting with MCP (Model Context Protocol) servers, which could lead to designers and developers being able to perform tasks in Penpot using AI that’s able to understand and interact with Penpot design files. Daniel Schwarz explains how &lt;a href="https://github.com/penpot/penpot-mcp">Penpot MCP&lt;/a> servers work, what they could mean for creating and managing designs in Penpot, and what you can do to help shape their development.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2026/01/penpot-experimenting-mcp-servers-ai-powered-design-workflows/" />
              <title>Penpot Is Experimenting With MCP Servers For AI-Powered Design Workflows</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Penpot Is Experimenting With MCP Servers For AI-Powered Design Workflows</h1>
                  
                    
                    <address>Daniel Schwarz</address>
                  
                  <time datetime="2026-01-08T08:00:00&#43;00:00" class="op-published">2026-01-08T08:00:00+00:00</time>
                  <time datetime="2026-01-08T08:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Penpot</b></p>
                

<p>Imagine that your Penpot file contains a full icon set in addition to the design itself, which uses some but not all of those icons. If you were to ask an AI such as Claude or Gemini to export only the icons that are being used, it wouldn’t be able to do that. It’s not able to interact with Penpot files.</p>

<p>However, a <strong>Penpot MCP server</strong> can. It can perform a handpicked number of operations under set rules and permissions, especially since Penpot has an extensive API and even more so because it’s <strong>open-source</strong>.</p>

<p>The AI’s job is simply to understand your intent, choose the right operation for the MCP server to perform (an export in this case), and pass along any parameters (i.e., icons that are being used). The MCP server then translates this into a structured API request and executes it.</p>

<p>It might help to think of AI as a server in a restaurant that takes your order, the MCP server as both the menu and chef, and the API request as (hopefully) a hot pizza pie on a warm plate.</p>

<p>Why MCP servers, exactly? Well, Penpot isn’t able to understand your intent because it’s not an LLM, nor does it allow third-party LLMs to interact with your Penpot files for the security and privacy of your Penpot data. Although Penpot MCP servers do act as a <strong>secure bridge</strong>, translating AI intent into API requests using your Penpot files and data as context.</p>

<p>What’s even better is that because Penpot takes a <strong>design-expressed-as-code approach</strong>, designs can be programmatically created, edited, and analyzed on a granular level. It’s more contextual, more particular, and therefore more powerful in comparison to what other MCP servers offer, and <em>far</em> more thoughtful than the subpar ‘Describe → Generate’ AI workflow that I don’t think anybody really wants. <a href="https://penpot.app/blog/penpot-ai-whitepaper/">Penpot’s AI whitepaper</a> describes this as the bad approach and the ‘Convert to Code’ approach as the ugly approach, whereas MCP servers are more refined and adaptable.</p>

<h2 id="features-and-technical-details">Features And Technical Details</h2>

<p>Before we move on to use cases, here are some features and technical details that further explain how Penpot MCP servers work:</p>

<ul>
<li>Complies with MCP standards;</li>
<li>Integrates with the Penpot API for real-time design data;</li>
<li>Includes a Python SDK, REST API, plugin system, and CLI tools;</li>
<li>Works with any MCP-enabled AI assistant (Claude in VS Code, Claude in Cursor, Claude Desktop, etc.);</li>
<li>Supports sharing design context with AI models, and letting them see and understand components;</li>
<li>Facilitates communication with Penpot using natural language.</li>
</ul>

<p>What, then, could MCP servers enable us to do in Penpot, and what have existing experiments already achieved? Let’s take a look.</p>

<h2 id="penpot-mcp-server-use-cases">Penpot MCP Server Use-Cases</h2>

<p>If you just want to skip to what Penpot MCP servers can do, Penpot have a few <a href="https://drive.google.com/drive/u/0/folders/1CCuBqHEevWsp15bYkf3W7CLXLja5R_M_">MCP demos</a> stashed in a Google Drive that are more than worth watching. Penpot CEO Pablo Ruiz-Múzquiz mentioned that videos 03, 04, 06, 08, and 12 are their favorites.</p>

<p>An even faster way to summarize MCP servers is to <a href="https://www.youtube.com/watch?v=KNsvFc4Elfs&amp;list=PLgcCPfOv5v57Sp_vY4VggqUpAbP5fTIkM">watch the unveiling from Penpot Fest 2025</a>.</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="KNsvFc4Elfs"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>Otherwise, let’s take a look at some of the more refined examples that Penpot demonstrated in their <a href="https://community.penpot.app/t/penpot-mcp-server-showcase-ask-for-help/10040?utm_source=SmashingMagazine&amp;utm_medium=Article&amp;utm_campaign=MCPserver">public showcase</a>.</p>

<h3 id="design-to-code-and-back-again-and-more">Design-to-Code and Back Again (and More)</h3>

<p>Running on from what I was saying earlier about how Penpot designs are expressed as code, this means that MCP servers can be used to convert design to code using AI, but also code to design, design to documentation, documentation to design system elements, design to code again <em>based</em> on said design system, and then completely new components based on said design system.</p>

<p>It sounds surreal, but the demo below shows off this <strong>transmutability</strong>, and it’s not from vague instruction but rather previous design choices, regardless of how they were expressed (design, code, or documentation). There are no surprises &mdash; these are simply the decisions that you would’ve made anyway based on previous decisions, executed swiftly.</p>

<p>In the demo, Juan de la Cruz García, Designer at Penpot, frictionlessly transmutes some simple components into documentation, design system elements, code, new components, and even a complete Storybook project like a piece of Play-Doh:</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/1152331606"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Quick demo: Penpot MCP server in action</figcaption>
	
</figure>

<h3 id="design-to-code-design-code-validation-and-simple-operations">Design-to-Code, Design/Code Validation, And Simple Operations</h3>

<p>In a similar demo below, Dominik Jain, Co-Founder at Oraios AI, creates a Node.js web app based on the design before updating the frontend styles, saves names and identifiers to memory to ensure smooth design-to-code translation before checking it for consistency, adds a comment next to the selected shape in Penpot, and then replaces a scribble in Penpot with an adapted component. There’s a lot happening here, but you can see exactly what Dominik is typing into Claude Desktop as well as Claude’s responses, and it’s <em>very</em> robust:</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/1152352889"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
		<figcaption>Penpot MCP Server: Developer Workflow. Applications</figcaption>
	
</figure>

<p>By the way, the previous demo used Claude in VS Code, so I should note that <strong>Penpot MCP servers are LLM-agnostic</strong>. Your tech stack is totally up to you. IvanTheGeek managed to <a href="https://community.penpot.app/t/penpot-mcp-server-showcase-ask-for-help/10040/3?utm_source=SmashingMagazine&amp;utm_medium=Article&amp;utm_campaign=MCPserver">set up their MCP server with the JetBrains Rider IDE and Junie AI</a>.</p>

<h3 id="more-use-cases">More Use Cases</h3>

<p>Translate a Penpot board to production-ready semantic HTML and modular CSS while leveraging any Penpot design tokens (remember that Penpot designs are already expressed as code, so this isn’t a shot in the dark):</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="yoOOOPPXfc4"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>Generate an interactive web prototype without changing the existing HTML:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="raha7nvY_j4"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>As shown earlier, convert a scribble into a component, leveraging existing design and/or design system elements:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="9fudhgOhMDc"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>Create design system documentation from a Penpot file:</p>


<figure class="video-embed-container">
  <div
  
  class="video-embed-container--wrapper">
		<lite-youtube
			videoid="kNc6kUZvB6k"
      
			
		></lite-youtube>
	</div>
	
</figure>

<p>And here are some more use-cases from Penpot and the community:</p>

<ul>
<li>Advanced exports,</li>
<li>Search for design elements using natural language,</li>
<li>Pull data from external APIs using natural language,</li>
<li>Easily connect Penpot to other external tools,</li>
<li>Saving repetitive tasks to memory and executing them,</li>
<li>Visual regression testing,</li>
<li>Design consistency and redundancy checking,</li>
<li>Accessibility and usability analysis and feedback,</li>
<li>Design system compliance checking,</li>
<li>Guideline compliance checking (brand, content, etc.),</li>
<li>Monitor adoption and usage with design analytics,</li>
<li>Automatically keep documentation in sync with design,</li>
<li>Design file organization (e.g., tagging/categorization).</li>
</ul>

<p>Essentially, Penpot MCP servers lead the way to an <strong>infinite number of workflows</strong> thanks to the efficiency and ease of your chosen LLM/LLM client, but without exposing your data to it.</p>

<h2 id="what-would-you-use-mcp-servers-for">What Would You Use MCP Servers For?</h2>

<p>Penpot MCP servers aren’t even at the beta stage, but it is an <strong>active experiment</strong> that you can be a part of. Penpot users have already begun exploring use cases for MCP servers, but Penpot wants to see more. To ensure that the next generation of design tools meets the needs of designers, developers, and product teams in general, they must be built <strong>collectively</strong> and <strong>collaboratively</strong>, especially where AI is concerned.</p>

<p><strong>Note</strong>: <em>Penpot is looking for beta testers eager to explore, experiment with, and help refine Penpot’s MCP Server. To join, write to <a href="mailto:support@penpot.app">support@penpot.app</a> with the subject line “MCP beta test volunteer.”</em></p>

<p>Is there anything that you feel Penpot MCP servers could do that current tools aren’t able to do well enough, fast enough, or aren’t able to do at all?</p>

<p>You can learn <a href="https://github.com/penpot/penpot-mcp">how to set up a Penpot MCP server right here</a> and start tinkering today, or if your brain’s buzzing with ideas already, Penpot want you to <a href="https://community.penpot.app/t/penpot-mcp-server-showcase-ask-for-help/10040?utm_source=SmashingMagazine&amp;utm_medium=Article&amp;utm_campaign=MCPserver">join the discussion</a>, share your feedback, and talk about your use-cases. Alternatively, the comment section right below isn’t a bad place to start either!</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>Vitaly Friedman</author><title>How To Measure The Impact Of Features</title><link>https://www.smashingmagazine.com/2025/12/how-measure-impact-features-tars/</link><pubDate>Fri, 19 Dec 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/12/how-measure-impact-features-tars/</guid><description>Meet TARS — a simple, repeatable, and meaningful UX metric designed specifically to track the performance of product features. Upcoming part of the &lt;a href="https://measure-ux.com/">Measure UX &amp;amp; Design Impact&lt;/a> (use the code 🎟 &lt;code>IMPACT&lt;/code> to save 20% off today).</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/12/how-measure-impact-features-tars/" />
              <title>How To Measure The Impact Of Features</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Measure The Impact Of Features</h1>
                  
                    
                    <address>Vitaly Friedman</address>
                  
                  <time datetime="2025-12-19T10:00:00&#43;00:00" class="op-published">2025-12-19T10:00:00+00:00</time>
                  <time datetime="2025-12-19T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                
                

<p>So we design and ship a <strong>shiny new feature</strong>. How do we know if it’s working? How do we measure and track its impact? There is <a href="https://measuringu.com/an-overview-of-70-ux-metrics/">no shortage in UX metrics</a>, but what if we wanted to establish a <strong>simple, repeatable</strong>, meaningful UX metric &mdash; specifically for our features? Well, let’s see how to do just that.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="975"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="Adrian Raudaschl&#39;s framework for measuring feature impact."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      With <a href='https://uxdesign.cc/tars-a-product-metric-game-changer-c523f260306a?sk=v2%2F2a9d7d1e-bae9-4875-9063-4b6a10ae110c'>TARS</a>, we can assess how effective features are and how well they are performing.(<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/1-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>I first heard about the <strong>TARS framework</strong> from Adrian H. Raudschl’s wonderful article on “<a href="https://uxdesign.cc/tars-a-product-metric-game-changer-c523f260306a?sk=v2%2F2a9d7d1e-bae9-4875-9063-4b6a10ae110c">How To Measure Impact of Features</a>”. Here, Adrian highlighted how his team tracks and decides which features to focus on &mdash; and then maps them against each other in a <strong>2×2 quadrants matrix</strong>.</p>

<p>It turned out to be a very useful framework to <strong>visualize</strong> the impact of UX work through the lens of business metrics.</p>

<p>Let’s see how it works.</p>

<h2 id="1-target-audience">1. Target Audience (%)</h2>

<p>We start by quantifying the <strong>target audience</strong> by exploring what percentage of a product’s users have the specific problem that a feature aims to solve. We can study existing or similar features that try to solve similar problems, and how many users engage with them.</p>

<p>Target audience <strong>isn’t the same</strong> as feature usage though. As Adrian noted, if we know that an existing Export Button feature is used by 5% of all users, it doesn’t mean that the target audience is 5%. <strong>More users</strong> might have the problem that the export feature is trying to solve, but they can’t find it.</p>

<blockquote>Question we ask: “What percentage of all our product’s users have that specific problem that a new feature aims to solve?”</blockquote>

<h2 id="2-a-adoption">2. A = Adoption (%)</h2>

<p>Next, we measure how well we are <strong>“acquiring”</strong> our target audience. For that, we track how many users actually engage <em>successfully</em> with that feature over a specific period of time.</p>

<p>We <strong>don’t focus on CTRs or session duration</strong> there, but rather if users <em>meaningfully</em> engage with it. For example, if anything signals that they found it valuable, such as sharing the export URL, the number of exported files, or the usage of filters and settings.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="395"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="The TARS Framework Step"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Adoption rates: from low adoption (<20%) to high adoption (>60%). Illustration by <a href='https://uxdesign.cc/tars-a-product-metric-game-changer-c523f260306a?sk=v2%2F2a9d7d1e-bae9-4875-9063-4b6a10ae110c'>Adrian Raudaschl</a>. (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/2-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>High <strong>feature adoption</strong> (&gt;60%) suggests that the problem was impactful. Low adoption (&lt;20%) might imply that the problem has simple workarounds that people have relied upon. Changing habits takes time, too, and so low adoption in the beginning is expected.</p>

<p>Sometimes, low feature adoption has nothing to do with the feature itself, but rather <strong>where it sits in the UI</strong>. Users might never discover it if it’s hidden or if it has a confusing label. It must be obvious enough for people to stumble upon it.</p>

<p>Low adoption doesn’t always equal failure. If a problem only affects 10% of users, hitting 50–75% adoption within that specific niche means the feature is a <strong>success</strong>.</p>

<blockquote>Question we ask: “What percentage of active target users actually use the feature to solve that problem?”</blockquote>

<h2 id="3-retention">3. Retention (%)</h2>

<p>Next, we study whether a feature is actually used repeatedly. We measure the frequency of use, or specifically, how many users who engaged with the feature actually keep using it over time. Typically, it’s a strong signal for <strong>meaningful impact</strong>.</p>

<p>If a feature has &gt;50% retention rate (avg.), we can be quite confident that it has a <strong>high strategic importance</strong>. A 25–35% retention rate signals medium strategic significance, and retention of 10–20% is then low strategic importance.</p>

<blockquote>Question we ask: “Of all the users who meaningfully adopted a feature, how many came back to use it again?”</blockquote>

<h2 id="4-satisfaction-score-ces">4. Satisfaction Score (CES)</h2>

<p>Finally, we measure the <strong>level of satisfaction</strong> that users have with that feature that we’ve shipped. We don’t ask everyone &mdash; we ask only “retained” users. It helps us spot hidden troubles that might not be reflected in the retention score.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="395"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="Customer Satisfaction Score, measured with a survey"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      We ask users how easy it was to solve a problem after they used a feature. Illustration by <a href='https://uxdesign.cc/tars-a-product-metric-game-changer-c523f260306a?sk=v2%2F2a9d7d1e-bae9-4875-9063-4b6a10ae110c'>Adrian Raudaschl</a>. (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/3-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Once users actually used a feature multiple times, we ask them <strong>how easy it was to solve</strong> a problem after they used that feature &mdash; between “much more difficult” and “much easier than expected”. We know how we want to score.</p>

<h2 id="using-tars-for-feature-strategy">Using TARS For Feature Strategy</h2>

<p>Once we start measuring with TARS, we can calculate an <strong>S÷T score</strong> &mdash; the percentage of Satisfied Users ÷ Target Users. It gives us a sense of how well a feature is performing for our intended target audience. Once we do that for every feature, we can map all features across 4 quadrants in a <strong>2×2 matrix</strong>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="400"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="Feature retention curves"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Evaluating features on a 2×2 matrix based on S/T score Illustration by <a href='https://uxdesign.cc/tars-a-product-metric-game-changer-c523f260306a?sk=v2%2F2a9d7d1e-bae9-4875-9063-4b6a10ae110c'>Adrian Raudaschl</a>. (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/4-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Overperforming features</strong> are worth paying attention to: they have low retention but high satisfaction. It might simply be features that users don’t have to use frequently, but when they do, it’s extremely effective.</p>

<p><strong>Liability features</strong> have high retention but low satisfaction, so perhaps we need to work on them to improve them. And then we can also identify <strong>core features</strong> and project features &mdash; and have a conversation with designers, PMs, and engineers on what we should work on next.</p>

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

<h2 id="conversion-rate-is-not-a-ux-metric">Conversion Rate Is Not a UX Metric</h2>

<p>TARS doesn’t cover conversion rate, and for a good reason. As <a href="https://www.linkedin.com/posts/fabian-lenz-digital-experience-leadership_conversion-rate-is-not-a-ux-metric-yes-activity-7394261839506739200-78G9">Fabian Lenz noted</a>, conversion is often considered to be the <strong>ultimate indicator of success</strong> &mdash; yet in practice it’s always very difficult to present a clear connection between smaller design initiatives and big conversion goals.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="274"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png"
			
			sizes="100vw"
			alt="Chart comparing Leading vs Lagging Measures for UX metrics"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Leading vs. Lagging Measures by <a href='https://measuringu.com/leading-vs-lagging/'>Jeff Sauro and James R. Lewis</a>. (But please do avoid NPS at all costs). (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/5-impact-features-tars.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>The truth is that almost everybody on the team is working towards better conversion. An uptick might be connected to <strong>many different initiatives</strong> &mdash; from sales and marketing to web performance boost to seasonal effects to UX initiatives.</p>

<p>UX can, of course, improve conversion, but it’s not really a UX metric. Often, people simply <strong>can’t choose the product</strong> they are using. And often a desired business outcome comes out of necessity and struggle, rather than trust and appreciation.</p>

<h3 id="high-conversion-despite-bad-ux">High Conversion Despite Bad UX</h3>

<p>As Fabian <a href="https://www.linkedin.com/posts/fabian-lenz-digital-experience-leadership_conversion-rate-is-not-a-ux-metric-yes-activity-7394261839506739200-78G9/">writes</a>, <strong>high conversion rate</strong> can happen despite poor UX, because:</p>

<ul>
<li><strong>Strong brand power</strong> pulls people in,</li>
<li>Aggressive but effective <strong>urgency tactics</strong>,</li>
<li>Prices are extremely attractive,</li>
<li>Marketing performs brilliantly,</li>
<li>Historical customer loyalty,</li>
<li>Users simply have no alternative.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="509"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="UX Scorecard and design metrics overview"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A practical overview of design metrics and UX scorecards: <a href='https://uxplanet.org/measuring-ux-your-first-step-towards-objective-evaluation-a408b312777b'>Measuring UX: Your First Step Towards Objective Evaluation</a> by Roman Videnov. (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/6-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="low-conversion-despite-great-ux">Low Conversion Despite Great UX</h3>

<p>At the same time, a low conversion rate can occur despite great UX, because:</p>

<ul>
<li><strong>Offers aren’t relevant</strong> to the audience,</li>
<li><strong>Users don’t trust the brand</strong>,</li>
<li>Poor business model or high risk of failure,</li>
<li>Marketing doesn’t reach the right audience,</li>
<li>External factors (price, timing, competition).</li>
</ul>

<p>An improved conversion is the <strong>positive outcome of UX initiatives</strong>. But good UX work typically improves task completion, reduces time on task, minimizes errors, and avoids decision paralysis. And there are plenty of <a href="https://www.linkedin.com/posts/vitalyfriedman_how-to-measure-ux-httpslnkdine5uedtzy-activity-7332664809382952960-HERA">actionable design metrics we could use</a> to track UX and drive sustainable success.</p>

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

<p><strong>Product metrics</strong> alone don’t always provide an accurate view of how well a product performs. Sales might perform well, but users might be extremely inefficient and frustrated. Yet the churn is low because users can’t choose the tool they are using.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg">
    
    <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/how-measure-impact-features-tars/7-impact-features-tars.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg"
			
			sizes="100vw"
			alt="Chart comparing Leading vs Lagging Measures for UX metrics"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <a href='https://www.linkedin.com/posts/vitalyfriedman_ux-design-activity-7140641630507687936-YTI7'>Design KPIs and UX Metrics</a>, a quick overview by yours truly. Numbers are, of course, placeholders. (<a href='https://files.smashing.media/articles/how-measure-impact-features-tars/7-impact-features-tars.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We need UX metrics to understand and improve user experience. What I love most about TARS is that it’s a neat way to connect customers’ usage and <strong>customers’ experience with relevant product metrics</strong>. Personally, I would extend TARS with <a href="https://www.linkedin.com/posts/vitalyfriedman_ux-design-activity-7140641630507687936-YTI7">UX-focused metrics and KPIs</a> as well &mdash; depending on the needs of the project.</p>

<p>Huge thanks to <a href="https://www.linkedin.com/in/adrian-raudaschl/">Adrian H. Raudaschl</a> for putting it together. And if you are interested in metrics, I highly recommend you follow him for practical and useful guides all around just that!</p>

<h2 id="meet-how-to-measure-ux-and-design-impact">Meet “How To Measure UX And Design Impact”</h2>

<p>You can find more details on <strong>UX Strategy</strong> in 🪴&nbsp;<a href="https://measure-ux.com/"><strong>Measure UX &amp; Design Impact</strong></a> (8h), a practical guide for designers and UX leads to measure and show your UX impact on business. Use the code 🎟 <code>IMPACT</code> to save 20% off today. <a href="https://measure-ux.com/">Jump to the details</a>.</p>

<figure style="margin-bottom:0;padding-bottom:0" class="article__image">
    <a href="https://measure-ux.com/" title="How To Measure UX and Design Impact, with Vitaly Friedman">
    <img width="900" height="466" style="border-radius: 11px" src="https://files.smashing.media/articles/ux-metrics-video-course-release/measure-ux-and-design-impact-course.png" alt="How to Measure UX and Design Impact, with Vitaly Friedman.">
    </a>
</figure>

<div class="book-cta__inverted"><div class="book-cta" data-handler="ContentTabs" data-mq="(max-width: 480px)"><nav class="content-tabs content-tabs--books"><ul><li class="content-tab"><a href="#"><button class="btn btn--small btn--white btn--white--bordered">
Video + UX Training</button></a></li><li class="content-tab"><a href="#"><button class="btn btn--small btn--white btn--white--bordered">Video only</button></a></li></ul></nav><div class="book-cta__col book-cta__hardcover content-tab--content"><h3 class="book-cta__title"><span>Video + UX Training</span></h3><span class="book-cta__price"><span><span class=""><span class="currency-sign">$</span>&nbsp;<span>495<sup class="sup">.00</sup></span></span> <span class="book-cta__price--old"><span class="currency-sign">$</span>&nbsp;<span>799<sup class="sup">.00</sup></span></span></span></span>
<a href="https://smart-interface-design-patterns.thinkific.com/enroll/3081832?price_id=3951439" class="btn btn--full btn--medium btn--text-shadow">
Get Video + UX Training<div></div></a><p class="book-cta__desc">25 video lessons (8h) + <a href="https://smashingconf.com/online-workshops/workshops/vitaly-friedman-impact-design/">Live UX Training</a>.<br>100 days money-back-guarantee.</p></div><div class="book-cta__col book-cta__ebook content-tab--content"><h3 class="book-cta__title"><span>Video only</span></h3><div data-audience="anonymous free supporter" data-remove="true"><span class="book-cta__price" data-handler="PriceTag"><span><span class=""><span class="currency-sign">$</span>&nbsp;<span>250<sup class="sup">.00</sup></span></span><span class="book-cta__price--old"><span class="currency-sign">$</span>&nbsp;<span>395<sup class="sup">.00</sup></span></span></span></div>
<a href="https://smart-interface-design-patterns.thinkific.com/enroll/3081832?price_id=3950630" class="btn btn--full btn--medium btn--text-shadow">
Get the video course<div></div></a><p class="book-cta__desc" data-audience="anonymous free supporter" data-remove="true">25 video lessons (8h). Updated yearly.<br>Also available as a <a href="https://smart-interface-design-patterns.thinkific.com/enroll/3570306?price_id=4503439">UX Bundle with 3 video courses.</a></p></div><span></span></div></div>

<h2 id="useful-resources">Useful Resources</h2>

<ul>
<li>“<a href="https://measure-ux.com">How To Measure UX and Design Impact</a>”, by yours truly</li>
<li>“<a href="https://thecdo.school/books">Business Thinking For Designers</a>”, by Ryan Rumsey</li>
<li>“<a href="https://www.linkedin.com/feed/update/urn:li:activity:7338462034763661312/">ROI of Design Project</a></li>
<li>“<a href="https://articles.centercentre.com/how-the-right-ux-metrics-show-game-changing-value/">How the Right UX Metrics Show Game-Changing Value</a>”, by Jared Spool</li>
<li>“<a href="https://www.linkedin.com/posts/vitalyfriedman_ux-design-research-activity-7164173642887606274-rEqq">Research Sample Size Calculators</a>”</li>
</ul>

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

<ul>
<li>“<a href="https://www.smashingmagazine.com/2025/11/designing-for-stress-emergency/">Designing For Stress And Emergency</a>”, Vitaly Friedman</li>
<li>“<a href="https://www.smashingmagazine.com/2025/10/ai-ux-achieve-more-with-less/">AI In UX: Achieve More With Less</a>”, Paul Boag</li>
<li>“<a href="https://www.smashingmagazine.com/2025/11/accessibility-problem-authentication-methods-captcha/">The Accessibility Problem With Authentication Methods Like CAPTCHA</a>”, Eleanor Hecks</li>
<li>“<a href="https://www.smashingmagazine.com/2025/09/from-prompt-to-partner-designing-custom-ai-assistant/">From Prompt To Partner: Designing Your Custom AI Assistant</a>”, Lyndon Cerejo</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</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-05-12T16:11:33+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>Mansoor Ahmed Khan</author><title>From Chaos To Clarity: Simplifying Server Management With AI And Automation</title><link>https://www.smashingmagazine.com/2025/11/simplifying-server-management-ai-automation/</link><pubDate>Tue, 18 Nov 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/simplifying-server-management-ai-automation/</guid><description>Server chaos doesn’t have to be the norm. AI-ready infrastructure and automation can bring clarity, performance, and focus back to your web work.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/simplifying-server-management-ai-automation/" />
              <title>From Chaos To Clarity: Simplifying Server Management With AI And Automation</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>From Chaos To Clarity: Simplifying Server Management With AI And Automation</h1>
                  
                    
                    <address>Mansoor Ahmed Khan</address>
                  
                  <time datetime="2025-11-18T10:00:00&#43;00:00" class="op-published">2025-11-18T10:00:00+00:00</time>
                  <time datetime="2025-11-18T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>Cloudways</b></p>
                

<p>If you build or manage websites for a living, you know the feeling. Your day is a constant juggle; one moment you’re fine-tuning a design, the next you’re troubleshooting a slow server or a mysterious error. Daily management of a complex web of plugins, integrations, and performance tools often feels like you’re just reacting to problems—putting out fires instead of building something new.</p>

<p>This reactive cycle is exhausting, and it pulls your focus away from meaningful work and into the technical weeds. A recent industry event, <a href="https://www.cloudways.com/en/bfcm-prepathon.php">Cloudways Prepathon 2025</a>, put a sharp focus on this very challenge. The discussions made it clear: the future of web work demands a better way. It requires an infrastructure that’s ready for AI; one that can actively help you turn this daily chaos into clarity.</p>

<p><em>The stakes for performance are higher than ever.</em></p>

<p>Suhaib Zaheer, SVP of Managed Hosting at DigitalOcean, and Ali Ahmed Khan, Sr. Director of Product Management, shared a telling statistic during their panel: <strong><a href="https://www.thinkwithgoogle.com/consumer-insights/consumer-trends/mobile-site-load-time-statistics/">53% of mobile visitors</a> will leave a site if it takes more than three seconds to load.</strong></p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg">
    
    <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/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg"
			
			sizes="100vw"
			alt="Google data showing mobile page speed"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Data from Google underscores the critical importance of mobile page speed for retaining visitors. (Image Source: <a href='https://www.thinkwithgoogle.com/consumer-insights/consumer-trends/mobile-site-load-time-statistics/'>Think with Google</a>) (<a href='https://files.smashing.media/articles/simplifying-server-management-ai-automation/1-google-data-mobile-page-speed.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Think about that for a second, and within half that time, your potential traffic is gone. This isn’t just about a slow website, but about lost trust, abandoned carts, and missed opportunities. Performance is no longer just a feature; it’s the foundation of user experience. And in today’s landscape, automation is the key to maintaining it consistently.</p>

<p>So how do we stop reacting and start preventing?</p>

<h2 id="the-old-way-a-constant-state-of-alert">The Old Way: A Constant State Of Alert</h2>

<p>For too long, server management has worked like this: something breaks, you receive an alert (or worse, a client complaint), and you start digging. You log into your server, check logs, try to correlate different metrics, and eventually (hopefully) find the root cause. Then you manually apply a fix.</p>

<p>This process is fragile and relies on your constant attention while eating up hours that could be spent on development, strategy, or client work. For freelancers and small teams, this time is your most valuable asset. Every minute spent manually diagnosing a disk space issue or a web stack failure is a minute not spent on growing your business.</p>

<p>The problem isn&rsquo;t a lack of tools. It&rsquo;s that most tools just show you the data; they don&rsquo;t help you understand it or act on it. They add to the noise instead of providing clarity.</p>

<h2 id="a-new-approach-from-diagnosis-to-automatic-resolution">A New Approach: From Diagnosis To Automatic Resolution</h2>

<p>This is where a shift towards intelligent automation changes the game. Tools like <a href="https://www.cloudways.com/en/cloudways-ai-copilot.php">Cloudways Copilot</a>, which became generally available earlier this year, are built specifically to simplify this workflow. The goal is straightforward: combine AI-driven diagnostics with automated fixes to predict and resolve performance issues before they affect your users.</p>

<p>Here’s a practical look at how it works.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.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/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png"
			
			sizes="100vw"
			alt="Cloudways Copilot workflow"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Cloudways Copilot workflow: Continuous monitoring leads to instant alerts, AI-powered diagnosis, and actionable recommendations. (Image source: <a href='https://www.cloudways.com/en/cloudways-ai-copilot.php'>Cloudways</a>) (<a href='https://files.smashing.media/articles/simplifying-server-management-ai-automation/2-cloudways-copilot-workflow.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Imagine your site starts running slowly. In the past, you&rsquo;d begin the tedious investigation.</p>

<h3 id="1-the-ai-insights">1. The AI Insights</h3>

<p>Instead of a generic &ldquo;high CPU&rdquo; alert, you get a detailed insight. It tells you what happened (e.g., &ldquo;MySQL process is consuming excessive resources&rdquo;), why it happened (e.g., &ldquo;caused by a poorly optimized query from a recent plugin update&rdquo;), and provides a step-by-step guide to fix it manually. This alone cuts diagnosis time from 30-40 minutes down to about five. You understand the problem, not just the diagnosis.</p>

<h3 id="2-the-smartfix">2. The SmartFix</h3>

<p>This is where it moves from helpful to transformative. For common issues, you don’t just get a manual guide. You get a one-click <em>SmartFix</em> button. After reviewing the actions Copilot will take, you can let it automatically resolve the issue. It applies the necessary steps safely and without you needing to touch a command line. This is the clarity we’re talking about. The system doesn’t just tell you about the problem; it solves it for you.</p>

<p>For developers managing multiple sites, this is a fundamental change. It means you can handle routine server issues at scale. A disk cleanup that would have required logging into ten different servers can now be handled with a few clicks. It frees your brain from repetitive troubleshooting and lets you focus on the work that actually requires your expertise.</p>

<h2 id="building-an-ai-ready-foundation">Building An AI-Ready Foundation</h2>

<p>The principles discussed at Prepathon go beyond any single tool. The theme was about building a resilient foundation. Meeky Hwang, CEO at Ndevr, introduced the <em>&ldquo;3E Framework,&rdquo;</em> which perfectly applies here. A strong platform must balance:</p>

<ul>
<li><strong>Audience Experience</strong><br />
What your visitors see and feel—blazing speed and seamless operation.</li>
<li><strong>Creator Experience</strong><br />
The workflow for you and your team—managing content and marketing without technical friction.</li>
<li><strong>Developer Experience</strong><br />
The backend foundation—server management that is secure, stable, and efficient.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.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/simplifying-server-management-ai-automation/3-3e-framework.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png"
			
			sizes="100vw"
			alt="3E Framework"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A balanced platform is a resilient one. The 3E Framework shows how a strong foundation depends on three connected experiences. (Image source: <a href='https://www.cloudways.com/en/video/event-replays/prepathon-2025/from-fragile-to-ai-ready-websites-prepathon-2025'>Meeky Hwang / Ndevr</a>) (<a href='https://files.smashing.media/articles/simplifying-server-management-ai-automation/3-3e-framework.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>AI-driven server management directly strengthens all three. A faster, more stable server improves the <em>Audience Experience</em>. Fewer emergencies and simpler workflows improve the <em>Creator</em> and <em>Developer Experience</em>. When these are aligned, you can scale with confidence.</p>

<h2 id="this-isn-t-about-replacing-you">This Isn’t About Replacing You</h2>

<p>It’s important to be clear. This isn’t about replacing the developer but about augmenting your capabilities. As Vito Peleg, Co-founder &amp; CEO at Atarim, noted during <a href="https://www.cloudways.com/en/video/event-replays/prepathon-2025/whats-truly-working-in-ai-marketing-and-tech-prepathon-2025">Prepathon</a>:</p>

<blockquote>“We're all becoming prompt engineers in the modern world. Our job is no longer to do the task, but to orchestrate the fleet of AI agents that can do it at a scale we never could alone.”<br /><br />&mdash; Vito Peleg, Co-founder & CEO at Atarim</blockquote>

<p>Think of <a href="https://www.cloudways.com/en/cloudways-ai-copilot.php">Cloudways Copilot</a> as an expert sysadmin on your team. It handles the routine, often tedious, work. It alerts you to what’s important and provides clear, actionable context. This gives you back the mental space and time to focus on architecture, innovation, and client strategy.</p>

<blockquote>“The challenge isn’t managing servers anymore &mdash; it’s managing focus,”<br /><br /><a href="https://www.linkedin.com/in/zaheersuhaib/">Suhaib Zaheer</a> noted.<br /><br />“AI-driven infrastructure should help developers spend less time reacting to issues and more time creating better digital experiences.”</blockquote>

<h2 id="a-practical-path-forward">A Practical Path Forward</h2>

<p>For freelancers, WordPress experts, and small agency developers, this shift offers a tangible way to:</p>

<ul>
<li>Drastically reduce the hours spent manually troubleshooting infrastructure issues.</li>
<li>Implement predictive monitoring that catches slowdowns and bottlenecks early.</li>
<li>Manage your entire stack through clear, plain-English AI insights instead of raw data.</li>
<li>Balance speed, security, and uptime without needing an enterprise-scale budget or team.</li>
</ul>

<p>The goal is to make powerful infrastructure simple, while also giving you back control and your time so you can focus on what you do best: creating exceptional web experiences.</p>

<p><em>You can <a href="https://unified.cloudways.com/signup?coupon=BFCM5050">use promo code BFCM5050</a> to get 50% off for 3 months plus 50 Free Migrations using Cloudways. This offer is valid from November 18th to December 4th, 2025.</em></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>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-05-12T16:11:33+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-05-12T16:11:33+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>Matt Zeunert</author><title>Effectively Monitoring Web Performance</title><link>https://www.smashingmagazine.com/2025/11/effectively-monitoring-web-performance/</link><pubDate>Tue, 11 Nov 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/11/effectively-monitoring-web-performance/</guid><description>There are lots of tips for &lt;a href="https://www.debugbear.com/blog/improve-website-performance?utm_campaign=sm-10">improving your website performance&lt;/a>. But even if you follow all of the advice, are you able to maintain an optimized site? And are you targeting the right pages? Matt Zeunert outlines an effective strategy for web performance optimization and explains the roles that different types of data play in it.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/11/effectively-monitoring-web-performance/" />
              <title>Effectively Monitoring Web Performance</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Effectively Monitoring Web Performance</h1>
                  
                    
                    <address>Matt Zeunert</address>
                  
                  <time datetime="2025-11-11T10:00:00&#43;00:00" class="op-published">2025-11-11T10:00:00+00:00</time>
                  <time datetime="2025-11-11T10:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>DebugBear</b></p>
                

<p><a href="https://www.smashingmagazine.com/2023/08/running-page-speed-test-monitoring-versus-measuring/">There’s no single way to measure website performance.</a> That said, the <a href="https://www.smashingmagazine.com/2024/04/monitor-optimize-google-core-web-vitals/">Core Web Vitals</a> metrics that Google <a href="https://www.debugbear.com/docs/page-speed-seo?utm_campaign=sm-10">uses as a ranking factor</a> are a great starting point, as they cover different aspects of visitor experience:</p>

<ul>
<li><strong>Largest Contentful Paint (LCP):</strong> Measures the initial page load time.</li>
<li><strong>Cumulative Layout Shift (CLS)</strong>: Measures if content is stable after rendering.</li>
<li><strong>Interaction to Next Paint (INP)</strong>: Measures how quickly the page responds to user input.</li>
</ul>

<p>There are also <a href="https://www.debugbear.com/docs/web-performance-metrics?utm_campaign=sm-10">many other web performance metrics</a> that you can use to track technical aspects, like page weight or server response time. While these often don’t matter directly to the end user, they provide you with insight into what’s slowing down your pages.</p>

<p>You can also use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing">User Timing API</a> to track page load milestones that are important on your website specifically.</p>

<h2 id="synthetic-and-real-user-data">Synthetic And Real User Data</h2>

<p>There are <a href="https://www.debugbear.com/blog/synthetic-vs-rum?utm_campaing=sm-10">two different types</a> of web performance data:</p>

<ul>
<li><strong>Synthetic tests</strong> are run in a controlled test environment.</li>
<li><strong>Real user data</strong> is collected from actual website visitors.</li>
</ul>

<p>Synthetic monitoring can provide super-detailed reports to help you identify page speed issues. You can configure exactly how you want to collect the data, picking a specific network speed, device size, or test location.</p>

<p>Get a hands-on feel for synthetic monitoring by using the free <a href="https://www.debugbear.com/test/website-speed?utm_campaign=sm-10">DebugBear website speed test</a> to check on your website.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="672"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png"
			
			sizes="100vw"
			alt="DebugBear website speed report"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/1-debugbear-page-speed-report.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>That said, your synthetic test settings might not match what’s typical for your real visitors, and you can’t script all of the possible ways that people might interact with your website.</p>

<p>That’s why you also need real user monitoring (RUM). Instead of looking at one experience, you see different load times and how specific visitor segments are impacted. You can review specific page views to identify what caused poor performance for a particular visitor.</p>

<p>At the same time, real user data isn’t quite as detailed as synthetic test reports, due to web API limitations and performance concerns.</p>

<p>DebugBear offers both <a href="https://www.debugbear.com/synthetic-website-monitoring?utm_campaign=sm-10">synthetic monitoring</a> and <a href="https://www.debugbear.com/real-user-monitoring?utm_campaign=sm-10">real user monitoring</a>:</p>

<ul>
<li>To set up synthetic tests, you just need to enter a website URL, and</li>
<li>To collect real user metrics, you need to install an analytics snippet on your website.</li>
</ul>

<h2 id="three-steps-to-a-fast-website">Three Steps To A Fast Website</h2>

<p>Collecting data helps you throughout the lifecycle of your web performance optimizations. You can follow this three-step process:</p>

<ol>
<li><strong>Identify</strong>: Collect data across your website and identify slow visitor experiences.</li>
<li><strong>Diagnose</strong>: Dive deep into technical analysis to find optimizations.</li>
<li><strong>Monitor</strong>: Check that optimizations are working and get alerted to performance regressions.</li>
</ol>

<p>Let’s take a look at each step in detail.</p>

<h2 id="step-1-identify-slow-visitor-experiences">Step 1: Identify Slow Visitor Experiences</h2>

<p>What’s prompting you to look into website performance issues in the first place? You likely already have some specific issues in mind, whether that’s from customer reports or because of poor scores in the <a href="https://www.debugbear.com/blog/search-console-core-web-vitals?utm_campaign=sm-10">Core Web Vitals section of Google Search Console</a>.</p>

<p>Real user data is the best place to check for slow pages. It tells you whether the technical issues on your site actually result in poor user experience. It’s easy to collect across your whole website (while synthetic tests need to be set up for each URL). And, you can often get a view count along with the performance metrics. A moderately slow page that gets two visitors a month isn’t as important as a moderately fast page that gets thousands of visits a day.</p>

<p>The Web Vitals dashboard in DebugBear’s RUM product checks your site’s performance health and surfaces the most-visited pages and URLs where many visitors have a poor experience.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.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/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png"
			
			sizes="100vw"
			alt="Web Vitals dashboard in DebugBear’s RUM product"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/2-web-vitals-dashboard-debugbear-rum-product.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can also run a <a href="https://www.debugbear.com/docs/website-scan?utm_campaign=sm-10">website scan</a> to get a list of URLs from your sitemap and then check each of these pages against real user data from Google’s <a href="https://developer.chrome.com/docs/crux">Chrome User Experience Report (CrUX)</a>. However, this will only work for pages that meet a minimum traffic threshold to be included in the CrUX dataset.</p>

<p>The scan result highlights pages with poor web vitals scores where you might want to investigate further.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="632"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png"
			
			sizes="100vw"
			alt="Website scan result for ahrefs.com"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/3-website-scan-result-debugbear.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If no real-user data is available, then there is a scanning tool called <a href="https://www.debugbear.com/software/unlighthouse-website-scan">Unlighthouse</a>, which is based on Google’s Lighthouse tool. It runs synthetic tests for each page, allowing you to filter through the results in order to identify pages that need to be optimized.</p>

<h2 id="step-2-diagnose-web-performance-issues">Step 2: Diagnose Web Performance Issues</h2>

<p>Once you’ve identified slow pages on your website, you need to look at what’s actually happening on your page that is causing delays.</p>

<h3 id="debugging-page-load-time">Debugging Page Load Time</h3>

<p>If there are issues with page load time metrics &mdash; like the <a href="https://www.debugbear.com/docs/metrics/largest-contentful-paint?utm_campaign=sm-10">Largest Contentful Paint (LCP)</a> &mdash; synthetic test results can provide a detailed analysis. You can also run <a href="https://www.debugbear.com/docs/experiments?utm_campaign=sm-10">page speed experiments</a> to try out and measure the impact of certain optimizations.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="652"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png"
			
			sizes="100vw"
			alt="Page speed recommendations in synthetic data"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/4-page-speed-recommendations.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Real user data can still be important when debugging page speed, as load time depends on many user- and device-specific factors. For example, depending on the size of the user’s device, the page element that’s responsible for the LCP can vary. RUM data can provide a breakdown of possible influencing factors, like CSS selectors and image URLs, across all visitors, helping you zero in on what exactly needs to be fixed.</p>

<h3 id="debugging-slow-interactions">Debugging Slow Interactions</h3>

<p>RUM data is also generally needed to properly diagnose issues related to the <a href="https://debugbear.com/docs/rum/fix-inp-issues?utm_campaign=sm-10">Interaction to Next Paint (INP)</a> metric. Specifically, real user data can provide insight into what causes slow interactions, which helps you answer questions like:</p>

<ul>
<li>What page elements are responsible?</li>
<li>Is time spent processing already-active background tasks or handling the interaction itself?</li>
<li>What scripts contribute the most to overall CPU processing time?</li>
</ul>

<p>You can view this data at a high level to identify trends, as well as review specific page views to see what impacted a specific visitor experience.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="642"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png"
			
			sizes="100vw"
			alt="Interaction to Next Paint metric, which reviews specific page views"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/5-inp-interaction-element.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="step-3-monitor-performance-respond-to-regressions">Step 3: Monitor Performance &amp; Respond To Regressions</h2>

<p>Continuous monitoring of your website performance lets you track whether the performance is improving after making a change, and alerts you when scores decline.</p>

<p>How you respond to performance regressions depends on whether you’re looking at lab-based synthetic tests or real user analytics.</p>

<h3 id="synthetic-data">Synthetic Data</h3>

<p>Test settings for synthetic tests are standardized between runs. While infrastructure changes, like browser upgrades, occasionally cause changes, performance is more generally determined by resources loaded by the website and the code it runs.</p>

<p>When a metric changes, DebugBear lets you view a before-and-after comparison between the two test results. For example, the next screenshot displays a regression in the First Contentful Paint (FCP) metric. The comparison reveals that new images were added to the page, <a href="https://www.debugbear.com/blog/bandwidth-competition-page-speed?utm_campaign=sm-10">competing for bandwidth with other page resources</a>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="720"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png"
			
			sizes="100vw"
			alt="Before-and-after comparison between the two synthetic test results"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/6-synthetic-tests.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>From the report, it’s clear that a CSS file that previously took 255 milliseconds to load now takes 915 milliseconds. Since stylesheets are required to render page content, this means the page now loads more slowly, giving you better insight into what needs optimization.</p>

<h3 id="real-user-data">Real User Data</h3>

<p>When you see a change in real user metrics, there can be two causes:</p>

<ol>
<li>A shift in visitor characteristics or behavior, or</li>
<li>A technical change on your website.</li>
</ol>

<p>Launching an ad campaign, for example, often increases redirects, reduces cache hits, and shifts visitor demographics. When you see a regression in RUM data, the first step is to find out if the change was on your website or in your visitor’s browser. Check for view count changes in ad campaigns, referrer domains, or network speed to get a clearer picture.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.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/effectively-monitoring-web-performance/7-lcp-utm-campaign.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png"
			
			sizes="100vw"
			alt="LCP by UTM campaign"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/7-lcp-utm-campaign.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If those visits have different performance compared to your typical visitors, then that suggests the repression is not due to a change on your website. However, you may still need to make changes on your website to better serve these visitor cohorts and deliver a good experience for them.</p>

<p>To identify the cause of a technical change, take a look at component breakdown metrics, such as <a href="https://www.smashingmagazine.com/2025/03/how-to-fix-largest-contentful-issues-with-subpart-analysis/">LCP subparts</a>. This helps you narrow down the cause of a regression, whether it is due to changes in server response time, new render-blocking resources, or the LCP image.</p>

<p>You can also check for shifts in page view properties, like different LCP element selectors or specific scripts that cause poor performance.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.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/effectively-monitoring-web-performance/8-lcp-subparts.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png"
			
			sizes="100vw"
			alt="INP subparts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/8-lcp-subparts.png'>Large preview</a>)
    </figcaption>
  
</figure>

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

<p>One-off page speed tests are a great starting point for optimizing performance. However, a monitoring tool like DebugBear can form the basis for a more comprehensive web <strong>performance strategy</strong> that helps you stay fast for the long term.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="477"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png"
			
			sizes="100vw"
			alt="Summary of performance metrics on DebugBear"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/effectively-monitoring-web-performance/9-debugbear-web-performance.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Get <a href="https://www.debugbear.com/?utm_campaign=sm-10">a free DebugBear trial</a> on our website!</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>Preethi Sam</author><title>SerpApi: A Complete API For Fetching Search Engine Data</title><link>https://www.smashingmagazine.com/2025/09/serpapi-complete-api-fetching-search-engine-data/</link><pubDate>Tue, 16 Sep 2025 17:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/09/serpapi-complete-api-fetching-search-engine-data/</guid><description>From competitive SEO research and monitoring prices to training AI and parsing local geographic data, real-time search results power smarter apps. Tools like SerpApi make it easy to pull, customize, and integrate this data directly into your app or website.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/09/serpapi-complete-api-fetching-search-engine-data/" />
              <title>SerpApi: A Complete API For Fetching Search Engine Data</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>SerpApi: A Complete API For Fetching Search Engine Data</h1>
                  
                    
                    <address>Preethi Sam</address>
                  
                  <time datetime="2025-09-16T17:00:00&#43;00:00" class="op-published">2025-09-16T17:00:00+00:00</time>
                  <time datetime="2025-09-16T17:00:00&#43;00:00" class="op-modified">2026-05-12T16:11:33+00:00</time>
                </header>
                <p>This article is sponsored by <b>SerpApi</b></p>
                

<p>SerpApi leverages the power of search engine giants, like Google, DuckDuckGo, Baidu, and more, to put together the most pertinent and accurate search result data for your users from the comfort of your app or website. It’s customizable, adaptable, and offers an easy integration into any project.</p>

<p>What do you want to put together?</p>

<ul>
<li>Search information on a brand or business for <a href="https://serpapi.com/use-cases/seo?utm_source=smashingmagazine">SEO purposes</a>;</li>
<li>Input data to <a href="https://serpapi.com/use-cases/machine-learning-and-artificial-intelligence?utm_source=smashingmagazine">train AI models</a>, such as the Large Language Model, for a customer service chatbot;</li>
<li>Top <a href="https://serpapi.com/use-cases/news-monitoring?utm_source=smashingmagazine">news</a> and websites to pick from for a subscriber newsletter;</li>
<li><a href="https://serpapi.com/google-flights-api?utm_source=smashingmagazine">Google Flights API</a>: collect flight information for your travel app;</li>
<li><a href="https://serpapi.com/use-cases/price-monitoring?utm_source=smashingmagazine">Price</a> comparisons for the same product across different platforms;</li>
<li>Extra definitions and examples for words that can be offered along a language learning app.</li>
</ul>

<p>The list goes on.</p>

<p>In other words, you get to leverage the most comprehensive source of data on the internet for any number of needs, from <a href="https://serpapi.com/use-cases/seo?utm_source=smashingmagazine">competitive SEO research</a> and <a href="https://serpapi.com/use-cases/news-monitoring?utm_source=smashingmagazine">tracking news</a> to <a href="https://serpapi.com/use-cases/local-seo?utm_source=smashingmagazine">parsing local geographic data</a> and even <a href="https://serpapi.com/use-cases/background-check-automation?utm_source=smashingmagazine">completing personal background checks</a> for employment.</p>

<h2 id="start-with-a-simple-get-request">Start With A Simple GET Request</h2>

<p>The results from the <a href="https://serpapi.com?utm_source=smashingmagazine/#integrationsMountPoint">search API</a> are <strong>only a URL request away</strong> for those who want a super quick start. Just add your search details in the URL parameters. Say you need the search result for “Stone Henge” from the location “Westminster, England, United Kingdom” in language “en-GB”, and country of search origin “uk” from the domain “google.co.uk”. Here’s how simple it is to put the GET request together:</p>

<div class="break-out">
<pre><code class="language-json">https://serpapi.com/search.json?q=Stone+Henge&location=Westminster,+England,+United+Kingdom&hl=en-GB&gl=uk&google_domain=google.co.uk&api_key=your_api_key
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.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/serpapi-complete-api-fetching-search-engine-data/1-get-request.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png"
			
			sizes="100vw"
			alt=""
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/1-get-request.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Then there’s the impressive list of libraries that seamlessly integrate the APIs into mainstream programming languages and frameworks such as JavaScript, Ruby, .NET, and more.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.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/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png"
			
			sizes="100vw"
			alt="JavaScript integration code for SerpApi"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      JavaScript integration code for SerpApi. (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/2-javascript-integration-code-serpapi.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.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/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png"
			
			sizes="100vw"
			alt="Table of SerpApi libraries showing information about seven libraries."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/3-table-serpapi-libraries.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="give-it-a-quick-try">Give It A Quick Try</h2>

<p>Want to give it a spin? <a href="https://serpapi.com/users/sign_up?utm_source=smashingmagazine">Sign up and start for free</a>, or tinker with the SerpApi’s <a href="https://serpapi.com/playground?utm_source=smashingmagazine">live playground</a> without signing up. The <strong>playground</strong> allows you to choose which search engine to target, and you can fill in the values for all the basic parameters available in the chosen API to customize your search. On clicking “Search”, you get the search result page and its extracted JSON data.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.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/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png"
			
			sizes="100vw"
			alt="Playground search for flights from LGW to MLA airport using SerpApi’s Google Flights API."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Playground search for flights from LGW to MLA airport using SerpApi’s Google Flights API. (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/4-serpapi-google-flights-api.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If you need to get a feel for the full API first, you can explore their easy-to-grasp <a href="https://serpapi.com/search-api?utm_source=smashingmagazine">web documentation</a> before making any decision. You have the chance to work with all of the APIs to your satisfaction before committing to it, and when that time comes, SerpApi’s multiple <a href="https://serpapi.com/pricing?utm_source=smashingmagazine">price plans</a> tackle anywhere between an economic few hundred searches a month and bulk queries fit for large corporations.</p>

<h2 id="what-data-do-you-need">What Data Do You Need?</h2>

<p>Beyond the rudimentary search scraping, SerpApi provides a range of configurations, features, and additional APIs worth considering.</p>

<h3 id="geolocation">Geolocation</h3>

<p>Capture the global trends, or refine down to more localized particulars by names of locations or Google’s place identifiers. SerpApi’s optimized routing of requests ensures <strong>accurate retrieval of search result</strong> data from any location worldwide. If locations themselves are the answers to your queries &mdash; say, a cycle trail to be suggested in a fitness app &mdash; those can be extracted and presented as maps using SerpApi’s <a href="https://serpapi.com/google-maps-api?utm_source=smashingmagazine">Google Maps API</a>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.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/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png"
			
			sizes="100vw"
			alt="SerpApi’s cycle route"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/5-serpapi-geolocation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="structured-json">Structured JSON</h3>

<p>Although search engines reveal results in a tidy user interface, deriving data into your application could cause you to end up with a large data dump to be sifted through &mdash; but not if you’re using SerpApi.</p>

<p>SerpApi pulls data in a <strong>well-structured JSON format</strong>, even for the popular kinds of <em>enriched search results</em>, such as knowledge graphs, review snippets, sports league stats, ratings, product listings, AI overview, and more.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.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/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png"
			
			sizes="100vw"
			alt="Example of SerpApi returning data in a JSON format."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      SerpApi returns data in a JSON format, making it easy to integrate into your application. (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/6-serpapi-data-json-format.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.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/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png"
			
			sizes="100vw"
			alt="Various types of search engine results, such as meta related to video, audio, geolocation, questions, and recipes."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Various types of search engine results, such as meta related to video, audio, geolocation, questions, and recipes. (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/7-types-search-engine-results.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="speedy-results">Speedy Results</h3>

<p>SerpApi’s baseline performance can take care of timely search data for real-time requirements. But what if you need more? SerpApi’s <a href="https://serpapi.com/ludicrous-speed"><strong>Ludicrous Speed</strong></a> option, easily enabled from the dashboard with an upgrade, provides a super-fast response time. More than twice as fast as usual, thanks to twice the server power.</p>

<p>There’s also <a href="https://serpapi.com/ludicrous-speed-max"><strong>Ludicrous Speed Max</strong></a>, which allocates four times more server resources for your data retrieval. Data that is time-sensitive and for monitoring things in real-time, such as sports scores and tracking product prices, will lose its value if it is not handled in a timely manner. Ludicrous Speed Max guarantees no delays, even for a large-scale enterprise haul.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.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/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png"
			
			sizes="100vw"
			alt="A list of flight prices from Google Flights"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      A list of flight prices from Google Flights. (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/8-list-flight-prices.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You can also use a relevant SerpApi API to hone in on your <strong>relevant category</strong>, like <a href="https://serpapi.com/google-flights-api?utm_source=smashingmagazine">Google Flights API</a>, <a href="https://serpapi.com/amazon-search-api?utm_source=smashingmagazine">Amazon API</a>, <a href="https://serpapi.com/google-news-api?utm_source=smashingmagazine">Google News API</a>, etc., to get fresh and apt results.</p>

<p>If you don’t need the full depth of the <a href="https://serpapi.com?utm_source=smashingmagazine/#integrationsMountPoint">search</a> <a href="https://serpapi.com?utm_source=smashingmagazine/#integrationsMountPoint">API</a>, there’s a <strong>Light version</strong> available for Google Search, Google Images, Google Videos, Google News, and DuckDuckGo Search APIs.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.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/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png"
			
			sizes="100vw"
			alt="Three-column list of 45 Search APIs that are supported by SerpApi"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/9-serpapi-api-list.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="search-controls-privacy">Search Controls &amp; Privacy</h3>

<p>Need the results asynchronously picked up? Want a refined output using advanced <a href="https://serpapi.com?utm_source=smashingmagazine/#integrationsMountPoint">search</a> <a href="https://serpapi.com?utm_source=smashingmagazine/#integrationsMountPoint">API</a> parameters and a JSON Restrictor? Looking for search outcomes for specific devices? Don’t want auto-corrected query results? <strong>There’s no shortage of ways to configure SerpApi to get exactly what you need.</strong></p>

<p>Additionally, if you prefer not to have your search metadata on their servers, simply turn on the <a href="https://serpapi.com/zero-trace-mode?utm_source=smashingmagazine"><strong>“ZeroTrace” mode</strong></a> that’s available for selected plans.</p>

<h3 id="the-x-ray">The X-Ray</h3>

<p>Save yourself a headache, literally, trying to play match between what you see on a search result page and its extracted data in JSON. SerpApi’s <a href="https://serpapi.com/xray?utm_source=smashingmagazine"><strong>X-Ray tool</strong></a> <strong>shows you where what comes from</strong>. It’s available and free in all plans.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.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/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png"
			
			sizes="100vw"
			alt="SerpApi’s X-Ray tool"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/serpapi-complete-api-fetching-search-engine-data/10-serpapi-x-ray.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="inclusive-support">Inclusive Support</h3>

<p>If you don’t have the expertise or resources for tackling the validity of scraping search results, here’s what SerpApi says:</p>

<blockquote>“SerpApi, LLC assumes scraping and parsing liabilities for both domestic and foreign companies unless your usage is otherwise illegal”.</blockquote>

<p>You can reach out and have a conversation with them regarding the legal protections they offer, as well as inquire about anything else you might want to know about, including SerpApi in your project, such as pricing, performance expected, on-demand options, and technical support.  Just drop a message at their <a href="https://serpapi.com/#contact">contact page</a>.</p>

<p>In other words, the SerpApi team has your back with the support and expertise to get the most from your fetched data.</p>

<h3 id="try-serpapi-free">Try SerpApi Free</h3>

<p>That’s right, you can get your hands on SerpApi today and start fetching data with absolutely no commitment, thanks to a free starter plan that gives you up to 250 free search queries. Give it a try and then bump up to one of the reasonably-priced monthly subscription plans with generous search limits.</p>

<ul>
<li><a href="https://serpapi.com/users/sign_up?utm_source=smashingmagazine">Try SerpApi</a></li>
</ul>

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


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

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

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

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

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

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

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

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

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


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

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














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

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

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

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

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














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

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

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














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

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

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














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

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

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

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














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

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

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

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














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

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

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

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














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

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

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

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

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

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

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

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














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

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

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

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

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

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

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

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


              </article>
            </body>
          </html>
        ]]></content:encoded></item></channel></rss>