<?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>Fri, 12 Jun 2026 15:09:48 +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>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>1</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/#respond</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>0</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>
		<item>
		<title>What’s !important #12: Safari Testing, ::checkmark, HTML Anchor Positioning, and More</title>
		<link>https://css-tricks.com/whats-important-12/</link>
					<comments>https://css-tricks.com/whats-important-12/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 29 May 2026 13:25:38 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395357</guid>

					<description><![CDATA[<p>The old (testing in Safari when you don’t have Safari), the new (::checkmark), the in-between (anchor positioning but with HTML), and more.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-12/">What’s !important #12: Safari Testing, ::checkmark, HTML Anchor Positioning, 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"><strong>What’s !important #12</strong> talks about the old (testing in Safari when you don’t have Safari), the new (<code>::checkmark</code>), the in-between (anchor positioning but with HTML), and more.</p>



<p class="wp-block-paragraph">Buckle up!</p>



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



<h2 id="testing-in-safari-when-you-dont-have-safari" class="wp-block-heading">Testing in Safari when you don’t have Safari</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="452" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378-1024x452.webp?resize=1024%2C452&#038;ssl=1" alt="A Safari browser window on macOS showing an About Safari dialogue box with a translucent red and orange noise filter." class="wp-image-395366" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378.webp?resize=1024%2C452&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378.webp?resize=300%2C132&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378.webp?resize=768%2C339&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378.webp?resize=1536%2C678&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/safari.jpg-e1780000053378.webp?w=2000&amp;ssl=1 2000w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://frontendmasters.com/blog/testing-safari-on-a-budget/" rel="noopener">Frontend Masters</a></figcaption></figure>



<p class="wp-block-paragraph">Safari is the second most popular web browser, but is only available to Apple users. Fair enough. I mean, Apple are heavily invested in making Safari a proprietary browser that’s deeply integrated with Apple’s software and hardware. However, this makes testing websites in Safari a bit of a pain. <a href="https://css-tricks.com/author/declanchidlow/">Declan Chidlow</a> explained what our options are in regards to <a href="https://frontendmasters.com/blog/testing-safari-on-a-budget/" rel="noopener">testing in Safari when you don’t have Safari</a>.</p>



<h2 id="a-first-look-at-checkmark" class="wp-block-heading">A first look at <code>::checkmark</code></h2>



<p class="wp-block-paragraph"><a href="https://css-tricks.com/author/sunkanmifafowora/">Sunkanmi Fafowora</a> gave us our <a href="https://piccalil.li/blog/navigating-the-age-old-problem-of-checkmarks-in-ui-with-progressive-enhancement/" rel="noopener">first look at the <code>::checkmark</code> pseudo-element</a>, which solves the age-old problem of not (really) being able to style checkmarks. Note that this also targets the checked state indicator of radios and selects, not just checkboxes!</p>



<h2 id="different-shape-styles-with-bordershape-shape" class="wp-block-heading">Different shape styles with <code>border-shape</code> + <code>shape()</code></h2>



<p class="wp-block-paragraph"><a href="https://css-tricks.com/author/afiftemani/">Temani Afif</a> pointed out that we can <a href="https://css-tip.com/shape-variation/" rel="noopener">create more shape styles when combining <code>border-shape</code> with the <code>shape()</code> function</a> (compared to <code>clip-path</code>), and, easily switch between them.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="898" height="282" src="https://css-tricks.com/wp-content/uploads/2026/05/border-shape-styles.avif" alt="Three variations of a wavy shape rendered in red, showing an outline version, a solid filled version, and a cutout version inside a solid red square." class="wp-image-395380" srcset="https://css-tricks.com/wp-content/uploads/2026/05/border-shape-styles.avif 898w, https://css-tricks.com/wp-content/uploads/2026/05/border-shape-styles.avif 300w, https://css-tricks.com/wp-content/uploads/2026/05/border-shape-styles.avif 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://css-tip.com/shape-variation/" rel="noopener">CSS Tip</a></figcaption></figure>



<h2 id="a-concise-guide-to-siblingindex-and-siblingcount" class="wp-block-heading">A concise guide to <code>sibling-index()</code> and <code>sibling-count()</code></h2>



<p class="wp-block-paragraph"><a href="https://css-tricks.com/author/durgeshpawar/">Durgesh Pawar</a> did a <a href="https://www.smashingmagazine.com/2026/05/mathematical-layouts-sibling-index-sibling-count/" rel="noopener">deep dive on <code>sibling-index()</code> and <code>sibling-count()</code></a>, showing us all of the cool things that we can do with these almost-Baseline CSS functions.</p>



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



<p class="wp-block-paragraph">Also, don&#8217;t miss Durgesh&#8217;s <a href="https://css-tricks.com/cross-document-view-transitions-part-1/">two-part series about View Transition gotchas</a> right here on CSS-Tricks.</p>



<h2 id="managing-anchor-associations-with-data-attributes-and-advanced-attr" class="wp-block-heading">Managing anchor associations with data attributes and advanced <code>attr()</code></h2>



<p class="wp-block-paragraph">This one’s actually from me! Disappointed to hear that the <code>anchor</code> attribute has been dropped, which would’ve provided a way of managing anchor associations using HTML, I demonstrated my alternative technique that involves <a href="https://frontendmasters.com/blog/managing-anchor-associations-with-data-attributes-and-advanced-attr/" rel="noopener">managing anchor associations with data attributes and advanced <code>attr()</code></a>.</p>



<p class="wp-block-paragraph">I won’t spoiler the CSS, but here are the different HTML syntaxes that I explored:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- anchor attribute -->
&lt;div anchor="anchorA">Boat A&lt;/div>
&lt;div id="anchorA">Anchor A&lt;/div>

&lt;!-- Data attributes with custom ident (requires attr()) -->
&lt;div data-boat="--anchorA">Boat A&lt;/div>
&lt;div data-anchor="--anchorA">Anchor A&lt;/div>

&lt;!-- Data attributes (requires attr() and ident()) -->
&lt;div data-boat="anchorA">Boat A&lt;/div>
&lt;div data-anchor="anchorA">Anchor A&lt;/div></code></pre>



<h2 id="take-the-state-of-css-2026-survey" class="wp-block-heading">Take the State of CSS 2026 survey</h2>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1488" height="790" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/Screenshot-2026-05-28-at-2.31.26-PM.png?resize=1488%2C790&#038;ssl=1" alt="The official graphic for the State of CSS 2026 survey, featuring a stylized CSS logo inside a pink and purple diamond emblem against a dark background." class="wp-image-395372" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/Screenshot-2026-05-28-at-2.31.26-PM.png?w=1488&amp;ssl=1 1488w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/Screenshot-2026-05-28-at-2.31.26-PM.png?resize=300%2C159&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/Screenshot-2026-05-28-at-2.31.26-PM.png?resize=1024%2C544&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/Screenshot-2026-05-28-at-2.31.26-PM.png?resize=768%2C408&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">It’s that time of the year again!</p>



<p class="wp-block-paragraph">I love these “state of” surveys (especially the <a href="https://survey.devographics.com/en-US/survey/state-of-css/2026" rel="noopener">State of CSS 2026</a> survey, but I’m sure you know that already). This year feels different though, and I’m not the only one that’s noticed.</p>



<p class="wp-block-paragraph">From the opening crawl:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Take a deep breath. Calm down. It&#8217;s ok if you don&#8217;t know every single new CSS property. The truth is, very few of us do.</p>



<p class="wp-block-paragraph">Look, one of this survey&#8217;s goals has always been to help keep developers up to date on the latest and greatest CSS improvements. But the downside is that all this progress can sometimes feel overwhelming.</p>



<p class="wp-block-paragraph">That’s why this year we made a conscious effort to reduce the number of features covered in the survey, focusing instead on the ones that matter most.</p>
</blockquote>
</blockquote>



<p class="wp-block-paragraph">I totally get it. It’s becoming more and more difficult to keep up with CSS. My “things to check out” list just keeps getting longer! That being said, there’s never been a more exciting time to be a fan of CSS. That feeling when you learn a new feature and then two more get shipped, is overwhelming but in the best way possible.</p>



<p class="wp-block-paragraph">But still, time doesn’t grow on trees, so we have to figure out which features to invest in, and that’s what these “state of” surveys are all about. And they’re going hard this year, really zeroing in on the most important ones.</p>



<p class="wp-block-paragraph">But, if you have an appetite for <em>all</em> things CSS, I hear there’s a <a href="https://css-tricks.com/">great blog</a> for that!</p>



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



<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/151" rel="noopener">Firefox 151</a>
<ul class="wp-block-list">
<li><a href="https://css-tricks.com/css-container-queries/#container-style-queries">Container style queries</a> (now Baseline)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API" rel="noopener"><em>Document</em> Picture-in-Picture API</a> (desktop only, no Safari support)</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Quality over quantity, I guess!</p>



<p class="wp-block-paragraph">Until next time.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-12/">What’s !important #12: Safari Testing, ::checkmark, HTML Anchor Positioning, 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-12/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395357</post-id>	</item>
		<item>
		<title>Revealing Text With CSS letter-spacing</title>
		<link>https://css-tricks.com/revealing-text-with-css-letter-spacing/</link>
					<comments>https://css-tricks.com/revealing-text-with-css-letter-spacing/#comments</comments>
		
		<dc:creator><![CDATA[Preethi]]></dc:creator>
		<pubDate>Wed, 27 May 2026 12:37:33 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[animation]]></category>
		<category><![CDATA[letter-spacing]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393536</guid>

					<description><![CDATA[<p>Until we get something like <code>::nth-letter</code>, there are still some really cool text effects we can make from existing CSS features, like <code>letter-spacing</code>, <code>::first-word</code> and <code>::first-line</code>.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/revealing-text-with-css-letter-spacing/">Revealing Text With CSS letter-spacing</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">Some text effects are relatively hard to pull in CSS, the main reason being <a href="https://css-tricks.com/spiral-scrollytelling-in-css-with-sibling-index/">we</a> <a href="https://css-tricks.com/spiral-scrollytelling-in-css-with-sibling-index/">are unable to target individual characters</a> (something many of us want <a href="https://css-tricks.com/using-nonexistent-nth-letter-selector-now/">in the form of <code>::nth-letter()</code></a>, although we have basis for it with <a href="https://css-tricks.com/almanac/pseudo-selectors/f/first-letter/"><code>::first-letter</code></a> that gives us access to a box element’s first glyph.</p>



<p class="wp-block-paragraph">But maybe there are a few things we can use today with what we already have.</p>



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



<p class="wp-block-paragraph">For example, the CSS <a href="https://css-tricks.com/almanac/properties/l/letter-spacing/"><code>letter-spacing</code></a> property adjusts the space between all characters in a block of text. <strong>Positive values add space</strong> to the right side of each glyph (in a left-to-right writing mode), and <strong>negative values shrink the width of the glyph box</strong>, causing letters to overlap and even move the other way.</p>



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



<p class="wp-block-paragraph">The <code>letter-spacing</code> accepts length units, and percentage (relative to font size). It is animateable, and as we saw before, the negative values can shrink it down or reverse it. Which is something we can make use of.</p>



<h3 class="wp-block-heading" id="overlapping-and-separating-letters">Overlapping and separating letters</h3>



<p class="wp-block-paragraph">It’s quite easy to completely overlap the characters as a starting point and setting it’s <code>color</code> to <code>transparent</code> to visually hide it.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">label {
  letter-spacing: -1ch;
  color: transparent;
  /* etc. */
}</code></pre>



<p class="wp-block-paragraph">From there, we can reveal the text by animating that <code>letter-spacing</code> value to a positive value and updating the <code>color</code> to a visible value, like when a checkbox is <code>:checked</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">li:nth-of-type(2) label {
  text-align: center;
}
li:nth-of-type(3) label {
  text-align: right;
}
input:checked + label {
  letter-spacing: 0ch;
  color: black;
  transition: letter-spacing 0.6s, color 0.4s;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_YPGdgeQ" src="//codepen.io/anon/embed/YPGdgeQ?height=450&amp;theme-id=1&amp;slug-hash=YPGdgeQ&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed YPGdgeQ" title="CodePen Embed YPGdgeQ" 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> The CSS <code>ch</code> unit is a <a href="https://css-tricks.com/css-length-units/#relative-units">relative length</a> representing the width of the zero (0) glyph.</p>



<p class="wp-block-paragraph">The labels go from negative <code>letter-spacing</code> to normal spacing and the <code>color</code> updates to <code>black</code>. Both these changes happen over a <code>transition</code>.</p>



<p class="wp-block-paragraph">The second and third labels are given center and right text alignments and thus when negative letter spacing is applied they bundle up at the given alignment position, center and right, respectively. When <code>letter``-``spacing</code> goes from negative to zero (or any positive value) the letters separate from that same alignment position.</p>



<p class="wp-block-paragraph">Thus, we get a text reveal effect! Let’s look at some more.</p>



<h3 class="wp-block-heading" id="showing-and-hiding-text">Showing and hiding text</h3>



<p class="wp-block-paragraph">Check this out. We can toggle a checkbox label as a fun interactive UI touch:</p>



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



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Simplified for brevity; additional accessibility considerations -->
&lt;input type="checkbox" id="cb">
&lt;label for="cb">
  &lt;span>Join the global club&lt;/span>
  &lt;span>You've begun your journey!&lt;/span>
&lt;/label></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">label {
  overflow: clip;
  /* etc. */
}

span {
  /* The first label */
  &amp;:nth-of-type(1) {
    /* Default spacing: letters are fully visible */
    letter-spacing: 0ch;
    /* When the checkbox is checked, target this text */
    :checked + * &amp; {
      /* collapse letters on top of each other, hiding them */
      letter-spacing: -2ch;
      text-indent: -1.5ch;
      /* Use a "bouncy" cubic-bezier for spacing */
      transition: 0.4s letter-spacing cubic-bezier(.8, -.5, .2, 1.4), 
                  0.1s text-indent;
    }
  }
  
  /* The second label */
  &amp;:nth-of-type(2) { 
    /* Initially collapsed (letters overlap) */
    letter-spacing: -1ch;
    color: transparent;
    /* When the checkbox is checked, target this text */
    :checked + * &amp; {
      /* Returns to normal spacing */
      letter-spacing: 0ch;
      color: black;
      /* Slightly delay the appearance so it starts after the first text begins to hide */
      transition:
        0.4s letter-spacing cubic-bezier(.8, -.5, .2, 1.4) 0.3s, 
        0.8s color 0.4s;
    }
  }
}</code></pre>



<p class="wp-block-paragraph">When the box is checked, a negative <code>letter-spacing</code> value (<code>-2ch</code>) and <code>text-indent</code> value (<code>-1.5ch</code>) is used on the first <code>&lt;span&gt;</code> to slide it out of the container box. We use <code>overflow: clip</code> to completely hide the text.</p>



<p class="wp-block-paragraph">Concurrently, the text in the second <code>&lt;span&gt;</code> text goes from a <code>letter-spacing</code> value of <code>-1ch</code> to <code>0ch</code>, which reveals it. To hide this overlapped text at <code>-1ch</code>, a <code>transparent</code> color was given that’s turned to <code>black</code> when the checkbox is checked.</p>



<h3 class="wp-block-heading" id="using-with-other-glyph-box-styling">Using with other glyph box styling</h3>



<p class="wp-block-paragraph">Here’s another fun one. We can start with an acronym that reveal the full text on hover. Again, we have existing features to help us pull this off, including <code>::first-letter</code> and <a href="https://css-tricks.com/almanac/pseudo-selectors/f/first-line/"><code>::first-line</code></a>.</p>



<p class="wp-block-paragraph">We’ll start with this markup:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Simplified for brevity -->
&lt;p id="acronym">
  &lt;span class="words">United&lt;/span>
  &lt;span class="words">Nations&lt;/span>
  &lt;span class="words">International&lt;/span>
  &lt;span class="words">Children's&lt;/span>
  &lt;span class="words">Emergency&lt;/span>
  &lt;span class="words">Fund&lt;/span>
&lt;/p></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.words {
  letter-spacing: -1ch;
  color: transparent;
  /* etc. */

  &amp;::first-letter {
    color: black;
  }

  figure:hover + #acronym &amp; {
    letter-spacing: 0ch;
    color: black;
    transition: letter-spacing 0.4s cubic-bezier(.8, -.5, .2, 1.4) /* etc. */;
  }
}</code></pre>



<p class="wp-block-paragraph">Each word in the UNICEF acronym initially has <code>letter-spacing: -1ch</code> to shrink the text, and <code>color: transparent</code> to keep the shrunk text hidden, except the <code>::first-letter</code> that has <code>color: black</code> so it remains visible even though the rest of the text is stacked beneath it.</p>



<p class="wp-block-paragraph">Now, we can target the image on <code>:hover</code> and select the entire text so that the <code>letter-spacing</code> value for each word decreases to <code>0ch</code> and <code>color: black</code> is applied, showing what’s remaining of the words:</p>



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



<h3 class="wp-block-heading" id="what-else-can-we-do-">What else can we do?</h3>



<p class="wp-block-paragraph">I don&#8217;t know! But that’s where you come in. Obviously, a hypothetical <code>::nth-letter</code> selector would be amazing for all kinds of text effects. But it’s neat that we can create some semblance of it today with existing features, like <code>letter-spacing</code>, <code>::first-letter</code>, and <code>::first-line</code>.</p>



<p class="wp-block-paragraph">What can you cook up knowing we have these constraints?</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/revealing-text-with-css-letter-spacing/">Revealing Text With CSS letter-spacing</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/revealing-text-with-css-letter-spacing/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393536</post-id>	</item>
		<item>
		<title>Technical Writing in the AI Age</title>
		<link>https://css-tricks.com/technical-writing-in-the-ai-age/</link>
					<comments>https://css-tricks.com/technical-writing-in-the-ai-age/#comments</comments>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Tue, 26 May 2026 13:49:28 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[writing]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=395048</guid>

					<description><![CDATA[<p>This isn’t totally about AI. It’s about technical writing in the age of AI. I have some thoughts on this and I hope it’s helpful to you humans reading.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/technical-writing-in-the-ai-age/">Technical Writing in the AI Age</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">This isn&#8217;t coming out of nowhere:</p>



<figure class="wp-block-embed is-type-rich is-provider-bluesky-social wp-block-embed-bluesky-social"><div class="wp-block-embed__wrapper">
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:pzgvqg4ihnaihkrpmxqz5pu6/app.bsky.feed.post/3mm7z5pw45c2g" data-bluesky-cid="bafyreidlojn3o4lv6porlhiljilwuojofvod4ovxvgqqqtgplfcn2pmmu4"><p lang="en">My apathy levels for the industry are absolutely sky high and I don’t even have a dickhead boss making me hit token targets</p>&mdash; <a href="https://bsky.app/profile/did:plc:pzgvqg4ihnaihkrpmxqz5pu6?ref_src=embed" rel="noopener">Andy Bell (@bell.bz)</a> <a href="https://bsky.app/profile/did:plc:pzgvqg4ihnaihkrpmxqz5pu6/post/3mm7z5pw45c2g?ref_src=embed" rel="noopener">2026-05-19T18:23:42.034Z</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
</div></figure>



<figure class="wp-block-embed is-type-rich is-provider-bluesky-social wp-block-embed-bluesky-social"><div class="wp-block-embed__wrapper">
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:xbj7qbl7qmoo7dpf5dheplex/app.bsky.feed.post/3mmejahzgas2w" data-bluesky-cid="bafyreiadck4lx6mue35y3fr6pgoh67ml3vlcw23cbcddlyke6xfx7swcee"><p lang="en">More and more people I deeply respect and have learned a lot from over the years feel like they are speaking into a void, to the point where they are losing motivation to continue making content, which got me to write my first post on my site in over a year&#8230;www.kevinpowell.co/article/tell&#8230;</p>&mdash; <a href="https://bsky.app/profile/did:plc:xbj7qbl7qmoo7dpf5dheplex?ref_src=embed" rel="noopener">Kevin Powell (@kevinpowell.co)</a> <a href="https://bsky.app/profile/did:plc:xbj7qbl7qmoo7dpf5dheplex/post/3mmejahzgas2w?ref_src=embed" rel="noopener">2026-05-21T13:22:13.240Z</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
</div></figure>



<figure class="wp-block-embed is-type-rich is-provider-bluesky-social wp-block-embed-bluesky-social"><div class="wp-block-embed__wrapper">
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:bhdap3w2bseikypfnjmaskzf/app.bsky.feed.post/3mmah3en5ok2y" data-bluesky-cid="bafyreiee5xsl5ehuljh2raziqs2xbiky3lako46yymv3z5rsk32me4a3ta"><p lang="en">It&#39;s not fun to make tech content as much anymore, too. I&#39;m doing some because I actually like it, but it feels like I&#39;m shouting into a slop-driven void nearly everywhere. It&#39;s really tiring.I get so much joy reading individual blogs and newsletters still (like yours!), at least.</p>&mdash; <a href="https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf?ref_src=embed" rel="noopener">Cassidy (@cassidoo.co)</a> <a href="https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf/post/3mmah3en5ok2y?ref_src=embed" rel="noopener">2026-05-19T22:32:55.483Z</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
</div></figure>



<figure class="wp-block-embed is-type-rich is-provider-bluesky-social wp-block-embed-bluesky-social"><div class="wp-block-embed__wrapper">
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:qcxqtc2yzznbaazu7egncqqx/app.bsky.feed.post/3mmbdphbxtc23" data-bluesky-cid="bafyreihbpwum4bydk5k2zzuglk6f3c6obvvjac2zbu5z26ce3rekuek53i"><p lang="en">I’m just 7 videos away from finishing a 45-video course about building for the web, and the fact that people are rarely choosing to learn from human-crafted content anymore therefore rendering hundreds of hours of my human effort completely pointless is burning me out catastrophically. I am a husk.</p>&mdash; <a href="https://bsky.app/profile/did:plc:qcxqtc2yzznbaazu7egncqqx?ref_src=embed" rel="noopener">⭑ salma (@whitep4nth3r.com)</a> <a href="https://bsky.app/profile/did:plc:qcxqtc2yzznbaazu7egncqqx/post/3mmbdphbxtc23?ref_src=embed" rel="noopener">2026-05-20T07:05:14.119Z</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
</div></figure>



<p class="wp-block-paragraph">These are all folks I deeply admire and I&#8217;d be lying if I said I disagreed with any of that. The fact is, demand for front-end technical writing has significantly dropped, and <a href="https://css-tricks.com/thank-you-2024-edition/#overall-traffic">has been for some time</a>. Just look at our stats for yourself:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="912" height="472" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/traffic-by-year-2020-2025.png?resize=912%2C472&#038;ssl=1" alt="" class="wp-image-395049" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/traffic-by-year-2020-2025.png?w=912&amp;ssl=1 912w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/traffic-by-year-2020-2025.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/traffic-by-year-2020-2025.png?resize=768%2C397&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">I&#8217;m not embarrassed to share this because it&#8217;s happening across the board. I mean, it&#8217;s the same thing over at Stack Overflow as <a href="https://css-tricks.com/stack-overflow-when-we-stop-asking/">we&#8217;ve already discussed in depth</a>. I work with other content creators and publications and it&#8217;s also the same story.</p>



<p class="wp-block-paragraph">Naturally, I tend to take personal responsibility for this sort of thing, but it&#8217;s hard not to see that the root cause might be out of my hands:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2530" height="1406" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=2530%2C1406&#038;ssl=1" alt="" class="wp-image-395051" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?w=2530&amp;ssl=1 2530w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=300%2C167&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=1024%2C569&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=768%2C427&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=1536%2C854&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/ai-business-adoption.png?resize=2048%2C1138&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption"><strong>Source:</strong> <a href="https://hai.stanford.edu/ai-index/2025-ai-index-report" rel="noopener">2025 AI Index</a>, Stanford HAI</figcaption></figure>



<p class="wp-block-paragraph">And yes, I&#8217;m pointing squarely at AI. It&#8217;s a little unfair to say there aren&#8217;t other factors out there impacting our work and this site specifically, but let&#8217;s call a spade a spade and identify the biggest headwind.</p>



<p class="wp-block-paragraph">This isn&#8217;t totally about AI. <strong>It&#8217;s about technical writing in the age of AI.</strong> I have some thoughts on this and I hope it&#8217;s helpful to you humans reading.</p>



<h2 id="we-still-need-technical-writing" class="wp-block-heading">We still need technical writing</h2>



<p class="wp-block-paragraph">The bots certainly aren&#8217;t learning new things on their own, right? They&#8217;re only as motivated as the prompt they&#8217;re given, and it&#8217;s human desire and effort that moves things forward.</p>



<p class="wp-block-paragraph">I want to acknowledge right away that I am not suggesting that AI is the new primary audience for technical writing. It&#8217;s real people like you with the motivation to learn that keeps this practice alive, even if your learnings become the prompts that allow AI to generate code for you.</p>



<h2 id="finding-your-place" class="wp-block-heading">Finding your place</h2>



<p class="wp-block-paragraph">Who (or what) are you writing for? It may be that your place in creating technical content needs a shift. For example, I&#8217;d wager that creating technical documentation is less valuable than it once was. Not only do we have a wealth of that in the specs and MDN, but those sorts of developer questions are now being answered directly in a chat that&#8217;s embedded right in the IDE.</p>



<p class="wp-block-paragraph"><em>&#8220;But CSS-Tricks is full of all that!&#8221;</em> you might say. And it&#8217;s true. We have a very extensive <a href="https://css-tricks.com/almanac/">Almanac of CSS goodness</a> that&#8217;s grown, evolved, and been tended since, I believe, 2009. <a href="https://css-tricks.com/re-working-the-css-almanac/">We even expanded it</a> just a couple years ago. That&#8217;s because I think CSS-Tricks has a clear place in the AI age&#8230;</p>



<p class="wp-block-paragraph">&#8230;and that&#8217;s making technical documentation more accessible to humans. Have you read <a href="https://www.w3.org/Style/CSS/specs.en.html" rel="noopener">the specs</a>? They&#8217;re dense, and by design for accuracy and clarity for browsers implementing CSS features. They are very <em>technical</em> explanations for very <em>technical</em> topics. I like to think that CSS-Tricks is that place you come to for very <em>human explanations</em> for very <em>technical</em> topics — more like sitting across the table from another developer over coffee (or kombucha, whatever). We serve the same sort of purpose but make a unique experience out of it. It&#8217;s all about <a href="https://rachelandrew.co.uk/archives/2019/01/30/html-css-and-our-vanishing-industry-entry-points/" rel="noopener">lowering the barriers to access</a> around here!</p>



<p class="wp-block-paragraph">But we&#8217;re also all about being a platform for great ideas. This site used to be a personal learning space for Chris Coyier when he started it way back in 2007. But that completely changed when Chris opened things up to guest authors. <a href="https://css-tricks.com/roll-simple-wordpress-podcast-plugin/">That&#8217;s a how I started here in 2015.</a> And we&#8217;ve brought in <a href="https://css-tricks.com/authors/">748 authors</a> (as I write this) since then. Just think of all the great ideas, tips — and yes — <em>tricks</em> that have come out that we may have missed without a CSS-Tricks soapbox to share them. I&#8217;ve quite literally learned everything I know about front-end from these folks.</p>



<h2 id="is-it-still-worth-it" class="wp-block-heading">Is it still worth it?</h2>



<p class="wp-block-paragraph">Only you can answer that. Is it about money? Is it about popularity? Is it about craft? Is it about personal learning? Some combination of things? I&#8217;m not here to say any of those is better than the others, but there&#8217;s definitely a reason you would decide to continue writing technical pieces (or any sort of writing, really) in this age.</p>



<p class="wp-block-paragraph">I think I&#8217;ve made a case for why I do it. But you can still think it&#8217;s not worth doing it at all, even if you believe you have purpose in it. Maybe it&#8217;s unsustainable to run a blog or newsletter financially at this scale. Maybe other things are competing for your time. Maybe a really great new job is more attractive.</p>



<p class="wp-block-paragraph">Or maybe you&#8217;re simply burned out. You saw the comments at the top of this. The time, effort, and expectations of a job like this are sky high and the payoff — whatever you measure that in — might not be enough. If that&#8217;s the case, then the outcome is apathy and burnout. I certainly feel that and admit some days don&#8217;t feel as good as others.</p>



<p class="wp-block-paragraph">But I still think I/CSS-Tricks have a place to fill. And there&#8217;s still a good deal of personal fulfillment in that every time an article comment, email, or social mention tells us our work is helping in some way. Really. And I&#8217;d suggest letting your favorite technical writers know you enjoy and rely on their work wherever you happen to be reading them.</p>



<h2 id="its-a-pendulum-swing-of-sorts" class="wp-block-heading">It&#8217;s a pendulum swing of sorts</h2>



<p class="wp-block-paragraph">No, no, no. I don&#8217;t see a future where AI suddenly disintegrates and we&#8217;re writing code entirely by hand again.</p>



<p class="wp-block-paragraph">What I mean is that technical writing — and creative web design as a whole — has kinda been here before. Sharing what we learn as we create new and interesting things was once totally against the grain, right? I love how <a href="https://css-tricks.com/chapter-6-web-design/">Jay Hoffman documented that</a> in our <a href="https://css-tricks.com/category/history/">History archives</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Articles that appeared on&nbsp;<em>Word</em>&nbsp;were one-of a kind, where the images, backgrounds and colors chosen helped facilitate the tone of a piece. These art-directed posts pulled from Levy’s signature style, a blend of 8-bit graphics and off-kilter layouts, with the chaotic bricolage of punk rock zines. Pages came alive, representing through design the personality of the post’s author.</p>
</blockquote>



<p class="wp-block-paragraph">Web design was so punk rock in the 90s and early 2000s. <a href="https://css-tricks.com/ever-evolving-spectrum-web/">Then it became a discipline.</a> Now it&#8217;s becoming automated. And, as such, human web design is punk rock once again. Or at least <a href="https://robinrendle.com/notes/anxious-punk-rock-web-design/" rel="noopener">it can be</a>.</p>



<h2 id="new-advice-for-technical-writing" class="wp-block-heading">New advice for technical writing</h2>



<p class="wp-block-paragraph">We published a <a href="https://css-tricks.com/advice-for-technical-writing/">comprehensive piece with technical writing advice in 2019</a>. I don&#8217;t think I&#8217;d subtract or change any of it. Everything in there is still as relevant and true today as it was then. The goal is writing on a timely topic for a specific audience in the clearest way possible that&#8217;s inclusive of different learning styles.</p>



<p class="wp-block-paragraph">But I do think I&#8217;d add to that piece as the landscape continues to shift. My advice?</p>



<h3 id="um-maybe-dont-use-ai" class="wp-block-heading">Um, maybe don&#8217;t use AI</h3>



<p class="wp-block-paragraph">For one, we know <a href="https://openai.com/index/why-language-models-hallucinate/" rel="noopener">it&#8217;s not always accurate</a>. Two, <a href="https://sites.google.com/view/llmwritingdistortion/home" rel="noopener">it dilutes your personal voice</a>. Both are very detrimental to the discipline of writing. And what&#8217;s the point of giving people an AI explanation of something they can already get way more easily in their IDE? That&#8217;s exactly what leads to AI slop.</p>



<p class="wp-block-paragraph">But I won&#8217;t dismiss AI usage wholesale. I&#8217;ll actually caveat the whole &#8220;don&#8217;t use AI thing&#8221; to pin it specifically to writing. Because a tool like Grammarly has always been helpful, even before it was marketed as a complete AI writing suite. You don&#8217;t have to use the beefier features that practically do the writing for you to get something out of it.</p>



<p class="wp-block-paragraph">I tend to reach for AI to help with menial tasks: Quick spell checks, converting Markdown to HTML, keeping the publishing calendar on schedule, things like that. AI is pretty good with low-lift work that doesn&#8217;t have much or anything to do with writing.</p>



<p class="wp-block-paragraph">(I&#8217;d also argue that a lot of what we call &#8220;AI&#8221; today is what we called &#8220;automation&#8221; before the hype. I&#8217;m more interested in the tools that help enhance our craft, not replace it.)</p>



<h3 id="more-reallife-experiences-less-documentation" class="wp-block-heading">More real-life experiences, less documentation</h3>



<p class="wp-block-paragraph">If you&#8217;re going to write about something, I&#8217;d lean into what AI can&#8217;t solve on its own, or at least can&#8217;t solve easily. It&#8217;s already really great at giving you the basic definition of any CSS property along with a quick code example, often pulled straight from existing technical documentation. That&#8217;s well-trodden territory unless you&#8217;re packaging it a different way.</p>



<p class="wp-block-paragraph">I learn best when I&#8217;m faced with a challenge, like something a client wants that I&#8217;ve never attempted before. I&#8217;d bet the dime in my pocket that&#8217;s how you learn best, too. We make mistakes and learn from them. And for technical writers, it&#8217;s the path from noob to understanding that makes all the difference. That&#8217;s the hook. That gives your readers a mental model to use in their own work — again, even if that mental model is going straight into writing AI prompts.</p>



<p class="wp-block-paragraph">Then again, I&#8217;m the product of the <a href="https://css-tricks.com/view-source/">View Source</a> generation. The same sort of way that many musicians would learn by <a href="https://www.guitarplayer.com/players/mike-campbell-reflects-on-his-long-career-as-the-rock-guitarists-guitarist" rel="noopener">slowing down a record player</a> to learn chords, notes, or beats. Maybe I&#8217;m the old man shouting at clouds.</p>



<h3 id="you-dont-have-to-be-the-authority" class="wp-block-heading">You don&#8217;t have to be the authority</h3>



<p class="wp-block-paragraph">I know there&#8217;s at least a little expectation that everything we publish on sites like CSS-Tricks are the &#8220;right&#8221; way to do this or that and that you ought to be able to copy-paste code snippets straight-up. I get that, but also don&#8217;t believe that&#8217;s a promise this site has ever made. I really do believe that <a href="https://css-tricks.com/hearts-in-html-and-css/">CSS is poetic</a> in the sense that there&#8217;s more than one way to do something, and the best way to do that thing is what fits your mental model the most snugly.</p>



<p class="wp-block-paragraph">And I believe there is real value in <a href="https://css-tricks.com/how-to-discover-a-css-trick/">trying something purely for the sake of trying it</a>.</p>



<p class="wp-block-paragraph">This site has always been about exploring, experimenting, and sharing along the way. And sometimes that means sharing just the germ of an idea. <a href="https://css-tricks.com/increasing-wariness-dogmatism/">There&#8217;s no dogma here.</a></p>



<p class="wp-block-paragraph">The goal: <a href="https://css-tricks.com/be-experienced-not-cynical/">be experienced, not cynical.</a> You don&#8217;t have to be the be-all-end-all authority on a subject for permission to share. Just experience, and that&#8217;s not always the same as being the expert.</p>



<p class="wp-block-paragraph">All of that really to say:</p>



<h3 id="learn-fast-share-often" class="wp-block-heading">Learn fast, share often</h3>



<p class="wp-block-paragraph">What you learn is going to be more meaningful to a real person than some generated replacement of s Stack Overflow answer.</p>



<ul class="wp-block-list">
<li>Did you try something and it didn&#8217;t work out?</li>



<li>Did you try something and it <em>did</em> work out?</li>



<li>Did you make a bunch of mistakes on the way to getting it right?</li>



<li>Do you still have open questions about the thing you learned?</li>
</ul>



<p class="wp-block-paragraph">That&#8217;s all worth sharing. That&#8217;s where the real-life experiences come from and the learning paths that you simply don&#8217;t get from generated responses claiming to have the best answer.</p>



<p class="wp-block-paragraph">Said another way: Solve real problems. Get into the edge cases. Leverage community knowledge.</p>



<h3 id="cite-generously" class="wp-block-heading">Cite generously</h3>



<p class="wp-block-paragraph">At the end of the day, it&#8217;s all about humans helping humans. We don&#8217;t wake up one day with all the answers. We learn from the things other people learn and share, and it&#8217;s worth pointing your readers in their direction. It&#8217;s easy to want to come off as the original source of everything you publish, but my experience is that&#8217;s never the true case.</p>



<p class="wp-block-paragraph">We build off each other. <a href="https://thehistoryoftheweb.com/do-blogs-need-to-be-so-lonely/" rel="noopener">It&#8217;s blogging at its core.</a> Let&#8217;s build each other up <a href="https://thehistoryoftheweb.com/the-right-to-link/" rel="noopener">the good ol&#8217; fashioned way with hyperlinks</a>.</p>



<h3 id="forget-about-seo-or-even-aio" class="wp-block-heading">Forget about SEO&#8230; or even AIO</h3>



<p class="wp-block-paragraph">We <a href="https://css-tricks.com/thank-you-2021-edition/#settting-2022-goals">toe-dipped into SEO</a> several years ago, but it&#8217;s never really been our jam. We&#8217;ve ridden and enjoyed the Organic Search Train forever. That&#8217;s liberating because we never really worry about keyword stuffing, click-baity headlines, or anything really <em>optimization</em> for that matter. We focus instead on writing for humans, good structure, and maintaining a certain voice and tone. All things Google used to care about.</p>



<p class="wp-block-paragraph">I say &#8220;used to care&#8221; there because <a href="https://blog.google/products-and-platforms/products/search/search-io-2026/?pubDate=20260520#powerful-ai" rel="noopener">that core value contains less value than ever</a>, even if <a href="https://developers.google.com/search/docs/fundamentals/creating-helpful-content" rel="noopener">they do still claim it does</a>.</p>



<p class="wp-block-paragraph">There&#8217;s no doubt about it; search traffic is gutted in favor of generated responses to queries connected to the services you use. That&#8217;s a clear hit to traffic and that&#8217;s a clear hit to the traditional ways we generate revenue that are measured by traffic analytics.</p>



<p class="wp-block-paragraph">And forget about whatever it is that&#8217;s called &#8220;Artificial Intelligence Optimization&#8221; (<abbr>AIO</abbr>). It&#8217;s still an evolving space of fluid and inconsistent “best” practices, like some agents keen on <a href="https://llmstxt.org" rel="noopener">proposals for text-based files</a> to help LLMs use your site that other agents, like Google, <a href="https://developers.google.com/search/docs/fundamentals/ai-optimization-guide#mythbusting" rel="noopener">just aren&#8217;t into</a>. Maybe it&#8217;ll become a discipline with clearer strategies, guidelines, and best practices in the future, but we just don&#8217;t know, and I&#8217;m feigning ignorance at least until the smoke clears.</p>



<p class="wp-block-paragraph">I&#8217;m honestly unsure whether I even care about CSS-Tricks popping up as a reference in a generated response. The landscape is evolving, and my thoughts on it have to as well.</p>



<h2 id="youve-got-this" class="wp-block-heading">You&#8217;ve got this.</h2>



<p class="wp-block-paragraph">I&#8217;m rooting for you. I&#8217;m rooting for all of us.</p>



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/technical-writing-in-the-ai-age/">Technical Writing in the AI Age</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/technical-writing-in-the-ai-age/feed/</wfw:commentRss>
			<slash:comments>12</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">395048</post-id>	</item>
		<item>
		<title>Cross-Document View Transitions: Scaling Across Hundreds of Elements</title>
		<link>https://css-tricks.com/cross-document-view-transitions-part-2/</link>
					<comments>https://css-tricks.com/cross-document-view-transitions-part-2/#comments</comments>
		
		<dc:creator><![CDATA[Durgesh Rajubhai Pawar]]></dc:creator>
		<pubDate>Mon, 25 May 2026 13:46:54 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393441</guid>

					<description><![CDATA[<p>Every <code>view-transition-name</code> on a page must be unique. The problem is that every pseudo-element selector in your CSS targets a specific name, so your animation styles explode into an unmanageable wall of selectors.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/cross-document-view-transitions-part-2/">Cross-Document View Transitions: Scaling Across Hundreds of Elements</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 <a href="https://css-tricks.com/cross-document-view-transitions-part-1/">Part 1</a>, we covered the gotchas that bite you first: the deprecated meta tag that silently does nothing, the 4-second timeout that kills transitions without telling you, the image distortion that turns every aspect ratio change into silly putty, and the <code>pagereveal</code>/<code>pageswap</code> events that give you hooks into the transition lifecycle.</p>



<p class="wp-block-paragraph">All of that gets you from &#8220;nothing works&#8221; to &#8220;one element transitioning nicely between two pages.&#8221; Which feels great. For about five minutes. Then you try to build a product listing page with 48 cards that each need to morph into a detail view, and you realize the tutorials left out the hard part.</p>



<p class="wp-block-paragraph">This is where it gets real. Let&#8217;s scale this thing.</p>



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



<div class="wp-block-group ticss-ad1a3c1b"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<h4 id="crossdocument-view-transitions-series" class="wp-block-heading">Cross-Document View Transitions Series</h4>



<ol class="wp-block-list">
<li><strong><a href="https://css-tricks.com/cross-document-view-transitions-part-1/">The Gotchas Nobody Mentions</a></strong></li>



<li><strong>Scaling View Transitions Across Hundreds of Elements</strong> <em>(You are here!)</em></li>
</ol>
</div></div>



<h3 class="wp-block-heading" id="the-dream-one-line-infinite-names">The Dream: One Line, Infinite Names</h3>



<p class="wp-block-paragraph">In a perfect world, you&#8217;d solve the scaling problem with pure CSS. No JavaScript. No server-side loops. Just this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  /* Generates card-1, card-2, card-3, etc. automatically */
  view-transition-name: ident("card-" sibling-index());
}</code></pre>



<p class="wp-block-paragraph">That&#8217;s <a href="https://www.bram.us/2024/12/18/the-future-of-css-construct-custom-idents-and-dashed-idents-with-ident/" rel="noopener"><code>ident()</code></a> — a CSS function <a href="https://github.com/w3c/csswg-drafts/issues/9141" rel="noopener">proposed by Bramus</a> (who works on Chrome) to the CSS Working Group. It takes strings, integers, or other identifiers, concatenates them, and spits out a valid CSS name. Pair it with <code>sibling-index()</code>, which returns an element&#8217;s position among its siblings (1, 2, 3&#8230;), and you get auto-generated unique names for every element in a list. One rule. Works for 10 cards or 10,000. The CSS doesn&#8217;t care.</p>



<p class="wp-block-paragraph">And it&#8217;s not just view transitions. The same pattern works for <code>scroll-timeline-name</code>, <code>container-name</code>, <code>view-timeline-name</code> — anywhere you need unique identifiers at scale. You could even pull names from HTML attributes with <code>attr()</code> instead of <code>sibling-index()</code>, constructing identifiers like <code>ident("--item-" attr(id) "-tl")</code>. The flexibility is real.</p>



<p class="wp-block-paragraph">Here&#8217;s the thing: half of this equation already exists. <code>sibling-index()</code> shipped in Chrome 138 — you can use it today for things like staggered animations and calculated styles. The missing piece is <code>ident()</code>. There&#8217;s a <a href="https://chromestatus.com/feature/6230159413477376" rel="noopener">Chrome Intent to Prototype</a> from May 2025, which means it&#8217;s on the radar. But &#8220;on the radar&#8221; and &#8220;in your browser&#8221; are very different things. No browser ships <code>ident()</code> yet, and there&#8217;s no timeline for when it&#8217;ll land.</p>



<p class="wp-block-paragraph">So we can&#8217;t use it yet. But it&#8217;s worth knowing about because once <code>ident()</code> ships, a huge chunk of the complexity you&#8217;re about to see just&#8230; evaporates. Until then, here&#8217;s how you solve the same problem efficiently today — with the tools that actually exist in browsers right now.</p>



<h2 class="wp-block-heading" id="100-products-100-names-1-nightmare">100 Products, 100 Names, 1 Nightmare</h2>



<p class="wp-block-paragraph">Here&#8217;s what happens when you follow a tutorial that shows one hero image transitioning between two pages and try to apply that pattern to a grid:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/*  THE NIGHTMARE - one rule per item, forever */
::view-transition-group(card-1),
::view-transition-group(card-2),
::view-transition-group(card-3),
::view-transition-group(card-4),
::view-transition-group(card-5),
::view-transition-group(card-6),
::view-transition-group(card-7),
::view-transition-group(card-8)
/* ... imagine 92 more of these */ {

  animation-duration: 0.35s;
  animation-timing-function: ease-out;
}

::view-transition-old(card-1),
::view-transition-old(card-2),
::view-transition-old(card-3)
/* kill me */ {
  object-fit: cover;
}</code></pre>



<p class="wp-block-paragraph">That&#8217;s what you end up with if you follow the tutorials that only show one or two named elements. They assign <code>view-transition-name: hero</code> to one image and call it a day. Cool. Now try building a product grid.</p>



<p class="wp-block-paragraph">Every <code>view-transition-name</code> on a page must be unique. That&#8217;s a hard rule — if two elements share a name, the browser doesn&#8217;t know which one maps to which on the next page, so it throws the whole transition out. On a listing page with 48 products, you need 48 unique names. On a photo gallery with 200 thumbnails, you need 200. The names aren&#8217;t the problem — you can generate those. The problem is that every pseudo-element selector in your CSS targets a <em>specific name</em>, so your animation styles explode into an unmanageable wall of selectors.</p>



<p class="wp-block-paragraph">This is where you need to understand the difference between two properties that sound like they do the same thing but absolutely do not.</p>



<h3 class="wp-block-heading" id="name-vs-class-the-distinction-that-changes-everything">Name vs. Class: The Distinction That Changes Everything</h3>



<p class="wp-block-paragraph">And yeah, the naming here is confusing. I&#8217;ll be honest: the first time I saw <a href="https://css-tricks.com/almanac/properties/v/view-transition-name/"><code>view-transition-name</code></a> and <a href="https://css-tricks.com/almanac/properties/v/view-transition-class/"><code>view-transition-class</code></a> next to each other, I thought they were interchangeable. They&#8217;re not, and the difference matters.</p>



<p class="wp-block-paragraph"><strong>Name = identity.</strong> It answers: &#8220;Which element on Page A is the <em>same element</em> on Page B?&#8221; When you give a thumbnail <code>view-transition-name: card-7</code> on the grid page and give the hero image <code>view-transition-name: card-7</code> on the detail page, you&#8217;re telling the browser those are the same thing and to animate between them. Names must be unique per page. Two elements can&#8217;t both be <code>card-7</code> or the whole thing breaks.</p>



<p class="wp-block-paragraph"><strong>Class = styling hook.</strong> It answers: &#8220;How should the animation <em>look</em>?&#8221; When fifty elements all have <code>view-transition-class: card</code>, you can write one CSS rule that controls the duration, easing, and <code>object-fit</code> for all of them. It&#8217;s the same mental model as CSS classes on regular elements — <code>.btn</code> doesn&#8217;t identify a specific button, it says &#8220;style me like a button.&#8221;</p>



<p class="wp-block-paragraph">Think of it like a database. The <code>name</code> is the primary key — unique, identifies one specific row. The <code>class</code> is a category column — groups rows together so you can run a query across all of them at once.</p>



<p class="wp-block-paragraph">Here&#8217;s what that looks like in practice:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019d6d66-12e8-7bcd-9484-6c2b4020ff38" src="//codepen.io/editor/anon/embed/preview/019d6d66-12e8-7bcd-9484-6c2b4020ff38?height=1150&amp;theme-id=1&amp;slug-hash=019d6d66-12e8-7bcd-9484-6c2b4020ff38&amp;default-tab=result" height="1150" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019d6d66-12e8-7bcd-9484-6c2b4020ff38" title="CodePen Embed 019d6d66-12e8-7bcd-9484-6c2b4020ff38" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">There it is. Six cards, six unique names, but exactly <em>three</em> CSS rules handling all the animation behavior. Could be sixty cards. Could be six hundred. The CSS doesn&#8217;t change.</p>



<p class="wp-block-paragraph">The key line is that selector: <code>::view-transition-group(*.card)</code>. The asterisk is a wildcard for the name, and <code>.card</code> matches the <code>view-transition-class</code>. It reads as &#8220;any view transition group whose element has <code>view-transition-class: card</code>, regardless of what its specific name is.&#8221;</p>



<p class="wp-block-paragraph">For cross-document multi-page application (MPA) transitions, the pattern is the same but you generate the names on the server:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Page A -->
&lt;div class="grid">

  &lt;!-- ... -->

  &lt;a
    href="/product/42"
    class="card"
    style="view-transition-name: product-42; view-transition-class: card"
  >
    &lt;img src="/images/42-thumb.jpg" alt="Widget" />
  &lt;/a>
  &lt;a
    href="/product/43"
    class="card"
    style="view-transition-name: product-43; view-transition-class: card"
  >
    &lt;img src="/images/43-thumb.jpg" alt="Gadget" />
  &lt;/a>
&lt;/div></code></pre>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Page B -->
&lt;div
  class="product-hero"
  style="view-transition-name: product-42; view-transition-class: card"
>
  &lt;img src="/images/42-hero.jpg" alt="Widget" />
&lt;/div></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* ONE stylesheet, shared by all pages, handles every product */
@view-transition {
  navigation: auto;
}

::view-transition-group(*.card) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

::view-transition-old(*.card),
::view-transition-new(*.card) {
  object-fit: cover;
}</code></pre>



<p class="wp-block-paragraph">That&#8217;s the entire animation stylesheet for a site with thousands of products. Three rules. No matter how many items you have in the database, you never add another line of transition CSS.</p>



<p class="wp-block-paragraph">Before <code>view-transition-class</code> existed, people were doing horrifying things — looping through items in JavaScript to generate <code>&lt;style&gt;</code> blocks with hundreds of selectors, or using CSS preprocessors to spit out every possible name permutation at build time. It worked, technically, the same way duct-taping a car bumper works technically.</p>



<p class="wp-block-paragraph"><code>view-transition-class</code> is the spec authors acknowledging that the original API just didn&#8217;t scale, and fixing it the right way.</p>



<p class="wp-block-paragraph">One gotcha: <code>view-transition-class</code> was added to the spec later to fix these exact scaling issues. The property landed in Chrome 125 and is now in Chrome, Edge, and Safari 18.2+. Older Chromium versions and Firefox won&#8217;t recognize it yet. The transitions will still <em>work</em>, they&#8217;ll just use the default fade animation instead of your custom timing. Not the worst fallback.</p>




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



<p class="wp-block-paragraph">You can also assign multiple classes to a single element, just like regular CSS classes. Something like <code>view-transition-class: card featured</code> is valid, and you can target it with either <code>::view-transition-group(*.card)</code> or <code>::view-transition-group(*.featured)</code>. Handy when you want most products to transition the same way but need a few to stand out with a different animation style.</p>



<h2 class="wp-block-heading" id="don-t-name-everything-upfront">Don&#8217;t Name Everything Upfront</h2>



<p class="wp-block-paragraph">Everything so far has had <code>view-transition-name</code> sitting right there in the HTML or CSS from the moment the page loads. That works. But it has a cost that&#8217;s not obvious until you hit real-world scale.</p>



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



<p class="wp-block-paragraph">Look at the CSS for both pages. Zero <code>view-transition-name</code> declarations. None. Every card in the grid is anonymous until the exact moment the user clicks one.</p>



<p class="wp-block-paragraph">Here&#8217;s why that matters. When you put <code>view-transition-name</code> on an element in your stylesheet — just sitting there in CSS, assigned from page load — you&#8217;re telling the browser, &#8220;This element participates in every transition that happens on this page.&#8221; Every single navigation. The browser has to snapshot it, calculate its position, and set up the pseudo-element tree for it. For one hero image, who cares? For a grid of 48 product cards, that&#8217;s 48 elements being individually captured, diffed, and animated when the user only clicked <em>one</em> of them. The other 47 snapshots are pure waste.</p>



<p class="wp-block-paragraph">On a fast machine you might not notice. On a mid-range Android phone loading a grid of product images over LTE? You&#8217;ll feel it. The transition stutters or the browser just skips it entirely because it can&#8217;t set everything up fast enough.</p>



<p class="wp-block-paragraph">The fix is to treat <code>view-transition-name</code> like a just-in-time thing. Assign it at the moment of interaction, not at page load.</p>



<p class="wp-block-paragraph">The lifecycle goes like this:</p>



<ol class="wp-block-list">
<li>User clicks a card on the listing page.</li>



<li>Browser starts navigating — <code>pageswap</code> fires on the old page.</li>



<li>Your <code>pageswap</code> handler looks at <code>event.activation.entry.url</code> to figure out <em>where</em> the user is going, finds the clicked card, slaps <code>view-transition-name: product-42</code> on it.</li>



<li>Browser snapshots that one named element (plus the default <code>root</code> transition).</li>



<li>Navigation happens, new page loads.</li>



<li><code>pagereveal</code> fires on the incoming page.</li>



<li>Your <code>pagereveal</code> handler reads the URL, finds the hero element, assigns the matching <code>view-transition-name: product-42</code>.</li>



<li>Browser sees matching names on old and new snapshots — morphs between them.</li>



<li>Transition finishes, your <code>.finished</code> promise resolves, you clear the names.</li>
</ol>



<p class="wp-block-paragraph">That&#8217;s it. One element named, one element transitioned, zero waste.</p>



<p class="wp-block-paragraph">The <code>event.activation</code> object is your best friend here. On the outgoing page, <code>event.activation.entry.url</code> tells you where the navigation is headed. On the incoming page, you just read <code>window.location</code>. Between the two, you have everything you need to figure out which element to name without any global state, no <code>sessionStorage</code> tricks, no query parameter gymnastics beyond what your app already uses.</p>



<p class="wp-block-paragraph">And about that cleanup step, removing the name after <code>.finished</code> resolves? It&#8217;s not just tidiness. If the user navigates back to the listing page and clicks a <em>different</em> card, you don&#8217;t want the old card still carrying a name from the previous transition. Stale names cause duplicate-name conflicts (instant transition death) or wrong-element matching (the new page morphs from the wrong card). Clean up after yourself.</p>



<p class="wp-block-paragraph">This pattern is basically what <a href="https://docs.astro.build/en/guides/view-transitions/#naming-a-transition" rel="noopener">Astro&#8217;s <code>transition:name</code></a> directive does under the hood. Same with <a href="https://nuxt.com/docs/3.x/getting-started/transitions" rel="noopener">Nuxt&#8217;s view transition support</a>. They dynamically assign and remove names around the navigation lifecycle. The frameworks just hide the <code>pageswap</code>/<code>pagereveal</code> wiring behind a component attribute. You&#8217;re doing the same thing, just without the abstraction layer. Fewer moving parts, same result.</p>



<h3 class="wp-block-heading" id="practical-patterns-for-real-content">Practical Patterns for Real Content</h3>



<p class="wp-block-paragraph">The product grid example covers the most common case, but let&#8217;s run through a couple of other patterns you&#8217;ll hit in the wild.</p>



<h4 class="wp-block-heading" id="photo-galleries-with-mixed-aspect-ratios">Photo Galleries with Mixed Aspect Ratios</h4>



<p class="wp-block-paragraph">Galleries are tricky because every thumbnail might have a different aspect ratio, and the full-size view definitely will. The taffy fix from the Part 1 article is essential here, but you also want the transition to feel intentional rather than chaotic.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Gallery items get their own class for targeted animation */
::view-transition-group(*.gallery-item) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

::view-transition-old(*.gallery-item),
::view-transition-new(*.gallery-item) {
  object-fit: cover;
  overflow: hidden;
}

/* Lightbox-style overlay - fade the background separately */
::view-transition-group(*.lightbox-bg) {
  animation-duration: 0.3s;
}</code></pre>



<p class="wp-block-paragraph">The trick with galleries is assigning the <code>view-transition-name</code> to the <code>&lt;img&gt;</code> itself rather than the surrounding card or container. You want the browser to morph the image from thumbnail size to lightbox size, not the card&#8217;s background, padding, and caption along with it. Name the image. Style the card. Keep them separate.</p>



<p class="wp-block-paragraph">For the lightbox background (that dark overlay), give it its own <code>view-transition-name</code> and <code>view-transition-class</code>. It&#8217;ll fade in independently while the image morphs. Two transitions running in parallel, each with their own timing. Looks polished, and it&#8217;s just two names.</p>



<h4 class="wp-block-heading" id="tab-or-section-transitions-within-a-page">Tab or Section Transitions Within a Page</h4>



<p class="wp-block-paragraph">Not everything is a grid-to-detail pattern. Sometimes you&#8217;re transitioning between sections on the same page, e.g., dashboard tabs, multi-step forms, content panels. Same-document view transitions work great here, and the <code>view-transition-class</code> approach scales the same way.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Shared header that persists across tabs */
::view-transition-group(*.persistent) {
  animation-duration: 0s; /* don't animate - it should feel anchored */
}

/* Tab content that swaps */
::view-transition-group(*.tab-content) {
  animation-duration: 0.25s;
}

::view-transition-old(*.tab-content) {
  animation: slide-out-left 0.25s ease-in;
}

::view-transition-new(*.tab-content) {
  animation: slide-in-right 0.25s ease-out;
}

@keyframes slide-out-left {
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

@keyframes slide-in-right {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
}</code></pre>



<p class="wp-block-paragraph">The <code>animation-duration: 0s</code> on persistent elements is worth calling out. If your site header has a <code>view-transition-name</code> (so it stays in place instead of participating in the default root cross-fade), you probably don&#8217;t want it animating at all. Zero-duration makes it snap to its new position instantly, which feels like it never moved. That&#8217;s the point — stable landmarks make the transitioning content feel grounded.</p>



<h4 class="wp-block-heading" id="dynamic-content-and-infinite-scroll">Dynamic Content and Infinite Scroll</h4>



<p class="wp-block-paragraph">Here&#8217;s a pattern that catches people off guard. You&#8217;ve got a product grid with infinite scroll, loading new items as the user scrolls down. Each new batch arrives via <code>fetch()</code> and gets appended to the DOM. Do those new items need <code>view-transition-name</code>?</p>



<p class="wp-block-paragraph">No. Not until someone clicks one.</p>



<p class="wp-block-paragraph">With the just-in-time pattern, it doesn&#8217;t matter whether an element existed at page load or was added dynamically five minutes later. The <code>pageswap</code> handler queries the DOM at the moment of navigation. If the element is there, it finds it, names it, done. Your infinite scroll items work identically to your initial page load items without any extra setup.</p>



<p class="wp-block-paragraph">The one thing to watch out for: make sure your <code>data-id</code> attributes (or whatever you&#8217;re using to match elements) are unique across all loaded batches. If your API returns items with IDs and you&#8217;re using those for the <code>view-transition-name</code>, you&#8217;re already fine. If you&#8217;re generating IDs client-side, make sure they don&#8217;t collide when new batches load.</p>



<h3 class="wp-block-heading" id="don-t-make-people-sick">Don&#8217;t Make People Sick</h3>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* The responsible way to set up view transitions */
@view-transition {
  navigation: auto;
}

/* All your animation customizations go INSIDE this media query */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(*.card) {
    animation-duration: 0.35s;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }

  ::view-transition-old(*.card),
  ::view-transition-new(*.card) {
    object-fit: cover;
  }

  /* Custom keyframes, staggered delays, the fun stuff - all in here */
  ::view-transition-old(root) {
    animation: fade-out 0.2s ease-in;
  }

  ::view-transition-new(root) {
    animation: fade-in 0.3s ease-out;
  }
}

/* If the user HAS requested reduced motion: instant cut, no animation */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0s !important;
  }
}

@keyframes fade-out {
  to {
    opacity: 0;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}</code></pre>



<p class="wp-block-paragraph">This isn&#8217;t a nice-to-have. I need to be blunt about that.</p>



<p class="wp-block-paragraph">People with vestibular disorders — and there are a lot more of them than most developers realize — can get physically nauseous from unexpected motion on screen. Not &#8220;mildly annoyed.&#8221; Nauseous. Dizzy. Migraines that last hours. The <a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/"><code>prefers-reduced-motion</code></a> media query exists because real people checked a box in their OS settings that says &#8220;please stop making me sick.&#8221; Ignoring it is the accessibility equivalent of removing a wheelchair ramp because stairs look cleaner.</p>



<p class="wp-block-paragraph">The <a href="https://css-tricks.com/almanac/rules/v/view-transition/"><code>@view-transition</code></a> opt-in can stay outside the media query. That&#8217;s fine, it just tells the browser, &#8220;I want cross-document transitions enabled.&#8221; The browser will still do an instant cut between pages, which is visually identical to a normal navigation. It&#8217;s the animation customizations that need to be gated: the durations, the easing curves, the custom keyframes. Wrap all of that in <code>prefers-reduced-motion: no-preference</code> and you&#8217;re covered.</p>



<p class="wp-block-paragraph">That <code>prefers-reduced-motion: reduce</code> block at the bottom is a belt-and-suspenders thing. Even if you miss wrapping some animation rule, forcing <code>animation-duration: 0s</code> on all the transition pseudo-elements ensures nothing actually moves. <a href="https://css-tricks.com/alternatives-to-the-important-keyword/#when-using-important-does-make-sense">The <code>!important</code> is ugly but justified here.</a> you genuinely want this to override everything, no exceptions.</p>



<p class="wp-block-paragraph">You already saw the conditional opt-in pattern back in Part 1:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* You can also just disable transitions entirely for reduced-motion users */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}</code></pre>



<p class="wp-block-paragraph">Either approach works. Wrapping the whole <code>@view-transition</code> rule means the browser won&#8217;t even attempt the transition &#8211; it&#8217;s a normal navigation, full stop. Keeping <code>@view-transition</code> active but killing the animation durations means the transition technically fires but completes instantly, which can matter if you have <code>pagereveal</code> logic that depends on <code>event.viewTransition</code> existing. Pick whichever fits your setup. Just don&#8217;t ship animated transitions without checking.</p>



<p class="wp-block-paragraph">A thing worth considering here: <a href="https://css-tricks.com/nuking-motion-with-prefers-reduced-motion/">&#8220;reduced motion&#8221; doesn&#8217;t necessarily mean &#8220;no motion.&#8221;</a> Some users with vestibular sensitivities are fine with fades but not with sliding or zooming. You could offer a gentler alternative instead of killing all animation entirely.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: reduce) {
  /* Instead of zero duration, use a quick crossfade only */
  ::view-transition-group(*) {
    animation-duration: 0.15s !important;
    animation-timing-function: linear !important;
  }

  ::view-transition-old(*) {
    animation: fade-out 0.15s linear !important;
  }

  ::view-transition-new(*) {
    animation: fade-in 0.15s linear !important;
  }
}</code></pre>



<p class="wp-block-paragraph">This is a judgment call. A fast, subtle cross-fade is less likely to trigger symptoms than a <code>400ms</code> morphing animation with easing curves. But the safest option is always zero motion, and if you&#8217;re not sure, go with <code>animation-duration: 0s</code>. You can always add a gentler alternative later once you&#8217;ve tested it with actual users who rely on the setting.</p>



<h3 class="wp-block-heading" id="handle-old-browsers-by-doing-basically-nothing-">Handle Old Browsers (By Doing Basically Nothing)</h3>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Feature detection, if you need it */
@supports (view-transition-name: none) {
  .card {
    /* maybe you want contain: paint for better snapshotting */
    contain: paint;
  }
}</code></pre>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// JS-side feature detection
if (document.startViewTransition) {
  // same-document transition API exists
}

// For cross-document transitions, there's no direct JS check -
// the browser either supports @view-transition in CSS or ignores it.
// That's... actually fine.</code></pre>



<p class="wp-block-paragraph">Here&#8217;s the thing though: you probably don&#8217;t even need that <code>@supports</code> check.</p>



<p class="wp-block-paragraph">View transitions are progressive enhancement in the purest sense of the term. If a browser doesn&#8217;t understand <code>@view-transition { navigation: auto; }</code>, it ignores the rule. That&#8217;s how CSS works. The user clicks a link, the browser navigates normally, the new page loads. No animation, no morphing, no cross-fade. Just a regular page load. Which is exactly what every website on the internet did for the first 25 years of the web. It&#8217;s fine.</p>



<p class="wp-block-paragraph">Nothing breaks. No JavaScript errors. No layout shifts. No fallback code to write. The <code>view-transition-name</code> properties get ignored. The <code>::view-transition-*</code> pseudo-element selectors match nothing. Your <code>pageswap</code> and <code>pagereveal</code> event listeners either don&#8217;t fire or <code>event.viewTransition</code> is <code>null</code> and your guard clause returns early. The whole feature is designed to be invisible when it&#8217;s absent.</p>



<p class="wp-block-paragraph">That&#8217;s the beauty of this API, honestly. It&#8217;s one of the rare web platform features where you don&#8217;t have to write a single line of fallback code. Firefox doesn&#8217;t support it yet? Fine — Firefox users get normal navigation. Safari&#8217;s working on it but hasn&#8217;t shipped? Cool, Safari users click links and pages load. Nobody gets an error. Nobody gets a broken layout. Nobody loses anything. They just don&#8217;t get the fancy animation, and most of them will never notice it was supposed to be there.</p>



<p class="wp-block-paragraph">Worth noting where things actually stand today: Chrome and Edge have full support for cross-document view transitions, including view-transition-class. Safari also ships full cross-document support as of Safari 18.2. The momentum is clearly toward universal support, even though Firefox still holds it behind a flag for now.</p>



<p class="wp-block-paragraph">The only time <code>@supports</code> matters is if you&#8217;re adding styles that <em>only</em> make sense in the context of view transitions — like <code>contain: paint</code> on elements to improve snapshot quality, or hiding some loading state that the transition would normally cover. Gate those behind <code>@supports (view-transition-name: none)</code> so non-supporting browsers don&#8217;t get the side effects without the payoff.</p>



<p class="wp-block-paragraph">Failure is invisible. That&#8217;s the whole point.</p>



<h3 class="wp-block-heading" id="ship-it">Ship It</h3>



<p class="wp-block-paragraph">Look, I&#8217;ve been building websites for a long time, and there&#8217;s always been this unspoken trade-off: you want smooth, app-like transitions, you adopt a framework and a client-side router and a build step and a hydration strategy and suddenly you&#8217;re maintaining a small aircraft carrier just so a card can animate into a hero image.</p>



<p class="wp-block-paragraph">That trade-off is dissolving.</p>



<p class="wp-block-paragraph">Cross-document view transitions let an <code>&lt;a href&gt;</code> feel like a native app navigation. Two HTML files. Some CSS. Maybe a little JavaScript for the fancy stuff. The browser does the rest. That&#8217;s not a small thing &#8211; it changes which projects <em>need</em> a framework and which ones just <em>assumed</em> they did.</p>



<p class="wp-block-paragraph">The spec is young. It&#8217;s Chromium-only right now. The rough edges are real &#8211; you&#8217;ve seen them across both parts of this series. But the API is designed so well that when it&#8217;s not supported, nothing breaks. Your site just works the way sites have always worked. And when it <em>is</em> supported, it feels like magic that came free.</p>



<p class="wp-block-paragraph">Here&#8217;s a quick cheat sheet to take with you:</p>



<ul class="wp-block-list">
<li><strong>Opt in with CSS</strong>, not the deprecated meta tag: <code>@view-transition { navigation: auto; }</code>.</li>



<li><strong>Both pages</strong> must opt in or no transition happens.</li>



<li><strong>4-second timeout</strong> starts at navigation, not at render &#8211; use <code>pagereveal</code> to catch <code>TimeoutError</code>.</li>



<li><strong>Images stretch</strong> during transitions because pseudo-elements default to <code>object-fit: fill</code> &#8211; fix it with <code>object-fit: cover</code> on <code>::view-transition-old</code> and <code>::view-transition-new</code>.</li>



<li><strong><code>view-transition-name</code></strong> = identity (unique per page), <strong><code>view-transition-class</code></strong> = styling hook (shared across elements).</li>



<li><strong>Don&#8217;t name elements upfront</strong> &#8211; use <code>pageswap</code> and <code>pagereveal</code> to assign names just-in-time. But keep your <code>pageswap</code> logic fast — the browser gives you a narrow window (10-50ms) before snapshots.</li>



<li><strong>Clean up names</strong> after <code>viewTransition.finished</code> resolves to avoid stale conflicts.</li>



<li><strong>Gate animations</strong> behind <code>prefers-reduced-motion: no-preference</code> — this is not optional.</li>



<li><strong>Progressive enhancement</strong> is built in — unsupported browsers just get normal page loads</li>
</ul>



<p class="wp-block-paragraph">The best animations are the ones you don&#8217;t have to maintain a framework to get.</p>



<div class="wp-block-group ticss-ad1a3c1b"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<h4 id="crossdocument-view-transitions-series" class="wp-block-heading">Cross-Document View Transitions Series</h4>



<ol class="wp-block-list">
<li><strong><a href="https://css-tricks.com/cross-document-view-transitions-part-1/">The Gotchas Nobody Mentions</a></strong></li>



<li><strong>Scaling View Transitions Across Hundreds of Elements</strong> <em>(You are here!)</em></li>
</ol>
</div></div>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/cross-document-view-transitions-part-2/">Cross-Document View Transitions: Scaling Across Hundreds of Elements</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/cross-document-view-transitions-part-2/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393441</post-id>	</item>
		<item>
		<title>The State of CSS Centering in 2026</title>
		<link>https://css-tricks.com/the-state-of-css-centering-in-2026/</link>
					<comments>https://css-tricks.com/the-state-of-css-centering-in-2026/#comments</comments>
		
		<dc:creator><![CDATA[Temani Afif]]></dc:creator>
		<pubDate>Fri, 22 May 2026 13:44:40 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[anchor positioning]]></category>
		<category><![CDATA[flexbox]]></category>
		<category><![CDATA[grid]]></category>
		<category><![CDATA[layout]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394429</guid>

					<description><![CDATA[<p>Despite the countless number of online resources, it’s easy to get confused when trying to center an element. There are documented solutions, but do you really understand why the code you picked works? Let's look at the current state of centering options today in 2026. </p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-state-of-css-centering-in-2026/">The State of CSS Centering in 2026</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">What? Another article about <a href="https://css-tricks.com/?s=centering">centering</a>?! But all we have to do is use <code>display: flex | grid</code>, then <code>align-items: center</code>. No, it’s <code>align-content</code>&#8230; wait&#8230; I think it’s <code>justify-content</code>. Well, let’s use <code>margin: auto</code>, this one works all the time, right?</p>



<p class="wp-block-paragraph">Despite the countless number of online resources (even <a href="https://css-tricks.com/centering-css-complete-guide/">CSS-Tricks has a full guide</a> on it), it’s easy to get confused when trying to center an element, whether vertically, horizontally, or both). I am sure you will find something that works by googling or trying different combinations. But do you really understand <em>why</em> the code you picked works? Is it the right one for your use case? Because it really does depend and require consideration!</p>



<p class="wp-block-paragraph">In this article, we will do a fresh exploration of centering in CSS, and hopefully, you will learn something new by the end of it.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I already master CSS centering. Should I skip this article?</p>
</blockquote>



<p class="wp-block-paragraph">Stay with me because we will explore hidden tricks and modern features that you may not know — safe centering, <code>text-box</code>, centering in anchor positioning, etc.</p>



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



<h3 class="wp-block-heading" id="is-centering-still-hard-">Is centering still hard?</h3>



<p class="wp-block-paragraph">No, centering is not hard. Considering all the different and various ways to center an element, it’s an easy task that generally requires two or three lines of code. But, how many ways do we have to center an element? I did the count, and I was able to enumerate <a href="https://css-generators.com/center/" rel="noopener">100 different ways to center an element vertically and horizontally within a container</a>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Are you serious,100 ways?! That’s insane.</p>
</blockquote>



<p class="wp-block-paragraph">Yes, 100 is a ridiculously high number for what should be a simple task, but that number is misleading. If you check the list, you will find I marked about 60 of them in <strong>red,</strong> meaning they are hacky and not recommended. This leaves us with roughly 30 valid approaches. And within those valid options, many are basically the same, only written differently, so we can consider them redundant.</p>



<p class="wp-block-paragraph">At the end of the day, the number of “unique” and “valid” ways to center an element is less than 15 (or even 10) but it was a fun exercise enumerating the different codes that can center an element. Go check <a href="https://css-generators.com/center/" rel="noopener">the full list</a>, you may learn something new!</p>



<p class="wp-block-paragraph">Let’s look at things from a beginner&#8217;s perspective. For me, who has been writing CSS day and night for years, it’s easy to say “centering is not hard,” but what about to a newcomer who reads this and confronted with all those different ways to center stuff? Nah, it’s not easy at all. <code>align-items</code>, <code>align-content</code>, <code>justify-content</code>, <code>place-self</code>, <code>margin: auto</code>. What the hell?!</p>



<p class="wp-block-paragraph">Too many properties for a task that everyone claims is easy! Well, let’s pick a code that works and move on. After all, if the item is in the center, then it’s fine, right? Let’s avoid making a lot of noise around this, or the CSS fanatics will shout at me.</p>



<p class="wp-block-paragraph">Don’t think that way! Centering can be hard, and that’s fine. It doesn’t mean you are stupid. It simply means you need to understand how it works.</p>



<p class="wp-block-paragraph">Don’t skip the important step of “learning” (like many do); otherwise you will find yourself doing a lot of copy/paste without really understanding what is going on. Sometimes it works, but sometimes it doesn&#8217;t, and it can be very frustrating.</p>



<h3 class="wp-block-heading" id="learn-how-to-align-before-how-to-center">Learn how to align before how to center</h3>



<p class="wp-block-paragraph">Centering is nothing but a special case of alignment in CSS, and alignment is a complex world. It’s not only left, center, right, or top, center, bottom. It’s more than that. The good news is that you can easily learn it. For this purpose, I wrote a deep dive I called <a href="https://css-tip.com/explore/alignment/" rel="noopener">“The fundamentals of alignment in CSS.”</a></p>



<p class="wp-block-paragraph">It’s probably one of my longest writings, but believe me, it&#8217;s worth your time (and effort). I explain how alignment works in all the different CSS layout methods. It starts with understanding the alignment theory, which has two levels of alignment (“content” and “item”) and two axes (horizontal and vertical).</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="885" height="341" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777322117445_image.png?resize=885%2C341&#038;ssl=1" alt="Diagram showing that place-content equals align-content plus justify-content, place-self equals align-self plus justify-self, and place-items equals align-items plus justify-items, alongside a visual example of all three inside a white container and black border." class="wp-image-394439" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777322117445_image.png?w=885&amp;ssl=1 885w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777322117445_image.png?resize=300%2C116&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777322117445_image.png?resize=768%2C296&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">Identifying the “content” and the “item” in every layout is the key to understanding how everything works. I insist on “every layout” because assuming it works the same everywhere is a very common mistake.</p>



<p class="wp-block-paragraph">Do yourself a favor and <a href="https://css-tip.com/explore/alignment/" rel="noopener">read that detailed article</a> — you will thank me later! And once you understand the core concept of alignment, centering will become child&#8217;s play.</p>



<h3 class="wp-block-heading" id="should-i-use-flexbox-or-grid-">Should I use Flexbox or Grid?</h3>



<p class="wp-block-paragraph">I see a lot of people who always use the same method to center an element, whatever the situation. You have the CSS Grid team and the Flexbox team. While both work, I don’t advise you to think that way. Remember that the goal is to <em>understand</em> and avoid quick copy/paste approaches.</p>



<p class="wp-block-paragraph">Study your layout and your requirements, then decide which method to use. Maybe your case requires <code>position: absolute</code> or a simple <code>text-align: center</code>. Flexbox or CSS Grid aren’t always mandatory for centering stuff, and there is no one way that&#8217;s better than another.</p>



<p class="wp-block-paragraph">That said, if I have to pick something, I would consider the following codes. Each one for each type of layout.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.container { 
  display: block;
  align-content: center;
  justify-items: center; 
}
.container {
  display: grid;
  place-content: center;
}
.container {
  display: flex;
  flex-wrap: wrap;
  place-content: center;
}</code></pre>



<p class="is-style-explanation wp-block-paragraph"><strong>Note:</strong> <code>justify-items</code> in the context of a block container is not supported by all the browsers. It’s Chrome-only for now, so consider using Chrome to see the following demos.</p>



<p class="wp-block-paragraph">The properties are defined in one place (the container), and the methods are suitable for centering one or multiple items.</p>



<p class="wp-block-paragraph">You won’t notice a difference when centering a single item. The three methods behave the same.</p>



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



<p class="wp-block-paragraph">With multiple items, Flexbox behaves differently. It has a responsive behavior where the items are initially laid out horizontally and wrap when the container is narrowed. Resize the container and see what happens.</p>



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



<p class="wp-block-paragraph">And with multiple items of different sizes, they all behave differently.</p>



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



<p class="wp-block-paragraph">We started with three approaches that give us the same “visual” result when working with a single item, but upon adding more items, we can clearly see they are different. This difference is important as it shows that it’s not about picking a random code to center stuff. It’s about understanding how each code behaves in different situations, then picking the most suitable one. It’s wrong to assume that we can center the same way using Flexbox, CSS Grid, etc. All the methods are different and rely on different mechanisms, even if they give the same result in the context of one item.</p>



<p class="wp-block-paragraph">This also explains why we technically have 100 ways to center stuff. We have different layout types, and each layout has its own alignment logic. But when the structure is reduced to one item inside a container, we have a lot of choices, and many methods may look identical even though they are not.</p>



<p class="wp-block-paragraph">So, let me repeat myself: Study the alignment logic behind each code to know which one is suitable for your use case. Don’t blindly copy/paste a code that simply “works.”</p>



<h3 class="wp-block-heading" id="what-about-centering-text-">What about centering text?</h3>



<p class="wp-block-paragraph">When centering “boxes,” we generally don’t have any issues if we apply the properties correctly. But once we start dealing with text, it can be tricky to perfectly center things vertically. You know the extra space above or below that you cannot really control and you have to use magic values for line-height or padding to rectify it.</p>



<p class="wp-block-paragraph">We now have a new property that allows us to fix this: <a href="https://css-tricks.com/almanac/properties/t/text-box/"><code>text-box</code></a>. It trims the extra space based on your configuration.</p>



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



<p class="wp-block-paragraph">In both boxes, I align the content in the center using a common code. Notice that the first box is not that good. The text seems to be off, even though I am using the CSS properties correctly.</p>



<p class="wp-block-paragraph">It’s frustrating, right? For CSS, everything is perfectly centered, but for us, it’s not. why!?</p>



<p class="wp-block-paragraph">It’s related to how the font is designed and the space reserved for each character. Adding a border around the text will make things clear.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="582" height="273" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777156519576_image.png?resize=582%2C273&#038;ssl=1" alt="Two examples of the word Text next to a red square. The first example is slightly off center due to line height and the second is perfect centered against the square due to removing extra line height." class="wp-image-394437" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777156519576_image.png?w=582&amp;ssl=1 582w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777156519576_image.png?resize=300%2C141&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">As you can see, the “text box” is centered, but there is unwanted space inside it. I was able to remove that space using one line of code:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">text-box: cap alphabetic;</code></pre>



<p class="wp-block-paragraph">Let’s try lowercase text without descenders or ascenders.</p>



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



<p class="wp-block-paragraph">This time I am using slightly different keywords:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">text-box: ex alphabetic;</code></pre>



<p class="wp-block-paragraph">&#8230;to remove the space for perfect centering</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="618" height="201" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157001493_image.png?resize=618%2C201&#038;ssl=1" alt="Two examples of the word awesome next to a red square. The first example is slightly off center due to line height and the second is perfect centered against the square due to removing extra line height." class="wp-image-394435" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157001493_image.png?w=618&amp;ssl=1 618w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157001493_image.png?resize=300%2C98&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">The values look strange and unintuitive, but I have created <a href="https://css-tip.com/text-box/" rel="noopener">a small generator</a> where you can easily specify which space you want to trim and get the code in no time.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="830" height="389" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157151200_image.png?resize=830%2C389&#038;ssl=1" alt="Highlighting the rendered line height of a text showing the space it adds to the content." class="wp-image-394434" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157151200_image.png?w=830&amp;ssl=1 830w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157151200_image.png?resize=300%2C141&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777157151200_image.png?resize=768%2C360&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">And if you want more detail on that feature, check Danny Schwarz’s <a href="https://css-tricks.com/two-css-properties-for-trimming-text-box-whitespace/">“Two CSS Properties for Trimming Text Box Whitespace.”</a></p>



<h3 class="wp-block-heading" id="centering-with-css-anchor-positioning">Centering with CSS Anchor Positioning</h3>



<p class="wp-block-paragraph">In some cases, you may need to use absolute or fixed position, which means we are dealing with an out-of-flow element and a different alignment logic; hence, another centering technique.</p>



<p class="wp-block-paragraph">The common way to do that is the classic <code>top</code>/<code>left</code> combined with <code>translate</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">left: 50%;
top: 50%;
transform: translate(-50%, -50%);</code></pre>



<p class="wp-block-paragraph">It works, and everyone is happy, but it’s not the suitable code to use. In 2026, I would consider that code hacky, and worth avoiding. It’s like <a href="https://css-tricks.com/in-defense-of-tables-and-floats-in-modern-day-development/">creating layouts using <code>float</code></a>. That a was a valid approach until we got Flexbox and CSS Grid, which were intentionally designed for this sort for thing.</p>



<p class="wp-block-paragraph">It’s the same thing with absolutely-positioned elements. Today, it’s better to rely on modern CSS features like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">inset: 0;
place-self: center;</code></pre>



<p class="wp-block-paragraph">The <a href="https://css-tricks.com/almanac/properties/i/inset/"><code>inset</code></a> property controls the “inset modified containing block” (IMCB) and <a href="https://css-tricks.com/almanac/properties/p/place-self/"><code>place-self</code></a> (the shorthand for <code>justify-self</code> and <code>align-self</code>) aligns the element inside the IMCB. <a href="https://css-tip.com/explore/alignment/#what-about-inline-elements" rel="noopener">I explain all those concepts in great detail in this article.</a></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Where is anchor positioning in all of this?</p>
</blockquote>



<p class="wp-block-paragraph">Great question! <a href="https://css-tricks.com/css-anchor-positioning-guide/">Anchor positioning</a> relies on absolute (or fixed) elements and has its own mechanism for controlling an element’s placement relative to its anchor. We are specifically dealing with centering, so we have to talk about a new value, <code>anchor-center</code>.</p>



<p class="wp-block-paragraph">Let’s start with the following example:</p>



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



<p class="wp-block-paragraph">I am placing the text box above the anchor using <code>position-area: top</code>. You can drag the anchor, and the text box will remain stuck to the top and centered.</p>



<p class="wp-block-paragraph">Let’s update the alignment and use <code>place-self: center</code>.</p>



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



<p class="wp-block-paragraph">The position looks a bit off at first glance, but if you drag the anchor and look closely, you will see the box centered within the top area.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="933" height="429" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777201251951_image.png?resize=933%2C429&#038;ssl=1" alt="A light blue label that says CSS is Awesome in the top center of a container that includes an anchor icon places at the center left of the container. The container includes dashed red lines that highlight the position of both items." class="wp-image-394432" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777201251951_image.png?w=933&amp;ssl=1 933w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777201251951_image.png?resize=300%2C138&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777201251951_image.png?resize=768%2C353&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">Centering is indeed not easy! It’s confusing if you don’t know in which area your element is centered. You will think that something is broken because your eyes might not see it as a centered element.</p>



<p class="wp-block-paragraph">If you want to get back to the previous position, you can use this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">place-self: end anchor-center;</code></pre>



<p class="wp-block-paragraph">&#8230;or this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">align-self: end;
justify-self: anchor-center;</code></pre>



<p class="wp-block-paragraph">What’s happening here is that, vertically, we place the element at the end (the bottom), and horizontally, we consider the center of the anchor element. In other words, the <code>anchor-center</code> value is what makes the element follow the anchor when you drag it!</p>



<p class="wp-block-paragraph">This means we have two different ways to use anchor positioning for centering: Either (1) center relative to the selected area using the <code>center</code> value, or (2) center relative to the anchor using the <code>anchor-center</code> value.</p>



<p class="wp-block-paragraph">You will rarely need to use the <code>anchor-center</code> value in most cases because anchor positioning comes with <a href="https://www.w3.org/TR/css-anchor-position-1/#position-area-alignment" rel="noopener">area-specific default alignment</a>. Setting <code>position-area</code> should be enough, but it’s good to know how to adjust the alignment and understand the difference between <code>center</code> and <code>anchor-center</code>.</p>



<p class="wp-block-paragraph">If you want to explore alignment in anchor positioning, I have create <a href="https://css-tip.com/position-area/" rel="noopener">an interactive demo</a> that allows you to set the area, adjust the alignment, and see the result. There are 36 different positions you can set using <code>position-area</code> and five alignment values per axis.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="835" height="622" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777202093171_image.png?resize=835%2C622&#038;ssl=1" alt="The UI for an interactive demo that places a label that says CSS is Awesome around different sides and edges of an anchor icon with controls to change that position and generate the CSS code for it." class="wp-image-394431" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777202093171_image.png?w=835&amp;ssl=1 835w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777202093171_image.png?resize=300%2C223&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777202093171_image.png?resize=768%2C572&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h3 class="wp-block-heading" id="safe-and-unsafe-centering">Safe and unsafe centering</h3>



<p class="wp-block-paragraph">You are probably wondering what safety has to do with centering, right? Don’t worry, centering doesn’t present security risks, per se, but it can be a risky thing for your content!</p>



<p class="wp-block-paragraph">Take the following example:</p>



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



<p class="wp-block-paragraph">I am using CSS Grid to center a red square within a container and we have two situations. The red square is smaller than the container (a classic situation), and the red square is bigger than the container (a less common situation).</p>



<p class="wp-block-paragraph">In both situations, the red square remains centered, i.e., its center point matches the container’s center point. This is an unsafe centering approach, and yet it’s the default behavior of many centering methods.</p>



<p class="wp-block-paragraph">Why is it unsafe? The content inside the container is overflowing from all sides, so if you decide to hide the overflow and add a scrollbar, some parts of the content cannot be reached, which is a form of <a href="https://css-tricks.com/overflow-and-data-loss-in-css/">data loss</a>. In this case, the top and left parts are lost. That’s what I mean by <em>unsafe</em>.</p>



<p class="wp-block-paragraph">Try scrolling the second container, and you will notice that you cannot see the red square’s top and left borders.</p>



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



<p class="wp-block-paragraph">We can fix this by using <code>safe</code> alignment like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">place-content: safe center;</code></pre>



<p class="wp-block-paragraph">Now, when an overflow occurs, the browser will shift the element to a “safer” position that displays the whole content in case we need to scroll. In other words, the browser prioritizes content visibility over centering (the exact opposite of an unsafe alignment).</p>



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



<p class="wp-block-paragraph">I know what you’re probably thinking, and you shouldn’t be thinking that! Adding <code>safe</code> everywhere isn&#8217;t a good idea. Sometimes the unsafe behavior is actually what we want, so only consider <code>safe</code>when you’re faced with content obstruction.</p>



<p class="wp-block-paragraph">Let’s get back to the anchor positioning demo:</p>



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



<p class="wp-block-paragraph">If you drag the anchor closer to the edges, the box is stopped by those edges (the containing block) and the default alignment is lost!</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="593" height="398" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777203629933_image.png?resize=593%2C398&#038;ssl=1" alt="A label that says CSS is Awesome centered above an anchor icon that sits toward the left edge of a container." class="wp-image-394430" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777203629933_image.png?w=593&amp;ssl=1 593w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_11AB7300F4C2B3C7258DA83D06B69BFA5810FEA16962608FDEF9E8D5EBE2CA4D_1777203629933_image.png?resize=300%2C201&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">In anchor positioning, the default behavior is <code>safe</code> alignment. If you don’t know about it, you may spend a lot of time trying to figure out why the element is not centered.</p>



<p class="wp-block-paragraph">You can change that behavior using the <code>unsafe</code> keyword:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">place-self: unsafe end unsafe anchor-center;</code></pre>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">justify-self: unsafe anchor-center;
align-self: unsafe end;</code></pre>



<p class="wp-block-paragraph">Now, the browser allows the box to overflow the container. It will prioritize alignment over potential content loss due to the overflow.</p>



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



<p class="wp-block-paragraph">And if you think it’s useless to work with an <code>unsafe</code> alignment in anchor positioning, then you are wrong. Here is one use case where I needed to switch to an unsafe alignment. We have a sticky header with a small icon next to the website title that you can hover to show a tooltip. The sticky header creates a containing block for the tooltip and, by default, prevents it from overflowing its boundary. I had to use an unsafe alignment to allow the overflow and keep the tooltip correctly placed.</p>



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



<p class="wp-block-paragraph">I know it can be confusing, but you will rarely need to mess with safety. Keep using the default browser behavior, but remember you have the <code>safe</code> and <code>unsafe</code> values you can use to rectify a misalignment.</p>



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



<p class="wp-block-paragraph">I hope that after this article you will see centering from a different angle. It’s not about picking a code that works, and you&#8217;re done. It’s about <a href="https://css-tip.com/explore/alignment/" rel="noopener">understanding how alignment works</a>, considering your specific use case and layout, picking the appropriate code, and, more importantly, understanding <em>why</em> it works.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-state-of-css-centering-in-2026/">The State of CSS Centering in 2026</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-state-of-css-centering-in-2026/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394429</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=10138 metric#misses=12 metric#hit-ratio=99.9 metric#bytes=7459185 metric#prefetches=214 metric#store-reads=230 metric#store-writes=2 metric#store-hits=524 metric#store-misses=8 metric#sql-queries=30 metric#ms-total=558.43 metric#ms-cache=39.39 metric#ms-cache-avg=0.1705 metric#ms-cache-ratio=7.1 -->
