<?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>Tue, 02 Jun 2026 13:34:31 +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>::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/#respond</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>0</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" fetchpriority="high" 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="(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>1</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>8</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>
		<item>
		<title>Stack Overflow: When We Stop Asking</title>
		<link>https://css-tricks.com/stack-overflow-when-we-stop-asking/</link>
					<comments>https://css-tricks.com/stack-overflow-when-we-stop-asking/#comments</comments>
		
		<dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
		<pubDate>Wed, 20 May 2026 13:51:34 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[opinion]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393076</guid>

					<description><![CDATA[<p>It still hits like a ton of bricks to see the steep decline in Stack Overflow questions. What does that mean about learning in our industry?</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/stack-overflow-when-we-stop-asking/">Stack Overflow: When We Stop Asking</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">Let&#8217;s play a quick game: I&#8217;ll show a graph and try to guess what it&#8217;s about.</p>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1014" height="599" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1772108797606_image.png?resize=1014%2C599&#038;ssl=1" alt="A line chart shaped like a giant bell curve that grows expontionally between 2009 and 2016 then sharply declines over the next ten years." class="wp-image-393159" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1772108797606_image.png?w=1014&amp;ssl=1 1014w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1772108797606_image.png?resize=300%2C177&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1772108797606_image.png?resize=768%2C454&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source:&nbsp;<a href="https://data.stackexchange.com/stackoverflow/query/1926661#graph" rel="noopener">Data Stack Exchange</a></figcaption></figure>



<p class="wp-block-paragraph">No, it isn&#8217;t a crypto coin crashing a few hours after being minted. And not, it is also not&nbsp;<a href="https://css-tricks.com/the-most-hated-css-feature-cos-and-sin/#aa-wavy-layouts">an oscillatory/wavy graph made with pure CSS</a>, but a harsher truth.</p>



<p class="wp-block-paragraph">I already gave it away with the title, but it still hits like a ton of bricks to know it is the steep decline in the&nbsp;<strong>number of questions asked on Stack Overflow</strong>. You can see its peak around 2014 with more than 200,000 questions asked in a single month. But now in 2026, it is struggling to even hit 3,000 questions a month.</p>



<p class="wp-block-paragraph">We don&#8217;t have to be experts in the field to find out the culprit. You guessed, it&#8217;s AI&#8230;&nbsp;<em>mostly</em>.</p>



<p class="wp-block-paragraph">While AI is painted as the Stack Overflow killer, the truth is Stack Overflow&#8217;s downfall started long before ChatGPT&#8217;s release in late 2022.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1014" height="599" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1773836280113_labelled.png?resize=1014%2C599&#038;ssl=1" alt="A line chart shaped like a giant bell curve that grows expontionally between 2009 and 2016 then sharply declines over the next ten years. The chart is labelled to show the various peaks and valleys the timeline." class="wp-image-393160" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1773836280113_labelled.png?w=1014&amp;ssl=1 1014w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1773836280113_labelled.png?resize=300%2C177&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_4CD7D9F6D63D15E0BB4DEE9C7E8B57FB6D78E90F4E1EA43D5FB77F65D6BEF1CB_1773836280113_labelled.png?resize=768%2C454&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">By&nbsp;<a href="https://stackoverflow.blog/2018/04/26/stack-overflow-isnt-very-welcoming-its-time-for-that-to-change/" rel="noopener">community accounts</a>&nbsp;and also from personal experience, moderation since its peak in 2014 has been (and still is) one of the leading causes for the lack of questions.</p>



<p class="wp-block-paragraph">As the site grew, Stack Overflow needed a better way to moderate the hundreds of thousands of questions asked every month: the inevitable wall that forum-based communities hit when they scale beyond a certain point. There are several ways to try to solve this, but <a href="https://meta.stackoverflow.com/questions/251175/stack-overflow-is-not-yet-a-vast-wasteland-a-history-of-moderator-tooling?ref=blog.pragmaticengineer.com" rel="noopener">the&nbsp;route Stack Overflow took</a>&nbsp;might not have been the best:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">On Stack Overflow, we close or delete questions that can&#8217;t be answered straight away &#8211; it&#8217;s not very sociable, but it scales wonderfully.</p>
</blockquote>



<p class="wp-block-paragraph">It&#8217;s clear Stack Overflow wasn&#8217;t focusing on the quantity of the questions but rather on the quality of them, while avoiding duplicates as much as possible. This pattern was in favor of Google searches for questions that were already answered and, hence, living on pre-answered questions instead of on users making new or duplicate ones.</p>



<p class="wp-block-paragraph">It wasn&#8217;t helpful either how the community seemed to close upon itself, making it harder for beginners to even ask a question. And if you&#8217;re like me, you probably want to inquire without being told you&#8217;re stupid, as if getting punished for wanting to learn.</p>



<p class="wp-block-paragraph">Generative AI was the final nail in the coffin. I can&#8217;t complain about this, as AI seemingly provides the same answers without judgment (in fact, maybe <em>too much</em> encouragement) nor delay, so I can see why people might prefer asking an LLM instead.</p>



<p class="wp-block-paragraph">However, as I dug deeper into this, my concern was no longer about just Stack Overflow, but the tech ecosystem at large. Questions like, <em>are we still asking questions?</em> <em>Are we still seeking to be better?</em> Or do we all rely on LLMs, and solely on LLMs, for advice? That kept ringing in my mind as I continued my research.</p>



<p class="wp-block-paragraph">I believe that, beyond the fall of Stack Overflow, those questions linger more than ever. How AI has generally impacted our workflow, how we can use it in problem-solving, and what we can do about this as developers.</p>



<h3 class="wp-block-heading" id="problem-solving-and-ai">Problem-Solving and AI</h3>



<p class="wp-block-paragraph">Is AI a better programmer than you? What makes a programmer better than others is as subjective as it gets, but some are eager to say that <a href="https://wandb.ai/telidavies/ml-news/reports/AlphaCode-DeepMind-s-Code-Competition-AI-Solves-Problems-At-Human-Level-Competency--VmlldzozMTE3Njgy#:~:text=AlphaCode&#039;s%20human%2Dlevel%20performance,optimized%20training%20data%20and%20architecture." rel="noopener">AI can write code better than you</a>. According to that research:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">AlphaCode achieves human-level problem solving skills and code writing ability as shown by performance in programming competitions.</p>
</blockquote>



<p class="wp-block-paragraph">At least that&#8217;s when it was tested against Codeforce&#8217;s (an online code competition site) problems, where I admit it can and will perform better than your average programmer. But most developers don&#8217;t care about Contest problems beyond a technical interview; they know being a software developer is so much more than that.</p>



<p class="wp-block-paragraph">AI writing quality code is an extremely nuanced topic and lacks a decisive conclusion. However, if you take the time to research, you&#8217;ll find that AI-generated code has lots of flagrant differences. According to the <a href="https://arxiv.org/abs/2508.21634" rel="noopener">research from Cornell</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">AI-generated code is generally simpler and more repetitive, yet more prone to unused constructs and hardcoded debugging, while human-written code exhibits greater structural complexity and a higher concentration of maintainability issues.</p>
</blockquote>



<p class="wp-block-paragraph">Okay, so it can generate simple code, but can it write <em>good</em> code? Even solve problems better than a software engineer would?</p>



<p class="wp-block-paragraph">According to&nbsp;<a href="https://arxiv.org/abs/2503.22625" rel="noopener">MIT research</a>, AI can write good code, but it cannot possibly think and make decisions like a software engineer. AI cannot compete on that level yet, at least&nbsp;<a href="https://stackoverflow.blog/2026/01/28/are-bugs-and-incidents-inevitable-with-ai-coding-agents/#h2-17e392245733" rel="noopener">without running into a lot of bugs</a>.</p>



<p class="wp-block-paragraph">Drawing on both first-hand experience and feedback, if all you do is copy-and-paste AI-generated code without careful consideration, you are bound to hit serious bugs and possibly even vulnerabilities. In fact, <a href="https://www.veracode.com/blog/ai-generated-code-security-risks/" rel="noopener">VeraCode published an article</a> stating that &#8220;[&#8230;] 45% of AI-generated code contains security flaws,&#8221; after testing for security vulnerabilities in 100 AI models. That&#8217;s a large percentage of code that&#8217;s flawed security-wise and would have cost implications for any user who wants to &#8220;vibe-code&#8221; without doing thorough checks.</p>



<p class="is-style-explanation wp-block-paragraph"><strong>Fun fact:</strong>&nbsp;<a href="https://github.blog/news-insights/research/survey-ai-wave-grows/" rel="noopener">GitHub released the results of its AI in software development survey in August 2024</a>, and over 97% of its respondents have used AI outside or inside their work. That&#8217;s even aside from the companies enforcing the use of AI in your current code workflow. It&#8217;s literally everywhere; there&#8217;s almost no escaping its usage</p>



<p class="wp-block-paragraph">But, does that mean it&#8217;s all bad? The answer to that, in my opinion, is no. According to&nbsp;<a href="https://hbr.org/2025/05/research-gen-ai-makes-people-more-productive-and-less-motivated" rel="noopener">research done by Harvard Business Review</a>, AI is effective for&nbsp;<em>helping</em>&nbsp;solve problems (let&#8217;s not also ignore the trade-off from the study that AI workflows result in less motivation). In essence, it is perhaps best used to enhance problem-solving effectiveness.</p>



<p class="wp-block-paragraph">This means that, as AI is taking over industries and being incorporated into our daily work, it still won&#8217;t replace your creativity and problem-solving approach, which you would need to tackle unique everyday challenges. It&#8217;s difficult to replicate.</p>



<p class="wp-block-paragraph">Like every other tool,&nbsp;<strong>AI has its limits, and without human craftsmanship behind it, the tool is almost useless</strong>.&nbsp;A good craftsman uses all the tools at his disposal to achieve his goals, AI being just one of them.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;The effectiveness of the tool is determined by the skill of the craftsman who created it and the ingenuity with which he utilizes it.&#8221;</p>
<cite>Craig D. Lounsbrough</cite></blockquote>



<p class="wp-block-paragraph">The big danger is not just security vulnerabilities, but over-dependence on the tool, which I believe will lead to an eventual decline in the number of code craftsmen in the coming generation. How should newer and experienced developers go about this?</p>



<h3 class="wp-block-heading" id="some-advice">Some Advice</h3>



<p class="wp-block-paragraph">Here is a list of questions I ask myself when picking up AI in my development work:</p>



<ol class="wp-block-list">
<li><strong>Am I asking the LLM smaller, specific questions?</strong>&nbsp;This way, I can verify each process step-by-step rather than eyeballing the whole system code as a whole. I&#8217;m still a developer in the sense that I am not leaving the LLM to do <em>all</em> the work.</li>



<li><strong>Am I evaluating the output when it&#8217;s finished?</strong> In other words, do I understand what it did? Would I be comfortable modifying the generated code if I know a better approach, or when I have to maintain it in the future?</li>



<li><strong>Am I checking the tool&#8217;s references?</strong> This may be more geared towards research instead of straight code output. Where exactly are its answers coming from? Are those good sources? Are there others? It&#8217;s important to know the tool is not citing a fictional source, but rather, coming up with modern and tried-and-true approaches.</li>



<li><strong>Have I tested the work?</strong>&nbsp;Did the tool understand the task and consider all edge cases? This is perhaps the most important question because knowing how people use your application is something a machine is less inclined to know than a human.</li>
</ol>



<h3 class="wp-block-heading" id="what-happens-when-we-stop-asking-">What happens when we stop asking?</h3>



<p class="wp-block-paragraph">Think about this: if we stop asking questions, how will AI be trained in the future? Technologies change and improve over time. What&#8217;s updated now will soon become old-fashioned. Take CSS, for example. With the recent CSS updates (nesting, view transitions, container queries, etc.), we are writing CSS vastly different than even a few short years ago. You wouldn&#8217;t want to be stuck with an outdated and clumsy solution trained from code written decades ago. If we stop asking questions and answering them, don&#8217;t you think that would make the LLMs lag behind? That&#8217;s just me speculating, but I think it&#8217;s easy to imagine that being the case.</p>



<p class="wp-block-paragraph">We cannot deny&nbsp;<a href="https://stackoverflow.blog/2023/09/26/celebrating-15-years-of-stack-overflow/" rel="noopener">Stack Overflow&#8217;s service over the years</a>. It got us asking. It got us answering. It got us thinking. The question we should all ask ourselves is,<em>Will LLMs do the same?</em></p>



<p class="wp-block-paragraph">I&#8217;ll leave you with this quote from Stack Overflow co-founder Jeff Atwood:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><a href="https://stackoverflow.blog/2008/11/25/stack-overflow-is-you/" rel="noopener">Stack Overflow is you.</a> This is the scary part, the great leap of faith that Stack Overflow is predicated on: trusting your fellow programmers. The programmers who choose to participate in Stack Overflow are the &#8220;secret sauce&#8221; that makes it work.</p>
</blockquote>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/stack-overflow-when-we-stop-asking/">Stack Overflow: When We Stop Asking</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/stack-overflow-when-we-stop-asking/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393076</post-id>	</item>
		<item>
		<title>Cross-Document View Transitions: The Gotchas Nobody Mentions</title>
		<link>https://css-tricks.com/cross-document-view-transitions-part-1/</link>
					<comments>https://css-tricks.com/cross-document-view-transitions-part-1/#comments</comments>
		
		<dc:creator><![CDATA[Durgesh Rajubhai Pawar]]></dc:creator>
		<pubDate>Mon, 18 May 2026 13:47:19 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393399</guid>

					<description><![CDATA[<p>This is Part 1 of a two-part series about cross-document view transitions, going over all the gotchas, from ditching the deprecated way to opt into them to a little-known 4-second timeout.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/cross-document-view-transitions-part-1/">Cross-Document View Transitions: The Gotchas Nobody Mentions</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 wasted an entire Saturday on this.</p>



<p class="wp-block-paragraph">Not a lazy Saturday either, but one of those rare, carved-out, &#8220;I&#8217;m finally going to build that thing&#8221; Saturdays. I&#8217;d seen <a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/" rel="noopener">Jake Archibald&#8217;s demos</a>. I&#8217;d watched the Chrome Dev Summit talk. I knew cross-document view transitions were real, that you could get those <a href="https://css-tricks.com/native-like-animations-for-page-transitions-on-the-web/">slick native-feeling page transitions on plain old multi-page sites</a> without a single framework. No React. No Astro. No client-side router pretending your multi-page application (MPA) is single-page application (SPA). Just HTML pages linking to other HTML pages, with the browser handling the animation between them. Hell yes.</p>



<p class="wp-block-paragraph">So I started building. And nothing worked.</p>



<p class="wp-block-paragraph">The first tutorial I found had me dropping <code>&lt;meta name="view-transition" content="same-origin"&gt;</code> into my <code>&lt;head&gt;</code>. Seemed simple enough. I added it to both pages, clicked my link, and&#8230; nothing. No transition. No error. Just a normal, instant page load like it was 2004. I opened DevTools, double-checked my syntax, restarted the server, tried Chrome Canary, cleared the cache. Nothing. I did what any self-respecting developer does at that point &#8211; I copied the code character by character from the blog post and pasted it in. Still nothing.</p>



<p class="wp-block-paragraph">I spent two hours convinced I was an idiot.</p>



<p class="wp-block-paragraph">Turns out that <code>&lt;meta&gt;</code> tag syntax? <a href="https://css-tricks.com/snippets/css/basic-view-transition/">Deprecated.</a> Gone. Chrome shipped it, then replaced it with a CSS-based opt-in, and half the internet&#8217;s tutorials still show the old way. Those older blog posts still rank well. They look authoritative. And they&#8217;re just wrong now. Not wrong because the authors were bad &#8211; wrong because the spec moved under everyone&#8217;s feet and nobody went back to update their posts.</p>



<p class="wp-block-paragraph">The other half of the tutorials I found were about same-document view transitions. SPA stuff. <code>document.startViewTransition()</code> called in JavaScript when you swap DOM content yourself, which is cool and useful but a completely different feature when you actually sit down to implement it. The API surface is different. The mental model is different. The gotchas are <em>very</em> different. And yet, Google &#8220;view transitions tutorial&#8221; and good luck figuring out which flavor you&#8217;re reading about until you&#8217;re three paragraphs deep.</p>



<p class="wp-block-paragraph">So if you&#8217;re here, I&#8217;m guessing you&#8217;ve been through some version of this. You tried the meta tag. It didn&#8217;t work. You tried the JavaScript API on a real multi-page site and realized it only fires within a single document. You maybe got something half-working in a demo but it fell apart the second you added real content â€” images stretching weird, transitions hanging for seconds with no explanation, or your CSS file turning into 200 lines of <a href="https://css-tricks.com/almanac/properties/v/view-transition-name/"><code>view-transition-name</code></a> declarations because you have a grid of 40 product cards. You blamed yourself. It wasn&#8217;t your fault. The documentation ecosystem around this feature is a mess right now, and the spec has been a moving target.</p>



<p class="wp-block-paragraph"><strong>This is Part 1 of a two-part series</strong>, and it&#8217;s the article I wish existed on that Saturday. We&#8217;re going to cover the actual current way to opt in with <a href="https://css-tricks.com/almanac/rules/v/view-transition/"><code>@view-transition</code></a> in CSS (not the meta tag, not JavaScript), then dig into the 4-second timeout that will silently kill your transitions on slow pages and how to debug it, then fix the aspect ratio warping that makes every image-heavy transition look like a fun house mirror, and finally get a proper handle on the <code>pagereveal</code> and <code>pageswap</code> events that give you programmatic control over the whole lifecycle.</p>



<p class="wp-block-paragraph">In Part 2, we&#8217;ll tackle the scaling problem &#8211; how to handle <code>view-transition-name</code> across dozens or hundreds of elements without your stylesheet becoming a disaster, the difference between <code>view-transition-name</code> and <a href="https://css-tricks.com/almanac/properties/v/view-transition-class/"><code>view-transition-class</code></a>, just-in-time naming patterns, and doing <a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/"><code>prefers-reduced-motion</code></a> the right way.</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>The Gotchas Nobody Mentions</strong> <em>(You are here!)</em></li>



<li><strong><a href="https://css-tricks.com/cross-document-view-transitions-scaling-across-hundreds-of-elements/">Scaling View Transitions Across Hundreds of Elements</a></strong></li>
</ol>
</div></div>



<p class="wp-block-paragraph">Grab coffee. Maybe a refill. This one&#8217;s dense and I&#8217;m not going to waste your time, but there&#8217;s a lot of ground here and none of it is obvious.</p>



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



<h2 class="wp-block-heading" id="the-old-way-is-dead">The Old Way is Dead</h2>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- THIS IS DEPRECATED - stop copying this from old tutorials -->
&lt;meta name="view-transition" content="same-origin"></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* THIS is the current opt-in - goes in your CSS */
@view-transition {
  navigation: auto;
}</code></pre>



<p class="wp-block-paragraph">Here&#8217;s the minimal setup. Two HTML files, one CSS rule on each. Note that, as of 2026, cross-document view transitions are supported in Chromium-based browsers and Safari 18.2+. Firefox support is in progress as I&#8217;m writing this.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019d4ecf-457e-77cb-8b8b-6c1796a9099d" src="//codepen.io/editor/anon/embed/019d4ecf-457e-77cb-8b8b-6c1796a9099d?height=450&amp;theme-id=1&amp;slug-hash=019d4ecf-457e-77cb-8b8b-6c1796a9099d&amp;default-tab=css,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019d4ecf-457e-77cb-8b8b-6c1796a9099d" title="CodePen Embed 019d4ecf-457e-77cb-8b8b-6c1796a9099d" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p class="wp-block-paragraph">That&#8217;s it. Two HTML files. One CSS rule on each. Click a link between them in a supporting browser (like modern Chromium or Safari 18.2+) and you get a smooth cross-fade. No JavaScript. No meta tags. No build step. The browser snapshots the old page, snapshots the new page, and animates between them automatically.</p>



<p class="wp-block-paragraph">Now, why did the spec move from a meta tag to a CSS at-rule? It wasn&#8217;t arbitrary.</p>



<p class="wp-block-paragraph">The meta tag was a blunt instrument. It was on or off for the entire page. You couldn&#8217;t say &#8220;enable transitions on desktop but not on mobile where the animations feel janky on low-end hardware.&#8221; You couldn&#8217;t conditionally opt in based on user preferences. It was just&#8230; there, or not.</p>



<p class="wp-block-paragraph">The CSS approach opens all of that up:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Only enable transitions if the user hasn't asked for reduced motion */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}</code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Only enable on viewports wide enough for the animation to feel good */
@media (min-width: 768px) {
  @view-transition {
    navigation: auto;
  }
}</code></pre>



<p class="wp-block-paragraph">That&#8217;s a real upgrade. You get the same conditional power you already have with every other CSS feature. Media queries, <a href="https://css-tricks.com/almanac/rules/s/supports/"><code>@supports</code></a>, whatever scoping logic you want — it all just works because the opt-in lives where your styles live.</p>



<p class="wp-block-paragraph">There&#8217;s also a subtlety that matters: the CSS rule can be different on the old page versus the new page. Both pages need to opt in for the transition to fire. If Page A has <code>@view-transition { navigation: auto; }</code> but Page B doesn&#8217;t, you get no transition. This is actually useful &#8211; it means your 404 page or your login redirect can skip transitions without any JavaScript coordination.</p>



<p class="wp-block-paragraph">One more thing worth noting here: <code>navigation: auto</code> only kicks in for user-initiated, same-origin navigations. If the user clicks a regular link or hits the browser&#8217;s Back button, you get a transition. But <code>window.location.href = "/somewhere"</code> set programmatically, or a cross-origin link, or a form submission with a <code>POST</code>? No transition. The browser is intentionally conservative about when it fires, and honestly that&#8217;s the right call. You don&#8217;t want a fancy cross-fade on a <code>POST</code> request that&#8217;s creating a payment.</p>



<p class="wp-block-paragraph">Look, if you&#8217;ve been following an outdated tutorial and your transitions just silently don&#8217;t work, this is almost certainly why. The meta tag <a href="https://developer.chrome.com/blog/new-in-chrome-111?hl=en" rel="noopener">shipped in Chrome 111</a>, got a few months of real-world use, and then the Chrome team deprecated it in favor of the CSS at-rule <a href="https://developer.chrome.com/release-notes/126" rel="noopener">starting around Chrome 126</a>. No console warning. No error. The old syntax just quietly does nothing now. Honestly, a deprecation warning in DevTools would&#8217;ve saved me (and probably you) a lot of grief, but here we are.</p>



<p class="wp-block-paragraph">Swap the meta tag for the CSS rule. That&#8217;s step one. Everything else in this article builds on it.</p>



<h2 class="wp-block-heading" id="your-transition-will-randomly-die-and-here-s-why">Your Transition Will Randomly Die, and Here&#8217;s Why</h2>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Drop this in your pages to see what's actually happening
window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) {
    console.log(
      "No view transition - page didn't opt in or browser skipped it",
    );
    return;
  } // This is the one that'll save your sanity

  event.viewTransition.finished
    .then(() => console.log("Transition completed &#x2705;"))
    .catch((err) => {
      // You'll see "TimeoutError" here and nowhere else
      console.error("Transition killed:", err.name, err.message);
    });
});</code></pre>



<p class="wp-block-paragraph">Here&#8217;s the thing nobody puts in their blog post: <strong>cross-document view transitions have a hard 4-second timeout.</strong> If the new page doesn&#8217;t reach a state the browser considers &#8220;renderable&#8221; within 4 seconds of the navigation starting, the transition just&#8230; dies. No animation. No cross-fade. The new page snaps in like view transitions don&#8217;t exist. And unless you&#8217;ve got that <code>pagereveal</code> listener wired up and your console open, you won&#8217;t get any indication that anything went wrong.</p>



<p class="wp-block-paragraph">Four seconds sounds generous, until it isn&#8217;t.</p>



<p class="wp-block-paragraph">Think about what happens on a real site. Your page loads. The HTML arrives, fine, that&#8217;s fast. But maybe you&#8217;ve got a big hero image that&#8217;s render-blocking. Maybe there&#8217;s a slow API call that your server waits on before sending the response &#8211; a product page hitting an inventory service, a dashboard waiting on analytics data, anything with server-side rendering that actually does work before responding. Maybe you&#8217;re on a decent connection but the page has three web fonts loading from Google Fonts with <code>font-display: block</code>. Any of these can push you past that 4-second window, and the timeout doesn&#8217;t care <em>why</em> you&#8217;re slow. It just cuts the transition.</p>



<p class="wp-block-paragraph">The really maddening part? It works perfectly on localhost. Your dev server responds in 80ms. The transition is butter. You deploy to production, your server&#8217;s cold-starting a lambda or your CDN cache missed, and suddenly users get zero transitions on the first click. You can&#8217;t reproduce it locally. You start questioning everything.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// You can also catch this on the OLD page using `pageswap`
// Useful for cleanup or logging which navigations fail
window.addEventListener("pageswap", (event) => {
  if (event.viewTransition) {
    event.viewTransition.finished.catch((err) => {
      // Log it, send it to your analytics, whatever
      console.warn("Outgoing transition aborted:", err.name);
    });
  }
});</code></pre>



<p class="wp-block-paragraph">So, what do you actually do about it?</p>



<p class="wp-block-paragraph"><strong>Option one: make your page faster.</strong> I know, groundbreaking advice. But seriously &#8211; if your cross-document transition is dying, that&#8217;s a signal your page load is genuinely slow. The timeout is acting as a performance canary. Look at your Performance tab in DevTools, run a Lighthouse audit (which may <a href="https://www.smashingmagazine.com/2024/11/why-optimizing-lighthouse-score-not-enough-fast-website/" rel="noopener">not be perfect</a>), figure out what&#8217;s blocking first render. This isn&#8217;t view-transition-specific advice, but the timeout forces you to care about it.</p>



<p class="wp-block-paragraph"><strong>Option two is more interesting</strong>, and it&#8217;s the thing I wish I&#8217;d known about immediately.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Note: rel="expect" is newer and browser support is rolling out -->
&lt;link rel="expect" href="#hero" blocking="render"></code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019d53a7-12ee-7d43-bafe-1f7350e0998e" src="//codepen.io/editor/anon/embed/019d53a7-12ee-7d43-bafe-1f7350e0998e?height=450&amp;theme-id=1&amp;slug-hash=019d53a7-12ee-7d43-bafe-1f7350e0998e&amp;default-tab=html,result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019d53a7-12ee-7d43-bafe-1f7350e0998e" title="CodePen Embed 019d53a7-12ee-7d43-bafe-1f7350e0998e" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



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



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;link rel="expect" href="#hero" blocking="render"></code></pre>



<p class="wp-block-paragraph">&#8230;tells the browser: &#8220;Don&#8217;t consider this page renderable until an element matching <code>#hero</code> is in the DOM.&#8221; That sounds like it would make things <em>slower</em>, and in a way it does &#8211; it delays first paint. But for view transitions, that&#8217;s exactly what you want: you&#8217;re telling the browser to hold the snapshot until the important content is actually there, rather than snapping a screenshot of a half-loaded page or, worse, timing out because some image in the footer is still downloading and blocking something.</p>



<p class="wp-block-paragraph">It&#8217;s a trade-off. You&#8217;re choosing a slightly delayed, but smooth, transition over a fast, but-broken, one.</p>



<p class="wp-block-paragraph">Honestly, the 4-second limit is probably the right call from the browser&#8217;s perspective. You don&#8217;t want a user clicking a link and staring at a frozen page for 10 seconds while the browser waits to do a fancy animation. At some point, just showing the damn page is better than a pretty transition. But I wish Chrome would surface the timeout more visibly &#8211; a DevTools warning, a performance marker, <em>something</em>. Right now it fails silently and that&#8217;s the whole problem.</p>



<p class="wp-block-paragraph">One more thing worth knowing: the timeout clock starts when navigation begins, not when the new page&#8217;s HTML starts arriving. Network latency counts. The Time to First Byte (TTFB) Core Web Vital counts. If your server takes 2 seconds to respond and your page takes 2.5 seconds to render after that, you&#8217;re over the limit even though neither half feels slow on its own.</p>



<p class="wp-block-paragraph">A debugging tip that&#8217;s saved me more than once: Chrome&#8217;s DevTools has an Animations panel (it&#8217;s under &#8220;More tools&#8221; if you don&#8217;t see it) that can actually capture view transitions in action. You can slow them down to 10% speed, replay them, and inspect the pseudo-element tree mid-animation. It&#8217;s not obvious that it works for view transitions, but it does. Between that and the <code>pagereveal</code> listener above, you can diagnose most timeout issues pretty quickly.</p>



<p class="wp-block-paragraph">Put that <code>pagereveal</code> listener in early. Watch your console during testing. You&#8217;ll thank yourself later.</p>



<h2 class="wp-block-heading" id="why-your-images-look-like-taffy">Why Your Images Look Like Taffy</h2>



<p class="wp-block-paragraph">This one&#8217;s easier to show with a same-document demo first (since you can actually run it in a single file), but the problem and the fix are identical for cross-document transitions.</p>



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



<p class="wp-block-paragraph">Run that. Click the image. Watch the dog turn into silly putty.</p>



<p class="wp-block-paragraph">The image itself has <code>object-fit: cover</code> on both sides. The thumbnail looks fine, the hero looks fine. But during the transition? The browser doesn&#8217;t transition your <code>&lt;img&gt;</code> element. It takes a <em>screenshot</em> of the old state, takes a screenshot of the new state, and morphs between them. Those screenshots are flat raster images. Your carefully applied <a href="https://css-tricks.com/almanac/properties/o/object-fit/"><code>object-fit</code></a>? Gone. The browser is just scaling a bitmap from one box size to another, and when a 150Ã—150 square gets stretched into a 600Ã—300 rectangle, you get taffy.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* THE FIX - target the transition pseudo-elements directly */
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
  /* Treat the snapshot like an image in a container - crop, don't stretch */
  object-fit: cover;
  overflow: hidden;
}</code></pre>



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



<p class="wp-block-paragraph">That&#8217;s the whole thing. Two properties on two pseudo-elements.</p>



<p class="wp-block-paragraph">What&#8217;s actually happening: the browser generates a tree of pseudo-elements for every named transition. For an element with <code>view-transition-name: hero-img</code>, you get this structure during the animation:</p>



<pre rel="" class="wp-block-csstricks-code-block language-none" data-line=""><code markup="tt">::view-transition
└── ::view-transition-group(hero-img)
    ├── ::view-transition-old(hero-img)
    └── ::view-transition-new(hero-img)</code></pre>



<p class="wp-block-paragraph">The <code>::view-transition-group</code> smoothly animates its width and height from the old dimensions to the new ones. That&#8217;s the morphing rectangle you see. Inside it, the <code>old</code> and <code>new</code> pseudo-elements hold the actual bitmap snapshots, and by default they&#8217;re set to <code>object-fit: fill</code> &#8211; meaning &#8220;stretch to fill whatever box you&#8217;re in, aspect ratio be damned.&#8221;</p>



<p class="wp-block-paragraph">Switching to <code>object-fit: cover</code> tells those snapshots to maintain their aspect ratio and crop the overflow instead. Same mental model as a background image with <code>background-size: cover</code>. The transition still animates the box from square to rectangle (or whatever your shapes are), but the image inside crops gracefully instead of warping.</p>



<p class="wp-block-paragraph">You could also use <code>object-fit: contain</code> here if you&#8217;d rather see the full image with letterboxing instead of cropping. It depends on what looks right for your content. But <code>cover</code> is what you&#8217;ll want 90% of the time, especially for product images and hero shots.</p>



<p class="wp-block-paragraph">For cross-document transitions, the CSS is identical &#8211; you just put it in both pages&#8217; stylesheets:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* This works cross-document. Same selectors, same fix. */
/* Put it in your shared CSS file that both pages load. */
@view-transition {
  navigation: auto;
}

::view-transition-old(hero-img),
::view-transition-new(hero-img) {
  object-fit: cover;
}

/* You can also control the animation timing on the group */
::view-transition-group(hero-img) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}</code></pre>



<p class="wp-block-paragraph">Honestly, I think <code>object-fit: cover</code> should be the <em>default</em> on these pseudo-elements instead of <code>fill</code>. I get why the spec chose <code>fill</code> &#8211; it&#8217;s predictable, it matches what <code>object-fit</code> defaults to on replaced elements everywhere else in CSS &#8211; but in practice, how often do you actually <em>want</em> a stretched bitmap during a transition? Almost never. You&#8217;ll be adding this override on basically every image transition you build.</p>



<p class="wp-block-paragraph">One more variant that&#8217;s useful when the aspect ratios are wildly different &#8211; say a tall portrait thumbnail transitioning into a cinematic widescreen hero:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Fine-tune where the crop happens on each side of the transition */
::view-transition-group(hero-img) {
  overflow: hidden;
  border-radius: 8px; /* keep it pretty mid-flight */
}

::view-transition-old(hero-img) {
  object-fit: cover;
  object-position: center center;
}

::view-transition-new(hero-img) {
  object-fit: cover;
  object-position: center top; /* keep the top of the hero visible */
}</code></pre>



<p class="wp-block-paragraph">You can set different <code>object-position</code> values on old versus new, which lets you control where the crop happens on each side of the transition independently. The old thumbnail might look best cropped from center. The new hero might need to anchor to the top. Mix and match.</p>



<p class="wp-block-paragraph">This took me an embarrassingly long time to figure out. The fix is two lines of CSS, but if you don&#8217;t know the pseudo-element tree exists, you don&#8217;t even know what to target. Now you do.</p>



<h2 class="wp-block-heading" id="the-two-events-that-tie-it-all-together">The Two Events That Tie it All Together</h2>



<p class="wp-block-paragraph">You&#8217;ve already seen <code>pagereveal</code> and <code>pageswap</code> show up in the code above, but let&#8217;s take a step back and talk about what they actually are. Understanding these two events is going to be important, because in Part 2 we&#8217;ll lean on them heavily for the just-in-time naming pattern that makes view transitions actually scale.</p>



<p class="wp-block-paragraph">Cross-document view transitions happen across two pages that have no JavaScript connection to each other. Page A doesn&#8217;t know about Page B&#8217;s DOM. Since the old and new pages have no way to communicate directly, these events are your only way to coordinate the transition on both sides. Page B didn&#8217;t exist when Page A was running. So how do you coordinate anything? How do you decide which elements to name, or customize the transition based on where the user is heading?</p>



<p class="wp-block-paragraph">That&#8217;s what these two events are for. They&#8217;re your hooks into the transition lifecycle, one on each side of the navigation.</p>



<p class="wp-block-paragraph"><strong><code>pageswap</code></strong> fires on the <strong>outgoing page</strong>, right before it gets replaced. This is your last chance to touch the old page&#8217;s DOM before the browser snapshots it. The event gives you two key properties:</p>



<ul class="wp-block-list">
<li><strong><code>event.viewTransition</code>:</strong> the ViewTransition object for this navigation, or <code>null</code> if no transition is happening.</li>



<li><strong><code>event.activation</code>:</strong> a NavigationActivation object that tells you <em>where the user is going</em>.</li>
</ul>



<p class="wp-block-paragraph">That <code>activation</code> property is the really useful one. <code>event.activation.entry.url</code> gives you the destination URL, and <code>event.activation.navigationType</code> tells you whether it&#8217;s a <code>push</code>, <code>replace</code>, <code>traverse</code> (back/forward), or <code>reload</code>. This means you can customize the outgoing side of the transition based on the destination. On a product listing page, for example, you can check which product the user clicked, find the matching card, and assign a <code>view-transition-name</code> to just that element right before the snapshot happens.</p>



<p class="wp-block-paragraph"><strong><code>pagereveal</code></strong> fires on the <strong>incoming page</strong>, right after the page becomes active but while the transition is still running. This is your chance to set up the new side. The event gives you:</p>



<ul class="wp-block-list">
<li><strong><code>event.viewTransition</code>:</strong> same deal, the ViewTransition object or <code>null</code>.</li>
</ul>



<p class="wp-block-paragraph">On the incoming page, you check where the user came <em>from</em> using <code>navigation.activation.from.url</code> (via the Navigation API), and you read the current URL from <code>window.location</code>. Between those two pieces of information, you know exactly what kind of navigation just happened and can set up the incoming page&#8217;s transition elements accordingly.</p>



<p class="wp-block-paragraph">Here&#8217;s the full lifecycle in order:</p>



<ol class="wp-block-list">
<li>User clicks a link on Page A.</li>



<li><code>pageswap</code> fires on Page A. This is your window to name elements and customize outgoing state.</li>



<li>Browser snapshots the old page (capturing any named elements).</li>



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



<li><code>pagereveal</code> fires on Page B. You can name elements, customize incoming state.</li>



<li>Browser snapshots the new page.</li>



<li>Transition animates between the two snapshots.</li>



<li><code>viewTransition.finished</code> resolves (or rejects) on both sides.</li>
</ol>



<p class="wp-block-paragraph">Three things to keep in mind with these events:</p>



<p class="wp-block-paragraph"><strong>First, always guard with <code>if (!event.viewTransition) return</code> at the top of your handlers.</strong> <code>pagereveal</code> actually fires on <em>every</em> navigation &#8211; initial page load, back/forward, the works &#8211; not just view transitions. If there&#8217;s no transition happening, <code>event.viewTransition</code> will be <code>null</code>, and your handler should bail out gracefully. These handlers are transition sugar, not application logic. Never put side effects in them that you <em>need</em> for the page to work.</p>



<p class="wp-block-paragraph"><strong>Second, <code>pageswap</code> only fires if the old page opted into view transitions and the navigation is same-origin.</strong> If the user middle-clicks to open in a new tab, or the navigation goes cross-origin, the event either won&#8217;t fire or <code>event.viewTransition</code> will be <code>null</code>. That&#8217;s fine, your guard clause handles it.</p>



<p class="wp-block-paragraph"><strong>Third, and this is easy to overlook:</strong> both events give you access to <code>viewTransition.finished</code>, which is a promise that resolves when the transition completes or rejects if something goes wrong (like a timeout). Always use this for cleanup, as in removing <code>view-transition-name</code> values you set dynamically, resetting state, whatever. Stale names from a previous transition will ruin your next one.</p>



<p class="wp-block-paragraph">We&#8217;ve been using these events lightly so far &#8211; a <code>pagereveal</code> listener to catch timeouts, a <code>pageswap</code> listener for logging. In Part 2 of this little series, they become the backbone of the whole scaling strategy. Stay tuned.</p>



<h2 class="wp-block-heading" id="what-s-next">What&#8217;s Next</h2>



<p class="wp-block-paragraph">That covers the three gotchas that&#8217;ll bite you first: the deprecated meta tag that silently does nothing, the 4-second timeout that kills transitions without telling you, and the image distortion that turns every aspect ratio change into a fun house mirror. Plus the two events that give you hooks into the whole lifecycle.</p>



<p class="wp-block-paragraph">In Part 2, we&#8217;ll tackle the scaling problem. When you&#8217;ve got a grid of 48 product cards and each one needs a unique <code>view-transition-name</code>, how do you keep your CSS from exploding? The answer involves <code>view-transition-class</code> (which is different from <code>view-transition-name</code> in ways that aren&#8217;t obvious), a just-in-time naming pattern using the <code>pageswap</code> and <code>pagereveal</code> events we just covered. And one critical note: we&#8217;ll cover <code>prefers-reduced-motion</code> in Part 2, but if you take nothing else from this series, take this: animations can literally make people physically nauseous. Always check that preference and respect it.</p>



<p class="wp-block-paragraph">The gotchas are behind you. Now it&#8217;s time to make it scale.</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>The Gotchas Nobody Mentions</strong> <em>(You are here!)</em></li>



<li><strong><a href="https://css-tricks.com/cross-document-view-transitions-scaling-across-hundreds-of-elements/">Scaling View Transitions Across Hundreds of Elements</a></strong></li>
</ol>
</div></div>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/cross-document-view-transitions-part-1/">Cross-Document View Transitions: The Gotchas Nobody Mentions</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-1/feed/</wfw:commentRss>
			<slash:comments>10</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393399</post-id>	</item>
		<item>
		<title>What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More</title>
		<link>https://css-tricks.com/whats-important-11/</link>
					<comments>https://css-tricks.com/whats-important-11/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 15 May 2026 13:16:34 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394879</guid>

					<description><![CDATA[<p>If 3D voxel scenes (that you can style), flying focus animations, or new CSS syntaxes sound like your kinda thing, then this issue of What’s !important is definitely for you.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-11/">What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, 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">If 3D voxel scenes (that you can style), flying focus animations, or new CSS syntaxes sound like your kinda thing, then this issue of <strong>What’s !important</strong> is definitely for you.</p>



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



<p class="wp-block-paragraph">Also featuring Polypane, scroll-driven animations, and the latest web platform updates from Chrome 148 and Safari 26.5.</p>



<h2 id="heerichjs-for-3d-voxel-scenes" class="wp-block-heading">Heerich.js for 3D voxel scenes</h2>



<p class="wp-block-paragraph">Inspired by the <a href="https://www.google.com/search?q=erwin+heerich+sculptures&amp;udm=2" rel="noopener">sculptures of Erwin Heerich</a>, <a href="https://elastiq.ch/" rel="noopener">David Aerne</a> created <a href="https://meodai.github.io/heerich/" rel="noopener">Heerich.js</a>, a tiny engine for creating 3D voxel scenes. They’re rendered as SVG, and because we can use CSS variables in SVG, the scenes are basically styleable using CSS.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="642" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1.png?resize=1024%2C642&#038;ssl=1" alt="A clean, minimal isometric visualization of a large 3D grid with a small black cube, accompanied by explanatory text about alignment logic and visual styles." class="wp-image-394882" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1-scaled.png?resize=768%2C482&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1-scaled.png?resize=1536%2C963&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/1-scaled.png?resize=2048%2C1285&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h2 id="polypane-snippets" class="wp-block-heading">Polypane snippets</h2>



<p class="wp-block-paragraph">Polypane, widely considered to be the best browser for web development, launched a <a href="https://polypane.app/snippets/" rel="noopener">snippet store</a>. So, if you wanted to click on a component and copy the basic HTML without all of the “bloaty crap”, the <a href="https://polypane.app/snippets/?snippet=1-click-de-crapulator" rel="noopener">1-Click De-crapulator</a> is what you’d want. Fantastic name, by the way.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="642" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2.png?resize=1024%2C642&#038;ssl=1" alt="A card-based web interface for the Polypane Snippet Store featuring a sidebar with various filters and a collection of snippets for web development and accessibility." class="wp-image-394883" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2-scaled.png?resize=768%2C482&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2-scaled.png?resize=1536%2C963&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/2-scaled.png?resize=2048%2C1285&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h2 id="animating-focus-with-view-transitions" class="wp-block-heading">Animating focus with view transitions</h2>



<p class="wp-block-paragraph">Chris Coyier showed us <a href="https://frontendmasters.com/blog/animating-focus-with-view-transitions/" rel="noopener">how to animate focus with view transitions</a>. He also contrasted “unnecessary motion” with WebAIM’s conditional <code>prefers-reduced-motion</code> implementation, which I think is the right approach, because I find it difficult to keep track of focus even when it’s really visible.</p>



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



<p class="wp-block-paragraph">Either way, it’s a fantastic exploration of techniques. In addition, way down in the comments, Kilian Valkhof (founder of Polypane, actually) shared his <a href="https://polypane.app/blog/css-only-floating-focus-with-anchor-positioning/" rel="noopener">CSS-only technique for floating focus</a> (or, as Chris calls it, “flying focus”).</p>



<h2 id="the-of-ltselectorgt-syntax" class="wp-block-heading">The <code>of &lt;selector&gt;</code> syntax</h2>



<p class="wp-block-paragraph"><a href="https://bsky.app/profile/pawelgrzybek.com/post/3ml4nheptqc2z" rel="noopener">Paweł Grzybek mentioned</a> that the <code>of &lt;selector&gt;</code> syntax is actually well supported (Baseline) now, but honestly, I hadn’t even heard of it.</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:mdzzw3bmt7jyif4bcvuyeboj/app.bsky.feed.post/3ml4nheptqc2z" data-bluesky-cid="bafyreigvvjuozllslr6jdvncmpm6iv3v575hesa2ck2uls32nj6vcpm7ya" data-bluesky-embed-color-mode="system"><p lang="en">I knew that the CSS :nth-child(n of selector) is a thing, but I didn’t know how well supported it is nowadays. Another thing I didn’t know is that I can use CSS nesting with it like in the example below. Modern CSS is incredible &#x2763;&#xfe0f;

developer.mozilla.org/en-US/docs/W&#8230;

#css<br><br><a href="https://bsky.app/profile/did:plc:mdzzw3bmt7jyif4bcvuyeboj/post/3ml4nheptqc2z?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; Paweł Grzybek (<a href="https://bsky.app/profile/did:plc:mdzzw3bmt7jyif4bcvuyeboj?ref_src=embed" rel="noopener">@pawelgrzybek.com</a>) <a href="https://bsky.app/profile/did:plc:mdzzw3bmt7jyif4bcvuyeboj/post/3ml4nheptqc2z?ref_src=embed" rel="noopener">17:51 · May 5, 2026</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>



<p class="wp-block-paragraph">The following selector means, “from all siblings, select the second <code>.intro</code>, but only if it’s a <code>&lt;div&gt;</code>.” It’s kind of like <code>div:nth-of-type(2)</code>, except <em>that</em> can only select elements of the same type, whereas <code>of &lt;selector&gt;</code> works with any selector.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">div:nth-child(2 of .intro) {
  /* ... */
}</code></pre>



<p class="wp-block-paragraph">Given that <code>&amp;</code> is equivalent to the <em>parent selector</em> (so, <code>.intro</code>), the second example means “from all siblings, select the second <code>.intro</code> within <code>.intro</code>. Also, because there isn’t anything before <code>:nth-child()</code>, <code>.intro</code> can be anything this time around.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.intro {
  :nth-child(2 of &amp;) {
    /* ... */
  }
}</code></pre>



<p class="wp-block-paragraph">There’s so much happening with CSS right now, so I find it really useful when something I’ve missed comes back around like this. Though funnily enough, as I’m typing this, I’m seeing that <a href="https://css-tricks.com/author/preethi/">Preethi Sam</a> wrote an article on the <a href="https://frontendmasters.com/blog/css-n-of-selectors-for-conditional-validation/" rel="noopener"><code>of &lt;selector&gt;</code> syntax</a> a bit over a week ago (<em>*adds to reading list*</em>).</p>



<h2 id="understanding-the-range-syntax" class="wp-block-heading">Understanding the range syntax</h2>



<p class="wp-block-paragraph">The range syntax is a new(ish), more readable syntax with comparison operators (<code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, and <code>&lt;=</code>) for media queries and container queries. Ahmad Shadeed expertly explained <a href="https://ishadeed.com/article/range-syntax/" rel="noopener">how the range syntax works</a>, but keep a close eye on browser support. Web browsers are still shipping container queries and the range syntax for those queries has to be shipped independently. For example, container style queries are shipping in Firefox 151 next week, but the range syntax for container style queries will ship with a flag.</p>



<p class="wp-block-paragraph">It’s an easy thing to miss (don’t ask me how I know).</p>



<h2 id="understanding-scrolldriven-animations" class="wp-block-heading">Understanding scroll-driven animations</h2>



<p class="wp-block-paragraph">Scroll-driven animations can be kinda tough (especially those with <code>view()</code> timelines), but Josh Comeau’s expert <a href="https://www.joshwcomeau.com/animation/scroll-driven-animations/" rel="noopener">explanation of scroll-driven animations</a> makes them so much easier to understand. With scroll-<em>triggered</em> animations on the way, I highly recommend mastering scroll-driven animations first (if you haven’t already). Again, don’t ask me how I know (<em>*cries in CSS*</em>).</p>



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



<ul class="wp-block-list">
<li><a href="https://developer.chrome.com/release-notes/148" rel="noopener">Chrome 148</a>
<ul class="wp-block-list">
<li>Name-only <a href="https://css-tricks.com/css-container-queries/">container queries</a> (now Baseline)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/revert-rule" rel="noopener"><code>revert-rule</code></a> keyword (no <em>stable</em> Safari support)</li>



<li><a href="https://css-tricks.com/almanac/rules/s/supports/#the-future-and-present-of-feature-support-queries:~:text=%40supports%20at%2Drule(%40layer)%20%7B%0A%20%20/*%20%40layer%20is%20supported%20*/%0A%7D"><code>at-rule()</code></a> function for feature queries (no Safari or Firefox support)</li>



<li><a href="https://web.dev/articles/browser-level-image-lazy-loading" rel="noopener"><code>loading</code></a> attribute for <code>&lt;video&gt;</code>/<code>&lt;audio&gt;</code> (no Safari or Firefox support)</li>
</ul>
</li>



<li><a href="https://webkit.org/blog/17938/webkit-features-for-safari-26-5/" rel="noopener">Safari 26.5</a>
<ul class="wp-block-list">
<li><a href="https://css-tricks.com/almanac/pseudo-selectors/o/open/"><code>:open</code></a> pseudo-class (now Baseline)</li>



<li>Updated <a href="https://css-tricks.com/almanac/functions/r/random/"><code>random()</code></a> function (no Chrome or Firefox support)</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Inspired by <a href="https://bsky.app/profile/georgerodier.com/post/3mkptwxibxc2u/" rel="noopener">this lovely comment</a>, we’d just like to thank our authors for all of the incredible work that they do, as well as the many other educators out there that we undoubtably learn from and become inspired by. Keep on keeping on, CSS-Tricksters!</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:dk66belhjtfadbud2kpbq6sc/app.bsky.feed.post/3mkptwxibxc2u" data-bluesky-cid="bafyreiaccutehsfigxok6x7mpz3s74odj6lnx3iz26sfkt2uno3azz6ka4" data-bluesky-embed-color-mode="system"><p lang="en">I&#x27;m not a CSS expert, but expert CSS educators are my favorite. Their design skills, ability to take full advantage of the web platform, AND general enthusiasm for building for the web is unmatched by other types of developers!</p>&mdash; George Rodier (<a href="https://bsky.app/profile/did:plc:dk66belhjtfadbud2kpbq6sc?ref_src=embed" rel="noopener">@georgerodier.com</a>) <a href="https://bsky.app/profile/did:plc:dk66belhjtfadbud2kpbq6sc/post/3mkptwxibxc2u?ref_src=embed" rel="noopener">15:42 · Apr 30, 2026</a></blockquote>



<p class="wp-block-paragraph">Until next time!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-11/">What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, 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-11/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394879</post-id>	</item>
		<item>
		<title>Computing and Displaying Discounted Prices in CSS</title>
		<link>https://css-tricks.com/computing-and-displaying-discounted-prices-in-css/</link>
					<comments>https://css-tricks.com/computing-and-displaying-discounted-prices-in-css/#comments</comments>
		
		<dc:creator><![CDATA[Preethi]]></dc:creator>
		<pubDate>Thu, 14 May 2026 14:05:01 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[CSS functions]]></category>
		<category><![CDATA[math functions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393498</guid>

					<description><![CDATA[<p>A clever use of CSS to calculate and display a discounted product price by providing a base price and discount amount, featuring modern CSS features like <code>attr()</code>, <code>mod()</code>, and <code>round()</code>.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/computing-and-displaying-discounted-prices-in-css/">Computing and Displaying Discounted Prices in CSS</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">CSS math isn’t just about how things look! It can also be used to work out useful numeric information. For instance, you could calculate and show the percentage of tasks completed in a to-do list with CSS, helping users keep track of their progress. No need for script or server computation. No latency. No use of additional browser resources.</p>



<p class="wp-block-paragraph">Working with math has become much simpler and more flexible. I’m going to give you an example using CSS to calculate and display a discounted price whenever you need it, using the base price and discount provided. It’s the sort of thing you see often on e-commerce sites where heavy JavaScript is used to show a product’s full price, its discount amount, and its sale price.</p>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2410" height="1032" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=2410%2C1032&#038;ssl=1" alt="A four column row of product cards showing sale clothing from Gap. Model photos are on top, followed by the product name, price, and sale price." class="wp-image-393499" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?w=2410&amp;ssl=1 2410w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=300%2C128&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=1024%2C438&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=768%2C329&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=1536%2C658&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_7E3C7C97B7D8D52C02F5092EAFB87771069C4AA5BAFDB9C502DA16E57E7200D2_1776091945816_gsp-sale-prices.png?resize=2048%2C877&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Screenshot taken from <a href="https://gap.com" rel="noopener">gap.com </a></figcaption></figure>



<p class="wp-block-paragraph">We can absolutely do that in CSS:</p>



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



<p class="wp-block-paragraph">It does rely on some bleeding-edge features that are waiting to gain more browser support, but I think it’s still a good exercise to dig into how we will eventually be able to put these things in practice and eventually use them in our everyday work.</p>



<p class="wp-block-paragraph">Here’s how I put it together.</p>



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



<p class="wp-block-paragraph">The interface in this specific demo displays a list of streaming services for the user to choose from — Netflix, Disney+, <del>HBO</del>, <del>HBO Now</del>, <del>HBO Go</del>, HBO Max, etc. There’s a student discount offer on each subscription that takes a certain percentage amount off the full price.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;li>
  &lt;!-- Service name, base price, and selection toggle -->
  &lt;label>
    &lt;span>Netflix&lt;/span>
    &lt;!-- data-price and data-discount store base price and discount offered -->
    &lt;div class="ott-price" data-price="7.99" data-discount="0.2">$7.99&lt;/div>
    &lt;!-- Checkbox to track if the user wants to add this service -->
    &lt;input type="checkbox" class="is-ott-selected">
  &lt;/label>

  &lt;!-- Toggle for the student discount -->
  &lt;label>
    &lt;span>Apply Student Discount &lt;br> 20%&lt;/span>
    &lt;input type="checkbox" class="is-ott-discounted">
  &lt;/label>
&lt;/li>

&lt;!-- etc. --></code></pre>



<p class="wp-block-paragraph">The base price and discount are included as <code>data-*</code> attributes in the element displaying the price. Just remember, the discount only kicks in when you select “Apply Student Discount,” and then you’ll see how much the price is after the discount is applied.</p>



<h2 class="wp-block-heading" id="calculating-the-price-cut">Calculating the price cut</h2>



<p class="wp-block-paragraph">When the discount kicks in, the first step is to slash the base price with a line across it.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* When the discount toggle is checked inside the .ott container */
.ott:has(.is-ott-discounted:checked) {
  /* Strike through the original price */
  .ott-price {
    text-decoration: line-through;
  }
}</code></pre>



<p class="wp-block-paragraph">Next, let’s figure out the new discounted price using the <code>data-price</code> and <code>data-discount</code> values.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.ott:has(.is-ott-discounted:checked) {
  .ott-price {
    text-decoration: line-through;
    /* 
        Calculate the new price from the data-* attributes:
        Original Price * (1 - Discount Applied)
    */
    --n: calc(attr(data-price number) * (1 - attr(data-discount number)));
  }
}</code></pre>



<p class="wp-block-paragraph">The <a href="https://css-tricks.com/almanac/functions/a/attr/"><code>attr(&lt;name&gt; &lt;type&gt;)</code> syntax</a> is relatively new. The function used to only work with the <code>content</code> property, but now supports any CSS property… and parses values into a range of data types, whereas before they were always parsed as strings.</p>



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



<ol class="wp-block-list">
<li><strong><code>&lt;name&gt;</code>:</strong> This is the name of the HTML attribute we want to look at (like <code>href</code>, <code>data-count</code>, or <code>title</code>).</li>



<li><strong><code>&lt;type&gt;</code>:</strong> This tells CSS how to &#8220;read&#8221; the value (like a <code>color</code>, a <code>number</code>, or a <code>length</code>). It’s the newer superpower that makes the work we’re doing here possible.</li>
</ol>



<p class="wp-block-paragraph">In our case, we’re using the function to parse both <code>data-price</code> and <code>data-discount</code> into <code>numbers</code>, and then we subtract the discount from the price with CSS math-iness.</p>



<p class="wp-block-paragraph">The upgraded <code>attr()</code> is super cool, but not Baseline as I’m writing this, so keep an eye on it.</p>




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



<h2 class="wp-block-heading" id="showing-the-discounted-price">Showing the discounted price</h2>



<p class="wp-block-paragraph">Here’s how we display the updated price once the discount is applied:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.ott:has(.is-ott-discounted:checked) {
  .ott-price {
    text-decoration: line-through;
    --n: calc(attr(data-price number) * (1 - attr(data-discount number)));

    &amp;::after {
      display: inline-block;
      /* Splits the variable --n into two counters: 
          'a' for the whole number (in dollars) and 'b' for the decimals (in cents) */
      counter-set: a calc(round(down, var(--n))) b calc((mod(var(--n), 1)) * 100);
      /* Output: two spaces (\2000), a dollar sign ($), the number, a dot, and the decimals */
      content: "\2000\2000$" counter(a) "." counter(b, decimal-leading-zero);
    }
  }
}</code></pre>



<p class="wp-block-paragraph">The <a href="https://css-tricks.com/styling-counters-in-css/"><code>counter()</code> function</a> helps us turn the numeric value of the <code>--n</code> varable into a <code>content</code> string. Since CSS counters can’t handle decimals (they round the value by default), we treat the numbers before and after the decimal as separate counters and then combine them as strings, adding a dot between them.</p>



<ol class="wp-block-list">
<li><strong><code>calc(round(down, var(--n)))</code></strong> takes the variable <code>--n</code> and rounds it down to get the whole dollar amount (stored as <code>counter(a)</code>).</li>



<li><strong><code>calc((mod(var(--n), 1)) * 100)</code></strong> uses the modulo <a href="https://css-tricks.com/almanac/functions/m/mod/"><code>mod()</code></a> function to isolate the fraction, then multiplies it by <code>100</code> to get the cents (stored as <code>counter(b)</code>).</li>



<li>The <code>content</code> property inserts a dollar sign before the two counters and then joins them with a dot.</li>
</ol>



<p class="wp-block-paragraph">We know that <a href="https://css-tricks.com/almanac/functions/c/calc/"><code>calc()</code></a> has plenty of browser support. And guess what? The <code>mod()</code> function is newly Baseline!</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="round-mod-rem"></baseline-status>



<p class="wp-block-paragraph">That’s only if you need decimals and all that. If you’re rounding prices, this would be plenty enough:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">counter-set: price calc(var(--n));
content: counter(price);</code></pre>



<p class="wp-block-paragraph">Here’s the demo once again:</p>



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



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



<p class="wp-block-paragraph">So, there we have it, a working combination of newer CSS features (the upgraded <code>attr()</code> function), CSS math functions (<code>mod()</code>, <code>round()</code>), and custom counters to nail down something that we see in so many websites, only without scripts. When <code>attr()</code>&#8216;s support for data types becomes a thing in all browsers, this is something you can use in your everyday work.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/computing-and-displaying-discounted-prices-in-css/">Computing and Displaying Discounted Prices in CSS</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/computing-and-displaying-discounted-prices-in-css/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393498</post-id>	</item>
		<item>
		<title>rotateX()</title>
		<link>https://css-tricks.com/almanac/functions/r/rotatex/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 13 May 2026 14:36:54 +0000</pubDate>
				<category><![CDATA[transform]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=393625</guid>

					<description><![CDATA[<p>The <code>rotateX()</code> function rotates an element around the x-axis in a three-dimensional space</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatex/">rotateX()</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>rotateX()</code>&nbsp;function rotates an element around the x-axis in a three-dimensional space. Specifically, it vertically flips the element, making it tilt backward or forward, depending on the angle set. It is one of many transform functions used in the&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform/"><code>transform</code></a>&nbsp;property.</p>



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



<p class="is-style-explanation wp-block-paragraph">The x-axis is the axis of rotation, so the element turns vertically. Imagine a pin is stuck to the left side of an element and it can only turn up or down.</p>



<p class="wp-block-paragraph">In the demo below,&nbsp;<code>rotateX(0)</code>&nbsp;is given as the element&#8217;s default rotation:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.demo-element {
  transform: rotateY(var(--deg));
  transition: transform 0.3s ease;
}</code></pre>



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



<p class="wp-block-paragraph">The&nbsp;<code>rotateX()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatex" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;specification.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">rotateX() = rotateX( [ &lt;angle> | &lt;zero> ] )</code></pre>



<h2 class="wp-block-heading" id="argument">Arguments</h2>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* angle in degrees */
rotateX(45deg) /* rotates 45 degrees backwards */
rotateX(-90deg) /* rotates 90 degrees forwards */

/* angle in turns */
rotateX(0.5turn) /* rotates 180 degrees (half a full turn) */
rotateX(1turn)   /* Rotates a full 360-degree turn */

/* angle in radians */
rotateX(1.57rad) /* Approximately 90 degrees */

/* angle in gradians */
rotate(200grad)  /* rotates 180 degrees */</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotateX()</code>&nbsp;function takes a single&nbsp;<a href="https://drafts.csswg.org/css-values-4/#angles" rel="noopener"><code>&lt;angle&gt;</code></a>&nbsp;argument, which defines how much the element is rotated around its vertical axis.</p>



<ul class="wp-block-list">
<li><strong><code>&lt;angles&gt;</code>:</strong> values like&nbsp;<code>45deg</code>,&nbsp;<code>0.5turn</code>,&nbsp;<code>-90deg</code>,&nbsp;<code>1.57rad</code>, etc. can be passed.</li>



<li>A positive angle tilts the top of the element toward the back and the bottom toward the front.</li>



<li>While a negative angle does otherwise: it tilts the element&#8217;s top towards you, and its bottom away from you.</li>
</ul>



<p class="wp-block-paragraph">The&nbsp;<code>&lt;angle&gt;</code>&nbsp;type can be one of four units:</p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>deg</code>:</strong> One degree is&nbsp;<code>1/360</code>&nbsp;of a full circle.</li>



<li><strong><code>grad</code>:</strong> One gradian is&nbsp;<code>1/400</code>&nbsp;of a full circle.</li>



<li><strong><code>rad</code>:</strong> A radian is the length of a circle&#8217;s diameter around the shape&#8217;s arc. One radian is&nbsp;<code>180deg</code>, or&nbsp;<code>1/2</code>&nbsp;of a full circle. One full circle is 2π radians, which is equal to&nbsp;<code>6.2832rad</code>&nbsp;or&nbsp;<code>360deg</code>.</li>



<li><strong><code>turn</code>:</strong> One turn is one full circle. So, halfway around a circle is equal to&nbsp;<code>.5turn</code>, or&nbsp;<code>180deg</code>.</li>
</ul>



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



<p class="wp-block-paragraph"><code>rotateX()</code>&nbsp;is part of the CSS 3D transform functions, so it&#8217;s better represented in a 3D view. For&nbsp;<code>rotateX()</code>&nbsp;to produce a visible 3D effect, you need to set the&nbsp;<a href="https://css-tricks.com/almanac/properties/p/perspective/"><code>perspective</code></a>&nbsp;property on the parent element. The perspective property determines how the element is projected, adding depth to the element and making it look natural and 3D.</p>



<p class="wp-block-paragraph">In this demo, we have two sliders to control the&nbsp;<code>rotateX()</code>&nbsp;degree and&nbsp;<code>perspective</code>&nbsp;property.</p>



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



<p class="wp-block-paragraph">In the absence of&nbsp;<code>perspective</code>, the element looks oddly skewed and ugly.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1886" height="673" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?resize=1886%2C673&#038;ssl=1" alt="" class="wp-image-393626" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?w=1886&amp;ssl=1 1886w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?resize=300%2C107&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?resize=1024%2C365&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?resize=768%2C274&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_5030899E08C5696DE43255ED314A7F00FDAB89F000ECCE9A9554C93265561AB8_1771667065487_Screenshot2026-02-21at10.44.10AM-e1777907568105.png?resize=1536%2C548&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">It&#8217;s also worth setting&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform-style/"><code>transform-style</code></a>&nbsp;to&nbsp;<code>preserve-3d</code>, which determines if that element&#8217;s children are positioned in 3D space or flattened.</p>



<h2 class="wp-block-heading" id="basic-usage">Basic usage</h2>



<p class="wp-block-paragraph">One of the most popular uses of&nbsp;<code>rotateX()</code>&nbsp;is creating flip cards that reveal content on the back when clicked or hovered. You can use this technique for pricing tables, profile cards, or interactive galleries.</p>



<p class="wp-block-paragraph">To set the stage and give the card a projection value and 3D presence, we set the&nbsp;<code>perspective</code>&nbsp;and&nbsp;<code>preserve-3d</code>&nbsp;styles on the parent elements.</p>



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

.flip-card-inner {
  transform-style: preserve-3d;
  transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}</code></pre>



<p class="wp-block-paragraph">Then we position the front and back faces of the card absolutely within the container while setting&nbsp;<code>backface-visibility</code>&nbsp;to&nbsp;<code>hidden</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card-front,
.flip-card-back {
  position: absolute;
  backface-visibility: hidden;
}</code></pre>



<p class="wp-block-paragraph">Next, we pre-rotate the back face by 180 degrees, so it is ready to be revealed when the card flips</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card-back {
  transform: rotateX(180deg);
}</code></pre>



<p class="wp-block-paragraph">And, finally, we flip the card when the parent is <code>:hover</code>-ed:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card:hover .flip-card-inner {
  transform: rotateX(180deg);
}</code></pre>



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



<h2 class="wp-block-heading" id="3d-loading-spinner">Example: 3D Loading spinner</h2>



<p class="wp-block-paragraph">We can also create engaging loading indicators with the&nbsp;<code>rotateX()</code>&nbsp;function..</p>



<p class="wp-block-paragraph">In this example, we&#8217;re not only using the&nbsp;<code>rotateX()</code>&nbsp;function, but we&#8217;re combining it with the&nbsp;<code>rotateY()</code>&nbsp;function for a two-axis rotation animation. By continuously rotating an element horizontally and vertically, we create a 3D spinning effect.</p>



<p class="wp-block-paragraph">Once again, we give the element&#8217;s parent a <code>perspective</code>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.spinner-wrapper {
  perspective: 1000px;
  margin-bottom: 2rem;
}</code></pre>



<p class="wp-block-paragraph">Then, we apply the animation to the element using the CSS&nbsp;<code>animation</code>&nbsp;shorthand property.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.spinner {
  width: 80px;
  height: 80px;
  animation: spin-3d 2s ease-in-out infinite;
}</code></pre>



<p class="wp-block-paragraph">Next, we define the keyframe, which dictates how the element rotates from one point to another.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@keyframes spin-3d {
  0% {
    transform: rotateX(0deg) rotateY(0deg);
  }
  25% {
    transform: rotateX(180deg) rotateY(90deg);
  }
  50% {
    transform: rotateX(180deg) rotateY(180deg);
  }
  75% {
    transform: rotateX(360deg) rotateY(270deg);
  }
  100% {
    transform: rotateX(360deg) rotateY(360deg);
  }
}
</code></pre>



<p class="wp-block-paragraph">The order in which the&nbsp;<code>transform</code>&nbsp;functions are defined is important. The effect of the first function comes to life before the second, so at <code>25%</code> the element flips halfway horizontally before the vertical flip, and the animation is so smooth you hardly notice it.</p>



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



<h2 class="wp-block-heading" id="accordion-with-3d-effect">Example: 3D accordion</h2>



<p class="wp-block-paragraph">Let&#8217;s skip the boring accordion component content reveal animation and make ours a bit interesting.</p>



<p class="wp-block-paragraph">We can enhance traditional accordions by adding a subtle&nbsp;<code>rotateX()</code>&nbsp;rotation when items expand or collapse, creating a more staggered fall effect that further engages the user and improves the experience, rather than the simple slide-down-and-back-up animation.</p>



<p class="wp-block-paragraph">Once again, as usual, we set the perspective on the parent</p>



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



<p class="wp-block-paragraph">Then, we define the&nbsp;<code>transform</code>&nbsp;and&nbsp;<code>transform-origin</code>. Since we want the&nbsp;<code>.accordion-content</code>&nbsp;to fall toward the user, we use a negative 90° angle to shift the element out of the user&#8217;s view.</p>



<p class="wp-block-paragraph">Also, we can change the default rotation axis from the center to the top using&nbsp;<code>transfom-origin: top;</code></p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.accordion-content {
  transform-origin: top;
  transform: rotateX(-90deg);
  overflow: hidden;
  transition:
    transform 0.4s ease,
    opacity 0.3s ease,
    max-height 0.4s ease;
}</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>transform-origin:top</code>&nbsp;ensures the rotation occurs from the top edge, making it look like a door opening downward, while&nbsp;<code>rotateX(-90deg)</code>&nbsp;makes the content appear to unfold into view.</p>



<p class="wp-block-paragraph">As the accordion opens, the&nbsp;<code>.accordion-content</code>&nbsp;element falls in a staggered manner to 0 degrees, which is the default position.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.accordion-item.open .accordion-content {
  transform: rotateX(0deg);
  opacity: 1;
  max-height: 500px;
}</code></pre>



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



<h2 class="wp-block-heading" id="a-little-note-about-transform-origin-and-rotatex-">A note about <code>transform-origin</code> and <code>rotateX()</code></h2>



<p class="wp-block-paragraph">By default, the&nbsp;<code>rotateX()</code>&nbsp;function rotates an element around its center. However, you can change this rotation axis using the CSS&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform-origin/"><code>transform-origin</code></a>&nbsp;property.&nbsp;<code>transform-origin</code>&nbsp;lets you change the point of origin of any&nbsp;<code>transform</code>&nbsp;function, so rather than being restricted to&nbsp;<code>center</code>, you can use&nbsp;<code>top center</code>,&nbsp;<code>top right</code>,&nbsp;<code>bottom left</code>, or even percentages and lengths.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.child {
  transform: rotateX(120deg);
  transform-origin: top center;
}</code></pre>



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



<p class="wp-block-paragraph">The CSS&nbsp;<code>rotateX()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatex" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;draft.</p>



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



<p class="wp-block-paragraph">The&nbsp;<code>rotateX()</code>&nbsp;function has baseline support on all modern browsers.</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="transforms2d"></baseline-status>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatex/">rotateX()</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">393625</post-id>	</item>
		<item>
		<title>rotateY()</title>
		<link>https://css-tricks.com/almanac/functions/r/rotatey/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 13 May 2026 14:33:58 +0000</pubDate>
				<category><![CDATA[transform]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=393615</guid>

					<description><![CDATA[<p>The <code>rotateY()</code> function rotates an element around its vertical y-axis.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatey/">rotateY()</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>rotateY()</code>&nbsp;function rotates an element around its vertical y-axis. Specifically, it horizontally flips an element from left to right (or right to left for that matter). It is one of many transform functions used along with the&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform/"><code>transform</code></a>&nbsp;property.</p>



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



<p class="is-style-explanation wp-block-paragraph">The y-axis is the axis of rotation, so the element turns horizontally. Imagine a pin is stuck to the top of an element and it can only rotate left or right.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.demo-element {
  transform: rotateY(var(--deg));
  transition: transform 0.3s ease;
}</code></pre>



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



<p class="wp-block-paragraph">The&nbsp;<code>rotateY()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatey" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;specification.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">rotateY() = rotateY( [ &lt;angle> | &lt;zero> ] )</code></pre>



<h2 class="wp-block-heading" id="argument">Arguments</h2>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* &lt;angle> in degrees */
rotateY(90deg)   /* Element rotates 90 degrees to the right */
rotateY(-180deg) /* Element rotates 180 degrees to the left */

/* &lt;angle> in turns */
rotateY(0.5turn) /* Element rotates 180 degrees (half a full rotation) */
rotateY(1turn)   /* Element completes a full 360-degree rotation */

/* &lt;angle> in radians */
rotateY(1.57rad) /* Approximately 90 degrees to the right */
rotateY(-3.14rad) /* Approximately 180 degrees to the left */</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotateY()</code>&nbsp;function takes a single&nbsp;<a href="https://drafts.csswg.org/css-values-4/#angles" rel="noopener"><code>&lt;angle&gt;</code></a>&nbsp;argument, which defines how much the element is rotated around its vertical axis.</p>



<ul class="wp-block-list">
<li><code>&lt;angles&gt;</code>: Values like&nbsp;<code>45deg</code>,&nbsp;<code>0.5turn</code>,&nbsp;<code>-90deg</code>,&nbsp;<code>1.57rad</code>, etc. When it is a positive angle, the right edge of the element rotates away from you (the element appears to rotate to the right). When the angle is a negative value, the left edge rotates and the element appears to rotate to the left.</li>
</ul>



<p class="wp-block-paragraph">The&nbsp;<code>&lt;angle&gt;</code>&nbsp;type has four units to choose from:</p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>deg</code>:</strong> One degree is&nbsp;<code>1/360</code>&nbsp;of a full circle.</li>



<li><strong><code>grad</code>:</strong> One gradian is&nbsp;<code>1/400</code>&nbsp;of a full circle.</li>



<li><strong><code>rad</code>:</strong> A radian is the length of a circle&#8217;s diameter around the shape&#8217;s arc. One radian is&nbsp;<code>180deg</code>, or&nbsp;<code>1/2</code>&nbsp;of a full circle. One full circle is 2π radians, which is equal to&nbsp;<code>6.2832rad</code>&nbsp;or&nbsp;<code>360deg</code>.</li>



<li><strong><code>turn</code>:</strong> One turn is one full circle. So, halfway around a circle is equal to&nbsp;<code>.5turn</code>, or&nbsp;<code>180deg</code>.</li>
</ul>



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



<p class="wp-block-paragraph">We&#8217;ve gotta talk about this first because, for any 3D&nbsp;<code>transform</code>&nbsp;function to create a visible 3D effect, you have to set the&nbsp;<a href="https://css-tricks.com/almanac/properties/p/perspective/"><code>perspective</code></a>&nbsp;property on the parent element.&nbsp;<code>perspective</code>&nbsp;defines the projection of the 3D element from the viewer&#8217;s eyes.</p>



<p class="wp-block-paragraph">Lower values (like&nbsp;<code>400px</code>) make the 3D element appear closer, while higher values (like&nbsp;<code>2000px</code>) make it appear farther, reducing the visibility of the 3D effect.</p>



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

.card {
  transform: rotateY(45deg);
}</code></pre>



<p class="wp-block-paragraph">Without perspective, the rotation will appear flat and shrunken, and the 3D depth won&#8217;t be visible.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="960" height="655" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697610201_Screenshot2026-02-21at7.13.23PM-e1777905340913.png?resize=960%2C655&#038;ssl=1" alt="60º rotation without perspective" class="wp-image-394552" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697610201_Screenshot2026-02-21at7.13.23PM-e1777905340913.png?w=960&amp;ssl=1 960w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697610201_Screenshot2026-02-21at7.13.23PM-e1777905340913.png?resize=300%2C205&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697610201_Screenshot2026-02-21at7.13.23PM-e1777905340913.png?resize=768%2C524&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">While with&nbsp;<code>perspective</code>, it looks slightly tilted to the right</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="645" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697653880_Screenshot2026-02-21at7.14.07PM-e1777905460763-1024x645.png?resize=1024%2C645&#038;ssl=1" alt="60º rotation with perspective" class="wp-image-394555" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697653880_Screenshot2026-02-21at7.14.07PM-e1777905460763.png?resize=1024%2C645&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697653880_Screenshot2026-02-21at7.14.07PM-e1777905460763.png?resize=300%2C189&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697653880_Screenshot2026-02-21at7.14.07PM-e1777905460763.png?resize=768%2C484&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/s_6B039D89EFA1511E842EE16753FF2394FCAA7484762E6E6BC6FFD189B47A5723_1771697653880_Screenshot2026-02-21at7.14.07PM-e1777905460763.png?w=1040&amp;ssl=1 1040w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="wp-block-paragraph">It&#8217;s also worth setting&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform-style/"><code>transform-style</code></a>&nbsp;to&nbsp;<code>preserve-3d</code>, which determines if that element&#8217;s children are positioned in 3D space, or flattened.</p>



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



<h2 class="wp-block-heading" id="basic-usage">Basic usage</h2>



<p class="wp-block-paragraph">One of the most popular uses of&nbsp;<code>rotateY()</code>&nbsp;is creating horizontal flip cards that show content on the back when clicked or hovered. To make one, we first set the 3D stage and projection by applying&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform-style/"><code>transform-style</code></a>&nbsp;to&nbsp;<code>preserve-3d;</code>&nbsp;to the card and&nbsp;<code>perspective</code>&nbsp;to&nbsp;<code>1000px;</code>&nbsp;styles to the parent elements.</p>



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

.flip-card-inner {
  transform-style: preserve-3d;
  transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}</code></pre>



<p class="wp-block-paragraph">Next, we position the front and back faces of the card absolutely within the container, while setting&nbsp;<code>backface-visibility</code>&nbsp;to&nbsp;<code>hidden</code>. It prevents the content of each face from showing through when rotated to the other side.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card-front,
.flip-card-back {
  position: absolute;
  backface-visibility: hidden;
}</code></pre>



<p class="wp-block-paragraph">We also need to pre-rotate the back face by <code>180deg</code>. This ensures the back face is readable when flipped and viewed from the front.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card-back {
  transform: rotateY(180deg);
}</code></pre>



<p class="wp-block-paragraph">And, finally, we flip the card when the parent is <code>:hover</code>-ed.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.flip-card:hover .flip-card-inner {
  transform: rotateX(180deg);
}</code></pre>



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



<h2 class="wp-block-heading" id="image-carousel">Example: Image carousel</h2>



<p class="wp-block-paragraph">The&nbsp;<code>rotateY()</code>&nbsp;function is also perfect for creating 3D image carousels that showcase products or galleries. Each item can be positioned around a cylinder and rotated to show the viewer.</p>



<p class="wp-block-paragraph">Once again, as usual, we set up the 3D stage with&nbsp;<code>perspective</code>&nbsp;and&nbsp;<code>preserve-3d</code>.</p>



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

.carousel-container {
  transform-style: preserve-3d;
  transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}</code></pre>



<p class="wp-block-paragraph">Afterwards, we try to position all&nbsp;<code>.carousel-item</code>&nbsp;in the center of the&nbsp;<code>.carousel-container</code>&nbsp;using&nbsp;<code>absolute</code>&nbsp;positioning</p>



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



<p class="wp-block-paragraph">Later, we reposition them to form a cylinder around the&nbsp;<code>.carousel-container</code>&nbsp;using&nbsp;<code>rotateY(calc(var(--n) * 72deg))</code>, which pushes each item forward with&nbsp;<code>translateZ(400px)</code>, without which the items would edge into one another.</p>



<p class="is-style-explanation wp-block-paragraph"><code>400px</code>&nbsp;serves as the cylinder&#8217;s radius. I tried different radii from 100 to see which one would make each item appear individually, and&nbsp;<code>400px</code>&nbsp;won.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.carousel-item {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotateY(calc(var(--n) * 72deg)) translateZ(400px);
}</code></pre>



<p class="wp-block-paragraph">Each&nbsp;<code>.carousel-item</code>&nbsp;has a variable,&nbsp;<code>--n: x</code>, where <code>x</code> is a number from <code>0</code> to <code>4</code>. Since there are five total items, we found the perfect angle for the&nbsp;<code>rotateY()</code>&nbsp;function by dividing <code>360deg</code> (a full turn) by <code>5</code> to get <code>72deg</code></p>



<p class="wp-block-paragraph">Now we use JavaScript to rotate the&nbsp;<code>.carousel-container</code> by <code>72deg</code> when the &#8220;Next&#8221; and &#8220;Prev&#8221; buttons are clicked. This pushes the next or previous panel to the front, depending on the button you click.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">let currentRotation = 0;
const anglePerItem = 72;

function rotateCarousel(direction) {
  currentRotation += direction * anglePerItem;
  carouselContainer.style.transform = `rotateY(${currentRotation}deg)`;
}

nextBtn.addEventListener("click", () => {
  rotateCarousel(1);
});

prevBtn.addEventListener("click", () => {
  rotateCarousel(-1);
});</code></pre>



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



<h2 class="wp-block-heading" id="book-page-turn-effect">Example: Page turn</h2>



<p class="wp-block-paragraph">Remember the horizontal flipping card we looked at earlier? We can build off of it to make it look like a book page turn.</p>



<p class="wp-block-paragraph">We&#8217;re going to add the&nbsp;<code>transform-origin</code> property to it. It defines the point on the axis at which the rotation occurs. By default, it&#8217;s the center, and that&#8217;s what we&#8217;ve used so far, but we&#8217;re changing it here to&nbsp;<code>left center</code>. The new position allows the element to be flipped from the center of the left edge, as in books, rather than from the main center in the flipping card effect.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.page {
  transform-origin: left center;
  transform-style: preserve-3d;
  transition: transform 1.5s cubic-bezier(0.645, 0.045, 0.355, 1);
}</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotateY()</code>&nbsp;function, when combined with&nbsp;<code>transform-origin: left center;</code>, can create a realistic page-turning effect for digital books, portfolios, or storytelling interfaces.</p>



<p class="wp-block-paragraph">You should know how to use&nbsp;<code>rotateY()</code>&nbsp;by now, so let&#8217;s skip to the magic. Only the right page is animated, so that&#8217;s where the transform is focused. We gave&nbsp;<code>.page</code>&nbsp;a&nbsp;<code>transform-origin</code>&nbsp;of&nbsp;<code>left center;</code>&nbsp;so it rotates on the vertical axis around the center of the left end.</p>



<p class="wp-block-paragraph">Then, when the&nbsp;<code>.turning</code>&nbsp;class is triggered on clicking the page,&nbsp;<code>rotateY(-180deg)</code>&nbsp;flip it over around the defined rotation point.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.page.turning {
  transform: rotateY(-180deg);
}</code></pre>



<p class="wp-block-paragraph">To prevent the content of the page&#8217;s front and back from showing through, we use&nbsp;<code>backface-visibility: hidden;</code>&nbsp;to hide it when it&#8217;s turned over.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.page-front,
.page-back {
  backface-visibility: hidden;
}</code></pre>



<p class="wp-block-paragraph">Also, we pre-rotate the back page so the content isn&#8217;t inverted when it&#8217;s turned toward the viewer.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.page-back {
  transform: rotateY(180deg);
}</code></pre>



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



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



<p class="wp-block-paragraph">The CSS&nbsp;<code>rotateY()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatey" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;draft.</p>



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




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



<p class="wp-block-paragraph">The&nbsp;<code>rotateY()</code>&nbsp;function is supported on all modern browsers.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatey/">rotateY()</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">393615</post-id>	</item>
		<item>
		<title>rotateZ()</title>
		<link>https://css-tricks.com/almanac/functions/r/rotatez/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 13 May 2026 14:33:03 +0000</pubDate>
				<category><![CDATA[transform]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=393630</guid>

					<description><![CDATA[<p>The <code>rotateZ()</code> function rotates an element around its z-axis, so clockwise or counterclockwise. </p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatez/">rotateZ()</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>rotateZ()</code>&nbsp;function rotates an element around its z-axis, so clockwise or counterclockwise. While it produces the same visual effect as the&nbsp;<code>rotate()</code>&nbsp;function, it&#8217;s best used in 3D transformations. It is one of many transform functions used along with the&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform/"><code>transform</code></a>&nbsp;property.</p>



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



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



<p class="wp-block-paragraph">In the demo, we first set up a stage for our 3D element with&nbsp;<a href="https://css-tricks.com/almanac/properties/p/perspective/"><code>perspective</code></a>. It represents the projection of the 3D element from the viewer&#8217;s perspective, indicating how far or close the object appears.</p>



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



<p class="wp-block-paragraph">We then simulate the tumbling effect of a coin that is spun on a table in slow motion, so we use three 3D rotation transform functions:&nbsp;<code><a href="https://css-tricks.com/almanac/functions/r/rotatex/">rotateX()</a></code>,&nbsp;<code><a href="https://css-tricks.com/almanac/functions/r/rotatey/">rotateY()</a></code>, and&nbsp;<code>rotateZ()</code>.</p>



<p class="is-style-explanation wp-block-paragraph">The <code><a href="https://css-tricks.com/almanac/functions/r/rotate/">rotate()</a></code>&nbsp;shorthand cannot be used here because it maps to a 2D matrix and could lead to wrong calculations in the browser&#8217;s matrix math when combined with 3D functions.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.tumbling-coin {
  animation: tumble 3s infinite linear;
}

@keyframes tumble {
  0% {
    transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg);
  }
  100% {
    transform: rotateX(360deg) rotateY(180deg) rotateZ(360deg);
  }
}</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotateZ()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatez" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;specification.</p>



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



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;rotateZ()> = rotateZ( [ &lt;angle> | &lt;zero> ] )</code></pre>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* &lt;angle> in degrees */
rotateZ(90deg)   /* Element rotates 90 degrees clockwise */
rotateZ(-180deg) /* Element rotates 180 degrees counterclockwise */

/* &lt;angle> in turns */
rotateZ(0.25turn) /* Element makes a quater turn clockwise */
rotateZ(1turn) /* Element completes a full 360-degree rotation */

/* &lt;angle> in radians */
rotateZ(1.57rad) /* Approximately 90 degrees clockwise */
rotateZ(-3.14rad) /* Approximately 180 degrees counterclockwise */</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotateZ()</code>&nbsp;function takes a single&nbsp;<a href="https://drafts.csswg.org/css-values-4/#angles" rel="noopener"><code>&lt;angle&gt;</code></a>&nbsp;argument, which specifies how much to rotate the element around the z-axis</p>



<p class="wp-block-paragraph">The direction the element spins depends on whether the angle value is positive or negative</p>



<ul class="wp-block-list">
<li>A positive angle spins in the clockwise direction, while</li>



<li>A negative angle spins in the counterclockwise direction</li>
</ul>



<p class="wp-block-paragraph">The&nbsp;<code>&lt;angle&gt;</code>&nbsp;type can be one of four units:</p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>deg</code>:</strong> One degree is&nbsp;<code>1/360</code>&nbsp;of a full circle.</li>



<li><strong><code>grad</code>:</strong> One gradian is&nbsp;<code>1/400</code>&nbsp;of a full circle.</li>



<li><strong><code>rad</code>:</strong> A radian is the length of a circle&#8217;s diameter around the shape&#8217;s arc. One radian is&nbsp;<code>180deg</code>, or&nbsp;<code>1/2</code>&nbsp;of a full circle. One full circle is 2π radians, which is equal to&nbsp;<code>6.2832rad</code>&nbsp;or&nbsp;<code>360deg</code>.</li>



<li><strong><code>turn</code>:</strong> One turn is one full circle. So, halfway around a circle is equal to&nbsp;<code>.5turn</code>, or&nbsp;<code>180deg</code>.</li>
</ul>



<h2 class="wp-block-heading" id="basic-usage">Basic usage</h2>



<p class="wp-block-paragraph">The&nbsp;<code>rotateZ()</code>&nbsp;and&nbsp;<code>rotate()</code>&nbsp;functions have the same visual effect, but their applications are best suited to 3D and 2D animations, respectively. Also,&nbsp;<code>rotateZ()</code>&nbsp;is a better option for any animation that requires the GPU compositing layer, as it&#8217;s hardware-accelerated.</p>



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



<p class="wp-block-paragraph">In this demo,&nbsp;<code>rotateZ()</code>&nbsp;is used instead of&nbsp;<code>rotate()</code>&nbsp;though they have the same visual effect. However, if you have a complex animation on a webpage with a lot of heavy DOM elements,&nbsp;<code>rotate()</code>&nbsp;might cause the browser to constantly recalculate the layout on the main thread. By using&nbsp;<code>rotateZ()</code>, you force browser to promote that specific element to its own layer on the computer&#8217;s GPU, making the animation smoother and faster.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.gpu-spinner {
  animation: gpu-spin 1s linear infinite;
}

@keyframes gpu-spin {
  from {
    transform: rotateZ(0deg);
  }
  to {
    transform: rotateZ(360deg);
  }
}</code></pre>



<h2 class="wp-block-heading" id="isometric-card-with-rotatez-">Example: Isometric card</h2>



<p class="wp-block-paragraph">When building 3D effects, you have to rotate elements on multiple axes. While combining&nbsp;<code>transform: rotateX(60deg) rotate(-45deg)</code>&nbsp;technically works, using&nbsp;<code>transform: rotateX(60deg) rotateZ(-45deg)</code>&nbsp;is the semantically correct approach.</p>



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

.isometric-card {
  transition: transform 0.5s ease;
  transform: rotateX(60deg) rotateZ(-45deg);
}

.isometric-card:hover {
  transform: rotateX(0deg) rotateZ(0deg) scale(1.1);
  box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
}</code></pre>



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



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



<p class="wp-block-paragraph">The&nbsp;<code>rotateZ()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms-2/#funcdef-rotatez" rel="noopener">CSS Transforms Module Level 2</a>&nbsp;specification.</p>



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




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



<p class="wp-block-paragraph">The&nbsp;<code>rotateZ()</code>&nbsp;function has baseline support on all modern browsers.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotatez/">rotateZ()</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">393630</post-id>	</item>
		<item>
		<title>rotate()</title>
		<link>https://css-tricks.com/almanac/functions/r/rotate/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 13 May 2026 14:32:47 +0000</pubDate>
				<category><![CDATA[transform]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=393611</guid>

					<description><![CDATA[<p>The <code>rotate()</code> function spins an element either clockwise or counterclockwise in a 2D plane.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotate/">rotate()</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>rotate()</code>&nbsp;function spins an element either clockwise or counterclockwise in a 2D plane. It is one of many transform functions used in the&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform/"><code>transform</code></a>&nbsp;property.</p>



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



<p class="wp-block-paragraph">For example, using&nbsp;<code>rotate()</code>&nbsp;we can rotate the hand around the clock:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.seconds-hand {
  transform: rotate(var(--deg));
  transform-origin: bottom center;
}</code></pre>



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



<p class="is-style-explanation wp-block-paragraph">For rotating elements tri-dimensionally, consider using&nbsp;<code><a href="https://css-tricks.com/almanac/functions/r/rotatex/">rotateX()</a></code>&nbsp;and&nbsp;<code><a href="https://css-tricks.com/almanac/functions/r/rotatey/">rotateY()</a></code>.</p>



<p class="wp-block-paragraph">The <code>rotate()</code> functions is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms/#funcdef-transform-rotate" rel="noopener">CSS Transforms Module Level 1</a>&nbsp;specification.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">rotate() = rotate( [ &lt;angle> | &lt;zero> ] )</code></pre>



<h2 class="wp-block-heading" id="argument">Arguments</h2>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* &lt;angle> */
rotate(90deg) /* Rotates 90 degrees clockwise  */
rotate(-180deg) /* Rotates 180 degrees counterclockwise */

/* &lt;angle> in turns */
rotate(0.25turn) /* Rotates a quater turn clockwise. */
rotate(1turn)    /* Rotates a full 360-degree turn. */

/* &lt;angle> in radians */
rotate(1.57rad)  /* Rotates ~90 degrees clockwise. */
rotate(-3.14rad) /* Rotate ~180 degrees counterclockwise. */</code></pre>



<p class="wp-block-paragraph">The&nbsp;<code>rotate()</code>&nbsp;function accepts a single&nbsp;<code>&lt;angle&gt;</code>&nbsp;argument, which dictates the direction of its spin. A positive argument spins the element clockwise, while a negative argument spins the element counterclockwise.</p>



<p class="wp-block-paragraph">The&nbsp;<code>&lt;angle&gt;</code>&nbsp;type has four units to choose from:</p>



<ul class="wp-block-list is-style-almanac-list">
<li><strong><code>deg</code>:</strong> One degree is&nbsp;<code>1/360</code>&nbsp;of a full circle.</li>



<li><strong><code>grad</code>:</strong> One gradian is&nbsp;<code>1/400</code>&nbsp;of a full circle.</li>



<li><strong><code>rad</code>:</strong> A radian is the length of a circle&#8217;s diameter around the shape&#8217;s arc. One radian is&nbsp;<code>180deg</code>, or&nbsp;<code>1/2</code>&nbsp;of a full circle. One full circle is 2π radians, which is equal to&nbsp;<code>6.2832rad</code>&nbsp;or&nbsp;<code>360deg</code>.</li>



<li><strong><code>turn</code>:</strong> One turn is one full circle. So, halfway around a circle is equal to&nbsp;<code>.5turn</code>, or&nbsp;<code>180deg</code>.</li>
</ul>



<p class="wp-block-paragraph">Also,&nbsp;<code>rotate()</code>&nbsp;spins the element around its center axis. To change the rotation point, we have to pass a specific point to the <a href="https://css-tricks.com/almanac/properties/t/transform-origin/"><code>transform-origin</code></a>&nbsp;property that&#8217;ll serve as the axis of rotation.</p>



<h2 class="wp-block-heading" id="basic-usage">Basic usage</h2>



<p class="wp-block-paragraph">The&nbsp;<code>rotate()</code>&nbsp;function is the backbone of some of the basic animations you&#8217;ve most likely come across on, like switching from &#8220;+&#8221; to &#8220;x&#8221; when an accordion is opened. We can do that by rotating the &#8220;+&#8221; symbol by <code>45deg</code>.</p>



<p class="wp-block-paragraph">So, if we have a button like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;button class="toggle">
  &lt;span class="icon">+&lt;/span>
  &lt;span class="label">Open Section&lt;/span>
&lt;/button></code></pre>



<p class="wp-block-paragraph">We can sprinkle a little JavaScript in there to trigger an <code>.active</code> class when the button is clicked, which rotates the icon:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.toggle.active .icon {
  transform: rotate(45deg);
}</code></pre>



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



<h2 id="example-hamburger-menu" class="wp-block-heading">Example: Hamburger menu</h2>



<p class="wp-block-paragraph">Have you seen those menus that switch from a hamburger icon to a closing &#8220;X&#8221; icon when a menu dropdown or sidebar is opened? That&#8217;s a rotation, too! </p>



<p class="wp-block-paragraph">We start with three spans that are styled as horizontal lines:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;button class="hamburger" id="hamburgerBtn">
  &lt;span class="bar top">&lt;/span>
  &lt;span class="bar middle">&lt;/span>
  &lt;span class="bar bottom">&lt;/span>
&lt;/button></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.bar {
  width: 100%;
  height: 4px;
  background: #333;
  transition: transform 0.3s ease, opacity 0.3s ease;
}</code></pre>



<p class="wp-block-paragraph">Notice we have a <code><a href="https://css-tricks.com/almanac/properties/t/transition/">transition</a></code> in there so that, when the button is clicked and the rotation happens (again, using JavaScript to toggle on an <code>.active</code> class), the spans transform smoothly:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.hamburger.active .top {
  transform: translateY(14px) rotate(45deg);
}

.hamburger.active .middle {
  opacity: 0;
}

.hamburger.active .bottom {
  transform: translateY(-14px) rotate(-45deg);
}</code></pre>



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



<h2 class="wp-block-heading" id="loading-icons">Example: Loading icons</h2>



<p class="wp-block-paragraph">We can also use&nbsp;<code>rotate()</code>&nbsp;for loading indicators. Loading indicators usually spin while a page is, you know, loading. A common example is a semi-circle that spins until the page is done loading.</p>



<p class="wp-block-paragraph">The&nbsp;<code>.spinner</code>&nbsp;uses the CSS&nbsp;<code><a href="https://css-tricks.com/almanac/properties/a/animation/">animation</a></code>&nbsp;shorthand to define an infinite spinning loading indicator, and the&nbsp;<code>@keyframes spin</code>&nbsp;defines a <code>360deg</code> spin with&nbsp;the <code>rotate()</code> function.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.spinner {
  animation: spin 0.8s linear infinite;
}

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



<p class="wp-block-paragraph">Now the spinner keeps on a&#8217;spinning:</p>



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



<h2 class="wp-block-heading" id="vertical-text-labels">Example: Rotating text</h2>



<p class="wp-block-paragraph">Rotating things isn&#8217;t always about animation! We can, for example, position something like a &#8220;Feature&#8221; label next  to a blog post and rotate it vertically for an interesting visual effect.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.vertical-header {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}</code></pre>



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



<h2 class="wp-block-heading" id="demo">Demo</h2>



<p class="wp-block-paragraph">Let&#8217;s look at a more complex animation to demonstrate just how neat it is to <code>rotate()</code> things with CSS. If you &#8220;Rerun&#8221; the demo, you&#8217;ll see the photo swing back and forth. You can also drag the photo from left to right to rotate it.</p>



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



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



<p class="wp-block-paragraph">The CSS&nbsp;<code>rotate()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-transforms/#funcdef-transform-rotate" rel="noopener">CSS Transforms Module Level 1</a>&nbsp;specification, which is currently in Editor&#8217;s Draft.</p>



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




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



<h2 id="related-tricks" class="wp-block-heading">Related tricks!</h2>



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

      <div class="tags">
      <a href="https://css-tricks.com/tag/cursor/" rel="tag">cursor</a>    </div>
  
  <time datetime="2019-08-28" title="Originally published Aug 28, 2019">
    <strong>
                
        Article
      </strong>

    on

    Aug 28, 2019  </time>

  <h3>
    <a href="https://css-tricks.com/can-you-rotate-the-cursor-in-css/">
      Can you rotate the cursor in CSS?    </a>
  </h3>

  
  <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 articles" id="mini-post-377074">

      <div class="tags">
      <a href="https://css-tricks.com/tag/cos/" rel="tag">cos()</a> <a href="https://css-tricks.com/tag/math/" rel="tag">math</a> <a href="https://css-tricks.com/tag/sin/" rel="tag">sin()</a>    </div>
  
  <time datetime="2023-03-08" title="Originally published Mar 8, 2023">
    <strong>
                
        Article
      </strong>

    on

    Mar 8, 2023  </time>

  <h3>
    <a href="https://css-tricks.com/creating-a-clock-with-the-new-css-sin-and-cos-trigonometry-functions/">
      Creating a Clock with the New CSS sin() and cos() Trigonometry Functions    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/madsstoumann/" aria-label="Author page of Mads Stoumann">
      <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/2021/01/xfKShH2E_400x400.jpg?resize=80%2C80&#038;ssl=1" width="80">    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/madsstoumann/">
      Mads Stoumann    </a>
  </div>

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

      <div class="tags">
      <a href="https://css-tricks.com/tag/transform/" rel="tag">transform</a>    </div>
  
  <time datetime="2020-03-30" title="Originally published Mar 30, 2020">
    <strong>
                
        Article
      </strong>

    on

    Mar 30, 2020  </time>

  <h3>
    <a href="https://css-tricks.com/how-they-fit-together-transform-translate-rotate-scale-and-offset/">
      How They Fit Together: Transform, Translate, Rotate, Scale, and Offset    </a>
  </h3>

  
  <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 articles" id="mini-post-335589">

  
  <time datetime="2021-03-02" title="Originally published Mar 2, 2021">
    <strong>
                
        Article
      </strong>

    on

    Mar 2, 2021  </time>

  <h3>
    <a href="https://css-tricks.com/how-to-animate-the-details-element/">
      How to Animate the Details Element    </a>
  </h3>

  
  <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 articles" id="mini-post-373184">

      <div class="tags">
      <a href="https://css-tricks.com/tag/conic-gradients/" rel="tag">conic gradients</a> <a href="https://css-tricks.com/tag/custom-properties/" rel="tag">custom properties</a> <a href="https://css-tricks.com/tag/dates/" rel="tag">dates</a> <a href="https://css-tricks.com/tag/gradients/" rel="tag">gradients</a>    </div>
  
  <time datetime="2022-09-19" title="Originally published Sep 19, 2022">
    <strong>
                
        Article
      </strong>

    on

    Sep 19, 2022  </time>

  <h3>
    <a href="https://css-tricks.com/making-a-real-time-clock-with-a-conic-gradient-face/">
      Making a Real-Time Clock With a Conic Gradient Face    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/brechtderuyte/" aria-label="Author page of Brecht De Ruyte">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/8322762f5ea3aaadd09f47aba3386e4f5eef9cb63c1f7feb4239bd257d94b9c0.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/8322762f5ea3aaadd09f47aba3386e4f5eef9cb63c1f7feb4239bd257d94b9c0.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/brechtderuyte/">
      Brecht De Ruyte    </a>
  </div>

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

      <div class="tags">
      <a href="https://css-tricks.com/tag/animation/" rel="tag">animation</a>    </div>
  
  <time datetime="2025-09-26" title="Originally published Sep 26, 2025">
    <strong>
                
        Article
      </strong>

    on

    Sep 26, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/recreating-gmails-google-gemini-animation/">
      Recreating Gmail’s Google Gemini Animation    </a>
  </h3>

  
  <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 articles" id="mini-post-262519">

  
  <time datetime="2017-11-20" title="Originally published Nov 20, 2017">
    <strong>
                
        Article
      </strong>

    on

    Nov 20, 2017  </time>

  <h3>
    <a href="https://css-tricks.com/recreating-apple-watch-breathe-app-animation/">
      Recreating the Apple Watch Breathe App Animation    </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>
<article class="in-article-card articles" id="mini-post-311036">

      <div class="tags">
      <a href="https://css-tricks.com/tag/tables/" rel="tag">tables</a>    </div>
  
  <time datetime="2020-06-01" title="Originally published Jun 1, 2020">
    <strong>
                
        Article
      </strong>

    on

    Jun 1, 2020  </time>

  <h3>
    <a href="https://css-tricks.com/rotated-table-column-headers-now-with-fewer-magic-numbers/">
      Rotated Table Column Headers… Now With Fewer Magic Numbers!    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/cappiepomeroy/" aria-label="Author page of Cappie Pomeroy">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/bb5f7941024fd947b8e497a095b80902da4f7bd57a1607d536eddb317aeaad07.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/bb5f7941024fd947b8e497a095b80902da4f7bd57a1607d536eddb317aeaad07.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/cappiepomeroy/">
      Cappie Pomeroy    </a>
  </div>

</article>
    </div>
  



<p class="wp-block-paragraph"></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/r/rotate/">rotate()</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">393611</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=8965 metric#misses=16 metric#hit-ratio=99.8 metric#bytes=7059745 metric#prefetches=471 metric#store-reads=33 metric#store-writes=2 metric#store-hits=479 metric#store-misses=12 metric#sql-queries=30 metric#ms-total=530.97 metric#ms-cache=31.33 metric#ms-cache-avg=0.9213 metric#ms-cache-ratio=5.9 -->
