<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>CSS-Tricks</title>
	<atom:link href="https://css-tricks.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://css-tricks.com</link>
	<description>Tips, Tricks, and Techniques on using Cascading Style Sheets.</description>
	<lastBuildDate>Mon, 22 Jun 2026 13:54:28 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&#038;ssl=1</url>
	<title>CSS-Tricks</title>
	<link>https://css-tricks.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">45537868</site>	<item>
		<title>Using Scroll-Driven Animations for Opposing Scroll Directions</title>
		<link>https://css-tricks.com/scroll-driven-animations-opposing-scroll-directions/</link>
					<comments>https://css-tricks.com/scroll-driven-animations-opposing-scroll-directions/#comments</comments>
		
		<dc:creator><![CDATA[Silvestar Bistrović]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 12:39:13 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[animations]]></category>
		<category><![CDATA[Scroll Driven Animation]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394890</guid>

					<description><![CDATA[<p class="wp-block-paragraph">Sometimes designers have silly ideas that eventually grow on you. That happened to me with this concept where I had to build columns of items moving in opposite directions when a user scrolls the page.</p>
<p>CodePen Embed Fallback</p>
<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> This &#8230;</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/scroll-driven-animations-opposing-scroll-directions/">Using Scroll-Driven Animations for Opposing Scroll Directions</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Sometimes designers have silly ideas that eventually grow on you. That happened to me with this concept where I had to build columns of items moving in opposite directions when a user scrolls the page.</p>



<span id="more-394890"></span>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019dcdfc-1e41-77c8-afdf-810ebc6f2480" src="//codepen.io/editor/anon/embed/019dcdfc-1e41-77c8-afdf-810ebc6f2480?height=450&amp;theme-id=1&amp;slug-hash=019dcdfc-1e41-77c8-afdf-810ebc6f2480&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019dcdfc-1e41-77c8-afdf-810ebc6f2480" title="CodePen Embed 019dcdfc-1e41-77c8-afdf-810ebc6f2480" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> This demo respects reduced motion settings, so you&#8217;ll need to enable motion to see the effect. And we&#8217;re looking at Chrome and Safari support as I&#8217;m writing this.</p>



<p class="wp-block-paragraph">It’s really not as hard as you might think, thanks to modern CSS features, specifically <a href="https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/">scroll-driven animations</a>. Not only that, but it’s fun to make, too! Let me show you how I approached it — and maybe you will want to share how you would do it differently.</p>



<h2 id="the-html" class="wp-block-heading">The HTML</h2>



<p class="wp-block-paragraph">The HTML consists of a parent element (<code>.opposing-columns</code>), its children (<code>.opposing-column</code>), and its children’s children (<code>.opposing-item</code>):</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="opposing-columns">
  &lt;!-- Column 1 -->
  &lt;div class="opposing-column">
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
  &lt;/div>
  &lt;!-- Column 2 -->
  &lt;div class="opposing-column">
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
  &lt;/div>
  &lt;!-- Column 3 -->
  &lt;div class="opposing-column">
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
    &lt;div class="opposing-item">...&lt;/div>
  &lt;/div>
&lt;/div></code></pre>



<p class="wp-block-paragraph">This is all we need in the markup. CSS will do the rest!</p>



<h2 id="styling-the-parent-container" class="wp-block-heading">Styling the parent container</h2>



<p class="wp-block-paragraph">First off, we’re going to set things up so that this effect only applies to larger screens — there’s no real sense in supporting something like this on smaller screens because we need the additional space for the effect.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Just on larger screens */
@media screen and (width >= 50rem) {
  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 50rem);
    margin-inline: auto;
  }
}</code></pre>



<h2 id="setting-up-a-masking-effect" class="wp-block-heading">Setting up a “masking” effect</h2>



<p class="wp-block-paragraph">We need to do a few more things with the parent container to get the illusion that items in each <code>.opposing-column</code> are disappearing as they scroll past it. The items in the outer columns move upward on scroll, and items in the center column move downward. As they cross the parents’ boundaries, we want them to sorta fade out.</p>



<figure class="wp-block-video"><video height="1256" style="aspect-ratio: 1914 / 1256;" width="1914" controls src="https://css-tricks.com/wp-content/uploads/2026/05/opposing-scrolling-logos.mov" playsinline></video></figure>



<p class="wp-block-paragraph">So, we’re going to do a few things. First, we’ll set a background color variable on the document as a whole:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    /* same styles as before */
  }
}</code></pre>



<p class="wp-block-paragraph">Second, we’ll apply that same background color on the parent’s <code>:before</code> and <code>:after</code> pseudo-elements:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    /* same styles as before */
  
    &amp;:before,
    &amp;:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}</code></pre>



<p class="wp-block-paragraph">Notice that we’ve established a stacking context on the pseudos and set them one layer above the parent and its descendants. This is key for masking the items in each column as they scroll in and out of the container. The items are technically sliding <em>under</em> the pseudo masks.</p>



<p class="wp-block-paragraph">Speaking of which, let’s create another variable called <code>--opposing-mask</code> that adds vertical space between the parent element and the three columns:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line="4,13"><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 50rem);
    margin-inline: auto;
    margin-block: var(--opposing-mask, 3rem);
    position: relative;
  }
}</code></pre>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" decoding="async" width="1937" height="1244" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?resize=1937%2C1244&#038;ssl=1" alt="Highlighting the vertical space between the parent container and its child elements." class="wp-image-394894" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?w=1937&amp;ssl=1 1937w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?resize=300%2C193&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?resize=1024%2C658&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?resize=768%2C493&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-column-margin.webp?resize=1536%2C986&amp;ssl=1 1536w" sizes="(min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">Let’s do the same thing to the parents’ pseudos, only applying <code>--opposing-mask</code> to their <code>block-size</code> by a multiple of three. This way, there’s additional vertical space between them and the parent.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line="15"><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    /* same styles as before */
  
    &amp;:before,
    &amp;:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}</code></pre>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1937" height="1244" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?resize=1937%2C1244&#038;ssl=1" alt="Highlighting the vertical space between the parent container and its before pseudo element." class="wp-image-394893" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?w=1937&amp;ssl=1 1937w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?resize=300%2C193&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?resize=1024%2C658&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?resize=768%2C493&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/scrolling-parent-pseudo-margin.webp?resize=1536%2C986&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">You might see where this is going. We have a nice amount of space between the parent container and its pseudos. We want the column items to appear as if they are fading out as they scroll out of the parent container. We don’t have to mess with their opacity or anything like that. Instead, we can add background gradients on the pseudos.</p>



<p class="wp-block-paragraph">The <code>:before</code> pseudo is at the top of the container, so we’ll give it a gradient that goes from a solid color that matches the document’s underlying background color to transparent, top-to-bottom. And since the <code>:after</code> pseudo sits at the bottom of the parent container, we’ll reverse the gradient so it goes transparent to the document’s background color, bottom-to-top.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    /* same styles as before */
  }
  
  .opposing-columns {
      /* same styles as before */
    
      &amp;:before,
      &amp;:after {
        /* same styles as before */
      }
      
      &amp;:before {
        background-image: linear-gradient(
          to bottom,
          var(--opposing-bg) var(--opposing-mask),
          transparent
        );
        inset-block-start: calc(var(--opposing-mask) * -1);
      }

      &amp;:after {
        background-image: linear-gradient(
          to top,
          var(--opposing-bg) var(--opposing-mask),
          transparent
        );
        inset-block-end: calc(var(--opposing-mask) * -1);
      }
    }
  }
}</code></pre>



<h2 id="the-column-layouts" class="wp-block-heading">The column layouts</h2>



<p class="wp-block-paragraph">Before we get to the magic, we ought to lay out the items in each column. Each column is a flex item inside the parent, which is a flex container. We’ll let them shrink (<code>flex-shrink: 1</code>) and grow (<code>flex-grow: 1</code>), capping the size at a certain point (<code>flex-basis: 10rem</code>).</p>



<p class="wp-block-paragraph">We can define all that with the <code><a href="https://css-tricks.com/almanac/properties/f/flex/">flex</a></code> shorthand property:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    flex: 1 1 10rem;
  }
}</code></pre>



<p class="wp-block-paragraph">Now I want those columns to be grid containers so I can use the <code>gap</code> property to insert space between items:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    flex: 1 1 10rem;
    display: grid;
    gap: 2rem;
  }
}</code></pre>



<p class="wp-block-paragraph">We totally could have used Flexbox here as well to get access to <code>gap</code>, but the default layout is set to <code>row</code> and we’d have to override that to <code>column</code>. Grid is a little more concise in this situation.</p>



<h2 id="the-animation-" class="wp-block-heading">The animation!</h2>



<p class="wp-block-paragraph">This is what you came for, right? We’ve set everything up so that column items can flow in and out of the parent container on scroll. Now we need to add that scrolling behavior.</p>



<p class="wp-block-paragraph">This is where the <code>animation-timeline</code> property comes real handy. Normally, a CSS animation just runs on its own. It starts when the page loads (or after a specific delay you set) and ends after however long you set the duration. With <code>animation-timeline</code>, we tell the animation to run based on its scroll position… hence the term “scroll-driven” animation.</p>



<p class="wp-block-paragraph">We have two supported functions here, <a href="https://css-tricks.com/almanac/functions/s/scroll/"><code>scroll()</code></a> and <a href="https://css-tricks.com/almanac/functions/v/view/"><code>view()</code></a>. They’re related but super different in that <code>scroll()</code> runs the animation based on an element’s scroll <em>position</em>. The <code>view()</code> function is similar, but tracks the element’s progress as it enters and exits the <a href="https://drafts.csswg.org/css-overflow-3/#scrollport" rel="noopener">scrollport</a> (i.e., the scrollable area of the container it is in).</p>



<p class="wp-block-paragraph">We’re going with the <code>view()</code> function because we’ve set this up where there is a clear scrollable area inside the parent container. We need to run the animation based on where it enters and exits that area rather than the scroll position of the column items.</p>



<p class="wp-block-paragraph">This is real interesting because we can tell <code>view()</code> where exactly we want the animation to start once it <em>enters</em> the scrollable area and where to stop once it <em>exits</em> that same area. Like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Official syntax */
animation-timeline:  view([ &lt;axis> || &lt;'view-timeline-inset'>]?);</code></pre>



<p class="wp-block-paragraph">Let’s start by defining the axes:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timeline: view();
    animation-range: entry cover;
  }
}</code></pre>



<p class="wp-block-paragraph">This is just partially what we want, but what we’re saying is we want the animation to (1) start the very moment is enters the scrollport (<code>entry</code>), and (2) end when it completely leaves the area (<code>cover</code>). We need to be explicitly about the insets because that’s what establishes the animation’s range relative to where it enters and exits. We want the full range, so the <code>entry</code> begins at <code>0%</code> and the exit is when an item is <code>cover</code>ed at <code>100%</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}</code></pre>



<p class="wp-block-paragraph">Lastly, we’ll set the animation to run linearly — no need for the items to slow up or down as they scroll.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timing-function: linear;
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}</code></pre>



<p class="wp-block-paragraph">OK, great. But what we haven’t done is create an animation. We&#8217;ve set up what we want it to do when it runs, but we need to define the actual movement.</p>



<p class="wp-block-paragraph">I want to set up three separate CSS animations:</p>



<ol class="wp-block-list">
<li>One that translates (moves) the items upward in the first column.</li>



<li>One that’s the reverse of the first animation for the items in the other column.</li>
</ol>



<p class="wp-block-paragraph">We could technically set the first animation on both of the outer columns, but I want a third one that is a little bit offset from the first so those columns appear staggered.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@keyframes scroll1 {
  from { transform: translateY(var(--opposing-mask)); }
  to { transform: translateY(calc(var(--opposing-mask) * -1)); }
}

@keyframes scroll2 {
  from { transform: translateY(calc(var(--opposing-mask) * -1)); }
  to { transform: translateY(var(--opposing-mask)); }
}

@keyframes scroll3 {
  from { transform: translateY(calc(var(--opposing-mask) * .66)); }
  to { transform: translateY(calc(var(--opposing-mask) * -.33)); }
}</code></pre>



<p class="wp-block-paragraph">We can create variables for these, of course, should we ever need to update them:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    --animation-1: scroll1;
    --animation-2: scroll2;
    --animation-3: scroll3;

    /* ... */
  }
}</code></pre>



<p class="wp-block-paragraph">&#8230;and apply them to each column:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* same styles as before */
  }

  :where(.opposing-column:nth-of-type(1)) {
    animation-name: var(--animation-1);
  }
  
  :where(.opposing-column:nth-of-type(2)) {
    animation-name: var(--animation-2);
  }

  :where(.opposing-column:nth-of-type(3)) {
    animation-name: var(--animation-3);
  }
}</code></pre>



<p class="wp-block-paragraph">While we’re at it, we should disable the animations to <a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/">respect the user’s settings for reduced motion</a> (and remove the mask, otherwise it might look weird):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: reduce) { 
  .opposing-column {
    animation: unset;

    &amp;:before,
    &amp;:after {
      content: unset;
    }
  }
}</code></pre>



<h2 id="wrapping-up" class="wp-block-heading">Wrapping up</h2>



<p class="wp-block-paragraph">So yeah, scroll-driven animations are really, really cool. We’re still waiting for Firefox support as I’m writing this, but you can certainly wrap this in <code>@supports</code> to provide a default experience that uses thew scroll annotations and then set a fallback experience for non-supporting browsers, like running on a normal animation timeline:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@supports (animation-timeline: view()) {
  /* ... */
}</code></pre>



<p class="wp-block-paragraph">This is just toe-dipping into what scroll-driven animations can do, of course. What sort of things have you made or experimented with? Or would you approach this one differently? Let me know!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/scroll-driven-animations-opposing-scroll-directions/">Using Scroll-Driven Animations for Opposing Scroll Directions</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/scroll-driven-animations-opposing-scroll-directions/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/05/opposing-scrolling-logos.mov" length="38364603" type="video/quicktime" />

		<post-id xmlns="com-wordpress:feed-additions:1">394890</post-id>	</item>
		<item>
		<title>A First Look at Scroll-Triggered Animations</title>
		<link>https://css-tricks.com/css-scroll-triggered-animations-first-look/</link>
					<comments>https://css-tricks.com/css-scroll-triggered-animations-first-look/#comments</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 19 Jun 2026 13:03:17 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[animation]]></category>
		<category><![CDATA[Scroll-Triggered Animation]]></category>
		<category><![CDATA[scrolling]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394364</guid>

					<description><![CDATA[<p>Let's poke at the differences between scroll-<em>driven</em> and scroll-<em>triggered</em> animations.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-scroll-triggered-animations-first-look/">A First Look at Scroll-Triggered Animations</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Chrome has shipped scroll-triggered animations, and is the first browser to do so. If you update to <a href="https://developer.chrome.com/release-notes/146" rel="noopener">Chrome 146</a>, you can view the demo below, where the background of a square fades in over the duration of <code>300ms</code>, but only once the whole element is within the viewport.</p>



<span id="more-394364"></span>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_WbGdJRe" src="//codepen.io/anon/embed/WbGdJRe?height=450&amp;theme-id=1&amp;slug-hash=WbGdJRe&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed WbGdJRe" title="CodePen Embed WbGdJRe" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/1-1.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">This is a bit different to how <a href="https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/">scroll-<em>driven</em> animations</a> work, so in this article I’ll compare them, and then show you how scroll-triggered animations work.</p>



<h2 id="scroll-triggered-animations-vs-scroll-driven-animations" class="wp-block-heading">Scroll-triggered animations vs. scroll-driven animations</h2>



<p class="wp-block-paragraph">Scroll-triggered animations play for a fixed duration once a certain scroll threshold has been surpassed. (Think JavaScript’s <a href="https://css-tricks.com/an-explanation-of-how-the-intersection-observer-watches/">Intersection Observer API</a> but for CSS animations.)</p>



<p class="wp-block-paragraph">This differs from scroll-driven animations, where animation progression is synchronized with scroll progression (<code>animation-timeline: scroll()</code>) or the degree of intersection (<code>animation-timeline: view()</code>), and thus has no duration.</p>



<h2 id="basic-scroll-triggered-animation-example" class="wp-block-heading">Basic scroll-triggered animation example</h2>



<p class="wp-block-paragraph">The key part is <code>timeline-trigger: view()</code> instead of <code>animation-timeline: view()</code>, which waits for the element to be within the threshold instead of measuring <em>how much</em> it’s within it and doing something accordingly. However, let’s start with the actual <code>@keyframes</code> animation, which sets the <code>background</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Define the animation */
@keyframes fade-bg-in {
  to {
    background: currentColor;
  }
}</code></pre>



<p class="wp-block-paragraph">It’s set on the <code>.square</code> over the duration of <code>300ms</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;
}</code></pre>



<p class="wp-block-paragraph">By default, CSS animations trigger when the declaration is applied, but in the expanded snippet below, <code>timeline-trigger</code> overwrites that behavior. Now the animation triggers when the element comes into <code>view()</code>. The <code>--trigger</code> is simply a dashed ident that acts as an identifier for the trigger, whereas <code>entry 100% exit 0%</code> is a timeline range. A timeline range specifies the scroll zone in which the animation activates and is allowed to remain active.</p>



<p class="wp-block-paragraph">In this case, the animation triggers when the bottom edge of the <code>.square</code> enters the (<code>entry 100%</code>) and untriggers (assuming that it’s still running) when the top edge exits the scrollport (<code>exit</code> <code>0%</code>). For clarity, <code>entry 0%</code> would trigger the animation when the <em>top</em> edge enters. <code>entry</code> handles the element coming in from the bottom of the scrollport, whereas <code>exit</code> handles it leaving through the top. It’s a bit confusing, but it’s easier to understand if I don’t over-explain it.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;

  /* Animation trigger conditions */
  timeline-trigger: --trigger view() entry 100% exit 0%;
}</code></pre>



<p class="wp-block-paragraph">For <code>animation-trigger</code>, we first specify which trigger we’re talking about, and then we declare some settings (e.g., <code>play-forwards</code>):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;

  /* Animation trigger conditions */
  timeline-trigger: --trigger view() entry 100% exit 0%;

  /* Animation trigger settings */
  animation-trigger: --trigger play-forwards;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_jEMmLPz" src="//codepen.io/anon/embed/jEMmLPz?height=450&amp;theme-id=1&amp;slug-hash=jEMmLPz&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed jEMmLPz" title="CodePen Embed jEMmLPz" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-2.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">The <code>play-forwards</code> keyword triggers the animation whenever the square becomes completely visible, and since we haven’t declared a fill mode for the animation (using <code>animation-fill-mode</code> or as part of the <code>animation</code> shorthand), which means that the square won’t retain the background after, the animation is more of a flash.</p>



<p class="wp-block-paragraph">So, we need to build upon this to achieve different results.</p>



<h2 id="-animation-fill-mode-vs-animation-action-" class="wp-block-heading"><code>animation-fill-mode</code> vs. <code>&lt;animation-action&gt;</code></h2>



<p class="wp-block-paragraph">First, a recap of what the different fill mode values do for <code>animation-fill-mode</code> or as part of the <code>animation</code> shorthand:</p>



<ul class="wp-block-list">
<li><strong><code>forwards</code>:</strong> the styles are retained <em>after</em> the animation.</li>



<li><strong><code>backwards</code>:</strong> the styles are applied <em>before</em> the animation.</li>



<li><strong><code>both</code>:</strong> both behaviors are applied.</li>
</ul>



<p class="wp-block-paragraph">Now, let’s assume that the <code>&lt;animation-action&gt;</code> is <code>play-forwards</code> (like before) and the fill mode is <code>forwards</code> (<code>both</code> would be redundant because <code>background</code> isn’t even set to begin with):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  animation: fade-bg-in 300ms forwards;
  timeline-trigger: --trigger view() entry 100% exit 0%;
  animation-trigger: --trigger play-forwards;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_wBzYaaP" src="//codepen.io/anon/embed/wBzYaaP?height=450&amp;theme-id=1&amp;slug-hash=wBzYaaP&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed wBzYaaP" title="CodePen Embed wBzYaaP" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-3.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">This causes the styles to be retained, but, if the square partially or completely exits the viewport and then reenters it, the animation restarts, which can cause a flash depending on how the animation ends, which is what happens in this instance.</p>



<p class="wp-block-paragraph">There are two different ways to solve this…</p>



<p class="wp-block-paragraph">The &#8220;lock-in&#8221; method: Use <code>play-once</code> instead of <code>play-forwards</code>, which, when combined with <code>forwards</code>, results in the animation playing once, never to restart, and then retaining the styles afterward.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Play once */
  animation-trigger: --trigger play-once;

  /* Retain the styles */
  animation: fade-bg-in 300ms forwards;

  timeline-trigger: --trigger view() entry 100% exit 0%;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_myrzJLB" src="//codepen.io/anon/embed/myrzJLB?height=450&amp;theme-id=1&amp;slug-hash=myrzJLB&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed myrzJLB" title="CodePen Embed myrzJLB" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">The &#8220;back-and-forth&#8221; method: <code>play-forwards play-backwards</code> animates the element normally when fully visible and in reverse when no longer fully visible. There’s no flash because the element animates backward as smoothly as it animates forward. In addition, even though the direction of the animation can change, the fill mode can remain at <code>forwards</code> instead of being set to <code>both</code>.</p>



<p class="wp-block-paragraph"><em>Why?</em></p>



<p class="wp-block-paragraph"><code>play-forwards</code> means &#8220;play the animation from 0% to 100%&#8221; whereas <code>play-backwards</code> means &#8220;play the animation from 100% to 0%.&#8221; Meanwhile, as I mentioned earlier, the <code>forwards</code> fill mode means &#8220;retain the styles when the animation completes’&#8221;— well, this is regardless of whether the final keyframe is 0% or 100%.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Play forward and backward, as appropriate */
  animation-trigger: --trigger play-forwards play-backwards;

  /* Retain the styles either way */
  animation: fade-bg-in 300ms forwards;

  timeline-trigger: --trigger view() entry 100% exit 0%;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_XJjxbwX" src="//codepen.io/anon/embed/XJjxbwX?height=450&amp;theme-id=1&amp;slug-hash=XJjxbwX&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed XJjxbwX" title="CodePen Embed XJjxbwX" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-5.mp4" playsinline></video></figure>



<p class="wp-block-paragraph"><code>play-forwards</code>, <code>play-once</code>, and <code>play-backwards</code> aren’t the only keywords for <code>&lt;animation-action&gt;</code>. Here’s a quick rundown:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong><code>&lt;animation-action&gt;</code></strong></th><th><strong>Effect</strong></th></tr></thead><tbody><tr><td><code>none</code></td><td>For disabling triggers conditionally, on entry but not exit (or vice-versa), or handling multiple triggers with one <code>animation-trigger</code></td></tr><tr><td><code>play-forwards</code></td><td>Allows the animation to play forward</td></tr><tr><td><code>play-backwards</code></td><td>Allows the animation to play backward</td></tr><tr><td><code>play-once</code></td><td>Forward or backward (whichever comes first)</td></tr><tr><td><code>play</code></td><td>Plays in the last specified direction, or forward if neither has been specified</td></tr><tr><td><code>pause</code></td><td>Pauses the animation</td></tr><tr><td><code>reset</code></td><td>Pauses the animation and sets progress to 0</td></tr><tr><td><code>replay</code></td><td>Sets progress to 0 but doesn’t pause the animation</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">These <code>&lt;animation-action&gt;</code>s not only allow for a significant amount of control over animations while scrolling, but different combinations of actions, fill modes, timeline ranges, and the fact that we can bake exit animations into <code>@keyframes</code> rules means that there are often multiple ways to achieve an outcome.</p>



<h2 id="scroll-triggering-multiple-elements" class="wp-block-heading">Scroll-triggering multiple elements</h2>



<p class="wp-block-paragraph">While scroll-triggered animations being made up of animation actions, fill modes, timeline ranges, and maybe more, might seem overcomplicated, the fact that these mechanics are decoupled enable us to reuse logic while maintaining flexibility, reducing repetition and making the mechanics more design system-friendly.</p>



<p class="wp-block-paragraph">Consider three squares this time, and for a bit of added complexity, we declare <code>scale: 70%</code> (animates to <code>initial</code>) and define two rotative animations.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div id="squares">
  &lt;div class="square rotate-left">&lt;/div>
  &lt;div class="square">&lt;/div>
  &lt;div class="square rotate-right">&lt;/div>
&lt;/div></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Define animations */
@keyframes intensify {
  to {
    scale: initial;
    background: currentColor;
  }
}

@keyframes rotate-left {
  to {
    rotate: -5deg;
  }
}

@keyframes rotate-right {
  to {
    rotate: 5deg;
  }
}

.square {
  /* Set starting value */
  scale: 70%;
}</code></pre>



<p class="wp-block-paragraph">After that it’s more of the same, and while it’s obviously a more complex example, being able to merge values into shorthand properties and decouple them into longhand properties, as well as the decoupled nature of the different mechanics, facilitates flexibility but also reusability (in this case, to stagger various animations using the same animation trigger settings):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.square {
  /* Set starting value */
  scale: 70%;

  /* Define animation name */
  --base-animation: intensify;

  /* Declare animation */
  animation: var(--base-animation) 300ms forwards;

  /* Define animation trigger settings */
  --animation-trigger: --trigger play-forwards play-backwards;

  /* Declare for intensify, then for one of either rotate animations */
  animation-trigger: var(--animation-trigger), var(--animation-trigger);

  /* Declare animation trigger conditions (without timeline ranges) */
  timeline-trigger: --trigger view();

  /* Declare active range end */
  timeline-trigger-active-range-end: normal;

  /* Append other animations */
  &amp;.rotate-left {
    animation-name: var(--base-animation), rotate-left;
  }

  &amp;.rotate-right {
    animation-name: var(--base-animation), rotate-right;
  }

  /* Stagger activation ranges */
  &amp;:first-child {
    timeline-trigger-activation-range-start: entry 33.3333%;
  }

  &amp;:nth-child(2) {
    timeline-trigger-activation-range-start: entry 66.6666%;
  }

  &amp;:last-child {
    timeline-trigger-activation-range-start: entry 99.9999%;
  }
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNGvRWo" src="//codepen.io/anon/embed/RNGvRWo?height=450&amp;theme-id=1&amp;slug-hash=RNGvRWo&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNGvRWo" title="CodePen Embed RNGvRWo" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-6.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">Here’s a cleaner, more robust version that uses <a href="https://css-tricks.com/almanac/functions/s/sibling-count/"><code>sibling-count()</code></a> and <a href="https://css-tricks.com/almanac/functions/s/sibling-index/"><code>sibling-index()</code></a> (which lack Firefox support) to stagger the animations:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_ogzOjEN" src="//codepen.io/anon/embed/ogzOjEN?height=450&amp;theme-id=1&amp;slug-hash=ogzOjEN&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ogzOjEN" title="CodePen Embed ogzOjEN" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">In this version, instead of setting <code>timeline-trigger-activation-range-start</code> on each individual square, we simply target <code>.square</code> and calculate the entry values on the fly:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Maximum entry ÷ number of squares */
--stagger-interval: calc(100% / sibling-count());

/* Current square’s index × stagger interval */
--entry: calc(sibling-index() * var(--stagger-interval));

/* Declare animation trigger conditions */
timeline-trigger: --trigger view() entry var(--entry) exit 0%;</code></pre>



<h2 id="making-one-element-trigger-other-elements" class="wp-block-heading">Making one element trigger other elements</h2>



<p class="wp-block-paragraph">In this case, we’ll shift the trigger and its ranges to the first square, and have the other squares follow according to a staggered animation delay. As you can see, all animations are triggered by <code>animation-trigger</code> once 50% of the first square has entered (<code>entry 50%</code>) the viewport (<code>view()</code>). <code>animation-trigger</code> is triggered by <code>timeline-trigger</code> because the dashed ident (the aptly named <code>--trigger</code>) links them:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Define animations */
@keyframes intensify {
  to {
    scale: initial;
    background: currentColor;
  }
}

@keyframes rotate-left {
  to {
    rotate: -5deg;
  }
}

@keyframes rotate-right {
  to {
    rotate: 5deg;
  }
}

.square {
  /* Set starting value */
  scale: 70%;

  /* Define animation name */
  --base-animation: intensify;

  /* Maximum delay ÷ number of squares */
  --stagger-interval: calc(300ms / sibling-count());

  /* Current square’s index × stagger interval */
  --animation-delay: calc(sibling-index() * var(--stagger-interval));

  /* Declare animation */
  animation: var(--base-animation) 300ms var(--animation-delay) forwards;

  /* Define animation trigger settings */
  --animation-trigger: --trigger play-forwards play-backwards;

  /* Declare for intensify, then for one of either rotate animations */
  animation-trigger: var(--animation-trigger), var(--animation-trigger);

  &amp;:first-child {
    /* Declare animation trigger conditions */
    timeline-trigger: --trigger view() entry 50%;

    /* Declare active range end */
    timeline-trigger-active-range-end: normal;
  }

  /* Append other animations */
  &amp;.rotate-left {
    animation-name: var(--base-animation), rotate-left;
  }

  &amp;.rotate-right {
    animation-name: var(--base-animation), rotate-right;
  }
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_raMbbyX" src="//codepen.io/anon/embed/raMbbyX?height=450&amp;theme-id=1&amp;slug-hash=raMbbyX&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed raMbbyX" title="CodePen Embed raMbbyX" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls src="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-7.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">One downside is that when <code>animation-trigger</code> is in <code>play-backwards</code> mode, the animations don’t stagger. This is because, <em>I think</em>, when the animation is reversed, the delay is included in that. This seems like an oversight to me, especially as that isn’t the case with <code>animation-direction: reverse</code>, but I could be completely wrong on this.</p>



<h2 id="understanding-timeline-ranges" class="wp-block-heading">Understanding timeline ranges</h2>



<p class="wp-block-paragraph">Timeline ranges are a big part of scroll-triggered animations, but they’re a separate mechanic. For scroll-<em>driven</em> animations, you’ll want <code>animation-range</code> and its longhand properties. With scroll-<em>triggered</em> animations, the syntax is fundamentally the same but uses different properties and two different ranges. The activation range determines the scroll zone in which the animation triggers, while the active range determines the zone in which it holds up (even if not in the activation range anymore).</p>



<p class="wp-block-paragraph">Timeline ranges are a bit heavy. However, <code>view() entry 100% exit 0%</code> (when fully visible) and <code>view() contain</code> (the same but also if larger than the viewport) will suffice most of the time.</p>



<p class="wp-block-paragraph">But if you’re keen to dive in, <a href="https://css-tricks.com/almanac/properties/a/animation-range/"><code>animation-range</code></a>, although it’s for scroll-<em>driven</em> animations, is lighter and offers a novice-level understanding of timeline ranges. After that, I recommend reading the <a href="https://drafts.csswg.org/animation-triggers-1/" rel="noopener">Animation Triggers spec</a> to cover the many intricacies of timeline ranges within the context of these scroll-triggered animations.</p>



<p class="wp-block-paragraph">Another ingredient of scroll-triggered animations that’s also its own thing is the <a href="https://css-tricks.com/almanac/functions/v/view/"><code>view()</code></a> function, but this one’s easier to summarize here. Basically, when it comes to scroll-triggered animations, <code>view()</code> is the viewport. So if you had a <code>5rem</code> sticky header, <code>view(y 0 5rem)</code> would make the timeline range factor that in along the y-axis.</p>



<h2 id="final-thoughts" class="wp-block-heading">Final thoughts</h2>



<p class="wp-block-paragraph">Scroll-triggered animations can be tricky because they’re similar to scroll-driven animations, they leverage older CSS features (mainly <code>animation</code>) as well as mechanics from other newer features (dashed idents, <code>view()</code>, timeline ranges), in addition to the CSS properties that are specific to scroll-triggered animations. There’s a whole lot happening at once.</p>



<p class="wp-block-paragraph">I’m not sure how I feel about them, to be honest. They’re definitely cool, fun, and useful, but they’re also complicated, and it’ll be a while before I really start to rave about them.</p>



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-scroll-triggered-animations-first-look/">A First Look at Scroll-Triggered Animations</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/css-scroll-triggered-animations-first-look/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/1-1.mp4" length="515618" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-2.mp4" length="192166" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-3.mp4" length="259874" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered.mp4" length="423606" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-5.mp4" length="491139" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-6.mp4" length="461888" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/scroll-triggered-7.mp4" length="757238" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">394364</post-id>	</item>
		<item>
		<title>The Siren Song of ariaNotify()</title>
		<link>https://css-tricks.com/the-siren-song-of-arianotify/</link>
					<comments>https://css-tricks.com/the-siren-song-of-arianotify/#comments</comments>
		
		<dc:creator><![CDATA[Mat Marquis]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 15:32:30 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[accessibility]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395567</guid>

					<description><![CDATA[<p>There's a brand new <code>ariaNotify()</code> method — defined by the WAI-ARIA 1.3 Specification — that provides a means of programmatically triggering narration in a screen reader.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-siren-song-of-arianotify/">The Siren Song of ariaNotify()</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I need you all to promise me you&#8217;ll be cool about this. I‘m here to tell you about an upcoming web platform feature that has been a <em>long</em> time coming; a feature that not only fulfills a use case sorely overdue for a better solution, but does so by way of a syntax that is both immediately understandable and deceptively powerful. That’s right, this thing is <em>developer catnip</em>, and I don&#8217;t mind saying that I was really excited to try it out — after which point I willed myself to tuck it away in a drawer and put it out of my mind. This is a tool only to be used in situations where it is absolutely, <em>one hundred percent</em> necessary, to solve a problem that cannot be solved in any other way, up to and including &#8220;push back against building a feature in the first place.&#8221; So just be <em>cool</em> about this, okay? Okay.</p>



<span id="more-395567"></span>



<p class="wp-block-paragraph">There&#8217;s a brand new <code>ariaNotify()</code> method — defined by the <a href="https://w3c.github.io/aria/#ARIANotifyMixin" rel="noopener">Accessible Rich Internet Applications (WAI-ARIA) 1.3 Specification</a> — that provides you with a means of programmatically triggering narration in a screen reader. It accepts a string as its first argument, and an optional configuration object as its second:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">document.ariaNotify( "Hello, World." );
// When invoked, a screen reader will narrate "Hello, World."</code></pre>



<p class="wp-block-paragraph">That might look like a simple solution to an equally simple use case here in print, but historically this has a tricky problem that could only be solved by slightly off-label usage of ARIA&#8217;s <strong>live regions</strong>. That means that understanding live regions — and their shortcomings — is the key to understanding what <code>ariaNotify</code> does for us. If you’ve worked with live regions before, you likely closed this tab right after that code snippet, and you’re currently on your third or fourth lap around the room with your arms held aloft in triumph. If you haven’t worked with live regions before, well, to put it in the strictest possible technical terms: <em>woof</em> what a mess.</p>



<p class="wp-block-paragraph">In an assisted browsing context, if some part of a page changes in response to a user interaction, or something is loaded and added to the page asynchronously, those changes aren’t discoverable until the user moved their focus to that changed content — a user would have no way of knowing that something <em>had</em> changed, let alone <em>what</em>. Live regions address that, at least by design: an element with an <code>aria-live</code> attribute will prompt narration for changes to the markup contained within that element — when the markup is changed, the changed markup is narrated aloud. If <code>aria-live</code> has a value of <code>assertive</code>, it informs assistive technology “this is urgent, and should be narrated right away.” If <code>aria-live</code> has a value of <code>polite</code>, it says “this should be narrated, at the next natural opportunity to do so.” Using <code>role="alert"</code> or <code>role="status"</code> on an element is functionally equivalent to <code>aria-live="assertive"</code> and <code>aria-live="polite"</code>, respectively. Sounds pretty reasonable on paper, right?</p>



<p class="wp-block-paragraph">Naturally, we needed a way to fine-tune exactly what information is narrated and how, so there are a few other attributes that determine a live region&#8217;s behavior:</p>



<p class="h4 wp-block-paragraph"><strong><code>aria-atomic</code></strong></p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>true</code>:</strong> narrate the entire contents of the live region when something in it changes</li>



<li><strong><code>false</code> (default):</strong> announce only the text that changes within the element</li>
</ul>



<p class="h4 wp-block-paragraph"><strong><code>aria-relevant</code></strong></p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>text</code>:</strong> notify the user when <a href="https://html.spec.whatwg.org/multipage/dom.html#phrasing-content" rel="noopener">phrasing content</a> — text, just like it says on the tin — changes inside the live region</li>



<li><strong><code>additions</code>:</strong> notify the user when a node is added to the live region</li>



<li><strong><code>removals</code>:</strong> notify the user when a node is removed from the live region</li>



<li><strong><code>all</code>:</strong> notify the user if text is changed, and/or elements are added to or removed from the DOM</li>
</ul>



<p class="wp-block-paragraph">Again, solid enough in theory! Problem solved, right up until the point where you try to <em>use</em> live regions, for pretty much anything, ever. In practice, <a href="https://vispero.com/resources/screen-reader-support-aria-live-regions/" rel="noopener">browsers and assistive technologies are</a> <a href="https://vispero.com/resources/screen-reader-support-aria-live-regions/" rel="noopener"><em>wildly</em></a> <a href="https://vispero.com/resources/screen-reader-support-aria-live-regions/" rel="noopener">inconsistent about implementation</a>, particularly as it relates to nested markup within a live region — if you want <code>aria-live</code> to work as expected, you&#8217;ll often end up needing to strip out all the otherwise semantically-meaningful markup that you <em>should</em> be using. In order to work reliably across assisted browsing contexts, a live region has to already <a href="https://codepen.io/scottohara/full/dyzxwyr" rel="noopener">meaningfully exist in the DOM at the time the narration is triggered</a>. A live region can&#8217;t be toggled from <code>display: none</code> or injected into the page along <em>with</em> the content to be narrated or you’ll run into timing issues that prevent the content from being narrated — when the browser first &#8220;sees&#8221; the live region, it locks in &#8220;okay, narrate anything that <em>changes</em> in this container,&#8221; which doesn&#8217;t necessarily include that initial content. The way the <code>"assertive"</code> and <code>"polite"</code> values work isn&#8217;t especially well-defined in the specification <em>or</em> <a href="https://adrianroselli.com/2026/01/live-region-support.html#Results" rel="noopener">realized across screen readers and browser combinations</a>, either. Again: they&#8217;re a <em>mess</em>.</p>



<p class="wp-block-paragraph">Even if all that <em>weren&#8217;t</em> the case, there&#8217;s a fundamental mismatch between the purpose of live regions and the way the modern web is built. Like I said, live regions only work when markup is added to/removed from the element in question, and that isn&#8217;t the reality of most interactions that result in a change to the visible contents of a page. Live regions are no help when you&#8217;re revealing markup that&#8217;s already in the document, but otherwise inert — for example, swapping between a visible <code>display</code> property and <code>display: none</code>. That use case is every bit as common as structural changes to the current document on-the-fly, if not more so.</p>



<p class="wp-block-paragraph">All these limitations have led to live regions almost exclusively being used as makeshift notification APIs: having one or more <code>aria-live</code> elements buried in the page, visually hidden (<a href="https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree" rel="noopener">but not removed from the accessibility tree</a> via <code>display: none</code>), that you update as-needed with whatever text you want narrated. I have been there and done this, and it&#8217;s clunky, not least of all because of how inconsistent live regions are at their core. That injected content is also necessarily available to a user navigating through the page via assistive tech, just floating in the document divorced from it&#8217;s original meaning — if you&#8217;re not meticulous about cleaning up afterwards, you&#8217;ve added a potentially confusing source of contextually-irrelevant narration to the page. Most of all, though, you&#8217;ve added a new and <em>invisible</em> concern; a feature that will need dedicated testing and upkeep, and something that can break in very literally unseen ways, and do so in ways that have the potential to be annoying, misleading, confusing, or all of the above. That’s what gets you, with accessibility work: quick-and-easy decisions made in isolation can have unforeseen consequences in the context of the overall experience, and unless those assumptions are tested very carefully — early and often — we can’t know what those consequences might be.</p>



<p class="wp-block-paragraph">The <code>ariaNotify</code> method takes the place of this kind of <a href="https://en.wikipedia.org/wiki/Rube_Goldberg" rel="noopener">Rube Goldberg</a> accessibility contraption, providing you with a <em>real</em> screen reader notification API, no convoluted (and flaky) markup required:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">document.ariaNotify( "Hello, World." );</code></pre>



<p class="wp-block-paragraph"><code>ariaNotify</code> is available as a method on the <code>Element</code> interface or on the <code>Document</code> interface — there&#8217;s no meaningful difference between the two for the examples you&#8217;re going to see here, but it’s worth knowing that calling <code>document.ariaNotify()</code> means that the <code>lang</code> attribute specified on the <code>html</code> element, specifically, will be used to infer the language of the notification:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ) {
  document.ariaNotify( "Hello, World." );
});
/*
* Clicking the button results in the "polite"-timed announcement "hello, world," 
* using the `lang` attribute specified on the `&lt;html>` element. If there isn't 
* one, the browser's default language is used.
/*</code></pre>



<p class="wp-block-paragraph">While calling <code>ariaNotify()</code> from an element means that the <code>lang</code> attribute of the element’s nearest ancestor will be used to determine the language of the notification:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ) {
  this.ariaNotify( "Hello, World." );
});
/*
* Clicking the button results in the "polite"-timed announcement "hello, world," 
* using the `lang` attribute of the `button` (or the closest parent element with 
* `lang`) to  determine pronunciation. If there isn't one in the document (all
* the way up to and including `&lt;html>`), the browser's default language is used.
/*</code></pre>



<p class="wp-block-paragraph"><code>ariaNotify</code> accepts a second parameter that allows you to set an explicit priority level:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ){
  this.ariaNotify( "Hello, world.", {
    priority: "high"
  });
});</code></pre>



<p class="wp-block-paragraph">The default priority for these notifications is <code>priority: "normal"</code>, which behaves like <code>aria-live="polite"</code> (or <code>role="status"</code>). Setting an explicit <code>priority: "high"</code> will prioritize and potentially interrupt the current narration, the way <code>aria-live="assertive"</code> (or <code>role="alert"</code>) would.</p>



<p class="wp-block-paragraph">That&#8217;s the entire API so far, right there. No fussing with markup, no finessing timings; if you need something narrated, you call <code>ariaNotify</code> and it is <em>narrated</em>, just like that. You can try this out in Firefox as we speak, though it doesn&#8217;t seem like <code>lang</code> attributes are factored in just yet:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019e2c40-99c5-7617-8163-23c489a628b5" src="//codepen.io/editor/anon/embed/019e2c40-99c5-7617-8163-23c489a628b5?height=450&amp;theme-id=1&amp;slug-hash=019e2c40-99c5-7617-8163-23c489a628b5&amp;default-tab=js,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019e2c40-99c5-7617-8163-23c489a628b5" title="CodePen Embed 019e2c40-99c5-7617-8163-23c489a628b5" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="h4 wp-block-paragraph"><strong>JAWS</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Polite, button. To activate, press space bar. [spacebar pressed] Space. Hello, World.</p>



<p class="wp-block-paragraph">Assertive, button. To activate, press space bar. [spacebar pressed] Space. Hello, World.</p>



<p class="wp-block-paragraph">Educado [pronounced correctly], button. To activate, press space bar. Space. Hola, Mundo [pronounced incorrectly].</p>
</blockquote>



<p class="h4 wp-block-paragraph"><strong>NVDA</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Polite, button. [spacebar pressed] Hello, World.</p>



<p class="wp-block-paragraph">Assertive, button. [spacebar pressed] Hello, World.</p>



<p class="wp-block-paragraph">Educado [pronounced correctly], button. [spacebar pressed] Hola, Mundo [pronounced incorrectly].</p>
</blockquote>



<p class="h4 wp-block-paragraph"><strong>VoiceOver</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Polite, button. You are currently on a button [spacebar pressed] inside of a frame. To click this button, press <kbd>Control</kbd>&#8211;<kbd>Option</kbd>&#8211;<kbd>Space</kbd>. To exit this web area, press <kbd>Control</kbd>&#8211;<kbd>Option</kbd>&#8211;<kbd>Shift-Up Arrow</kbd>. Hello, World.</p>



<p class="wp-block-paragraph">Assertive, button. You are currently on a button [spacebar pressed] Hello, World.</p>



<p class="wp-block-paragraph">Educado [pronounced correctly], button. You are currently on a button [spacebar pressed] inside of a frame. To click this button, press <kbd>Control</kbd>&#8211;<kbd>Option</kbd>&#8211;<kbd>Space</kbd>. To exit this web area, press <kbd>Control</kbd>&#8211;<kbd>Option</kbd>&#8211;<kbd>Shift-Up Arrow</kbd>. Hola, Mundo [pronounced incorrectly].</p>
</blockquote>



<p class="wp-block-paragraph">Pretty solid, huh? <em>Huge</em> improvement over how we&#8217;ve all been stuck using live regions. I, for one, can&#8217;t wait to <em>almost</em> use <code>ariaNotify</code>, then — again — promptly talk myself out of it!</p>



<p class="wp-block-paragraph">Why the reluctance? Well, in accessibility circles, it is sometimes said that there are three stages of learning to use ARIA:</p>



<ol class="wp-block-list">
<li>You don&#8217;t use ARIA.</li>



<li>You use ARIA.</li>



<li>You don&#8217;t use ARIA.</li>
</ol>



<p class="wp-block-paragraph">The W3C puts this in more formal terms, as is their specialty:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">If you can use a native HTML element [HTML] or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.</p>
<cite>—<a href="https://w3c.github.io/using-aria/#rule1" rel="noopener">Using ARIA: First Rule of ARIA Use</a></cite></blockquote>



<p class="wp-block-paragraph">That second stage of ARIA mastery is where we get ourselves into trouble. The web is a chaotic place, but assistive technologies have evolved alongside it, and they’ve learned to paper over some of the more common issues a user might encounter. For example, say you have an <code>h2</code> that reveals some visually-hidden content that follows it when clicked — that element might be presented in a way that makes that interaction clear <em>visually</em>, but without our intervention, it might not otherwise be signaled to a user browsing via screen reader. To work around this, assistive technologies can helpfully narrate that heading element as “clickable” when it receives user focus upon finding a <code>click</code> event listener bound to that heading. Granted, that isn’t as good as explicitly signaling the purpose of this element to the user, but it is <em>workable</em>, even if we didn’t make that interaction explicit.</p>



<p class="wp-block-paragraph">The catch is in <em>how</em> we that make that interaction explicit. If you‘re somewhat familiar with ARIA, you might find yourself thinking “well, this element behaves like a button, so I should put <code>role="button"</code> on it to inform a user that this does something.” That impulse isn’t <em>strictly</em> wrong, but with that attribute comes a likely unintended consequence: by being explicit about the element’s role, we remove its <em>implicit</em> meaning. You’re telling the browser and assistive technology, in no uncertain terms, that this <em>is not</em> a heading — so if the user is navigating <a href="https://webaim.org/projects/screenreadersurvey10/#finding" rel="noopener">by way of the document outline</a>, this element will no longer be part of that navigation, and what felt like a simple, helpful quick-fix ends up having an unintended consequence. ARIA leaves no room for interpretation; what we say goes, full stop. We say “narrate this,” it gets narrated. Non-negotiable.</p>



<p class="wp-block-paragraph">So, given a very easy-to-use feature that inarguably says “when I tell you to narrate this, you narrate it,” please assume that I am making steely, unblinking eye contact here while I say, aloud, &#8220;<code>alert()</code>&#8221; — an imagined scenario made all the more unsettling by the fact that I have somehow managed to say it <em>in a monospaced font.</em></p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">window.alert( "Hello world, like it or not." );</code></pre>



<p class="wp-block-paragraph">You remember <code>alert()</code> from way back in the day, right? A method as infamous as it is obnoxious. If you’re newer to the industry, you might not be familiar with it first-hand, for a blessing. Like <code>ariaNotify</code>, it was — <em>is</em>, technically — a quick and easy API for immediately presenting information to a user:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="876" height="608" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/s_D001C67931903D7057449C5AC820101F7F153CB201332AFD0DF2BBB3E6313CC4_1779810124342_image.png?resize=876%2C608&#038;ssl=1" alt="A CSS-Tricks article with an alert on top that identifies the site address and says 'Hello World, like it or not.'" class="wp-image-395568" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/s_D001C67931903D7057449C5AC820101F7F153CB201332AFD0DF2BBB3E6313CC4_1779810124342_image.png?w=876&amp;ssl=1 876w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/s_D001C67931903D7057449C5AC820101F7F153CB201332AFD0DF2BBB3E6313CC4_1779810124342_image.png?resize=300%2C208&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/s_D001C67931903D7057449C5AC820101F7F153CB201332AFD0DF2BBB3E6313CC4_1779810124342_image.png?resize=768%2C533&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph"><code>alert()</code> is simple, effective, consistent, and — back when it saw widespread usage — <em>incredibly</em> annoying. <code>ariaNotify()</code> entrusts you with this same power, this time backed by the invisible, unyielding authority of ARIA. With <code>ariaNotify()</code> in your pocket, “this might be confusing, so I’ll narrate exactly what’s going on” will be a quick and easy decision, and might mean that a savvy user — already skilled at navigating the web despite all its inherent chaos and inconsistency — finds their browsing interrupted by a lecture about an interaction they already understand.</p>



<p class="wp-block-paragraph">It isn’t hard to imagine a developer — their heart squarely in the right place — using <code>ariaNotify</code> to inform a user that content has been revealed by an interaction on the page. In context, however, revealing that content likely meant interacting with an element already narrated as “clickable” thanks to the presence of an associated event listener, the element’s inherent semantics, or the presence of an <code>aria-expanded="false"</code> attribute (the <em>correct</em> approach to signaling that interacting with an element will reveal associated content). In that case, all we’ve done is add noise to the user’s experience of the page, and nobody needs that. I mean, imagine being partway through reading this sentence when <code>alert( "There's a new comment on this article!" )</code> interrupts you for the third time, or hovering over <code>&lt;button&gt;Navigation&lt;/button&gt;</code> only to get hit with <code>alert( "Click here to open the navigation." )</code> like some unskippable video game tutorial? Ugh. I’d close the tab.</p>



<p class="wp-block-paragraph">Even worse, if a narrated instruction falls out of sync with the reality of the interaction itself — an invisible inconsistency that wouldn’t be caught by a QA process that lacks dedicated screen reader testing — we could end up making the user sit through an argument between the underlying page and their own screen reader while they’re just trying to get things done and get on with their day.</p>



<p class="wp-block-paragraph">ARIA is powerful stuff. It gives us the ability to define the meanings, states, and relationships between elements on a page as absolute, iron-clad <em>fact</em> — it provides a line of communication between those of us building the web and those of us using it. Nowhere is that line of communication more direct than with <code>ariaNotify()</code>, a feature that effectively allows us to speak <em>directly</em> to an end user using the voice of the browser and assistive technology they know and trust. That’s a lot of responsibility bound up in a single method. It solves a very real problem, but like so many technologies: if not used carefully, it can cause just as many.</p>



<p class="wp-block-paragraph">I am excited about <code>ariaNotify()</code>, y’know, in a measured, cautious way. It finally gives us a way to address a use case that has plagued the web — and me, personally — for years, in a shockingly easy way. So easy, in fact, that it makes <code>ariaNotify()</code> just a little bit dangerous.</p>



<p class="wp-block-paragraph">I mean, not for <em>us</em> though, right? Because we’re all gonna be <em>cool</em> about this, <em>right</em>?</p>



<p class="wp-block-paragraph"><em>Right.</em></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-siren-song-of-arianotify/">The Siren Song of ariaNotify()</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/the-siren-song-of-arianotify/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395567</post-id>	</item>
		<item>
		<title>Prop For That</title>
		<link>https://css-tricks.com/prop-for-that/</link>
					<comments>https://css-tricks.com/prop-for-that/#comments</comments>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Tue, 16 Jun 2026 18:36:25 +0000</pubDate>
				<category><![CDATA[Links]]></category>
		<category><![CDATA[custom properties]]></category>
		<category><![CDATA[framework]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395776</guid>

					<description><![CDATA[<p>Props for That creates live props based things CSS can't normally see in the browser. Things like cursor position, progress values, certain form states, current time, scroll velocity.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/prop-for-that/">Prop For That</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">No secret that Adam&#8217;s all about <em>props</em>. Dude gave us <a href="https://open-props.style" rel="noopener">Open Props</a> a good while back for a slew of preconfigured variables for color, shadows, sizing, typography, among much much more. Now he&#8217;s back with Prop For That, a similar sorta idea, but mind-blowing in the sense that it creates <em>live</em> props based things CSS can&#8217;t normally see in the browser. Things like cursor position, progress values, certain form states, current time, scroll velocity — you know, the stuff that JavaScript sniffs and passes to CSS.</p>



<span id="more-395776"></span>



<p class="wp-block-paragraph">My understanding is that all the script-y stuff is already in the background. All that&#8217;s needed is to import the library, declare it in HTML, then style away in CSS.</p>



<p class="wp-block-paragraph">Like, here&#8217;s Chris a long while back with <a href="https://css-tricks.com/updating-a-css-variable-with-javascript/">custom properties registered in JavaScript to track cursor position</a>:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yxVQJG" src="//codepen.io/anon/embed/yxVQJG?height=450&amp;theme-id=1&amp;slug-hash=yxVQJG&amp;default-tab=css,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yxVQJG" title="CodePen Embed yxVQJG" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph"><a href="https://prop-for-that.netlify.app/#:~:text=Track%20the%20pointer" rel="noopener">Prop For That has that nicely covered.</a> The difference is that we&#8217;re working with data attributes that trigger <a href="https://github.com/argyleink/prop-for-that/blob/main/src/plugins/pointer-local.ts" rel="noopener">the scripts</a>:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="mover" data-props-for="pointer">...&lt;/div></code></pre>



<p class="wp-block-paragraph">And plop the relevant props into the styles:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.mover {
  aspect-ratio: 1;
  width: 50px;
  background: red;
  position: absolute;
  left: calc(var(--live-pointer-x, 0) * 1px);
  top: calc(var(--live-pointer-y, 0) * 1px);
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019ed129-e979-7188-a4f1-7ee214e57365" src="//codepen.io/editor/anon/embed/019ed129-e979-7188-a4f1-7ee214e57365?height=450&amp;theme-id=1&amp;slug-hash=019ed129-e979-7188-a4f1-7ee214e57365&amp;default-tab=css,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019ed129-e979-7188-a4f1-7ee214e57365" title="CodePen Embed 019ed129-e979-7188-a4f1-7ee214e57365" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph"><a href="https://prop-for-that.netlify.app/docsite/demos/pointer/" rel="noopener">The demos are where it&#8217;s at.</a> Good lord, can Adam put together some classy work.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/prop-for-that/">Prop For That</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/prop-for-that/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395776</post-id>	</item>
		<item>
		<title>What’s !important #13: @function, alpha(), CSS Wordle, and More</title>
		<link>https://css-tricks.com/whats-important-13/</link>
					<comments>https://css-tricks.com/whats-important-13/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 13:15:33 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395752</guid>

					<description><![CDATA[<p>CSS functions, the alpha() function, Grid Lanes, some things about Dialog that you might not know, CSS Wordle, and more — this is What’s !important right now.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-13/">What’s !important #13: @function, alpha(), CSS Wordle, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">CSS functions, the <code>alpha()</code> function, Grid Lanes, some things about <code>&lt;dialog&gt;</code> that you might not know, CSS Wordle, and more — this is <strong>What’s !important</strong> right now.</p>



<span id="more-395752"></span>



<h2 id="css-functions-expertly-explained" class="wp-block-heading">CSS functions, expertly explained</h2>



<p class="wp-block-paragraph">Jane Ori expertly explained <a href="https://frontendmasters.com/blog/the-fundamentals-and-dev-experience-of-css-function/" rel="noopener">how CSS functions work</a>. <code>@function</code> will <em>probably</em> be the biggest CSS feature to <em>probably</em> become Baseline this year, so I definitely found it a bit intimidating at first. That is, until I read Jane’s baby-step-by-baby-step walkthrough, which eases you into it really well.</p>



<p class="wp-block-paragraph">In addition, <a href="https://css-tricks.com/author/declanchidlow/">Declan Chidlow</a> wrote our <a href="https://css-tricks.com/almanac/rules/f/function/"><code>@function</code> documentation</a>, which you might want to bookmark for quick reference in the future.</p>



<h2 id="the-alpha-function" class="wp-block-heading">The <code>alpha()</code> function</h2>



<p class="wp-block-paragraph">Speaking of functions, the <code>alpha()</code> function caught me by surprise. Firstly, because I hadn’t heard of it, and secondly, because…<em>why?</em> We can already change the alpha channel:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* This */
color: alpha(from var(--color) / 0.5);

/* Instead of this */
color: oklch(from var(--color) l c h / 0.5);</code></pre>



<p class="wp-block-paragraph">Well, <a href="https://github.com/mozilla/standards-positions/issues/1396#issuecomment-4363280736" rel="noopener">this comment</a> from Jason Leo sums it up. Firstly, it means that we won’t need to hard-code color values when we already have CSS variables. For years I’ve circumvented this by only storing the actual values in CSS variables, but having to wrap them in the color function every single time is <em>really</em> monotonous:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Just the values */
--color: 0.65 0.23 230;

/* Then use them flexibly */
color: oklch(var(--color));
color: oklch(var(--color) / 0.5);</code></pre>



<p class="wp-block-paragraph">But it’s better than this (in my opinion):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Function and values */
--color: oklch(0.65 0.23 230);

/* Delightful */
color: var(--color);

/* Delightless */
color: oklch(from var(--color) l c h / 0.5);</code></pre>



<p class="wp-block-paragraph"><code>alpha()</code> offers the best of both worlds:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Less delightless */
color: alpha(from var(--color) / 0.5);</code></pre>



<p class="wp-block-paragraph">Secondly, the color format is actually irrelevant in this context, so <code>alpha(from var(--color) / 0.5)</code> communicates the intention more clearly than <code>oklch(from var(--color) l c h / 0.5)</code> does. It also makes the declaration inherently shorter.</p>



<p class="wp-block-paragraph">Credit to Adam Argyle for <a href="https://nerdy.dev/relative-alpha" rel="noopener">bringing <code>alpha()</code> up</a>.</p>



<h2 id="the-field-guide-to-grid-lanes" class="wp-block-heading">The Field Guide to Grid Lanes</h2>



<p class="wp-block-paragraph">WebKit launched the <a href="https://gridlanes.webkit.org/" rel="noopener">Field Guide to Grid Lanes</a> (formerly known as CSS masonry layout). If you’ve ever read one of our <a href="https://css-tricks.com/guides/">CSS-Tricks Guides</a>, it’s similar to that (<em><a href="https://webkit.org/blog/18098/introducing-the-field-guide-to-grid-lanes/" rel="noopener">their words</a> — just sayin’</em>). It’s a smooth introduction with a variety of barebones and real-world demos.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2560" height="1539" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=2560%2C1539&#038;ssl=1" alt="Six CSS Grid Lanes demos organized in a three-by-two grid — Photos, Recipes, Newspaper, Mega Menu, Timeline, and Pinboard." class="wp-image-395754" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=300%2C180&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=1024%2C616&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=768%2C462&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=1536%2C923&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/1-scaled.png?resize=2048%2C1231&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://gridlanes.webkit.org/" rel="noopener">WebKit</a>.</figcaption></figure>



<h2 id="qualityoflife-upgrades-for-ltdialoggt" class="wp-block-heading">Quality-of-life upgrades for <code>&lt;dialog&gt;</code></h2>



<p class="wp-block-paragraph">Una Kravets talked about two <a href="https://bsky.app/profile/una.im/post/3mnf4c2gb5s2m" rel="noopener">quality-of-life upgrades for <code>&lt;dialog&gt;</code></a> — the new <code>closedby</code> attribute, which isn’t supported by Safari yet, and <code>overscroll-behavior: contain</code>. There are some nuggets in the comments too, including a tip about <code>scrollbar-gutter: stable</code>.</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:kesmfbtx2loscqj7ktw5shtt/app.bsky.feed.post/3mnf4c2gb5s2m" data-bluesky-cid="bafyreigfkns6s2pockz4ufa5drxw3d2l26i5qcyhjbqzmb2yyxsxlfl67i" data-bluesky-embed-color-mode="system"><p lang="en">&#x2705; Nice lil btn scale-down 
&#x1f648; Layout change bc the scroll bar disappears
&#x1f648; No light dismiss

These UX problems can easily be solved with:

&#8211; closedby=&quot;any&quot;
&#8211; overscroll-behavior: contain<br><br><a href="https://bsky.app/profile/did:plc:kesmfbtx2loscqj7ktw5shtt/post/3mnf4c2gb5s2m?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; Una Kravets (<a href="https://bsky.app/profile/did:plc:kesmfbtx2loscqj7ktw5shtt?ref_src=embed" rel="noopener">@una.im</a>) <a href="https://bsky.app/profile/did:plc:kesmfbtx2loscqj7ktw5shtt/post/3mnf4c2gb5s2m?ref_src=embed" rel="noopener">13:28 · Jun 3, 2026</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>



<p class="wp-block-paragraph">Also, Chris Coyier showed us <a href="https://frontendmasters.com/blog/in-n-out-animations-dialogs-part-1-3/" rel="noopener">how to animate <code>&lt;dialog&gt;</code>s</a>, which I think many of us know how to do already, but it’s so easy to mess up. I have to Google it every time (it’s those bleeping <code>@starting-style</code>s).</p>



<h2 id="what-happened-at-css-day-2026" class="wp-block-heading">What happened at CSS Day 2026?</h2>



<p class="wp-block-paragraph"><a href="https://cssday.nl/" rel="noopener">CSS Day</a>, the annual CSS community conference, was held in Amsterdam on the 11th and 12th of this month (so two days, technically). While there wasn’t a livestream this year, recordings will become available in late June. Until then, check out <a href="https://bsky.app/profile/cssday.nl" rel="noopener">CSS Day on Bluesky</a> as well as the <a href="https://bsky.app/search?q=%23cssday" rel="noopener">#CSSDay Bluesky feed</a> to see what happened on stage, what happened behind the scenes, and even the slides from some of the talks.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1200" height="656" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/2.webp?resize=1200%2C656&#038;ssl=1" alt="Portrait photos of the event speakers for CSS Day 2026." class="wp-image-395756" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/2.webp?w=1200&amp;ssl=1 1200w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/2.webp?resize=300%2C164&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/2.webp?resize=1024%2C560&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/2.webp?resize=768%2C420&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://cssday.nl/" rel="noopener">CSS Day</a>.</figcaption></figure>



<p class="wp-block-paragraph">And no, there weren’t any flamethrowers this year, but it wasn’t DOOM-free either (if you know, you know).</p>



<h2 id="css-wordle" class="wp-block-heading">CSS Wordle</h2>



<p class="wp-block-paragraph">What a week it’s been, especially with everything that transpired at CSS Day, but if you have any energy left I highly recommend a round (or several rounds) of <a href="https://css-tricks.com/author/sunkanmifafowora/">Sunkanmi Fafowora</a>’s <a href="https://css-questions.com/css-wordle" rel="noopener">CSS Wordle</a>, which I’ve literally been obsessed with for the last week.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2560" height="1607" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=2560%2C1607&#038;ssl=1" alt="An online game interface for CSS Wordle featuring a completed puzzle." class="wp-image-395758" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=1024%2C643&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=768%2C482&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=1536%2C964&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/06/3-scaled.png?resize=2048%2C1285&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://css-questions.com/css-wordle" rel="noopener">CSS-Questions</a> (don’t worry, this was yesterday&#8217;s answer).</figcaption></figure>



<h2 id="new-web-platform-features-and-updates" class="wp-block-heading">New web platform features and updates</h2>



<ul class="wp-block-list">
<li><a href="https://developer.chrome.com/release-notes/149" rel="noopener">Chrome 149</a>
<ul class="wp-block-list">
<li><a href="https://developer.chrome.com/blog/gap-decorations-stable" rel="noopener">Gap decorations</a> (now Baseline)</li>



<li><a href="https://css-tricks.com/almanac/properties/i/image-rendering/#:~:text=crisp%2Dedges%3A%20the%20contrast%2C%20colors%20and%20edges%20of%20the%20image%20will%20be%20preserved%20without%20any%20smoothing%20or%20blurring.%20This%20is%20intended%20for%20icons%2C%20data%20visualizations%2C%20diagrams%2C%20maps%2C%20pixel%20art%2C%20and%20anything%20with%20lines.%20This%20value%20applies%20to%20images%20scaled%20up%20or%20down."><code>image-rendering: crisp-edges</code></a> (now Baseline)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/basic-shape/rect" rel="noopener"><code>rect()</code></a> and <a href="https://css-tricks.com/almanac/functions/x/xywh/"><code>xywh()</code></a> for <a href="https://css-tricks.com/almanac/properties/s/shape-outside/"><code>shape-outside</code></a> (now Baseline)</li>



<li><a href="https://css-tricks.com/almanac/functions/p/path/"><code>path()</code></a> and <a href="https://css-tricks.com/almanac/functions/s/shape/"><code>shape()</code></a> for <a href="https://css-tricks.com/almanac/properties/s/shape-outside/"><code>shape-outside</code></a> (no Safari or Firefox support)</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Until next time!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-13/">What’s !important #13: @function, alpha(), CSS Wordle, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/whats-important-13/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395752</post-id>	</item>
		<item>
		<title>Why Isn&#8217;t My 3D View Transition Working?</title>
		<link>https://css-tricks.com/why-isnt-my-3d-view-transition-working/</link>
					<comments>https://css-tricks.com/why-isnt-my-3d-view-transition-working/#comments</comments>
		
		<dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
		<pubDate>Fri, 12 Jun 2026 15:09:00 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[perspective]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395594</guid>

					<description><![CDATA[<p>Why isn't my 3D view transition working?! Sunkanmi tackles this frustration and offers an elegant fix for it.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/why-isnt-my-3d-view-transition-working/">Why Isn&#8217;t My 3D View Transition Working?</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you have played around with view transition a bunch, you may have noticed that 3D transitions between two pages (i.e., cross-document view transitions) don&#8217;t seem to work. That is, at least not without the browsers flattening things first.</p>



<p class="wp-block-paragraph">Image elements are the best example to demonstrate this because, like the snapshots a browser takes of the before-after states in a view transition, images are <a href="https://developer.mozilla.org/en-US/docs/Glossary/Replaced_elements" rel="noopener">replaced elements</a> so, in theory, we should be able to use them as a sort of reduced test case for 3D animations. For example, flipping one image to reveal another on click looks like this:</p>



<span id="more-395594"></span>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019ce239-f88b-7c5c-8d0c-1dd74c0499d4" src="//codepen.io/editor/anon/embed/019ce239-f88b-7c5c-8d0c-1dd74c0499d4?height=450&amp;theme-id=1&amp;slug-hash=019ce239-f88b-7c5c-8d0c-1dd74c0499d4&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019ce239-f88b-7c5c-8d0c-1dd74c0499d4" title="CodePen Embed 019ce239-f88b-7c5c-8d0c-1dd74c0499d4" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">It&#8217;s important to note that, for the animation to work properly, we need to set the&nbsp;<a href="https://css-tricks.com/almanac/properties/p/perspective/"><code>perspective</code></a>&nbsp;property on the image’s parent container (in our case, it&#8217;s the&nbsp;<code>.scene</code>&nbsp;element). Otherwise, the 3D transformation is merely flat. It sort of <em>angles</em> the element’s appearance:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_bNBYqxM" src="//codepen.io/anon/embed/bNBYqxM?height=450&amp;theme-id=1&amp;slug-hash=bNBYqxM&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bNBYqxM" title="CodePen Embed bNBYqxM" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">In CSS, the parent’s&nbsp;<code>persepective</code>&nbsp;is <a href="https://css-tricks.com/almanac/properties/p/perspective/#:~:text=it%20simply%20enables%20a%203D%2Dspace%20for%20children%20elements">applied to all its children</a>, excluding itself:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.scene {
  perspective: 1200px;

  .card { /* gets perspective */ }
}</code></pre>



<p class="wp-block-paragraph">What&#8217;s important here is the HTML structure. Specifically how the&nbsp;<code>.scene</code>&nbsp;container sits on top of the child <code>.card</code> elements, making the 3D effect come to life so the flip looks how it should:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="scene">
  &lt;div class="card">
    &lt;!-- Card Content Here -->
  &lt;/div>
&lt;/div></code></pre>



<p class="wp-block-paragraph">Perhaps our keyframe animation to flip the <code>.cards</code> is something like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@keyframes flipOut {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(-90deg);
  }
}</code></pre>



<p class="wp-block-paragraph">Which we apply to the <code>.cards</code> like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card.flip-out {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.card.flip-in {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards reverse;
}</code></pre>



<p class="wp-block-paragraph">…where the animates runs <code>forwards</code> when the <code>.flip-out</code> class is appended to the <code>.card</code> (courtesy of JavaScript watching for a click) and runs in <code>reverse</code> when the <code>.flip-in</code> class is appended.</p>



<p class="wp-block-paragraph">That’s the setup for how a cross-document view transition ought to work, too, right? If an image supports a 3D animation, then a view transitions snapshot should do the same. Let’s poke at that.</p>



<h2 id="setting-up-the-view-transition" class="wp-block-heading">Setting up the view transition</h2>



<p class="wp-block-paragraph">First things first, we have to opt into view transitions on both pages with the&nbsp;<code><a href="https://css-tricks.com/almanac/rules/v/view-transition/">@view-transition</a></code>&nbsp;at-rule by setting the&nbsp;<code>navigation</code>&nbsp;descriptor to&nbsp;<code>auto</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@view-transition {
  navigation: auto;
}</code></pre>



<p class="wp-block-paragraph">If we were to do nothing else, then one page fades into another when navigating between the two. It’s <a href="https://css-tricks.com/snippets/css/basic-view-transition/">the most basic</a> of all cross-document view transitions.</p>



<p class="wp-block-paragraph">How do we customize things? We use the&nbsp;<code>::view-transition-old()</code>&nbsp;and&nbsp;<code>::view-transition-new()</code>&nbsp;pseudo-classes, where the former is the “old” snapshot and the latter is the “new” one. Like the <code>.card</code> elements we used in the last example, that’s where we set the keyframe animation:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">::view-transition-old(root) {
  /* animation goes here */
}

::view-transition-new(root) {
  /* animation goes here */
}</code></pre>



<p class="is-style-explanation wp-block-paragraph">The&nbsp;<code>root</code>&nbsp;parameter tells the view transition to target the whole page and all the elements created (and not created) by the view transition&#8217;s default snapshot group.</p>



<h2 id="heres-the-problem" class="wp-block-heading">Here’s the problem</h2>



<p class="wp-block-paragraph">Let&#8217;s say we want to apply that same 3D flip to the entire webpage, where the snapshot of the “old” page flips into the “new” page. Again, a 3D animation asks us for two things:</p>



<ol class="wp-block-list">
<li>The&nbsp;<code>perspective</code>&nbsp;property on the parent element so its children get that 3D effect</li>



<li>An animation on the page for when the view transition happens</li>
</ol>



<p class="wp-block-paragraph">But: What exactly do we set the perspective on, as in, what is the parent element here?</p>



<p class="wp-block-paragraph">Since&nbsp;<a href="https://www.w3.org/TR/css-view-transitions-1/#view-transition-pseudos" rel="noopener">view transitions take snapshots of the entire webpage</a>, we might assume (logically) it would be the <code>&lt;html&gt;</code> element (or the <code>:root</code>), right? I mean, the DOM tree looks like this when a view transition is present:</p>



<pre rel="" class="wp-block-csstricks-code-block language-none" data-line=""><code markup="tt">html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  └─ ::view-transition-group(name)
  │     └─ ::view-transition-image-pair(name)
  │        ├─ ::view-transition-old(name)
  │        └─ ::view-transition-new(name)
  ├─ head
  └─ body
        └─ …</code></pre>



<p class="wp-block-paragraph">So, the entire snapshot should be where we put the <code>perspective</code>. Right? Turns out, no.</p>



<p class="wp-block-paragraph">In fact, does nothing at all! You&#8217;re left with this instead of the beautiful 3D flip we were able to use on the cards earlier:</p>



<figure class="wp-block-video"><video height="780" style="aspect-ratio: 1914 / 780;" width="1914" controls src="https://css-tricks.com/wp-content/uploads/2026/06/video_1.mp4"></video><figcaption class="wp-element-caption"><a href="https://github.com/sunkanmii/perspective-demo-on-root-and-body" rel="noopener">GitHub Source</a>&nbsp;and&nbsp;<a href="https://sunkanmii.github.io/perspective-demo-on-root-and-body/" rel="noopener">Live Demo</a></figcaption></figure>



<p class="wp-block-paragraph">Here’s the code I was working with:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Cross-document View Transition opt-in */
@view-transition {
  navigation: auto;
}

/* 3D flip: Old page flips away, new page flips in */
@keyframes flip-out {
  0% {
    transform: rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: rotateY(-90deg);
    opacity: 0;
  }
}

@keyframes flip-in {
  0% {
    transform: rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: rotateY(0deg);
    opacity: 1;
  }
}

::view-transition-old(root) {
  animation: flip-out 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
  transform-origin: center center;
}

::view-transition-new(root) {
  animation: flip-in 0.3s cubic-bezier(0, 0, 0.6, 1) 0.3s backwards;
  transform-origin: center center;
}</code></pre>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> I didn&#8217;t reverse the animation here since we flip to&nbsp;<code>-90deg</code>&nbsp;and then from&nbsp;<code>90deg</code>. Not exactly the same!</p>



<p class="wp-block-paragraph">And it doesn’t work, no matter if <code>perspective</code> is on <code>html</code> or <code>:root</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* &#x1f44e; */
html {
  perspective: 1100px;
}

/* &#x1f44e; */
:root {
  perspective: 1100px;
}</code></pre>



<p class="wp-block-paragraph">I did some digging and discovered that&nbsp;<code>perspective</code>&nbsp;(and 3D transformations in general) is one of several&nbsp;<a href="https://developer.chrome.com/docs/css-ui/view-transitions/nested-view-transition-groups#:~:text=This%20flat%20tree%20approach%20is%20sufficient%20for%20many%20use%2Dcases%2C%20but%20there%20are%20some%20styling%20use%2Dcases%20that%20cannot%20be%20achieved%20with%20it" rel="noopener">CSS properties that would produce an unusual effect</a>. (Leave it to Bramus to have the answer!)</p>



<p class="wp-block-paragraph">So&#8230; What do we do? Some ideas came to mind, but sadly failed:</p>



<ul class="wp-block-list">
<li>I tried setting the&nbsp;<code>perspective</code>&nbsp;property on the&nbsp;<code>body</code>.</li>



<li>I tried setting&nbsp;<code>perspective</code> inside&nbsp;<code>::view-transition-group(root)</code>.</li>



<li>I tried setting&nbsp;<code>perspective</code> inside the&nbsp;<a href="https://css-tricks.com/almanac/pseudo-selectors/v/view-transition/"><code>::view-transition</code></a>&nbsp;pseudo.</li>
</ul>



<p class="wp-block-paragraph">There&#8217;s actually a super simple workaround to this, and I can&#8217;t believe it took me this long to figure it out — don&#8217;t use&nbsp;<code>perspective</code> at all!</p>



<h2 id="the-solution" class="wp-block-heading">The solution</h2>



<p class="wp-block-paragraph">Short story: we have to use the&nbsp;<code><a href="https://css-tricks.com/almanac/functions/p/perspective">perspective()</a></code> function instead of the <code>perspective</code> property. And not inside any of the&nbsp;<code>::view-transition-*</code>&nbsp;pseudos as you might expect, but inside the&nbsp;<code>@keyframes</code>&nbsp;animation:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line="3,7,13,17"><code markup="tt">@keyframes flip-out {
  0% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: perspective(1100px) rotateY(-90deg);
    opacity: 0;
  }
}
@keyframes flip-in {
  0% {
    transform: perspective(1100px) rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
}</code></pre>



<p class="wp-block-paragraph">This simple, but big change moves the scene from a flat <em>meh</em> to a beautiful <em>ah yeah</em>:</p>



<figure class="wp-block-video"><video height="780" style="aspect-ratio: 1914 / 780;" width="1914" controls src="https://css-tricks.com/wp-content/uploads/2026/06/video_2.mp4"></video><figcaption class="wp-element-caption"><a href="https://github.com/sunkanmii/persepective-inside-animation" rel="noopener">GitHub Source</a> and&nbsp;<a href="https://sunkanmii.github.io/persepective-inside-animation/" rel="noopener">Live Demo</a></figcaption></figure>



<p class="wp-block-paragraph">Here’s why, apparently. The view transition pseudo-element tree is rendered <em>outside</em> the normal HTML flow. More specifically, the entire view transition tree is&nbsp;<a href="https://www.w3.org/TR/css-view-transitions-1/#view-transition-rendering" rel="noopener">rendered above the DOM in its own layer</a>. However, particularly for&nbsp;<code>::view-transition</code>, I&#8217;m not too sure why this is the case, but my best guess would be that each&nbsp;<a href="https://www.w3.org/TR/css-view-transitions-1/#ua-styles" rel="noopener">view transition group automatically has its position and transform values overridden by the browser</a>; hence, interfering with the <code>perspective</code>.</p>



<p class="is-style-default wp-block-paragraph">The difference between <code>perspective</code> and <code>perspective()</code>? The <code>perspective</code> property is applied to the parent element, while <code>perspective()</code> is a <code>transform</code> property function applied directly to the element itself. And since the view transition pseudo tree does not have a true parent, we’ve gotta use <code>perspective()</code> since it doesn’t require a parent. <em>Phew.</em></p>



<h2 id="to-recap" class="wp-block-heading">To recap…</h2>



<p class="wp-block-paragraph">Setting&nbsp;<code>perspective</code>&nbsp;on the&nbsp;<code>html</code>,&nbsp;<code>:root</code>, or any of the view transition pseudo-class won&#8217;t work. And if you have been struggling to find the solution, like I was, I think this little, but big&nbsp;<code>perspective()</code>&nbsp;change will solve that issue if you ever come across it. Take it from me, I battled with this for weeks till I came back today to rant about it and discovered a solution to it. A perk of writing!</p>



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/why-isnt-my-3d-view-transition-working/">Why Isn&#8217;t My 3D View Transition Working?</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/why-isnt-my-3d-view-transition-working/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/06/video_1.mp4" length="2984445" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/06/video_2.mp4" length="3598101" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">395594</post-id>	</item>
		<item>
		<title>There’s no need to include ‘navigation’ in your navigation labels</title>
		<link>https://css-tricks.com/navigation-in-your-navigation-labels/</link>
					<comments>https://css-tricks.com/navigation-in-your-navigation-labels/#comments</comments>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Fri, 12 Jun 2026 15:08:38 +0000</pubDate>
				<category><![CDATA[Links]]></category>
		<category><![CDATA[accessibility]]></category>
		<category><![CDATA[alt text]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395744</guid>

					<description><![CDATA[<p>One of those nuances to keep in your back pocket when writing for screen readers.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/navigation-in-your-navigation-labels/">There’s no need to include ‘navigation’ in your navigation labels</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://www.tempertemper.net/blog/theres-no-need-to-include-navigation-in-your-navigation-labels" rel="noopener">Mark Underhill</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">And now to the reason I wrote this post: including the word “navigation” in your&nbsp;<code>&lt;nav&gt;</code>&nbsp;labels. There’s no need. If we did, we’d hear something like “Navigation, Primary navigation”. Not the end of the world, but unnecessarily repetitive for screen reader users.</p>
</blockquote>



<span id="more-395744"></span>



<p class="wp-block-paragraph">One of those nuances to keep in your back pocket when writing for screen readers. Reminds me, too, that there&#8217;s no need to say something like &#8220;image&#8221; when describing one in the <code>alt</code> text. That&#8217;s sorta implied. While I&#8217;m no screen reading native, I imagine these sorts of things are minor pet peeves that, given a little love and consideration, make navigating that much more enjoyable.</p>



<p class="wp-block-paragraph">While we&#8217;re on the UX of accessible text, another consideration: <a href="https://piccalil.li/blog/accessible-faux-nested-interactive-controls/#a-tangent-about-accessible-name-length" rel="noopener">keep it succinct</a>. <a href="https://css-tricks.com/just-how-long-should-alt-text-be/">It doesn&#8217;t have to be a novel.</a></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/navigation-in-your-navigation-labels/">There’s no need to include ‘navigation’ in your navigation labels</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/navigation-in-your-navigation-labels/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395744</post-id>	</item>
		<item>
		<title>Creating Memorable Web Experiences: A Modern CSS Toolkit</title>
		<link>https://css-tricks.com/creating-memorable-web-experiences-a-modern-css-toolkit/</link>
					<comments>https://css-tricks.com/creating-memorable-web-experiences-a-modern-css-toolkit/#comments</comments>
		
		<dc:creator><![CDATA[Mariana Beldi]]></dc:creator>
		<pubDate>Wed, 10 Jun 2026 13:02:10 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[animation]]></category>
		<category><![CDATA[SVG]]></category>
		<category><![CDATA[ux]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393676</guid>

					<description><![CDATA[<p>There are many ways to create memorable experiences. Sometimes it's as simple as a form that completes smoothly. But here I'm interested in sharing techniques I reach for when I want a site to feel alive and be remembered.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/creating-memorable-web-experiences-a-modern-css-toolkit/">Creating Memorable Web Experiences: A Modern CSS Toolkit</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I love the fact that CSS is finally reclaiming control over visual interactions, taking charge of the styling, the animation, and the accessibility exactly as it should. Today, native browser capabilities allow us to move the heavy lifting away from the JavaScript main thread and closer to the GPU. By letting the browser’s engine optimize performance under the hood, we save energy and processing power while building code that is robust, accessible, and independent of external libraries that might deprecate tomorrow.</p>



<p class="wp-block-paragraph">We have 3D, modern layout techniques, clip-paths, transforms, custom properties, scroll-driven animations, view-transitions, <a href="https://css-tricks.com/almanac/rules/p/property/"><code>@property</code></a> — and we can animate almost anything, <a href="https://css-tricks.com/transitioning-to-auto-height/">even to auto-height</a>!</p>



<p class="wp-block-paragraph">And, of course, there’s SVG, which isn’t new, but allows us to build entire websites through illustrations and animations. Take the example below: it’s responsive, lightweight, accessible, and powered primarily by CSS Grid + SVG.</p>



<span id="more-393676"></span>



<figure class="wp-block-video"><video height="1788" style="aspect-ratio: 3434 / 1788;" width="3434" controls src="https://css-tricks.com/wp-content/uploads/2026/04/nerd2-f.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">We can even build an <a href="https://www.youtube.com/watch?v=tga0DzSCKt0&amp;t=1s" rel="noopener">entire video game</a> including the UI using only SVG:</p>



<figure class="wp-block-video"><video height="608" style="aspect-ratio: 1200 / 608;" width="1200" controls src="https://css-tricks.com/wp-content/uploads/2026/04/do-14.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">What follows is not a complete guide to modern CSS, but an opinionated selection of techniques I reach for when I want a site to feel alive and be remembered. There are many ways to create memorable experiences. Sometimes it&#8217;s as simple as a form that completes smoothly. But here I&#8217;m interested in the expressive end of the spectrum.</p>



<h2 id="motion-as-communication-defining-your-intent" class="wp-block-heading">Motion as Communication: Defining Your Intent</h2>



<p class="wp-block-paragraph">Before we dive into the technical side, I want to clarify something: <strong>we shouldn’t move things just because we can.</strong></p>



<p class="wp-block-paragraph">Everything communicates, and our animations are no exception. We must take the time to design movements that support the message we want to convey in order to keep our intents tightly scoped without overdoing it.</p>



<p class="wp-block-paragraph">Here’s a methodology I use when planning the design and animation of a site.</p>



<p class="wp-block-paragraph">Imagine we’re working on a project for a <strong>nature event focused on mushrooms</strong>. The design language changes completely depending on the “vibe”: selling a “Psychedelic Mushroom Rave” is worlds apart from a “Spiritual Mushroom Retreat” focused on ancestral medicine.</p>



<p class="wp-block-paragraph">Every design decision communicates. I like to create what I call <strong>keyword lists</strong> to define my intent and scope. For example, I might break things down into different options:</p>



<h3 id="option-a-the-psychedelic-event" class="wp-block-heading">Option A: The Psychedelic Event</h3>



<ul class="wp-block-list is-style-almanac-list">
<li><strong>Visuals:</strong> Colorful, saturated, high-contrast, illustrations, distortions</li>



<li><strong>Movement:</strong> Fast, frantic, unpredictable, morphing, rhythmic, synced loops, hypnotic</li>



<li><strong>Feeling:</strong> Fun, chaotic, energetic, stimulating, surprising</li>



<li><strong>Typography:</strong> Funk, “psych-rock”</li>



<li><strong>Style References:</strong> Pop Art, 60s/70s op art, rave flyers</li>



<li><strong>Actions:</strong> Dancing</li>



<li><strong>Extras:</strong> Emojis, films (e.g., <em>Fear and Loathing in Las Vegas</em>)</li>
</ul>



<h3 id="option-b-the-spiritual-retreat" class="wp-block-heading">Option B: The Spiritual Retreat</h3>



<ul class="wp-block-list is-style-almanac-list">
<li><strong>Visuals:</strong> Earth tones, neutral tones, de-saturated, photograph-heavy, nature, whitespace</li>



<li><strong>Movement:</strong> Slow, fluid, organic, breathing, subtle parallax, smooth scrolling.</li>



<li><strong>Feeling:</strong> Calm, serene, introspective, contemplative, safe</li>



<li><strong>Typography:</strong> Elegant Serif, minimalist sans-serif, wide spacing, legible</li>



<li><strong>Style References:</strong> Scandinavian design, Japanese <em>Wabi-sabi</em>, wellness/spa aesthetics, botanical books</li>



<li><strong>Actions:</strong> Breathing</li>



<li><strong>Extras:</strong> Healing sounds, film (e.g., <em>Eat Pray Love</em>)</li>
</ul>



<p class="wp-block-paragraph">This is the kind of exercise I do to guide my design and animation decisions. The lists will help me select everything from which CSS properties I plan to use and how to use them. I even share them with the client and, together, we choose a direction.</p>



<p class="wp-block-paragraph">Let’s say we go with Option A and look at a few examples of what I think are essential ingredients for creating memorable user experiences.</p>



<h2 id="split-text-animations" class="wp-block-heading">Split Text Animations</h2>



<p class="wp-block-paragraph">These animations became popular thanks to the <a href="https://gsap.com/docs/v3/Plugins/SplitText/" rel="noopener">GSAP SplitText plugin</a>. It splits text by character (or words, or lines if you like) so we can create interesting text effects, like staggered animations.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h1 class="reveal-text">
  &lt;span style="--i:0">H&lt;/span>
  &lt;span style="--i:1">O&lt;/span>
  &lt;span style="--i:2">L&lt;/span>
  &lt;span style="--i:3">A&lt;/span>
&lt;/h1></code></pre>



<p class="wp-block-paragraph">This approach wraps each letter in “Hola” in a span. From there, each span is inline-styled with a custom property indexing the spans in order. Which is something that will get a lot easier when the <a href="https://css-tricks.com/almanac/functions/s/sibling-index/"><code>sibling-index()</code></a> function gains broad browser support.</p>



<p class="wp-block-paragraph">But for now, each custom property value acts as a multiplier that increases an <code>animation-delay</code>, staggering each span. In this case we fade in each character as it moves up.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.reveal-text span {
  animation: slideUp 0.6s ease-out forwards;
  animation-delay: calc(var(--i) * 0.1s);
  display: inline-block;
  opacity: 0;
  transform: translateY(3rem);
}

@keyframes slideUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}</code></pre>



<p class="wp-block-paragraph">Accessibility is the tricky part here. The instinct is to hide all the individual spans from assistive technology with <code>aria-hidden="true"</code> and add a visually hidden version of the full word for screen readers:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h1>
  &lt;span class="sr-only">HOLA&lt;/span>
  &lt;span aria-hidden="true" class="reveal-text">
    &lt;span style="--i:0">H&lt;/span>
    &lt;span style="--i:1">O&lt;/span>
    &lt;span style="--i:2">L&lt;/span>
    &lt;span style="--i:3">A&lt;/span>
  &lt;/span>
&lt;/h1></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}</code></pre>



<p class="wp-block-paragraph">But be warned: this pattern doesn&#8217;t guarantee a good experience across all screen readers. <a href="https://adrianroselli.com/2026/02/you-know-what-just-dont-split-words-into-letters.html" rel="noopener">Adrian Roselli tested GSAP&#8217;s SplitText</a> across eight screen reader and browser combinations and found it only worked correctly in two of them. If you ship this technique, test it with real assistive technology.</p>



<p class="wp-block-paragraph">If that risk feels too high, there&#8217;s a very <a href="https://css-tricks.com/revealing-text-css-letter-spacing">clever alternative from Preethi</a> worth knowing that uses the <a href="https://css-tricks.com/almanac/properties/l/letter-spacing/"><code>letter-spacing</code></a> property. It accepts negative values that collapse characters on top of each other, hiding them without touching the DOM at all. Animate it back to <code>0</code> and you get a similar reveal effect without accessibility overhead.</p>



<p class="wp-block-paragraph">What would be great is <a href="https://css-tricks.com/using-nonexistent-nth-letter-selector-now/">a pseudo-selector like <code>::nth-letter</code></a> to target individual glyphs directly from CSS the way <a href="https://css-tricks.com/almanac/pseudo-selectors/f/first-letter/"><code>::first-letter</code></a> selects the first character. But unfortunately, there’s no <code>::nth-letter</code>&#8230; at least yet.</p>



<p class="wp-block-paragraph">Remember to <a href="https://css-tricks.com/revisiting-prefers-reduced-motion/">respect the user’s motion preferences</a> on every animation:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: reduce) {
  .reveal-text span {
    animation: none; /* or a softer animation */
  }
}</code></pre>



<p class="wp-block-paragraph">And here we go:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_EaPqxjN" src="//codepen.io/anon/embed/preview/EaPqxjN?height=450&amp;theme-id=1&amp;slug-hash=EaPqxjN&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed EaPqxjN" title="CodePen Embed EaPqxjN" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">It might not scale too much when we have a lot of text and different animations we want to apply. For the psychedelic event, I wanted to try splitting text with <a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/" rel="noopener">SMIL</a>, but it was verbose. This is the code for animating two letters alone:</p>



<pre rel="SVG" class="wp-block-csstricks-code-block language-svg" data-line=""><code markup="tt">&lt;svg role="img" aria-label="TODOS LOS HONGOS" viewBox="0 0 1366 938.96">
  &lt;title>TODOS LOS HONGOS&lt;/title>
  &lt;g aria-hidden="true">
    &lt;text transform="rotate(-9.87 2181.107 -1635.1)" opacity="0">T
      &lt;animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0s" fill="freeze"/>
      &lt;animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0s" fill="freeze"/>
    &lt;/text>
    &lt;text transform="rotate(-8.92 2372.854 -2084.755)" opacity="0">O
      &lt;animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0.1s" fill="freeze"/>
      &lt;animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0.1s" fill="freeze"/>
    &lt;/text>
    &lt;!-- rest of letters... -->
  &lt;/g>
&lt;/svg></code></pre>



<p class="wp-block-paragraph">Add <code>role="img"</code> and a <code>&lt;title&gt;</code> to the <code>&lt;svg&gt;</code>, and wrap the individual letters in <code>&lt;g aria-hidden="true"&gt;</code>. That gives screen readers one clean label to read. It works well in some combinations and badly in others, so if the text is critical, don&#8217;t animate it.</p>



<p class="wp-block-paragraph">Here is the complete code. It’s easier to write it when you have an AI to do it for you:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_EaPqxEw" src="//codepen.io/anon/embed/preview/EaPqxEw?height=550&amp;theme-id=1&amp;slug-hash=EaPqxEw&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed EaPqxEw" title="CodePen Embed EaPqxEw" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">For longer text, a library like GSAP gives you more control, but the same accessibility risks we discussed earlier apply, and the results across screen readers are inconsistent:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h1>
  &lt;span class="splitfirst">Todos los hongos son&lt;/span>
  &lt;span class="splitlast">mágicos&lt;/span>
&lt;/h1></code></pre>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const splitFirst = SplitText.create('.splitfirst', {
  type: "chars",
});
const splitLast = SplitText.create('.splitlast', {
  type: "chars, lines",
  mask: "lines"
});

const tween = gsap.timeline()
.from(splitFirst.chars, {
  xPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1, 
})
.from(splitLast.chars, {
  yPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1,
});</code></pre>



<p class="wp-block-paragraph">This would be a nice approach for Option B if we had gone that route. See how “serene” things feel as the text fades in.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_JoGQpVN" src="//codepen.io/anon/embed/preview/JoGQpVN?height=500&amp;theme-id=1&amp;slug-hash=JoGQpVN&amp;default-tab=result" height="500" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed JoGQpVN" title="CodePen Embed JoGQpVN" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="masking-clipping" class="wp-block-heading">Masking &amp; Clipping</h2>



<p class="wp-block-paragraph">The <a href="https://css-tricks.com/almanac/properties/c/clip-path/"><code>clip-path</code></a> and <a href="https://css-tricks.com/almanac/properties/m/mask/"><code>mask</code></a> properties allow us to hide portions of an element, but they work on fundamentally different principles. <strong>Clipping</strong> is a binary decision: pixels are either fully visible or completely gone,  making it the right choice for clean geometric shapes, like polygons, circles, or SVG paths, where the browser can also optimize rendering more efficiently. <strong>Masking</strong>, on the other hand, uses luminance or alpha channel values: white reveals, black hides, and everything in between produces partial transparency. This makes it the tool for soft edges, gradient fades, and irregular textures. Keep in mind that if you have a very complex vector shape, it might be more performant to use a <code>mask</code> than a vector <code>clip-path</code>. <a href="https://css-tricks.com/masking-vs-clipping-use/">Sarah Drasner has a nice write-up on when it makes sense to use one over the other.</a></p>



<p class="wp-block-paragraph">Our project is a very clear use case for <code>clip-path</code>. We have a circle shape that starts with <code>clip-path: circle(0%)</code>, which makes the element invisible (the clipping circle has zero radius). Over the duration of the animation it expands to <code>circle(100%)</code>, which fully reveals the element as the circle grows outward from its center. Meanwhile, we fade things in with the help of <code>opacity</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">#rainbow, #floor, #mushroom, #flores {
  opacity: 0;
  animation: maskAnim 2s ease-in forwards;
}

@keyframes maskAnim {
  0%, 1% { 
    clip-path: circle(0%);
    opacity: 1; 
  }
  100% { 
    clip-path: circle(100%); 
    opacity: 1; 
  }
}</code></pre>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> The <code>1%</code> keyframe is there to make sure the browser starts the <code>clip-path</code> interpolation from <code>circle(0%)</code> rather than from whatever value the element might already have. Without it, some browsers will unexpectedly jump at the very start. A cleaner alternative is to use <code>animation-fill-mode: both</code> because it locks the element in its <code>from</code> state before the animation begins.</p>



<p class="wp-block-paragraph">From there, we apply the same animation to the different SVG groups in our illustration:</p>



<pre rel="SVG" class="wp-block-csstricks-code-block language-svg" data-line=""><code markup="tt">&lt;g id="rainbow">...&lt;/g>
&lt;g id="floor">...&lt;/g>
&lt;g id="mushroom">...&lt;/g>
&lt;g id="flowers">...&lt;/g></code></pre>



<p class="wp-block-paragraph">How psychedelic is this?!</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_MYKNYjO" src="//codepen.io/anon/embed/preview/MYKNYjO?height=700&amp;theme-id=1&amp;slug-hash=MYKNYjO&amp;default-tab=result" height="700" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed MYKNYjO" title="CodePen Embed MYKNYjO" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="scroll-driven-animations" class="wp-block-heading">Scroll-Driven Animations</h2>



<p class="wp-block-paragraph">Scroll-driven animations are great because we can connect an animation’s progress to the user’s scrolling instead of a typical timeline that runs and stops.</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="scroll-driven-animations"></baseline-status>



<p class="wp-block-paragraph">We can use it for subtle and somewhat “trippy” movement, like a light parallax effect. In this case, we can make things that appear closer to the user move faster than the ones that are more distant.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNGqQgR" src="//codepen.io/anon/embed/preview/RNGqQgR?height=450&amp;theme-id=1&amp;slug-hash=RNGqQgR&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNGqQgR" title="CodePen Embed RNGqQgR" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">This is the full CSS:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">#estrellas, #arcoiris, .text-line, #fecha, #arco, #flores, #dir, #piso, #barras {
  animation: moveUp both;
  animation-timeline: view();
}

@keyframes moveUp {
  from { transform: translateY(var(--offset)); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

#estrellas { --offset: 10vh; }
#arcoiris { --offset: 20vh; }
#fecha { --offset: 45vh; }
#arco { --offset: 50vh; }
#dir { --offset: 50vh; }
#flores { --offset: 65vh; }
#piso { --offset: 85vh; }
#barras { --offset: 90vh; }</code></pre>



<p class="wp-block-paragraph">The <code>animation-timeline: view()</code> says that things should start the animation as soon as an element enters the scrollport when the user scrolls into it, and fully completes when it scrolls out of view. To make things move at different velocities, we place them at different offsets using an indexed <code>--offset</code> custom property like we did earlier for splitting text.</p>



<h2 id="3d-transforms" class="wp-block-heading">3D Transforms</h2>



<p class="wp-block-paragraph">This one is trickier and we need to keep an eye on performance. A tool like <a href="https://layoutit.com" rel="noopener">Layoutit</a> <a href="https://layoutit.com" rel="noopener"></a>can help carry the lift because it has a <a href="https://voxels.layoutit.com/" rel="noopener">voxels</a> and <a href="http://terra.layoutit.com" rel="noopener">terrain generator</a> built entirely with CSS 3D. It can go even further when it’s complemented with <a href="https://voxcss.com/" rel="noopener">VoxCSS</a>, a full voxel engine that renders 3D cuboids using only CSS Grid layers and transforms without the complexity of Canvas or WebGL.</p>



<p class="wp-block-paragraph">Let’s put together some combination scrolling and 3D effects. It’s the sort of thing that supports the “hypnotic” and “dancing” ideas in the Option A keyword list. Check this out:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_bNwQLEW" src="//codepen.io/anon/embed/preview/bNwQLEW?height=650&amp;theme-id=1&amp;slug-hash=bNwQLEW&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bNwQLEW" title="CodePen Embed bNwQLEW" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">Here, I’ve set up a scene with depth using the <a href="https://css-tricks.com/almanac/properties/p/perspective/"><code>perspective</code></a> property and then wrap all the child elements inside the scene in a 3D space with <code>transform-style: preserve-3d</code>. This way, all the child image elements rotate and translate along the depth axis (or z-axis).</p>



<p class="wp-block-paragraph">Let’s connect that to a scroll-driven animation that uses <code>transform: rotateY</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.scene {
  perspective: 1200px;
}

.img-wrapper { 
  transform-style: preserve-3d; 
  animation: rotateImg linear;
  animation-timeline: scroll();

  > img {
    transform: rotateY(270deg) translate3d(0, 50px, var(--distance));
  }

  > img:nth-child(2) {
    transform: rotateY(180deg) translate3d(0, 50px, var(--distance));
  }
}

/* etc. */

@keyframes rotateImg {
  to { transform: rotateY(360deg); }
}</code></pre>



<h2 id="custom-cursors" class="wp-block-heading">Custom Cursors</h2>



<p class="wp-block-paragraph"><code>cursor</code> might be one of the most unused CSS properties. There are <a href="https://css-tricks.com/almanac/properties/c/cursor/">many cursor types</a> we can use, although there are <a href="https://dbushell.com/2025/10/27/custom-cursor-accessibility/" rel="noopener">definitely opinions on just how far to go with this</a>.</p>



<p class="wp-block-paragraph">And we can use it to play around with the images, displaying different cursors on different containers when the user hovers them. I would personally use an SVG and PNG image for transparency support, though the property supports any raster image.</p>



<p class="wp-block-paragraph">It’s worth noting that cursor sizes vary by browser: Firefox caps custom cursors at 32×32px, while Chrome supports up to 128×128px. Most browsers refuse to display — or will downscale — cursors that are larger than 32×32px on high-DPI (retina) screens. Keeping your cursor at 32×32px is the safest choice to ensure consistency.</p>



<p class="wp-block-paragraph">For example:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.box1 {
  cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAZCAMAAAD63NUrAAAACVBMVEX///8AAAD///9+749PAAAAAXRSTlMAQObYZgAAAFZJREFUeNqdzksKwDAIAFHH+x+6lIYOVPOhs5OHJnES/5UkYKEkU7xjijSIm50iFh4fAXgYDd/yumVVRSwsqq/nRA3xVK0oo06d5U6DpQZ7PV7lMxH7LkaQAbYFwryzAAAAAElFTkSuQmCC),auto; 
}</code></pre>



<p class="wp-block-paragraph">We can even set multiple fallbacks to ensure the widest level of browser support:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  cursor: url('path-to-image.png'), url('path-to-image-2.svg'), url('path-to-image-3.jpeg'), auto;
}</code></pre>



<p class="wp-block-paragraph">While this is cool and all, we have to keep accessibility in mind for something that changes default web behavior like this. Custom cursors could be fun to apply to very specific elements rather than wholesale across the board.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yyYKxBb" src="//codepen.io/anon/embed/preview/yyYKxBb?height=400&amp;theme-id=1&amp;slug-hash=yyYKxBb&amp;default-tab=result" height="400" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yyYKxBb" title="CodePen Embed yyYKxBb" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="bonus-anchor-positioning" class="wp-block-heading">Bonus: Anchor Positioning</h2>



<p class="wp-block-paragraph">One more thing before we wrap up. I&#8217;ve been playing with <a href="https://css-tricks.com/css-anchor-positioning-guide/">CSS Anchor Positioning</a>, inspired by a <a href="https://www.youtube.com/watch?v=8_NQ7ARXz8c" rel="noopener">Kevin Powell demo</a>. We can use it to attach a single pseudo-element to a currently-hovered item instead of attaching a pseudo-element for each and every item. In other words, we create a single element and anchor it to a hovered element, like highlighting cards:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_myrgRgR" src="//codepen.io/anon/embed/preview/myrgRgR?height=980&amp;theme-id=1&amp;slug-hash=myrgRgR&amp;default-tab=result" height="980" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed myrgRgR" title="CodePen Embed myrgRgR" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">That opens up interesting possibilities, like being able to transition the hover state between cards. In this case, I’m using the <a href="https://css-tricks.com/almanac/functions/l/linear/"><code>linear()</code></a> function to get that natural bounce with help from <a href="https://easingwizard.com" rel="noopener">Easing Wizard</a>.</p>



<h2 id="conclusion" class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">The technical barriers for creating memorable web experiences are mostly gone now. I hope everything we’ve covered here gives you an idea of just how far we can go with modern CSS features that completely remove the need for additional JavaScript. We have more possibilities than ever before, all without the need for complex technical overhead like days past.</p>



<p class="wp-block-paragraph">So, instead of asking, <em>is this possible?</em>, the most important question becomes, <em>does this movement tell a better story?</em> If yes, ship it. Use these tools not because you can, but because they help you tell a better story, one that is also accessible and performant.</p>



<p class="wp-block-paragraph">And, of course, everything in here is just a handful of ways to do that. But what sort of memorable experiences have you used in your work? Or what have you seen on other sites?</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/creating-memorable-web-experiences-a-modern-css-toolkit/">Creating Memorable Web Experiences: A Modern CSS Toolkit</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/creating-memorable-web-experiences-a-modern-css-toolkit/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/nerd2-f.mp4" length="8810169" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/do-14.mp4" length="637889" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">393676</post-id>	</item>
		<item>
		<title>Scroll-Driven, Scroll-Triggered, Scroll States, and View Transitions</title>
		<link>https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/</link>
					<comments>https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/#respond</comments>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Mon, 08 Jun 2026 13:00:34 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[container-queries]]></category>
		<category><![CDATA[Scroll Driven Animation]]></category>
		<category><![CDATA[Scroll-Triggered Animation]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393503</guid>

					<description><![CDATA[<p>I've said one and mean another, and I've used one when I needed another. Comparing scroll-driven animations, scroll-triggered animations, container query scroll states, and view transitions for my future self.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/">Scroll-Driven, Scroll-Triggered, Scroll States, and View Transitions</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ve said one and meant another, and I&#8217;ve used one when I needed another. Please bear with me as I note the high-level similarities and differences between <strong>scroll-driven animations</strong>, <strong>scroll-triggered animations</strong>, <strong>container query scroll states</strong>, and <strong>view transitions</strong> for my future self.</p>



<span id="more-393503"></span>



<h2 id="scrolldriven-animations" class="wp-block-heading">Scroll-Driven Animations</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="scroll-driven-animations"></baseline-status>



<p class="wp-block-paragraph">A scroll-<em>driven</em> animation is an animation that responds to, yeah, scrolling. Specifically, there’s a direct link between scrolling progress and the animation’s progress. Scroll forwards, the animation moves forward. Scroll backwards, the animation runs backwards. Stop scrolling, the animations stops.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.element {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_LENgEQG" src="//codepen.io/anon/embed/LENgEQG?height=750&amp;theme-id=1&amp;slug-hash=LENgEQG&amp;default-tab=result" height="750" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed LENgEQG" title="CodePen Embed LENgEQG" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="scrolltriggered-animations" class="wp-block-heading">Scroll-Triggered Animations</h2>



<p class="wp-block-paragraph">A scroll-<em>triggered</em>&nbsp;animation executes on scroll and runs in its entirety.&nbsp;In other words, there&#8217;s no direct link with the scroll progress here. When an element crosses some defined threshold — called the <em>trigger activation range</em> — the animation runs, runs, runs. For example, when that element enters and exits the scrollport.</p>



<figure class="wp-block-video"><video height="1176" style="aspect-ratio: 1786 / 1176;" width="1786" controls src="https://css-tricks.com/wp-content/uploads/2026/04/Screen-Recording-2026-04-14-at-10.16.47-AM.mov" playsinline></video></figure>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_qEaLPGE" src="//codepen.io/anon/embed/qEaLPGE?height=850&amp;theme-id=1&amp;slug-hash=qEaLPGE&amp;default-tab=result" height="850" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed qEaLPGE" title="CodePen Embed qEaLPGE" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="container-query-scroll-state" class="wp-block-heading">Container Query Scroll State</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="container-scroll-state-queries"></baseline-status>



<p class="wp-block-paragraph">This one&#8217;s in the <a href="https://drafts.csswg.org/css-conditional-5/#container-scroll-state-query" rel="noopener">working draft</a> of CSS Conditional Rules Module Level 5 specification. Here&#8217;s how the spec defines it:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">[&#8230;] allows querying a container for state that depends on scroll position.&nbsp;</p>
</blockquote>



<p class="wp-block-paragraph">This is why my brain hurts so much. It&#8217;s sorta like a scroll-driven animation, sorta like a scroll-triggered animation, but updates styles when a container reaches some sort of scroll condition, say:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.sticky-nav {
  container-type: scroll-state;
  position: sticky;
  top: 0;

  @container scroll-state(stuck: top) {
    background: orangered;
    border-radius: 0;
    flex-direction: row;
    width: 100%;

    a {
      text-decoration: none;
    }
  }
}</code></pre>



<figure class="wp-block-video"><video height="1176" style="aspect-ratio: 1786 / 1176;" width="1786" controls src="https://css-tricks.com/wp-content/uploads/2026/04/Screen-Recording-2026-04-14-at-9.29.54-AM.mov" playsinline></video></figure>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_OPLwNma" src="//codepen.io/anon/embed/OPLwNma?height=650&amp;theme-id=1&amp;slug-hash=OPLwNma&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed OPLwNma" title="CodePen Embed OPLwNma" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="view-transition" class="wp-block-heading">View Transition</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="view-transitions"></baseline-status>



<p class="wp-block-paragraph">This has nothing to do with scroll! And it has nothing to do with <code>view()</code>. We&#8217;re actually talking about a complete API with interlocking CSS and JavaScript features that can do two things:</p>



<h3 id="samedocument-transitions" class="wp-block-heading"><strong>Same-document transitions</strong></h3>



<p class="wp-block-paragraph">An element changes from one state to another in response to a user interaction. I was really tickled by this one from Modern Web Weekly animating radio button check states where the state moves from one input to the other.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_MYWwJEd" src="//codepen.io/anon/embed/MYWwJEd?height=450&amp;theme-id=1&amp;slug-hash=MYWwJEd&amp;default-tab=css,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed MYWwJEd" title="CodePen Embed MYWwJEd" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">Basically, the state changes on the same page it started. Bramus is king of all-thing view transitions with oodles of beautiful examples in <a href="https://view-transitions.chrome.dev/my-patagonia-trip/" rel="noopener">this collection</a> from the Chrome team.</p>



<h3 id="crossdocument-transitions" class="wp-block-heading">Cross-document transitions</h3>



<p class="wp-block-paragraph">Animating from one page to the next. The default usage is a crossfade from Page A to Page B (and back again) and <a href="https://css-tricks.com/snippets/css/basic-view-transition/">is really easy to implement</a>. It can get much more complex from there, of course. <a href="https://css-tricks.com/7-view-transitions-recipes-to-try/">Sunkanmi recently shared several recipes</a>, like this neat one that wipes out the first page with a circular <code>clip-path</code> revealing the second page.</p>



<figure class="wp-block-video"><video height="602" style="aspect-ratio: 1280 / 602;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Circular-Wipe.mp4" playsinline></video></figure>



<h2 id="thats-all" class="wp-block-heading">That&#8217;s all!</h2>



<p class="wp-block-paragraph">It helps me to spell things out like this.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Type</th><th>What it does</th></tr></thead><tbody><tr><td><strong>Scroll-Driven Animations</strong></td><td>Scroll forwards, the animation moves forward. Scroll backwards, the animation runs backwards. Stop scrolling, the animations stops.</td></tr><tr><td><strong>Scroll-Triggered Animations</strong></td><td>When an element crosses some defined threshold — called the <em>trigger activation range</em> — the animation runs, runs, runs. </td></tr><tr><td><strong>Container Query Scroll State</strong></td><td>Updates styles when a container reaches some sort of scroll condition.</td></tr><tr><td><strong>View Transition</strong></td><td>API for <em>same-document transitions</em> (element changes from one state to another on the page) and <em>cross-document transitions</em> (transitioning from one page to the next, and back).</td></tr></tbody></table></figure>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/">Scroll-Driven, Scroll-Triggered, Scroll States, and View Transitions</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/scroll-driven-scroll-triggered-scroll-states-and-view-transitions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/Screen-Recording-2026-04-14-at-10.16.47-AM.mov" length="3122373" type="video/quicktime" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/Screen-Recording-2026-04-14-at-9.29.54-AM.mov" length="21639274" type="video/quicktime" />
<enclosure url="http://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Circular-Wipe.mp4" length="0" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">393503</post-id>	</item>
		<item>
		<title>Another Stab at the Perfect CSS Pie Chart&#8230; Sans JavaScript!</title>
		<link>https://css-tricks.com/another-stab-at-the-perfect-css-pie-chart-sans-javascript/</link>
					<comments>https://css-tricks.com/another-stab-at-the-perfect-css-pie-chart-sans-javascript/#comments</comments>
		
		<dc:creator><![CDATA[Antoine Villepreux]]></dc:creator>
		<pubDate>Thu, 04 Jun 2026 13:14:49 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[charts]]></category>
		<category><![CDATA[data visualization]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393178</guid>

					<description><![CDATA[<p>We dive again into CSS Pie Charts! This time, Author Antoine Villepreux delivers semantic and flexible charts without a single line of JS.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/another-stab-at-the-perfect-css-pie-chart-sans-javascript/">Another Stab at the Perfect CSS Pie Chart&#8230; Sans JavaScript!</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Recently,&nbsp;<a href="https://css-tricks.com/trying-to-make-the-perfect-pie-chart-in-css/">Juan Diego Rodríguez published an excellent article</a>&nbsp;exploring how far CSS can be pushed to build a semantic and customizable pie chart while keeping JavaScript to a minimum.</p>



<span id="more-393178"></span>



<p class="wp-block-paragraph">Citing Juan himself:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">In this article, we&#8217;ll try making the perfect pie chart in CSS. That means avoiding as much JavaScript as possible while addressing major headaches that comes with handwriting pie charts.</p>
</blockquote>



<p class="wp-block-paragraph">And it stated some goals that I want to go through again in order of priority:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<ul class="wp-block-list">
<li>This must be semantic! Meaning a screen reader should be able to understand the data shown in the pie chart.</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">To my understanding, the original article&#8217;s solution reached that goal. Its semantic approach (labels in plain HTML + values as attributes reinjected into the DOM via pseudo-elements) is clean, expressive, and hopefully accessible.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<ul class="wp-block-list">
<li>This should be HTML-customizable! Once the CSS is done, we only have to change the markup to customize the pie chart.</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">The original article reached that goal as well.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<ul class="wp-block-list">
<li>This should keep JavaScript to a minimum! No problem with JavaScript in general, it&#8217;s just more fun this way.</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">The original article aimed to use as little JavaScript as possible, mainly for fun. I tend to disagree slightly. For me, it should not be just for fun, since&#8230;</p>



<ul class="wp-block-list">
<li>JavaScript is there to deal with states and logic, and</li>



<li>CSS is there to style the markup.</li>
</ul>



<p class="wp-block-paragraph">The initial &#8220;no JavaScript&#8221; constraint was meaningful to me. CSS should be powerful enough to let us style a pie chart. JavaScript should not be required. So, I decided to see whether there was a way to 100% get rid of it and, for fun, forked <a href="https://codepen.io/monknow/pen/pvbrmGL" rel="noopener">the article&#8217;s CodePen</a> during a lunch break.</p>



<p class="wp-block-paragraph">I kept the original code as unchanged as possible, preserving its semantic approach and HTML-side customizability. If It Ain&#8217;t Broke, Don&#8217;t Fix It&#x2122;.</p>



<p class="wp-block-paragraph">Coincidentally, this article came right after a&nbsp;<a href="https://codepen.io/villepreux/pen/NPrBEgE" rel="noopener">recent short pen of mine toying with bar charts</a>. So I was already in the mood for charts. But bar charts are far easier: each bar&#8217;s position or size does not depend on the others. A pie chart is a different beast: each slice&#8217;s position depends on the previous one. Luckily, this made it more of a fun challenge.</p>



<p class="wp-block-paragraph">But before diving into my take on pie charts, let&#8217;s see how these have been approached by other web developers.</p>



<h3 id="prior-art" class="wp-block-heading">Prior Art</h3>



<p class="wp-block-paragraph">I read many blogs, articles, and code examples from professional front-end developers, but I am not one myself, so I am not entirely certain of my ability to identify the most relevant and up-to-date prior art&#8230; Let&#8217;s try anyway.</p>



<p class="wp-block-paragraph">It is easy to find many JavaScript libraries dealing with charts. I have used them a lot in my work. However, due to our no-JavaScript constraint, we shall exclude them.</p>



<p class="wp-block-paragraph">I started looking for CSS-only pie charts, and one of the first libraries that pops up is <a href="https://chartscss.org/charts" rel="noopener">Chart-CSS</a>. It advertises semantic structure, HTML tags to display data, accessibility, and raw data inside the markup. It seems to be a very good library and does not use any JavaScript.</p>



<p class="wp-block-paragraph">Instead, it uses HTML tables, which, in my opinion and experience, makes total sense (most of the time, source data comes in a table). However, it does not solve the specific challenge of letting the user set only the values while having the start and end angles of each slice automatically computed. In this case, users still have to manually define them.</p>



<p class="wp-block-paragraph">There are also very good articles discussing charts or data visualization in general. To name a few:</p>



<ul class="wp-block-list">
<li>Vitaly Friedman&#8217;s&nbsp;<a href="https://www.smashingmagazine.com/2021/03/complete-guide-accessible-front-end-components/#accessible-data-visualizations" rel="noopener">&#8220;2022 Guide to Accessible Front-End Components&#8221;</a>&nbsp;is still relevant.</li>



<li>Sarah L. Fossheim&nbsp;<a href="https://fossheim.io/writing/tag/dataviz/" rel="noopener">wrote extensively about data visualization accessibility</a>&nbsp;between 2020 and 2024</li>
</ul>



<p class="wp-block-paragraph">They just have one small (but very important to us) drawback. While these resources are valuable in explaining how chart accessibility should work, they do not really address easy HTML &#8220;interface&#8221; nor pure CSS implementations.</p>



<h3 id="how-i-tackled-the-problem" class="wp-block-heading">How I Tackled the Problem</h3>



<p class="wp-block-paragraph">If you are still reading, I assume you are at least somewhat interested in my approach. Understandably, If you just want to see the code, here it is!</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_KwMrGGa" src="//codepen.io/anon/embed/KwMrGGa?height=550&amp;theme-id=1&amp;slug-hash=KwMrGGa&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed KwMrGGa" title="CodePen Embed KwMrGGa" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">Initially, the reason JavaScript was required was that each slice needed to know the value of the previous one. However, due to how CSS property inheritance works, a child cannot know the state of another child. Despite knowing this, I first tried to determine whether there were niche or &#8220;voodoo&#8221; techniques that would allow me to keep the original HTML markup and attribute-based approach while removing JavaScript.</p>



<p class="wp-block-paragraph">I know that people like&nbsp;<a href="https://kizu.dev/" rel="noopener">Roman Komarov</a>&nbsp;can do incredible things with CSS, so I even considered exploring techniques involving property animations. But I clearly did not have the time to investigate that direction.</p>



<p class="wp-block-paragraph">I returned to the core issue: because of how CSS inheritance works, children cannot know the state of their siblings. I obviously needed a &#8220;surrounding entity&#8221; to handle this.</p>



<p class="wp-block-paragraph">In Juan&#8217;s post, that &#8220;entity&#8221; was JavaScript, which could loop through all children and compute the appropriate slice accumulations.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const pieChartItems = document.querySelectorAll(".pie-chart li");

let accum = 0;

pieChartItems.forEach((item) => {
  item.style.setProperty("--accum", accum);
  accum += parseFloat(item.getAttribute("data-percentage"));
});</code></pre>



<p class="wp-block-paragraph">The JavaScript code sets an&nbsp;<code>--accum</code>&nbsp;value for each slice, which holds the percentage values of all charts prior to it. Without it, we wouldn&#8217;t know where to position each slice and its corresponding label.</p>



<p class="wp-block-paragraph">In HTML/CSS, that entity exists too: the classic parent element. Therefore, my solution was to move the percentage values to the parent.</p>



<p class="wp-block-paragraph">First, let&#8217;s remember what the original markup for the pie chart looked like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;ul class="pie-chart">
  &lt;li data-percentage-1="10">Apple&lt;/li>
  &lt;li data-percentage-2="30">Banana&lt;/li>
  &lt;li data-percentage-3="20">Orange&lt;/li>
  &lt;li data-percentage-4="40">Strawberry&lt;/li>
&lt;/ul></code></pre>



<p class="wp-block-paragraph">While the version we&#8217;ll be using looks like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;ul class="pie-chart" data-percentage-1="10" data-percentage-2="30" data-percentage-3="20" data-percentage-4="40">
  &lt;li>Apple&lt;/li>
  &lt;li>Banana&lt;/li>
  &lt;li>Orange&lt;/li>
  &lt;li>Strawberry&lt;/li>
&lt;/ul></code></pre>



<p class="wp-block-paragraph">We&#8217;ve moved all values to the parent <code>&lt;ul&gt;</code> and given each item a dedicated name — effectively indexing them.</p>



<p class="wp-block-paragraph">I had previously experimented with this kind of &#8220;indexing&#8221; CSS workaround, for example, to compensate for the lack of&nbsp;<a href="https://css-tricks.com/almanac/functions/s/sibling-index/"><code>sibling-index()</code></a>&nbsp;and&nbsp;<a href="https://css-tricks.com/almanac/functions/s/sibling-count/"><code>sibling-count()</code></a>&nbsp;functions to&nbsp;<a href="https://codepen.io/villepreux/pen/azdgZRG" rel="noopener">generate random numbers</a>. I knew this was the right direction and that the rest would follow logically on the CSS side.</p>



<p class="is-style-explanation wp-block-paragraph"><strong>Spoiler:</strong>&nbsp;<code>sibling-index()</code>&nbsp;and&nbsp;<code>sibling-count()</code>&nbsp;are becoming Baseline soon!</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="sibling-count"></baseline-status>



<p class="wp-block-paragraph">It may look like duplication since we didn&#8217;t add anything but rather moved the attributes. However, this slight change allows us to manage all labels and values from the parent in CSS. What&#8217;s best, we still keep all attributes close together. And while you may say that this won&#8217;t scale as well, if we have data with tons of entries, then a pie chart is rarely the best choice to show it.</p>



<p class="wp-block-paragraph">Optionally, we could add&nbsp;<code>data-label</code>&nbsp;attributes to the labels just to pair labels and values visually.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;ul class="pie-chart" data-percentage-1="10" data-percentage-2="30" data-percentage-3="20" data-percentage-4="40">
  &lt;!-- Optional data-label attributes: just visual hints-->
  &lt;li data-label-1>Apple&lt;/li>
  &lt;li data-label-2>Banana&lt;/li>
  &lt;li data-label-3>Orange&lt;/li>
  &lt;li data-label-4>Strawberry&lt;/li>
&lt;/ul></code></pre>



<p class="wp-block-paragraph">Now let&#8217;s examine the CSS. The implementation requires two sets of some repetitive but straightforward CSS rules.</p>



<p class="wp-block-paragraph">Firstly, we&#8217;ll need to pass down each percentage to its corresponding slice. To do so, we use Juan&#8217;s and get the&nbsp;<code>data-percentage</code>&nbsp;attributes into CSS through the upgraded&nbsp;<a href="https://developer.chrome.com/blog/advanced-attr" rel="noopener"><code>attr()</code></a>&nbsp;function. In parallel, we&#8217;ll assign them to the corresponding slice using the&nbsp;<a href="https://css-tricks.com/almanac/pseudo-selectors/n/nth-child/"><code>nth-child()</code></a>&nbsp;selector.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.pie-chart {
   /* We write one for each slice we think we'll need */
  --p-100-1: attr(data-percentage-1 type(&lt;number>)); :nth-child(1) { --p-100: var(--p-100-1) }
  --p-100-2: attr(data-percentage-2 type(&lt;number>)); :nth-child(2) { --p-100: var(--p-100-2) }
  --p-100-3: attr(data-percentage-3 type(&lt;number>)); :nth-child(3) { --p-100: var(--p-100-3) }
  --p-100-4: attr(data-percentage-4 type(&lt;number>)); :nth-child(4) { --p-100: var(--p-100-4) }
   /*...*/
}</code></pre>



<p class="is-style-explanation wp-block-paragraph">For that kind of repetitive/incremental code, I keep it as a one-liner without carriage return. <abbr>IMHO</abbr> it&#8217;s a very acceptable exception to common formatting rules as it prevents typos by easing scan-ability of and also eases further iterations (e.g., adding support for more slices). But your mileage may vary.</p>



<p class="wp-block-paragraph">Let&#8217;s look a little closer at what&#8217;s going on here. At the level of the whole pie, we access the percentages for each slice through their index and store them in a corresponding CSS variable, so the fourth element gets&nbsp;<code>--p-100-4</code>, the fifth element gets&nbsp;<code>--p-100-5</code>, and so on:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">--p-100-4: attr(data-percentage-4 type(&lt;number>));</code></pre>



<p class="wp-block-paragraph">Next, we pass each a&nbsp;<code>--p-100</code>&nbsp;variable that&#8217;s local to each slice.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:nth-child(4) {
  --p-100: var(--p-100-4);
}</code></pre>



<p class="wp-block-paragraph">We now have all these slice values accessible at two levels:</p>



<ul class="wp-block-list">
<li>On the pie, via indexed variables:&nbsp;<code>--p-100-1</code>,&nbsp;<code>--p-100-2</code>,&nbsp;<code>--p-100-3</code></li>



<li>On each slice, via the&nbsp;<code>--p-100</code>&nbsp;variable</li>
</ul>



<p class="wp-block-paragraph">Now, we&#8217;ll need to calculate the corresponding&nbsp;<code>--accum</code>&nbsp;value, which is the sum of the values of all previous slices. To do so, we&#8217;ll have to progressively sum each percentage after each slice, then assign the value to the slice using&nbsp;<code>nth-child()</code>&nbsp;again.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.pie-chart {
  /* ... */
  --accum-1: 0;                                     :nth-child(1) { --accum: var(--accum-1) }
  --accum-2: calc(var(--accum-1) + var(--p-100-1)); :nth-child(2) { --accum: var(--accum-2) }
  --accum-3: calc(var(--accum-2) + var(--p-100-2)); :nth-child(3) { --accum: var(--accum-3) }
  --accum-4: calc(var(--accum-3) + var(--p-100-3)); :nth-child(4) { --accum: var(--accum-4) }
  /*...*/
}</code></pre>



<p class="wp-block-paragraph">Again, we first work at the pie level, where we compute one dedicated variable per slice. The first slice is a special case: there is no previous slice, so the accumulation is <code>0</code>. While in the rest, the accumulation for the slice&nbsp;<code>n</code>&nbsp;is the accumulation of the slices before&nbsp;<code>n−1</code>&nbsp;plus the value of the slice&nbsp;<code>n−1</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">--accum-4: calc(var(--accum-3) + var(--p-100-3));</code></pre>



<p class="wp-block-paragraph">The fourth element gets&nbsp;<code>--accum-4</code>, the fifth element gets&nbsp;<code>--accum-5</code>, and so on. Just as the percentages, at the level of each slice, we assign them to the local variable&nbsp;<code>--accum</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:nth-child(4) {
  --accum: var(--accum-4);
}</code></pre>



<p class="wp-block-paragraph">Once again, we have all these slice accumulation values accessible at two levels:</p>



<ul class="wp-block-list">
<li>On the pie, via indexed variables:&nbsp;<code>--accum-1</code>,&nbsp;<code>--accum-2</code>,&nbsp;<code>--accum-3</code></li>



<li>On each slice, via the&nbsp;<code>--accum</code>&nbsp;variable</li>
</ul>



<p class="wp-block-paragraph">I hope future native CSS features (perhaps&nbsp;<code><a href="https://css-tricks.com/functions-in-css/">@function</a></code>?) will prevent us from having to resort to such repetitive code. In the meantime, this can be simplified with a CSS preprocessor (Sass, Less).</p>



<p class="wp-block-paragraph">While forking the original Pen, some questions popped out in my mind — that I did not actually explore — to keep the original code as unchanged as possible:</p>



<ul class="wp-block-list">
<li>What about using&nbsp;<code>&lt;label&gt;</code>&nbsp;and&nbsp;<code>&lt;meter&gt;</code>&nbsp;for labels and values?</li>



<li>What about using a&nbsp;<code>&lt;table&gt;</code>&nbsp;(since charts are often extracted from tables with rows like <code>[label, value]</code>)?</li>
</ul>



<h3 id="note-about-accessibility" class="wp-block-heading">Note About Accessibility</h3>



<p class="wp-block-paragraph">In my fork, I handled accessibility the same way Juan did, but with one slight modification: I used&nbsp;<a href="https://css-tricks.com/almanac/properties/c/counter-reset/"><code>counter-reset</code></a>&nbsp;/&nbsp;<a href="https://css-tricks.com/almanac/functions/c/counter/"><code>counter()</code></a>&nbsp;instead of&nbsp;<code>attr()</code>&nbsp;to assign the percentages to the&nbsp;<a href="https://css-tricks.com/almanac/properties/c/content/"><code>content</code></a>&nbsp;property. This should work just as good as&nbsp;<code>attr()</code>, but let&#8217;s make sure it is still screenreader-friendly:</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://css-tricks.com/wp-content/uploads/2026/03/screenreader.mp4" playsinline></video></figure>



<p class="wp-block-paragraph">Another thing I thought of changing was the label elements inside each&nbsp;<code>&lt;li&gt;</code>. In the original article, Juan uses a&nbsp;<code>&lt;strong&gt;</code> element, while I opted for&nbsp;<code>&lt;span&gt;</code>&nbsp;instead. However, I think it may be totally acceptable to use the&nbsp;<code>&lt;label&gt;</code>&nbsp;itself. We normally think of them as being bounded inside&nbsp;<code>&lt;form&gt;</code>&nbsp;elements, but&nbsp;<a href="https://html.spec.whatwg.org/multipage/forms.html#the-label-element" rel="noopener">the spec says</a>&nbsp;that we could expect to use them in contexts &#8220;where phrasing content is expected.&#8221; So I could not find any obligation to use them only in the context of forms.</p>



<h3 id="default-colors" class="wp-block-heading">Default Colors</h3>



<p class="wp-block-paragraph">Juan&#8217;s article also called upon&nbsp;<a href="https://css-tricks.com/trying-to-make-the-perfect-pie-chart-in-css/#aa-thats-about-it-for-now">some improvements</a>, which I tried to address in my fork:</p>



<ul class="wp-block-list">
<li><code>data-color</code>&nbsp;can be omitted, and colors are then generated.</li>



<li>Colors can be defined either on the parent or on the children (user&#8217;s choice; both are supported).</li>
</ul>



<p class="wp-block-paragraph">This translates to the next snippet for each slice:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.pie-chart li {
  --color: attr(data-color type(&lt;color>));
  --bg-color: var(--color, hsl(calc(360deg * sibling-index() / sibling-count()) 90% 40%));
}</code></pre>



<p class="wp-block-paragraph">I refrained from using&nbsp;<code>sibling-index()</code>&nbsp;and&nbsp;<code>sibling-count()</code>&nbsp;in the main part, since they aren&#8217;t Baseline (yet, but soon!), but I couldn&#8217;t hold myself back since calculating the color hue is so much fancier with them. These functions really allow some&nbsp;<a href="https://codepen.io/villepreux/pen/pvEjVdK" rel="noopener">magic</a>!</p>



<p class="wp-block-paragraph">Still, here is my &#8220;CSS-only polyfill&#8221;; repetitive (yet simple) code:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.pie-chart {
  :has(:nth-child(1)) { --sibling-count: 1 } :nth-child(1) { --sibling-index: 1; }
  :has(:nth-child(2)) { --sibling-count: 2 } :nth-child(2) { --sibling-index: 2; }
  :has(:nth-child(3)) { --sibling-count: 3 } :nth-child(3) { --sibling-index: 3; }
  :has(:nth-child(4)) { --sibling-count: 4 } :nth-child(4) { --sibling-index: 4; }
  /* ... */
}</code></pre>



<h3 id="more-chart-types-" class="wp-block-heading">More Chart Types?</h3>



<p class="wp-block-paragraph">We now have a common foundation for other chart types. As a proof of concept, I implemented a bar chart mode in&nbsp;<a href="https://codepen.io/villepreux/pen/KwMrGGa?editors=0100" rel="noopener">my fork</a>.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_KwMrGGa" src="//codepen.io/anon/embed/KwMrGGa?height=550&amp;theme-id=1&amp;slug-hash=KwMrGGa&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed KwMrGGa" title="CodePen Embed KwMrGGa" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1390" style="aspect-ratio: 2008 / 1390;" width="2008" controls src="https://css-tricks.com/wp-content/uploads/2026/03/pie-bar-charts.mov" playsinline></video></figure>



<h3 id="a-web-component-" class="wp-block-heading">A Web Component, Perhaps?</h3>



<p class="wp-block-paragraph">In a way, we already have a web component here — one without JavaScript, using&nbsp;<a href="https://meyerweb.com/eric/thoughts/2023/11/01/blinded-by-the-light-dom/" rel="noopener">light DOM</a>.</p>



<p class="wp-block-paragraph">And to me&nbsp;<code>&lt;pie-char attributes...&gt;</code>&nbsp;is not fundamentally different that either&nbsp;<code>&lt;div class="pie chart" attributes...&gt;</code>&nbsp;or&nbsp;<code>&lt;div pie chart attributes...&gt;</code>. I can see value in this approach when considering progressive enhancement, though.</p>



<p class="wp-block-paragraph">For example, a chart that refreshes automatically and fetches live data. But that would require JavaScript — which we are deliberately avoiding today.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/another-stab-at-the-perfect-css-pie-chart-sans-javascript/">Another Stab at the Perfect CSS Pie Chart&#8230; Sans JavaScript!</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/another-stab-at-the-perfect-css-pie-chart-sans-javascript/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/03/screenreader.mp4" length="644313" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/03/pie-bar-charts.mov" length="6872003" type="video/quicktime" />

		<post-id xmlns="com-wordpress:feed-additions:1">393178</post-id>	</item>
		<item>
		<title>offset-path</title>
		<link>https://css-tricks.com/almanac/properties/o/offset-path/</link>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 15:02:38 +0000</pubDate>
				<category><![CDATA[animation]]></category>
		<category><![CDATA[offset-path]]></category>
		<guid isPermaLink="false">http://css-tricks.com/?page_id=243521</guid>

					<description><![CDATA[<p class="wp-block-paragraph">The <code>offset-path</code> property in CSS defines a movement path for an element to follow during animation.</p>
<p class="explanation wp-block-paragraph">This property began life as <code>motion-path</code>. This, and all other related <code>motion-*</code> properties, are being renamed <code>offset-*</code> in <a href="https://drafts.fxtf.org/motion-1/" rel="noopener">the spec</a>. We&#8217;re changing &#8230;</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/properties/o/offset-path/">offset-path</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The <code>offset-path</code> property in CSS defines a movement path for an element to follow during animation.</p>



<span id="more-243521"></span>



<p class="explanation wp-block-paragraph">This property began life as <code>motion-path</code>. This, and all other related <code>motion-*</code> properties, are being renamed <code>offset-*</code> in <a href="https://drafts.fxtf.org/motion-1/" rel="noopener">the spec</a>. We&#8217;re changing the names here in the almanac. If you want to use it right now, probably best to use both syntaxes.</p>



<p class="wp-block-paragraph">Here&#8217;s an example using the SVG path syntax:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.thing-that-moves {
  /* "Old" syntax. Available in Blink browsers as of ~October 2015 */
  motion-path: path("M 5 5 m -4, 0 a 4,4 0 1,0 8,0 a 4,4 0 1,0 -8,0");
 
  /* Currently spec'd syntax. Should be in stable Chrome as of ~December 2016 */
  offset-path: path("M 5 5 m -4, 0 a 4,4 0 1,0 8,0 a 4,4 0 1,0 -8,0");
}</code></pre>



<p class="wp-block-paragraph">This property cannot be animated, rather it defines the path for animation. We use the <code><a href="https://css-tricks.com/almanac/properties/o/offset-distance/">offset-pat</a></code><a href="https://css-tricks.com/almanac/properties/o/offset-distance/"><code>h</code></a> property to create the animation. Here&#8217;s a simple example of animating motion-offset with a @keyframes animation:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.thing-that-moves {
  offset-path: path('M 5 5 m -4, 0 a 4,4 0 1,0 8,0 a 4,4 0 1,0 -8,0');
  animation: move 3s linear infinite;
}

@keyframes move {
  100% { 
    motion-offset: 100%;   /* Old */
    offset-distance: 100%; /* New */
  }
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_zEpLpK" src="//codepen.io/anon/embed/zEpLpK?height=450&amp;theme-id=1&amp;slug-hash=zEpLpK&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed zEpLpK" title="CodePen Embed zEpLpK" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">In this demo, the orange circle is being animated along the <code>offset-path</code> we set in CSS. We actually <em>drew</em> that path in SVG with the exact same <code>path()</code> data, but that&#8217;s not necessary to get the motion.</p>



<p class="wp-block-paragraph">Say we drew a funky path like this in some SVG editing software:</p>



<figure class="wp-block-image align-none media-243695" id="post-243695"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2016/07/funky-path.png?ssl=1" alt=""/></figure>



<p class="wp-block-paragraph">We would find a path like:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt"></code></pre>



<p class="wp-block-paragraph">The <code>d</code> attribute value is what we&#8217;re after, and we can move it straight to CSS and use it as the <code>offset-path</code>:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_zEpLRo" src="//codepen.io/anon/embed/zEpLRo?height=450&amp;theme-id=1&amp;slug-hash=zEpLRo&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed zEpLRo" title="CodePen Embed zEpLRo" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">Note the unitless values in the path syntax. If you&#8217;re applying the CSS to an element within SVG, those coordinate values will use the coordinate system set up within that SVG&#8217;s <code>viewBox</code>. If you&#8217;re applying the motion to some other HTML element, those values will be pixels.</p>



<p class="wp-block-paragraph">Also note we used a graphic of a finger pointing to show how the element is automatically rotated so it kinda faces forward. You can control that with <code>offset-rotate</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.mover {
  offset-rotate: auto; /* default, faces forward */
  offset-rotate: reverse; /* faces backward */
  offset-rotate: 30deg; /* set angle */
  offset-rotate: auto 30deg; /* combine auto behavior with a set rotation */
}</code></pre>



<h2 id="values" class="wp-block-heading">Values</h2>



<p class="explanation wp-block-paragraph"><strong>As best as we can tell, <code>path()</code> and <code>none</code> are the only <em>working</em> values for <code>offset-path</code>.</strong></p>



<p class="wp-block-paragraph">The <code>offset-path</code> property is supposed to accept all the following values.</p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code><a href="https://css-tricks.com/almanac/functions/p/path/">path()</a></code>:</strong> Specifies a path in the SVG coordinates syntax</li>



<li><strong><code><a href="https://css-tricks.com/almanac/functions/s/shape/">shape()</a></code>:</strong> Creates a path with CSS-y commands instead of SVG</li>



<li><strong><code><a href="https://css-tricks.com/almanac/functions/u/url/">url()</a></code>:</strong> References the ID of an SVG element to be used as a movement path</li>



<li><strong><code>none</code>:</strong> Specifies no offset path at all</li>



<li><strong>Shape functions:</strong> A set of CSS functions that specify a shape in accordance to the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape" rel="noopener">CSS Shapes specification</a>, which includes:
<ul class="wp-block-list">
<li><code><a href="https://css-tricks.com/almanac/functions/c/circle/">circle()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/e/ellipse/">ellipse()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/i/inset/">inset()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/p/path/">path()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/p/polygon/">polygon()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/s/shape/">shape()</a></code></li>



<li><code><a href="https://css-tricks.com/almanac/functions/x/xywh/">xyzh()</a></code></li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Here&#8217;s some tests:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_WZdKMq" src="//codepen.io/anon/embed/WZdKMq?height=550&amp;theme-id=1&amp;slug-hash=WZdKMq&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed WZdKMq" title="CodePen Embed WZdKMq" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">Even telling an SVG element to reference a path definied the same SVG via <code>url()</code> <a href="http://codepen.io/chriscoyier/pen/Wxrazd" rel="noopener">doesn&#8217;t seem to work</a>.</p>



<h3 id="with-the-web-animations-api" class="wp-block-heading">With the Web Animations API</h3>



<p class="wp-block-paragraph">Dan Wilson explored some of this in <a href="https://codepen.io/danwilson/post/css-motion-paths" rel="noopener">Future Use: CSS Motion Paths</a>. You have access to all this same stuff in JavaScript through the Web Animations API. For example, say you&#8217;ve defined a <code>offset-path</code> in CSS, you can still control the animation through JavaScript:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_pWpZGj" src="//codepen.io/anon/embed/pWpZGj?height=450&amp;theme-id=1&amp;slug-hash=pWpZGj&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed pWpZGj" title="CodePen Embed pWpZGj" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h3 id="more-examples" class="wp-block-heading">More Examples</h3>



<p class="explanation wp-block-paragraph"><strong>Heads up!</strong> A lot of these were created before the change from <code>motion-*</code> naming to <code>offset-*</code>. Should be pretty easy to fix them if you&#8217;re so inclined.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_pJarJO" src="//codepen.io/anon/embed/pJarJO?height=550&amp;theme-id=1&amp;slug-hash=pJarJO&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed pJarJO" title="CodePen Embed pJarJO" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_jbaxJG" src="//codepen.io/anon/embed/jbaxJG?height=550&amp;theme-id=1&amp;slug-hash=jbaxJG&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed jbaxJG" title="CodePen Embed jbaxJG" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_qbJyGE" src="//codepen.io/anon/embed/qbJyGE?height=650&amp;theme-id=1&amp;slug-hash=qbJyGE&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed qbJyGE" title="CodePen Embed qbJyGE" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_rOqQqz" src="//codepen.io/anon/embed/rOqQqz?height=650&amp;theme-id=1&amp;slug-hash=rOqQqz&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed rOqQqz" title="CodePen Embed rOqQqz" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_MwLmby" src="//codepen.io/anon/embed/MwLmby?height=450&amp;theme-id=1&amp;slug-hash=MwLmby&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed MwLmby" title="CodePen Embed MwLmby" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_ZGmeRO" src="//codepen.io/anon/embed/ZGmeRO?height=650&amp;theme-id=1&amp;slug-hash=ZGmeRO&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ZGmeRO" title="CodePen Embed ZGmeRO" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_zGzJYd" src="//codepen.io/anon/embed/zGzJYd?height=650&amp;theme-id=1&amp;slug-hash=zGzJYd&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed zGzJYd" title="CodePen Embed zGzJYd" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="browser-support" class="wp-block-heading">Browser support</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="motion-path"></baseline-status>



<h2 id="is-there-another-way-to-do-this" class="wp-block-heading">Is There Another Way to Do This?</h2>



<p class="wp-block-paragraph">Our own Sarah Drasner <a href="https://css-tricks.com/smil-is-dead-long-live-smil-a-guide-to-alternatives-to-smil-features/">wrote about SMIL</a>, SVG&#8217;s native method for animations, and how <code>animateMotion</code> is used to animate objects along a SVG path. It looks like:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt"></code></pre>



<p class="wp-block-paragraph">GreenSock is another way though. Sarah talks about this in <a href="https://davidwalsh.name/gsap-svg" rel="noopener">GSAP + SVG for Power Users: Motion Along A Path</a> (SVG not required). Example:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_aOZOwj" src="//codepen.io/anon/embed/aOZOwj?height=450&amp;theme-id=1&amp;slug-hash=aOZOwj&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed aOZOwj" title="CodePen Embed aOZOwj" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="related-properties" class="wp-block-heading">Related Properties</h2>



    		
    <div class="in-article-cards">
      <article class="in-article-card " id="mini-post-14041">

      <div class="tags">
      <a href="https://css-tricks.com/tag/clip/" rel="tag">clip</a> <a href="https://css-tricks.com/tag/clip-path/" rel="tag">clip-path</a>    </div>
  
  <time datetime="2011-09-05" title="Originally published Sep 5, 2011">
    <strong>
                
      Almanac
      </strong>

    on

    Sep 5, 2011  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/properties/c/clip-path/">
      clip-path    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/properties/c/clip-path/" class="almanac-example">
      <code class="language-css">.element { clip: rect(110px, 160px, 170px, 60px); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/saracope/" aria-label="Author page of Sara Cope">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/82ba1e1fe4aca49cdcd66ff387fee32e787abbd0ae6d42750be22ea7c89a5102.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/82ba1e1fe4aca49cdcd66ff387fee32e787abbd0ae6d42750be22ea7c89a5102.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/saracope/">
      Sara Cope    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-270333">

      <div class="tags">
      <a href="https://css-tricks.com/tag/offset-path/" rel="tag">offset-path</a>    </div>
  
  <time datetime="2018-05-04" title="Originally published May 4, 2018">
    <strong>
                
      Almanac
      </strong>

    on

    May 4, 2018  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/properties/o/offset-anchor/">
      offset-anchor    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/properties/o/offset-anchor/" class="almanac-example">
      <code class="language-css">.element { offset-anchor: right top; }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/geoffgraham/" aria-label="Author page of Geoff Graham">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/geoffgraham/">
      Geoff Graham    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-243593">

  
  <time datetime="2016-07-22" title="Originally published Jul 22, 2016">
    <strong>
                
      Almanac
      </strong>

    on

    Jul 22, 2016  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/properties/o/offset-distance/">
      offset-distance    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/properties/o/offset-distance/" class="almanac-example">
      <code class="language-css">.element { offset-distance: 50%; }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/geoffgraham/" aria-label="Author page of Geoff Graham">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/geoffgraham/">
      Geoff Graham    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-243778">

      <div class="tags">
      <a href="https://css-tricks.com/tag/offset/" rel="tag">offset</a> <a href="https://css-tricks.com/tag/offset-rotate/" rel="tag">offset-rotate</a>    </div>
  
  <time datetime="2016-07-22" title="Originally published Jul 22, 2016">
    <strong>
                
      Almanac
      </strong>

    on

      </time>

  <h3>
    <a href="https://css-tricks.com/almanac/properties/o/offset-rotate/">
      offset-rotate    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/properties/o/offset-rotate/" class="almanac-example">
      <code class="language-css">.element { offset-rotate: 30deg; }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/chriscoyier/" aria-label="Author page of Chris Coyier">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/41a6f9778d12dfedcc7ec3727d64a12491d75d9a65d4b9323feb075391ae6795.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/41a6f9778d12dfedcc7ec3727d64a12491d75d9a65d4b9323feb075391ae6795.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/chriscoyier/">
      Chris Coyier    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387637">

  
  <time datetime="2025-07-09" title="Originally published Jul 9, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jul 9, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/c/circle/">
      circle()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/c/circle/" class="almanac-example">
      <code class="language-css">.shape { clip-path: circle(100px); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387653">

  
  <time datetime="2025-07-09" title="Originally published Jul 9, 2025">
    <strong>
                
      Almanac
      </strong>

    on

      </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/e/ellipse/">
      ellipse()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/e/ellipse/" class="almanac-example">
      <code class="language-css">.shape { clip-path: ellipse(60px 40px); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387729">

  
  <time datetime="2025-07-15" title="Originally published Jul 15, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jul 15, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/i/inset/">
      inset()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/i/inset/" class="almanac-example">
      <code class="language-css">.element { clip-path: inset(10px 2em 30% 3vw); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387225">

  
  <time datetime="2025-06-18" title="Originally published Jun 18, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jun 18, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/p/path/">
      path()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/p/path/" class="almanac-example">
      <code class="language-css">.element { clip-path: path("…"); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-388121">

  
  <time datetime="2025-07-24" title="Originally published Jul 24, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jul 24, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/p/polygon/">
      polygon()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/p/polygon/" class="almanac-example">
      <code class="language-css">.element { clip-path: polygon(50% 0%, 75% 6.7%, 93.3% 25%, 100% 50%, 93.3% 75%, 75% 93.3%, 50% 100%, 25% 93.3%, 6.7% 75%, 0% 50%, 6.7% 25%, 25% 6.7%); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387003">

  
  <time datetime="2025-06-10" title="Originally published Jun 10, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jun 10, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/s/shape/">
      shape()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/s/shape/" class="almanac-example">
      <code class="language-css">.triangle { clip-path: shape(from 50% 0%, line by 50% 100%, hline to 0%, line to 50% 0%, close); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-388474">

  
  <time datetime="2025-08-14" title="Originally published Aug 14, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Aug 14, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/u/url/">
      url()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/u/url/" class="almanac-example">
      <code class="language-css">.element { background-image: url("https://example.com/image.png"); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/gabrielshoyombo/" aria-label="Author page of Gabriel Shoyombo">
      <img data-recalc-dims="1" loading="lazy" decoding="async" alt="" class="avatar avatar-80 photo avatar-default" height="80" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/03/gabriel-shoyombo.jpeg?resize=80%2C80&#038;ssl=1" width="80">    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/gabrielshoyombo/">
      Gabriel Shoyombo    </a>
  </div>

</article>
<article class="in-article-card " id="mini-post-387736">

  
  <time datetime="2025-07-15" title="Originally published Jul 15, 2025">
    <strong>
                
      Almanac
      </strong>

    on

    Jul 15, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/almanac/functions/x/xywh/">
      xywh()    </a>
  </h3>

          <a href="https://css-tricks.com/almanac/functions/x/xywh/" class="almanac-example">
      <code class="language-css">.element { clip-path: xywh(60px 4em 50% 10vw round 10px 30px); }</code>
    </a>
  
  <div class="author-row">
    <a href="https://css-tricks.com/author/johnrhea/" aria-label="Author page of John Rhea">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/db5b77793bb2e052dabd9b140ffcedfb11acd4f33f3597c04c43d63a59eb7f59.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/johnrhea/">
      John Rhea    </a>
  </div>

</article>
    </div>
  
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/properties/o/offset-path/">offset-path</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">243521</post-id>	</item>
		<item>
		<title>@custom-media</title>
		<link>https://css-tricks.com/almanac/rules/c/custom-media/</link>
		
		<dc:creator><![CDATA[Declan Chidlow]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 13:03:21 +0000</pubDate>
				<guid isPermaLink="false">https://css-tricks.com/?page_id=394739</guid>

					<description><![CDATA[<p>The CSS <code>@custom-media</code> at-rule allows creating aliases for media queries.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/rules/c/custom-media/">@custom-media</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The CSS <code>@custom-media</code> at-rule allows creating aliases for <a href="https://css-tricks.com/a-complete-guide-to-css-media-queries/">media</a> <a href="https://css-tricks.com/a-complete-guide-to-css-media-queries/"></a><a href="https://css-tricks.com/a-complete-guide-to-css-media-queries/">queries</a>. This is particularly valuable if you have long or complex media queries that you use multiple times across your codebase. The feature is similar in nature to a media query version of <a href="https://css-tricks.com/a-complete-guide-to-custom-properties/">CSS custom properties (CSS variables)</a>.</p>



<span id="more-394739"></span>



<h2 id="syntax" class="wp-block-heading">Syntax</h2>



<p class="wp-block-paragraph">The syntax for <em>defining</em> an alias is:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media (&lt;dashed-ident>) [&lt;media-query-list> | true | false ];</code></pre>



<p class="wp-block-paragraph">For example:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --modern-touch (pointer: coarse) and (min-width: 1024px);</code></pre>



<p class="wp-block-paragraph">&#8230;where the dashed ident is <code>--modern-touch</code>.</p>



<p class="wp-block-paragraph">The syntax for <em>using</em> an alias is the same as using any media query, but instead of providing media types or media features, you provide the <code>&lt;dashed-ident&gt;</code> of your defined <code>@custom-media</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@cutom-media &lt;dashed-ident> {
  /* ... */
}</code></pre>



<h2 id="arguments-and-descriptors" class="wp-block-heading">Arguments and Descriptors</h2>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>&lt;dashed-ident&gt;</code>:</strong> A user-defined identifier that must start with two dashes (<code>--</code>), similar to functions or custom properties. Just like custom properties, the name is case-sensitive. For example, <code>--mobile-breakpoint</code> and <code>--Mobile-Breakpoint</code> would refer to different custom media definitions.</li>



<li><strong><code>&lt;media-query-list&gt;</code>:</strong> A list of media queries, separated by operators.</li>



<li><strong><code>true</code>/<code>false</code>:</strong> Always-match / never-match toggles.</li>
</ul>



<p class="wp-block-paragraph">Let’s look at how these work in different contexts, such as how they’re scoped, using them with booleans, defining complex logic, setting rules with the CSS range syntax, and even nesting aliases.</p>



<h3 id="scope-and-placement" class="wp-block-heading">Scope and Placement</h3>



<p class="wp-block-paragraph">Unlike custom properties, which are <a href="https://css-tricks.com/breaking-css-custom-properties-out-of-root-might-be-a-good-idea/">scoped to the element they are defined on</a> (and their children), <code>@custom-media</code> rules are global. They&nbsp;are evaluated in the global scope&nbsp;of the stylesheet and will always apply to the entire document. If multiple <code>@custom-media</code> rules are defined with the same name, the one in scope at the time of evaluation is the one that is used.</p>



<p class="wp-block-paragraph">When a <code>@media</code> rule uses a custom alias, i.e. the dashed ident, it looks at the current definition of that alias at that point in the stylesheet. If the alias is redefined later, it does not &#8220;update&#8221; the media queries that were already processed. For example, in this case <code>margin-block: 1rem</code> will only be applied to <code>body</code> if it is <code>fullscreen</code> and not <code>browser</code> despite the later declaration using the same name.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --screen-display (display-mode: fullscreen);

@media (--screen-display) {
  body {
    margin-block: 1rem;
  }
}

@custom-media --screen-display (display-mode: browser);</code></pre>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> This scoping behavior <a href="https://github.com/w3c/csswg-drafts/issues/13041" rel="noopener">is still being discussed</a> and is subject to change in the future.</p>



<h3 id="boolean-constants" class="wp-block-heading">Boolean Constants</h3>



<p class="wp-block-paragraph">In the Syntax section above, note that a <code>@custom-media</code> rule can be explicitly set to <code>true</code> or <code>false</code>. This is useful for &#8220;toggling&#8221; entire blocks of CSS during development or for feature flagging.</p>



<h3 id="operators-and-complex-logic" class="wp-block-heading">Operators and Complex Logic</h3>



<p class="wp-block-paragraph">As <code>@custom-media</code> utilizes the exact same logical operators (<code>and</code>, <code>,</code>, <code>or</code>, <code>not</code>, <code>only</code>) and grouping rules as <code>@media</code>, you can build complex, parentheses-grouped logic just as you normally would. For a full breakdown of how to use operators, negate features, or hide stylesheets from older browsers, reference the <a href="https://css-tricks.com/almanac/rules/m/media/#operators">Logic and Operators section of the <code>@media</code> almanac</a>. It is also worth referencing the <a href="https://css-tricks.com/almanac/rules/m/media/#nesting-and-complex-decision-making">section on nesting and complex decision-making</a> when building complex queries.</p>



<p class="wp-block-paragraph">To, for example, construct a query using the <code>and</code> logical operator, you can write this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --modern-touch (pointer: coarse) and (min-width: 1024px);</code></pre>



<h3 id="range-syntax" class="wp-block-heading">Range Syntax</h3>



<p class="wp-block-paragraph">The same as any other <code>&lt;media-query-list&gt;</code>, <code>@custom-media</code> has support for the <a href="https://css-tricks.com/the-new-css-media-query-range-syntax/">ranged media query syntax</a> which uses operators, e.g. greater than (<code>&gt;</code>), less than (<code>&lt;</code>), and equals (<code>=</code>), to evaluate conditions:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Old way */
@custom-media --tablet (min-width: 768px) and (max-width: 1024px);

/* New, cleaner way */
@custom-media --tablet (768px &lt;= width &lt;= 1024px);</code></pre>



<h3 id="nested-aliases" class="wp-block-heading">Nested Aliases</h3>



<p class="wp-block-paragraph">One unique feature of <code>@custom-media</code> aliases is that they can reference each other. This allows you to build layered, semantic conditions:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --narrow-window (width &lt; 30rem);
@custom-media --small-and-hover (--narrow-window) and (hover: hover);

@media (--small-and-hover) {
  /* Styles for mobile-sized screens with hover capabilities */
}</code></pre>



<p class="wp-block-paragraph">However, if a loop is detected, all involved custom media queries are treated as undefined. For instance, if <code>--query-a</code> references <code>--query-b</code>, then <code>--query-b</code> cannot reference <code>--query-a</code>. Similarly, a custom media query <em>cannot</em> refer to itself.</p>



<p class="wp-block-paragraph">Also be aware of over-nesting, as that can make debugging and identifying which layer of query is having the relevant impact in your browser&#8217;s developer tools very difficult.</p>



<h2 id="example-defining-common-breakpoints" class="wp-block-heading">Example: Defining Common Breakpoints</h2>



<p class="wp-block-paragraph">Instead of remembering if your &#8220;tablet&#8221; breakpoint is <code>768px</code> or <code>800px</code>, you can define it once at the top of your stylesheet.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --tablet (min-width: 768px);

.sidebar {
  display: none;

  @media (--tablet) {
    display: block;
  }
}</code></pre>



<h2 id="example-defining-shorthands-for-existing-properties" class="wp-block-heading">Example: Defining Shorthands for Existing Properties</h2>



<p class="wp-block-paragraph">Standard boilerplate such as <code>(prefers-reduced-motion: reduce)</code> can be used many times across a codebase, and those bytes add up. You can use <code>@custom-media</code> to define simpler alternatives:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@custom-media --prefers-reduced-motion (prefers-reduced-motion: reduce);

@media (--prefers-reduced-motion) {
  /* ... */
}
@custom-media --js-enabled (scripting: enabled);
@custom-media --js-disabled (scripting: none);

@media (--js-disabled) {
  .no-js-banner {
    display: block;
  }
}</code></pre>



<p class="wp-block-paragraph">There are a great number of <a href="https://css-tricks.com/open-props-custom-media-recipes/">Open Props <code>@custom-media</code> Recipes</a> you may consider using.</p>



<h2 id="javascript-support" class="wp-block-heading">JavaScript Support</h2>



<p class="wp-block-paragraph"><code>@custom-media</code> aliases are not exposed to the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener">JavaScript <code>matchMedia()</code> method</a>, meaning this code will <em>not</em> work, even if you have the alias defined somewhere on your page.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">matchMedia("(--tablet)")</code></pre>



<h2 id="specification" class="wp-block-heading">Specification</h2>



<p class="wp-block-paragraph">The <code>@custom-media</code> at-rule is defined in the <a href="https://www.w3.org/TR/mediaqueries-5/#custom-mq" rel="noopener">Media Queries Level 5</a> specification.</p>



<h2 id="browser-support" class="wp-block-heading">Browser Support</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="custom-media-queries"></baseline-status>



<p class="wp-block-paragraph">Unsupported browsers largely ignore <code>@custom-media</code>, so fallback declarations and progressive enhancement strategies can be advantageous. You can use <code>@supports</code> to check if <code>@custom-media</code> is supported in the user&#8217;s browser, like so:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@supports (at-rule(@custom-media)) {
  /* ... */
}</code></pre>



<p class="wp-block-paragraph">Ironically, however, at time of writing, the <code>@supports</code> at-rule evaluation functionality doesn&#8217;t have full support across browsers (<a href="https://github.com/w3c/csswg-drafts/issues/2463#issuecomment-1016720310" rel="noopener">Chrome 148</a>+ only), so you will need to check if it is supported in your case. You can see the discussion on this in <a href="https://github.com/w3c/csswg-drafts/issues/2463#issuecomment-1016720310" rel="noopener">CSS Drafts Issue #2463</a>.</p>



<p class="wp-block-paragraph">Another approach is to use a tool such as <a href="https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media" rel="noopener">PostCSS Custom Media</a>, which will expand the rules in a build step to achieve wider browser support.</p>



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/rules/c/custom-media/">@custom-media</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394739</post-id>	</item>
		<item>
		<title>@function</title>
		<link>https://css-tricks.com/almanac/rules/f/function/</link>
		
		<dc:creator><![CDATA[Declan Chidlow]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 13:02:39 +0000</pubDate>
				<category><![CDATA[CSS functions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=394732</guid>

					<description><![CDATA[<p>The <code>@function</code> at-rule defines CSS custom functions. These custom functions are reusable blocks of CSS that can accept arguments, contain complex logic, and return values based on that logic. </p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/rules/f/function/">@function</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The <code>@function</code> at-rule defines CSS custom functions. These custom functions are reusable blocks of CSS that can accept arguments, contain complex logic, and return values based on that logic. The feature is similar in nature to a more dynamic version of <a href="https://css-tricks.com/a-complete-guide-to-custom-properties/">custom properties (CSS variables)</a>.</p>



<span id="more-394732"></span>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> There is also a <code>@function</code> at-rule in Sass which is similar in purpose but <a href="https://sass-lang.com/documentation/at-rules/function/" rel="noopener">different in function</a> to the native CSS <code>@function</code>. Be aware of this if Sass is part of your stack or when searching for resources as it is easy to conflate one with the other.</p>



<h2 id="syntax" class="wp-block-heading">Syntax</h2>



<p class="wp-block-paragraph">The <code>@function</code> at-rule defines a custom function, using the following syntax:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --function-name(&lt;function-parameter>#?) [returns &lt;css-type>]? {
  &lt;declaration-rule-list>
}

&lt;function-parameter> = &lt;custom-property-name> &lt;css-type>? [ : &lt;default-value> ]?</code></pre>



<p class="wp-block-paragraph">In other words, we define the function’s name as a dashed ident (<code>--my-function</code>), supply some condition we want to match (<code>&lt;function-parameter&gt;</code>), and say what sort of thing we want to return, say, a <a href="https://css-tricks.com/css-length-units/">CSS[<code>&lt;length&gt;</code>] value</a>. And, if that condition matches, we apply styles (<code>&lt;declaration-rule-list&gt;</code>).</p>



<p class="wp-block-paragraph">Let’s dig deeper into what those things actually mean.</p>



<h2 id="arguments-and-descriptors" class="wp-block-heading">Arguments and Descriptors</h2>



<p class="wp-block-paragraph">There are a number of parts to the syntax for <code>@function</code> to handle different parts of the feature. It may all look very complex — and it is — but it&#8217;ll become clearer later when we look at some examples.</p>



<h3 id="-function-token-" class="wp-block-heading"><code>--function-token</code></h3>



<p class="wp-block-paragraph">A user-defined identifier that must start with two dashes (<code>--</code>), similar to the <code>dashed-ident</code> of custom properties. Just like custom properties, the name is case-sensitive. For example, <code>--conversion</code> and <code>--Conversion</code> would refer to different custom function definitions.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --progression()</code></pre>



<h3 id="-function-parameter-optional-" class="wp-block-heading"><code>&lt;function-parameter&gt;</code> (optional)</h3>



<p class="wp-block-paragraph">An optional comma-separated list of inputs that can include:</p>



<ul class="wp-block-list">
<li><strong><code>--param-name</code>:</strong> The name of the argument (must start with <code>--</code>).</li>



<li><strong><code>&lt;css-type&gt;</code> (optional):</strong> A keyword or type (e.g., <code>&lt;length&gt;</code>, <code>&lt;color&gt;</code>) that tells the function what sort of input or result it’s returning when it hits a matched condition.</li>



<li><strong><code>&lt;default-value&gt;</code> (optional):</strong> A fallback value that’s returned if the result is invalid, such as the argument is omitted during the function call. If you provide a default value, it must be valid to the aforementioned <code>&lt;css-type&gt;</code> (e.g. a <code>&lt;length&gt;</code> must default to a valid CSS length). It is separated from the rest of the parameter definition with a colon (<code>:</code>).</li>



<li><strong><code>returns &lt;css-type&gt;</code> (optional):</strong> Defines the expected output type of the function. This helps the browser validate logic before rendering. If a type isn&#8217;t specified then, anything will be valid (like writing <code>returns type(*)</code>).</li>



<li><strong><code>&lt;declaration-rule-list&gt;</code>:</strong> CSS declarations and at-rules that construct the function&#8217;s body and logic. It can include custom properties and the <code>result</code> descriptor — either at the root or nested within an at-rule.</li>
</ul>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --progression(--current &lt;number>, --total &lt;number>) returns &lt;percentage> {
  result:
}</code></pre>



<p class="wp-block-paragraph">The <code>result</code> descriptor that defines what the custom function will return. If a custom function forgoes the <code>result</code> descriptor, it will always return <a href="https://www.w3.org/TR/css-variables-1/#guaranteed-invalid-value" rel="noopener"><code>guaranteed-invalid</code> value</a>, just like a broken custom property.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --progression(--current &lt;number>, --total &lt;number>) returns &lt;percentage> {}</code></pre>



<h2 id="basic-usage" class="wp-block-heading">Basic Usage</h2>



<p class="wp-block-paragraph">For an example of the most basic function you could make, we have a function that calculates a provided value (e.g. <code>20px</code>) in half (e.g. <code>10px</code>), and returns returns it as a length unit (e.g. <code>px</code>):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --half(--size &lt;length>) {
  result: calc(var(--size) / 2);
}</code></pre>



<p class="wp-block-paragraph">Here, we are &#8216;naming&#8217; our function by setting the <code>function-token</code> to <code>--half</code>. We are then creating a <code>function-parameter</code> called <code>--size</code>, and setting the <code>css-type</code> to <code>&lt;length&gt;</code>, so that it will only accept length values. The result descriptor is set to <code>calc(--size / 2)</code>, which uses the <a href="https://css-tricks.com/a-complete-guide-to-calc-in-css/">CSS Calculating Function</a> to halve the value sourced from the <code>size</code> <code>function-parameter</code>.</p>



<p class="wp-block-paragraph">We then use the function like so:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.container {
  margin-inline: --half(20px); /* This will resolve to 10px */
}</code></pre>



<h2 id="type-checking" class="wp-block-heading">Type Checking</h2>



<p class="wp-block-paragraph">Just like when writing JavaScript or other languages, sometimes we want to ensure a function only accepts certain arguments. For example, what if we want to ensure that only numbers can be input and that only a percentage can be output?</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --progression(--current &lt;number>, --total &lt;number>) returns &lt;percentage> {
  result: calc(var(--current) / var(--total) * 100%);
}

.progress-bar {
  width: --progression(3, 5); /* Evaluates to 60% */
}</code></pre>



<p class="wp-block-paragraph">The <code>&lt;css-type&gt;</code> is enclosed in angle brackets in a manner identical to how you type-check a custom property via <a href="https://css-tricks.com/almanac/rules/p/property/"><code>@property</code></a>. If an argument does not match the declared type (e.g. <code>&lt;color&gt;</code>), the function call becomes invalid, which is very valuable for catching bugs early in large codebases.</p>



<p class="wp-block-paragraph">You can use a <code>&lt;syntax-combinator&gt;</code> to allow multiple types by wrapping the types in <code>type()</code> and using <code>|</code> as a separator. For example, <code>--alpha</code> here allows both <code>&lt;number&gt;</code> and <code>&lt;percentage&gt;</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --transparent(--color &lt;color>, --alpha type(&lt;number> | &lt;percentage>));</code></pre>



<h2 id="comma-separated-lists" class="wp-block-heading">Comma-Separated Lists</h2>



<p class="wp-block-paragraph">CSS uses commas to separate the inputs of a custom function, which begs the question: <em>what if you wish to provide a list of values?</em> To provide a list of values as one input rather than several separate inputs, you must first mark a function to expect a list.</p>



<p class="wp-block-paragraph">To do this, you suffix the <code>#</code> character to the <code>&lt;css-type&gt;</code>. When calling the function, you then wrap the list of values in curly braces, which tells the browser to treat everything inside the braces as a single argument.</p>



<p class="wp-block-paragraph">For instance:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Calculates the distance between the highest and lowest values in a list, plus another input */
@function --get-range(--list &lt;length>#, --n &lt;length>) {
  result: calc(max(var(--list)) - min(var(--list)) + var(--n));
}

div {
  /* Finds the difference between 10px and 100px, then adds 200px */
  padding-block: --get-range({10px, 100px, 50px, 25px}, 200px); /* 290px */
}</code></pre>



<h2 id="constructs-and-the-css-cascade" class="wp-block-heading">Constructs and the CSS Cascade</h2>



<p class="wp-block-paragraph">The <code>result</code> descriptor follows the rules of the CSS Cascade. That means you can declare multiple result values, and the last valid matching value will win, just like any other properties. As such, conditional group rules (<code>@media</code>, <code>@container</code>, <code>@supports</code>) and other functions, such as <code>if()</code>, provide a lot of additional possibilities.</p>



<p class="wp-block-paragraph">In this case, we return a <code>--suitable-font-size</code> that defaults to <code>16px</code> when the screen is less than <code>1000px</code> pixels. If the screen is greater than <code>1000px</code> then the “winning” style is what’s in the <code>@media</code> block.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --suitable-font-size() returns &lt;length> {
  result: 16px;

  @media (width > 1000px) {
    result: 20px;
  }
}

body {
  font-size: --suitable-font-size();
}</code></pre>



<p class="wp-block-paragraph">Keep in mind that the last defined value always wins, so if you were to write the example below, the result would <em>always</em> be <code>16px</code>, regardless of the media query being triggered.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --suitable-font-size() returns &lt;length> {
  @media (width > 1000px) {
    result: 20px;
  }

  result: 16px;
}</code></pre>



<p class="wp-block-paragraph">The adherence to the established cascade also allows you to use custom properties within a function. These custom properties are locally scoped, so they are only accessible in your custom function and any custom function that references it and thus won&#8217;t unexpectedly leak out globally and interact with the rest of your CSS.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --spacing-scale(--multiplier) {
  --base-unit: 8px;
  result: calc(var(--base-unit) * var(--multiplier));
}</code></pre>



<p class="wp-block-paragraph">You can also use <em>other</em> custom functions within a custom function, essentially nesting one function within another. This allows for very clean code, where each section only does one job and functions can be widely reused.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@function --square(--n) {
  result: calc(var(--n) * var(--n));
}

@function --circle-area(--radius) {
  --pi: 3.14159;
  result: calc(var(--pi) * --square(var(--radius)));
}

.blob {
  width: calc(--circle-area(10) * 1px); /* 314.159px */
}</code></pre>



<h2 id="defaults" class="wp-block-heading">Defaults</h2>



<p class="wp-block-paragraph">Functions can handle multiple arguments and provide default values. The default value is defined by including it at the end of the function parameter, separated by a colon (<code>:</code>).</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Define the function */
@function --brand-glass(--opacity &lt;number>: 0.5) returns &lt;color> {
  result: rgb(10 120 255 / var(--opacity));
}

/* Use the function */
.header {
  background: --brand-glass(); /* Defaults to 0.5 */
}

.header:hover {
  background: --brand-glass(0.8); /* Overrides to 0.8 */
}</code></pre>



<h2 id="no-side-effects" class="wp-block-heading">No Side Effects</h2>



<p class="wp-block-paragraph">A CSS <code>@function</code> can only return a value; it cannot do anything else. For example, you cannot change a property inside of a function or use a function to generate multiple declarations. For such abilities, one must look to the proposed <a href="https://css-tricks.com/css-functions-and-mixins-module-notes/"><code>@mixin</code> at-rule</a>, which would provide functionality in this manner, allowing multiple lines of CSS properties and other complex logic.</p>



<h2 id="circular-dependencies" class="wp-block-heading">Circular Dependencies</h2>



<p class="wp-block-paragraph">CSS is very strict about circular logic. If Function A calls Function B, and Function B calls Function A, the browser will catch this cyclic dependency and immediately mark both as invalid.</p>



<p class="wp-block-paragraph">This also applies to CSS Custom Properties and referring to the custom function itself. If a function relies on a custom property or function that is itself calculated by that same function, the browser will end the calculation to prevent an infinite recursion.</p>



<h2 id="specification" class="wp-block-heading">Specification</h2>



<p class="wp-block-paragraph">The <code>@function</code> at-rule is defined in the <a href="https://www.w3.org/TR/css-mixins-1/" rel="noopener">CSS Custom Functions and Mixins Module Level 1</a> specification.</p>



<h2 id="browser-support" class="wp-block-heading">Browser Support</h2>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="function"></baseline-status>



<p class="wp-block-paragraph">Unsupported browsers ignore <code>@function</code>, so fallback declarations and progressive enhancement strategies can be advantageous. You can use <code>@supports</code> to check if <code>@function</code> is supported in the user&#8217;s browser, like so:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@supports (at-rule(@function)) {
  /* ... */
}</code></pre>



<p class="wp-block-paragraph">Ironically, however, at time of writing, the <code>@supports</code> at-rule evaluation functionality doesn&#8217;t have full support across browsers (<a href="https://github.com/w3c/csswg-drafts/issues/2463#issuecomment-1016720310" rel="noopener">Chrome 148</a>+ only), so you will need to check if it is supported in your case. You can see the discussion on this in <a href="https://github.com/w3c/csswg-drafts/issues/2463#issuecomment-1016720310" rel="noopener">CSS Drafts Issue #2463</a>.</p>



<h2 id="more-information" class="wp-block-heading">More Information</h2>



<ul class="wp-block-list">
<li><a href="https://css-tricks.com/functions-in-css/">Functions in CSS?!</a></li>



<li><a href="https://www.bram.us/2025/02/09/css-custom-functions-teaser/" rel="noopener">CSS Custom Functions are coming … and they are going to be a game changer!</a></li>
</ul>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/rules/f/function/">@function</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394732</post-id>	</item>
		<item>
		<title>::search-text</title>
		<link>https://css-tricks.com/almanac/pseudo-selectors/s/search-text/</link>
		
		<dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 12:59:21 +0000</pubDate>
				<category><![CDATA[custom highlight api]]></category>
		<category><![CDATA[highlight]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=393544</guid>

					<description><![CDATA[<p>The <code>CSS ::search-text</code> pseudo-element selects the matching text from your browser's "find in page" feature. </p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/pseudo-selectors/s/search-text/">::search-text</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The CSS&nbsp;<code>::search-text</code>&nbsp;pseudo-element selects matching text from your browser&#8217;s &#8220;find in page&#8221; feature. For example, if you use your browser search to find &#8220;search-text&#8221; on this page, all instances of it will highlight. This pseudo-element lets us style the appearance of that highlight.</p>



<span id="more-393544"></span>



<p class="wp-block-paragraph">And a bonus! If there are multiple matches on the page, then <code>::search-text</code> can be used with the&nbsp;<a href="https://css-tricks.com/almanac/pseudo-selectors/c/current/"><code>:current</code></a>&nbsp;pseudo-class to style the match that&#8217;s currently in focus.</p>



<p class="is-style-explanation wp-block-paragraph">You can &#8220;find in page&#8221; using the&nbsp;<kbd>CTRL + F</kbd>&nbsp;(for Windows) or&nbsp;<kbd>"⌘F"</kbd>&nbsp;(for Mac) keyboard shortcuts.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">::search-text {
  background: oklch(87% 0.17 90) /* yellow */;
  color: black;
}

::search-text:current {
  background: oklch(62% 0.22 38) /* red */;
  color: white;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_LERXqoG" src="//codepen.io/anon/embed/LERXqoG?height=600&amp;theme-id=1&amp;slug-hash=LERXqoG&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed LERXqoG" title="CodePen Embed LERXqoG" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">The CSS&nbsp;<code>::search-text</code>&nbsp;pseudo-element is defined in the&nbsp;<a href="https://drafts.csswg.org/css-pseudo-4/#selectordef-search-text" rel="noopener">CSS Pseudo-Elements Module Level 4 specification</a>.</p>



<h2 id="syntax" class="wp-block-heading">Syntax</h2>



<p class="wp-block-paragraph">Pretty straightforward! Declare the pseudo-element and add your style rules:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">&lt;element-selector>::search-text{
  /* ... */
}</code></pre>



<h2 id="usage" class="wp-block-heading">Usage</h2>



<p class="wp-block-paragraph">It&#8217;s typically declared by itself (<code>::search-text</code>), but can be appended to specific elements as well:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* All text */
::search-text {}
html::search-text {} /* kind of redundant */
/* Specific element */
section::search-text {}
strong::search-text {}</code></pre>



<p class="wp-block-paragraph">We&#8217;re a little limited as far as what CSS properties we can declare in <code>::search-text</code>. Here is what it supports:</p>



<ul class="wp-block-list">
<li><a href="https://css-tricks.com/almanac/properties/b/background/background-color/"><code>background-color</code></a></li>



<li><a href="https://css-tricks.com/almanac/properties/c/color/"><code>color</code></a></li>



<li><a href="https://css-tricks.com/almanac/properties/t/text-decoration/"><code>text-decoration</code></a>&nbsp;and its associated properties (<code>text-underline-position</code>&nbsp;and&nbsp;<a href="https://css-tricks.com/almanac/properties/t/text-underline-offset/"><code>text-underline-offset</code></a>), as well as its associated constituent properties:
<ul class="wp-block-list">
<li><a href="https://css-tricks.com/almanac/properties/t/text-decoration/text-decoration-color/"><code>text-decoration-color</code></a></li>



<li><code><a href="https://css-tricks.com/almanac/properties/t/text-decoration/text-decoration-line/">text-decoration-line</a></code>: But only the&nbsp;<code>grammar-error</code>,&nbsp;<code>spelling-error</code>,&nbsp;<code>line-through</code>,&nbsp;<code>none</code>, and&nbsp;<code>underline</code>&nbsp;values.</li>



<li><a href="https://css-tricks.com/almanac/properties/t/text-decoration-skip-ink/"><code>text-decoration-skip-ink</code></a></li>



<li><a href="https://css-tricks.com/almanac/properties/t/text-decoration/text-decoration-style/"><code>text-decoration-style</code></a></li>



<li><a href="https://css-tricks.com/almanac/properties/t/text-decoration/text-decoration-thickness/"><code>text-decoration-thickness</code></a></li>
</ul>
</li>



<li><a href="https://css-tricks.com/almanac/properties/t/text-shadow/"><code>text-shadow</code></a></li>



<li><a href="https://drafts.csswg.org/css-variables-2/#custom-property" rel="noopener">Custom properties</a></li>
</ul>



<p class="wp-block-paragraph">And, yes, we can use it with <a href="https://css-tricks.com/a-complete-guide-to-custom-properties/">custom properties</a>, like:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --color-blueberry: oklch(0.5458 0.1568 241.39);
}
::search-text {
  background-color: var(--color-blueberry);
}</code></pre>



<h2 id="basic-usage" class="wp-block-heading">Basic usage</h2>



<p class="wp-block-paragraph">With the&nbsp;<code>::search-text</code>&nbsp;pseudo-element, we can style the matching text results from &#8220;Find in page&#8221;. Plus, if we want to style the currently focused matching text, then we attach the&nbsp;<code>:current</code>&nbsp;pseudo-class after&nbsp;<code>::search-text</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* matches all searched text */
::search-text {
  color: green;
  background-color: white;
}
/* matches any header level 1 searched text */
h1::search-text {
  text-shadow: 12px 1px lightgrey;
  background-color: black;
  color: white;
}
/* the current searched header level 1 text */
h1::search-text:current {
  color: red;
  background: white;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_XJjoNPG" src="//codepen.io/anon/embed/XJjoNPG?height=600&amp;theme-id=1&amp;slug-hash=XJjoNPG&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed XJjoNPG" title="CodePen Embed XJjoNPG" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="inheritance-chain" class="wp-block-heading">Inheritance chain</h2>



<p class="wp-block-paragraph">All descendants always inherit styles applied through the highlight pseudo-elements. This way, individual properties set on highlights will cascade to all elements down the three. Take for example the following HTML:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;article>
  &lt;h2>Highlight inheritance demo&lt;/h2>
  &lt;p>Lorem ipsum dolor sit amet. &lt;strong>Lorem&lt;/strong> appears again here. Another lorem appears here.
  &lt;/p>
&lt;/article></code></pre>



<p class="wp-block-paragraph">We have an&nbsp;<code>&lt;article&gt;</code>&nbsp;container with two children:&nbsp;<code>&lt;h2&gt;</code>&nbsp;and&nbsp;<code>&lt;p&gt;</code>, the latter having a&nbsp;<code>&lt;strong&gt;</code>&nbsp;descendant of its own. We could style&nbsp;<code>::search-text</code>&nbsp;in&nbsp;<code>&lt;article&gt;</code>&nbsp;with the following CSS, which would apply to all elements in&nbsp;<code>&lt;article&gt;</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">article::search-text {
  background: gold;
  color: black;
  text-decoration: underline;
}</code></pre>



<p class="wp-block-paragraph">Then, override the&nbsp;<code>color</code>&nbsp;property for only&nbsp;<code>&lt;p&gt;</code>&nbsp;and its descendants:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">p::search-text {
  color: orange;
}</code></pre>



<p class="wp-block-paragraph">And do the same for&nbsp;<code>text-decoration</code>&nbsp;on the&nbsp;<code>&lt;strong&gt;</code>&nbsp;element:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">strong::search-text {
  text-decoration: line-through;
}</code></pre>



<p class="wp-block-paragraph">When you search for &#8220;lorem&#8221;, the background of the first instance (inside&nbsp;<code>&lt;p&gt;</code>&nbsp;but outside&nbsp;<code>&lt;strong&gt;</code>) will inherit both the&nbsp;<code>background</code>&nbsp;and&nbsp;<code>text-decoration</code>&nbsp;values from&nbsp;<code>&lt;article&gt;</code>, while overriding its&nbsp;<code>color</code>&nbsp;value with its own&nbsp;<code>orange</code>.</p>



<p class="wp-block-paragraph">Onto&nbsp;<code>&lt;strong&gt;</code>&#8216;s &#8220;lorem&#8221; text, it will inherit the properties we set in its parent&nbsp;<code>&lt;p&gt;</code>&nbsp;and grandparent&nbsp;<code>&lt;article</code>. So the&nbsp;<code>color</code>&nbsp;and&nbsp;<code>background</code>&nbsp;values are inherited directly from its parent, and since they haven&#8217;t been overridden, they stay. While we override the&nbsp;<code>text-decoration</code>&nbsp;value to&nbsp;<code>line-through</code>.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_ByLvRMd" src="//codepen.io/anon/embed/ByLvRMd?height=300&amp;theme-id=1&amp;slug-hash=ByLvRMd&amp;default-tab=result" height="300" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ByLvRMd" title="CodePen Embed ByLvRMd" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">The key takeaway from this example is that properties for highlight elements are also individually inherited and overridden.</p>



<h2 id="targeting-a-text" class="wp-block-heading">Targeting a text</h2>



<p class="wp-block-paragraph">In the demo below, we set&nbsp;<code>text-decoration</code>&nbsp;to&nbsp;<code>underline</code>&nbsp;to give any searched text a blue underline. This way, we can customize matching text while also leaving the default background color, which prevents people from getting confused about what&#8217;s going on.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">::search-text {
  text-decoration: underline;
  text-decoration-color: oklch(65% 0.18 240);
  text-decoration-thickness: 0.22em;
  text-underline-offset: 0.15em;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_gbwZWra" src="//codepen.io/anon/embed/gbwZWra?height=500&amp;theme-id=1&amp;slug-hash=gbwZWra&amp;default-tab=result" height="500" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed gbwZWra" title="CodePen Embed gbwZWra" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="using-current" class="wp-block-heading">Using <code>:current</code></h2>



<p class="wp-block-paragraph">Using&nbsp;<code>::search-text</code>&nbsp;with&nbsp;<code>:current</code>, we can style the currently focused match. For example, below we apply a light orange hue text decoration color with&nbsp;<code>0.3em</code>&nbsp;thickness to the currently matched searched text:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">::search-text:current {
  text-decoration-color: oklch(85% 0.22 38);
  text-decoration-thickness: 0.3em;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_jEMXmGa" src="//codepen.io/anon/embed/jEMXmGa?height=500&amp;theme-id=1&amp;slug-hash=jEMXmGa&amp;default-tab=result" height="500" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed jEMXmGa" title="CodePen Embed jEMXmGa" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<h2 id="some-accessibility-notes" class="wp-block-heading">Some accessibility notes</h2>



<p class="wp-block-paragraph">For WCAG&#8217;s contrast standards, <a href="https://www.w3.org/WAI/WCAG22/quickref/" rel="noopener">you need a contrast ratio of at least 4.5:1</a> between the text and background. Another piece of advice is not to change the search colors too much. In fact, this feature should be used sparingly since it may cause issues for users with cognitive issues, and as a core part of the browser, it can be generally confusing. My personal advice is to stick to&nbsp;only <code>text-decoration</code>&nbsp;and its associated properties since they are more subtle than the rest.</p>



<h2 id="about-the-past-and-future" class="wp-block-heading">There&#8217;s also <code>:past</code> and <code>:future</code></h2>



<p class="wp-block-paragraph">The&nbsp;<a href="https://drafts.csswg.org/selectors-5/#past-pseudo" rel="noopener"><code>:past</code></a>&nbsp;and&nbsp;<a href="https://drafts.csswg.org/selectors-5/#future-pseudo" rel="noopener"><code>:future</code></a>&nbsp;pseudo-classes are supposed to match the element entirely prior and entirely after a <code>:current</code>&nbsp;element, respectively.</p>



<p class="wp-block-paragraph">However, the specification says:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The&nbsp;<code>:past</code>&nbsp;and&nbsp;<code>:future</code>&nbsp;pseudo-classes are reserved for analogous use in the future. Any unsupported combination of these pseudo-classes with&nbsp;<code>::search-text</code>&nbsp;must be treated as invalid</p>
</blockquote>



<p class="wp-block-paragraph">Meaning, you can&#8217;t use&nbsp;<code>:past</code>,&nbsp;<code>:future</code>&nbsp;or any other pseudo-class with the&nbsp;<code>::search-text</code>&nbsp;pseudo-element. If your browser somehow works with them, kindly report the unexpected behavior by opening an issue with them.</p>



<h2 id="specification" class="wp-block-heading">Specification</h2>



<p class="wp-block-paragraph">The CSS&nbsp;<code>::search-text</code>&nbsp;pseudo-element is defined in the&nbsp;<a href="https://drafts.csswg.org/css-pseudo-4/#selectordef-search-text" rel="noopener">CSS Pseudo-Elements Module Level 4 specification</a>. This is still being tested and improved upon.</p>



<h3 id="browser-support" class="wp-block-heading">Browser support</h3>



<p class="wp-block-paragraph">Very wide support:</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="highlight"></baseline-status>



<h3 id="related-tricks" class="wp-block-heading">Related tricks!</h3>



    		
    <div class="in-article-cards">
      <article class="in-article-card articles" id="mini-post-391705">

      <div class="tags">
      <a href="https://css-tricks.com/tag/pseudo-elements/" rel="tag">pseudo elements</a>    </div>
  
  <time datetime="2026-01-28" title="Originally published Jan 28, 2026">
    <strong>
                
        Article
      </strong>

    on

    Jan 28, 2026  </time>

  <h3>
    <a href="https://css-tricks.com/how-to-style-the-new-search-text-and-other-highlight-pseudo-elements/">
      Styling ::search-text and Other Highlight-y Pseudo-Elements    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/danielschwarz/" aria-label="Author page of Daniel Schwarz">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/b6c928ce1b901c34f86f3856b5a3bcefe9a4fb94698b9778fb3df5802d66e25d.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/b6c928ce1b901c34f86f3856b5a3bcefe9a4fb94698b9778fb3df5802d66e25d.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/danielschwarz/">
      Daniel Schwarz    </a>
  </div>

</article>
<article class="in-article-card articles" id="mini-post-388219">

      <div class="tags">
      <a href="https://css-tricks.com/tag/accessibility/" rel="tag">accessibility</a> <a href="https://css-tricks.com/tag/attributes/" rel="tag">attributes</a> <a href="https://css-tricks.com/tag/html-2/" rel="tag">HTML</a>    </div>
  
  <time datetime="2025-08-15" title="Originally published Aug 15, 2025">
    <strong>
                
        Article
      </strong>

    on

    Aug 15, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/covering-hiddenuntil-found/">
      Covering hidden=until-found    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/geoffgraham/" aria-label="Author page of Geoff Graham">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/geoffgraham/">
      Geoff Graham    </a>
  </div>

</article>
    </div>
  



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/pseudo-selectors/s/search-text/">::search-text</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393544</post-id>	</item>
		<item>
		<title>Astro Markdown Component Utility for Any Framework</title>
		<link>https://css-tricks.com/astro-markdown-component-utility-any-framework/</link>
					<comments>https://css-tricks.com/astro-markdown-component-utility-any-framework/#comments</comments>
		
		<dc:creator><![CDATA[Zell Liew]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 13:25:00 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[astro]]></category>
		<category><![CDATA[markdown]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392775</guid>

					<description><![CDATA[<p class="wp-block-paragraph">In the previous article, I spoke about the <a href="https://css-tricks.com/astro-markdown-component/">why and how to use a Markdown component in Astro</a>.</p>
<p class="wp-block-paragraph">Here, we’re going to expand on that and help you use Markdown everywhere — regardless of the framework you use. So, &#8230;</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/astro-markdown-component-utility-any-framework/">Astro Markdown Component Utility for Any Framework</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In the previous article, I spoke about the <a href="https://css-tricks.com/astro-markdown-component/">why and how to use a Markdown component in Astro</a>.</p>



<p class="wp-block-paragraph">Here, we’re going to expand on that and help you use Markdown everywhere — regardless of the framework you use. So, this works for React, Vue, and Svelte.</p>



<p class="wp-block-paragraph">The entire process hinges on the <a href="https://splendidlabz.com/docs/utils/markdown/" rel="noopener">Markdown utility</a> I’ve built for <a href="https://splendidlabz.com/" rel="noopener">Splendid Labz</a>.</p>



<span id="more-392775"></span>



<h2 id="why-this-utility-" class="wp-block-heading">Why This Utility?</h2>



<p class="wp-block-paragraph">I hit a snag when using most Markdown libraries. I naturally write Markdown content like this:</p>



<pre rel="Markdown" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;!-- prettier-ignore -->
    &lt;Markdown>
      This is a paragraph

      This is a second paragraph
    &lt;/Markdown>
  &lt;/div>
&lt;/div></code></pre>



<p class="wp-block-paragraph">But since most markdown libraries don&#8217;t account for whitespace indentation, they create an output with <code>&lt;pre&gt;</code> and <code>&lt;code&gt;</code> tags.</p>



<p class="wp-block-paragraph">This is because Markdown treats the indentation beyond four spaces as a code block:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;pre>&lt;code>  This is a paragraph

      This is a second paragraph
    &lt;/code>&lt;/pre>
  &lt;/div>
&lt;/div></code></pre>



<p class="wp-block-paragraph">So you&#8217;re forced to strip all indentation and write it like this instead:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;!-- prettier-ignore -->
    &lt;Markdown>
  This is a paragraph

  This is a second paragraph
    &lt;/Markdown>
  &lt;/div>
&lt;/div></code></pre>



<p class="wp-block-paragraph">That&#8217;s hard to read and annoying to maintain.</p>



<p class="wp-block-paragraph">My Markdown utility handles this whitespace issue and generates the correct HTML regardless of how your code is indented:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;p>This is a paragraph&lt;/p>
    &lt;p>This is a second paragraph&lt;/p>
  &lt;/div>
&lt;/div></code></pre>



<h2 id="using-this-in-your-framework" class="wp-block-heading">Using This in Your Framework</h2>



<p class="wp-block-paragraph">It&#8217;s easy. You have to pass the Markdown text into the utility. If <code>inline</code> is <code>true</code>, then <code>markdown</code> will return an output without paragraph tags.</p>



<p class="wp-block-paragraph">Here’s an example with Astro.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">---
import { markdown } from '@splendidlabz/utils'
const { inline = false, content } = Astro.props
const slotContent = await Astro.slots.render('default')

// Process content
const html = markdown(content || slotContent, { inline })
---

&lt;Fragment set:html={html} /></code></pre>



<p class="wp-block-paragraph">You can then use it like this:</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;Markdown>
   &lt;!-- Your content here -->
&lt;/Markdown></code></pre>



<p class="wp-block-paragraph">Here’s another example for Svelte.</p>



<p class="wp-block-paragraph">Svelte cannot read dynamic content from slots, so we can only pass it through a prop.</p>



<pre rel="Svelte" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;script>
  import { markdown } from '@splendidlabz/utils'
  const { content, inline = false } = $props()
  const html = markdown(content, { inline })
&lt;/script>

&lt;!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html html}</code></pre>



<p class="wp-block-paragraph">And you can use it like this:</p>



<pre rel="Svelte" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;Markdown content=`
  ### This is a header

  This is a paragraph
`/></code></pre>



<p class="wp-block-paragraph">It’s rather simple to build the same for React and Vue so I’d leave that up to you.</p>



<h2 id="taking-it-further" class="wp-block-heading">Taking it Further</h2>



<p class="wp-block-paragraph">I’ve been building for the web — long enough to experience the frustration of doing the same things over and over again.</p>



<p class="wp-block-paragraph">So I consolidated everything I use into a few simple libraries — like <a href="https://splendidlabz.com/docs/utils" rel="noopener">Splendid Utils</a>, and a few others for layouts, Astro and Svelte components.</p>



<p class="wp-block-paragraph">I write about all of them on <a href="https://zellwk.com/newsletter/css-tricks/" rel="noopener">my blog</a>. Come by if you&#8217;re interested in better DX as you build your sites and apps!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/astro-markdown-component-utility-any-framework/">Astro Markdown Component Utility for Any Framework</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/astro-markdown-component-utility-any-framework/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392775</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=10390 metric#misses=8 metric#hit-ratio=99.9 metric#bytes=7198225 metric#prefetches=514 metric#store-reads=28 metric#store-writes=2 metric#store-hits=522 metric#store-misses=4 metric#sql-queries=30 metric#ms-total=490.53 metric#ms-cache=18.94 metric#ms-cache-avg=0.6530 metric#ms-cache-ratio=3.9 -->
