<?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>Thu, 14 May 2026 14:05:05 +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>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/#respond</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>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>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" fetchpriority="high" 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="(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>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>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>Here’s how I put it together.</p>



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



<p>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>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>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>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>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>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>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>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>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>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>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>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>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>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>0</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>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">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>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>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>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>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><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>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>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" 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" 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="(min-width: 735px) 864px, 96vw" /></figure>



<p>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>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>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>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>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>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>We can also create engaging loading indicators with the&nbsp;<code>rotateX()</code>&nbsp;function..</p>



<p>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>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>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>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>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>Let&#8217;s skip the boring accordion component content reveal animation and make ours a bit interesting.</p>



<p>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>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>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>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>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>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>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>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>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>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">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>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>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>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>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>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>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" 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="(min-width: 735px) 864px, 96vw" /></figure>



<p>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" 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>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>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>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>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>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>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>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>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>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"><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>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>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>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>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>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>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>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>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>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>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>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>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>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>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">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>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>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>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>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>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>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>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>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>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>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>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">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>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> */
rotateZ(90deg) /* Rotates 90 degrees clockwise  */
rotateZ(-180deg) /* Rotates 180 degrees counterclockwise */

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

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



<p>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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" 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></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>
		<item>
		<title>Soon We Can Finally Banish JavaScript to the ShadowRealm</title>
		<link>https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/</link>
					<comments>https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/#comments</comments>
		
		<dc:creator><![CDATA[Mat Marquis]]></dc:creator>
		<pubDate>Tue, 12 May 2026 13:59:35 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393604</guid>

					<description><![CDATA[<p>The proposed ShadowRealm API introduces a new kind of realm specifically designed for isolation, and <em>only</em> that.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/">Soon We Can Finally Banish JavaScript to the ShadowRealm</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>It&#8217;s gonna be tough to keep it together on this one. Okay. I got this. I am a <em>professional technical writer</em>. Straight face; all-business. Ahem: if you&#8217;ve been following the ongoing work at TC39 (the standards body responsible for maintaining and developing the standards that inform JavaScript) you may have encountered some of <a href="https://github.com/tc39/proposal-shadowrealm" rel="noopener">their recent work on ShadowRealms</a>— <em>snrk</em>. Sorry! Sorry, I&#8217;m good! Just, whew ­— what a name, &#8220;ShadowRealms.&#8221; Okay, hang on, let me start at the beginning. Maybe that will help.</p>



<p>It&#8217;s exceptionally likely you&#8217;ve seen JavaScript described as &#8220;single-threaded&#8221; at some point — that&#8217;s usually pretty high up on the list of JavaScript fundamentals, alongside &#8220;case sensitive,&#8221; &#8220;whitespace insensitive,&#8221; and &#8220;bad at math.&#8221; That is <em>correct</em>, in the strict &#8220;computer science&#8221; sense, but it still gets my hackles up a little whenever I see it.</p>



<p>I mean, accurate in that JavaScript isn&#8217;t multi-threaded, for sure. A script is always executed in a very linear way — top to bottom, left to right, one execution context after another, winding up the call stack and then back down again. It&#8217;s just that you eventually come to learn about something like <a href="https://css-tricks.com/off-the-main-thread/">Web Workers</a>, which — not to put too fine a point on this — allow you to execute JavaScript code in <em>another thread</em>. That&#8217;s where I think &#8220;JavaScript is single-threaded&#8221; becomes a less helpful framing, because even though JavaScript isn&#8217;t a multi-threaded <em>language</em>, a JavaScript <em>application</em> can make use of multiple threads.</p>



<p>It&#8217;s a better framing — and every bit as technically accurate — to say that a JavaScript <strong>realm</strong> is single-threaded. A realm refers to the environment where code is executed: a browser tab is a realm, and within that realm is the single thread where JavaScript is executed — the <strong>main thread</strong>. A Web Worker is a realm with a <strong>worker thread</strong>. JavaScript running in a cross-origin <code>iframe</code> is running in <em>that</em> <code>iframe</code> realm’s main thread. We can&#8217;t, for example, offload the execution of a single function to another thread — JavaScript is <em>itself</em> single-threaded, as a language. But a JavaScript application can span multiple realms and make use of multiple execution threads, and each of those realms can communicate with other realms in specific ways.</p>



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



<p>Each JavaScript realm has its own global environment. In a browser tab, the global object is the <code>Window</code> interface. The same is true in a non-same-origin <code>iframe</code> within that browser tab — the global object is the <code>Window</code> “owned&#8221; by that <code>iframe</code>:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;html>
  &lt;head>&lt;/head>
  &lt;body>
    &lt;iframe id="theIframe">&lt;/iframe>
  &lt;/body>

  &lt;script>
  ( () => {
    console.log( window.globalThis );
    // Result: Window {}

    console.log( theIframe.contentWindow.globalThis );
    // Result: Window {}
  })();

  &lt;/script>
&lt;/html></code></pre>



<p>These aren&#8217;t the <em>same</em> global object:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;html>
  &lt;head>&lt;/head>
  &lt;body>
    &lt;iframe id="theIframe">&lt;/iframe>
  &lt;/body>

  &lt;script>
  ( () => {
    console.log( window.globalThis === theIframe.contentWindow.globalThis );
    // Result: false
  })();
  &lt;/script>
&lt;/html></code></pre>



<p>The outer page and the inner <code>iframe</code> are two separate realms, both single-threaded, each with their own global objects and their own intrinsic objects:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;html>
  &lt;head>&lt;/head>
  &lt;body>
    &lt;iframe id="theIframe">&lt;/iframe>
  &lt;/body>

  &lt;script>
    (() => {
      console.log( window.Array );
      /* Result (expanded):
        function Array()
        from: function from()
        fromAsync: function fromAsync()
        isArray: function isArray()
        length: 1
        name: "Array"
        of: function of()
        prototype: Array []
        Symbol(Symbol.species): undefined
        &lt;prototype>: function ()
      */

      console.log( theIframe.contentWindow.Array );
      /* Result (expanded):
        function Array()
        from: function from()
        fromAsync: function fromAsync()
        isArray: function isArray()
        length: 1
        name: "Array"
        of: function of()
        prototype: Array []
        Symbol(Symbol.species): undefined
        &lt;prototype>: function ()
      */

      console.log( window.Array === theIframe.contentWindow.Array );
      // Result: false

    })();
  &lt;/script>
&lt;/html></code></pre>



<p>So, as you might expect, any global properties defined in the context of one realm will be unavailable to another:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;html>
  &lt;head>&lt;/head>
  &lt;body>
    &lt;iframe id="theIframe">&lt;/iframe>
  &lt;/body>

  &lt;script>
  function globalFunction() {};

  console.log( window.globalFunction );
  // Result: function globalFunction()

  console.log( theIframe.contentWindow.globalFunction );
  // Result: undefined
  &lt;/script>
&lt;/html></code></pre>



<p>&#8220;Unavailable&#8221; — or, depending on how you look at it, unable to <em>interfere</em> with the the global object of another realm. If you&#8217;ve been JavaScripting for a while, you know that no matter how meticulous we are about managing scope, the global environment can get pretty messy despite our best efforts. Some of that is on us, sure — a stray variable binding happens to the best of us — but a lot of that clutter is a result of the early design decisions that went into the language itself, like the function declaration in the previous example. When you consider the staggering amount of JavaScript we don&#8217;t control that can get piled onto the average project — from frameworks to third-party helper libraries to polyfills to user analytics to advertisements — there&#8217;s potential for collisions, to say the least.</p>



<p>Given the global scope pollution that has haunted the language since time immemorial (the 90s), it isn&#8217;t hard to imagine the use cases for offloading code to a realm that can act as a sandbox for the execution of JavaScript we don&#8217;t want to impact, or be impacted by, whatever is already cluttering up the global scope. We might want to run part of our test suite in a &#8220;clean room&#8221; where <em>performing</em> the testing can&#8217;t potentially interfere with the results of your testing and mock data can&#8217;t run afoul of the real thing, or a place to run code we want quarantined away from the realm that contains our JavaScript application itself to prevent third-party libraries that don&#8217;t <em>need</em> access to the global environment from cluttering it up, to no benefit.</p>



<p>We can&#8217;t do that with realms, as they stand right now — remember, JavaScript is single-threaded in that each <em>realm</em> is single-threaded, and communication between those threads is limited. As undeniable as the use case is, we can&#8217;t repurpose an alternate realm to execute code on its single thread of execution, then weave the results of that execution back into the main thread of our primary realm. That&#8217;s multi-threaded execution by definition, and not just contrary to the fundamental nature of JavaScript, but, well, let me put it this way: JavaScript allowing multiple threads of execution at the same time <em>mean would problems us new for</em>.</p>



<p>To offload code in this way would require a new <em>kind</em> of realm — one that has its own global and intrinsic objects, but <em>not</em> it&#8217;s own thread — a realm where code offloaded to it will still be executed on the main thread of the realm that &#8220;owns&#8221; that script. A dark reflection of our own realms; a realm the light can never touch, where only fleeting, ephemeral shadows of our banished code can dwell! Imagine a distant peal of thunder, here; maybe also imagine that I&#8217;m wearing a cape, maybe I hurl a wine glass to the floor. Y&#8217;know, have fun with it. How could you not? I mean, they&#8217;re called:</p>



<h3 class="wp-block-heading" id="shadowrealms">ShadowRealms</h3>



<p>The proposed <a href="https://github.com/tc39/proposal-shadowrealm" rel="noopener">ShadowRealm API</a> introduces a new kind of realm specifically designed for <em>isolation</em>, and only that. A ShadowRealm does <em>not</em> have an execution context of its own — code offloaded to a ShadowRealm will exist in a pseudo-realm with its own global and built-in objects. That code continues to run on the same thread as the code where the ShadowRealm is created; we&#8217;re not forced to communicate and share resources back and forth between two separate threads in limited ways. In short, a script is executed the way it would if limited to a single realm, but quarantined away from that outer realm&#8217;s intrinsic objects, APIs, global object, and anything our script has <em>done</em> to that global object.</p>



<p>That sounds complicated, but the proposed API would be exceptionally simple in practice:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Create a ShadowRealm:
const shadow = new ShadowRealm();

function globalFunction() {};

console.log( globalthis.globalFunction );
// Result: function globalFunction()

// Evaluate `globalThis.globalFunction` inside the ShadowRealm:
console.log( shadow.evaluate( 'globalThis.globalFunction' ) );
// Result: undefined</code></pre>



<p class="is-style-explanation"><strong>Note:</strong> Keep in mind that this code is still theoretical — it doesn&#8217;t exist in the ES-262 standard or browsers just yet.</p>



<p><code>globalFunction</code> is defined on the outer realm&#8217;s global object just like we saw earlier, but it isn&#8217;t defined on the global object inside of our newly-created ShadowRealm — that ShadowRealm&#8217;s global object remains pristine, no matter what we do <em>outside</em> of it. The inverse is true, naturally:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Create a ShadowRealm:
const shadow = new ShadowRealm();

// Declare a global function inside the ShadowRealm:
shadow.evaluate( 'function globalFunction() {};' );

// It doesn't exist in the outer realm's global object:
console.log( globalthis.globalFunction );
// Result: undefined

// But when we evaluate `globalThis.globalFunction` inside the ShadowRealm:
console.log( shadow.evaluate( 'globalThis.globalFunction' ) );
// Result: function globalFunction()</code></pre>



<p>We&#8217;ve declared that function inside the ShadowRealm, and we can call it by way of the variable that references that ShadowRealm object. That function remains quarantined away from the outer global object and that of any other ShadowRealm:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Create a ShadowRealm:
const firstShadow = new ShadowRealm();
const secondShadow = new ShadowRealm();

// Declare a global function inside the ShadowRealm referenced by `secondShadow`:
secondShadow.evaluate( 'function globalFunction() {};' );

// It doesn't exist in the outer realm's global object:
console.log( globalthis.globalFunction );
// Result: undefined

// It doesn't exist in the global object of the ShadowRealm referencd by `firstShadow`:
console.log( firstShadow.evaluate( 'globalThis.globalFunction' ) );
// Result: undefined

// It only exists within the ShadowRealm referenced by `secondShadow`:
console.log( secondShadow.evaluate( 'globalThis.globalFunction' ) );
// Result: function globalFunction()</code></pre>



<p>“Quarantined” to an extent, that is. ShadowRealms don’t provide a true security boundary in that code running inside a ShadowRealm can still make inferences about code running in other realms. They <em>can</em> be thought of as an <em>integrity</em> boundary, in that code running inside a ShadowRealm can’t directly interfere with another realm — unless we let it, of course. Even though code shunted off into a ShadowRealm can&#8217;t interfere with the objects outside of it, we&#8217;re still free to use the results of those operations the way we would use the results of that same operation in the host realm:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Create a ShadowRealm:
const shadow = new ShadowRealm();

// Create a binding that calls a function inside the ShadowRealm:
const shadowFunction = shadow.evaluate( '( value ) => globalThis.someValue = value );

// ...and call our wrapped function using that binding:
shadowFunction( "Hello from the ShadowRealm!" );

// Executing this function in the host realm doesn't _change_ anything here, of course:
console.log( globalThis.someValue );
// Result: undefined

// But we can grab the result from the ShadowRealm:
const shadowValue = shadow.evaluate( 'globalThis.someValue' );

// And use it here in the host realm:
console.log( shadowValue );
// Result: Hello from the ShadowRealm!</code></pre>



<p>Infinite disposable cleanrooms! Pocket dimensions where we can execute whatever code we want, without fear of that code interfering with the scope of any other ShadowRealm <em>or</em> the outer realm — the &#8220;light realm,&#8221; if you will.</p>



<p>Now, some of you — especially those of you who&#8217;ve been doing this since the early days of JavaScript — have probably been recoiling at these examples. You&#8217;d be forgiven for thinking that ShadowRealm API is just goth <code>eval</code>, and you wouldn’t be strictly wrong: apart from running in the context of a ShadowRealm, what you’ve seen so far here are basically indirect calls to <code>eval</code> — even subject to the same <code>unsafe-eval</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy" rel="noopener">Content Security Policy</a> rule.</p>



<p>Fear not for your workflows, however: while these are <em>illustrative</em> examples, this isn&#8217;t the only way to put ShadowRealms to use. The proposal includes an <code>importValue</code> method on the ShadowRealm object’s prototype, which allows you to dynamically import modules, then capture and work with exported values and functions:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// spookycode.js
export function greeting() {
 return "Hello from the ShadowRealm!";
}</code></pre>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">async function shadowGreeter() {
  // I INVOKE THE DARK POWER OF THE SHADOWREALM- ahem. Sorry.
  const shadow = new ShadowRealm();

  /* 
  * `importValue` returns a promise that resolves with the value of the function 
  * specified in the second argument: 
  */
  const shadowGreet = await shadow.importValue( "./spookycode.js", "greeting" );

  // Call our wrapped function, annnnd...
  shadowGreet();
}

shadowGreeter();
// Result: Hello from the ShadowRealm!</code></pre>



<h3 class="wp-block-heading" id="the-shadow-hasn-t-fallen-yet">The shadow hasn’t fallen yet</h3>



<p>I&#8217;m pleased to say that you&#8217;ve now seen the <em>entirety</em> of the proposed ShadowRealms API, at this point. The proposal includes only those the two methods you&#8217;ve seen here — <code>evaluate</code> and <code>importValue</code> — both means of <del>banishing</del> evaluating code in the context of a ShadowRealm instance while still <em>executing</em> that code in the context of the host realm&#8217;s thread.</p>



<p>Again, though: none of this can be put to use just yet. The proposed specification is currently at <a href="https://tc39.es/process-document/" rel="noopener">Stage 2.7</a> — &#8220;approved in principle and undergoing validation,&#8221; meaning that it&#8217;s only likely to change as a result of feedback from tests and trial implementations in browsers, if at all. You&#8217;re playing a move ahead by reading this. When this proposal reaches Stage 3 and we start to see implementations in browsers, you&#8217;ll be ready to try it out for yourself. Nay, more than ready — at such time as the awesome power of the ShadowRealm is loosed upon the web, you shall stand at the ready to command its dark and fearsome majjycks! <em>The very realm upon which our code stands shall quake, as</em>— okay, okay, sorry. Look, I can&#8217;t help it! I mean, &#8220;<em>ShadowRealm</em>,&#8221; for cryin&#8217; out loud.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/">Soon We Can Finally Banish JavaScript to the ShadowRealm</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/soon-we-can-finally-banish-javascript-to-the-shadowrealm/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393604</post-id>	</item>
		<item>
		<title>Using CSS corner-shape For Folded Corners</title>
		<link>https://css-tricks.com/using-css-corner-shape-for-folded-corners/</link>
					<comments>https://css-tricks.com/using-css-corner-shape-for-folded-corners/#comments</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 08 May 2026 13:54:10 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[border]]></category>
		<category><![CDATA[corner-shape]]></category>
		<category><![CDATA[shapes]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393431</guid>

					<description><![CDATA[<p>I came across Kitty Giraudel’s folded corners technique. I’ve been on a bit of a <code>corner-shape</code> kick lately, so I figured that <code>corner-shape</code> could be used to create folded corners as well.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/using-css-corner-shape-for-folded-corners/">Using CSS corner-shape For Folded Corners</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>I came across <a href="https://kittygiraudel.com/2026/03/05/folded-corner-with-css/" rel="noopener">Kitty Giraudel’s folded corners technique</a>. It leverages CSS <code>clip-path</code>, and I thought that that was such a cool way to do it. <code>clip-path</code> has been trending lately, most likely because web browsers support the <a href="https://css-tricks.com/almanac/functions/s/shape/"><code>shape()</code></a> function now.</p>



<p>However, I’ve been on a bit of a <code>corner-shape</code> kick lately (have a look at my <a href="https://css-tricks.com/what-can-we-actually-do-with-corner-shape/">introduction to <code>corner-shape</code></a> as well as these <a href="https://css-tricks.com/experimenting-with-scroll-driven-corner-shape-animations/">scroll-driven <code>corner-shape</code> animations</a>), so I figured that <code>corner-shape</code> could be used to create folded corners as well, and this is what I came up with:</p>



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



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1768" height="916" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1768%2C916&#038;ssl=1" alt="White paper with the top-right corner folded in." class="wp-image-393432" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?w=1768&amp;ssl=1 1768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1024%2C531&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=768%2C398&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1536%2C796&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>So open Chrome, which supports <code>corner-shape</code>, and let’s dig in (if you’re looking at this in other browsers, it basically falls back to a rounded corner).</p>




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




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



<h2 class="wp-block-heading" id="step-1-set-some-css-variables">Step 1: Set some CSS variables</h2>



<p>Elements have four corners, but when we use <code>border-radius</code>, each corner is split into two coordinates. The x-axis coordinate moves along the x-axis, away from its associated corner, while the y-axis coordinate does the same thing along the y-axis. It’s from these coordinates that <code>border-radius</code> draws the curvature of the rounded corners.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2560" height="1665" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=2560%2C1665&#038;ssl=1" alt="Diagramming the shape showing border-radius applied to the bottom-left corner. The rounded corner is 50% on the y-axis and 50% on the x-axis." class="wp-image-392507" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=300%2C195&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=1024%2C666&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=768%2C499&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=1536%2C999&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/s_4A82C1A16CF8BD3E78A39757A7700A0B223569FCA13B21C3FBDA9426FB071038_1771786781751_3-scaled.png?resize=2048%2C1332&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>First, store the coordinates as CSS variables. We’ll need the values that they hold more than once, so this simplifies things, makes the fold animatable, and maintains some degree of realism.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  /* x-axis coordinate */
  --x-coord: 9rem;

  /* y-axis coordinate */
  --y-coord: 5rem;
}</code></pre>



<h2 class="wp-block-heading" id="step-2-establishing-the-fold">Step 2: Establishing the fold</h2>



<p>Given what we now know about <code>border-radius</code>, it should be obvious what <code>border-top-right-radius</code> does. As for <code>corner-top-right-shape: bevel</code>, that ensures that a straight line is drawn between the coordinates instead of rounded corners (<code>corner-top-right-shape: round</code>). That’s right, <code>border-radius</code> <em>includes</em> <code>corner-shape: round</code> by default (behind the scenes, of course).</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Square */
div {
  /* Place coordinates */
  border-top-right-radius: var(--x-coord) var(--y-coord);

  /* Draw line between coordinates */
  corner-top-right-shape: bevel;
}</code></pre>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1768" height="916" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?resize=1768%2C916" alt="White paper with a diagonal cut in the top-right corner." class="wp-image-393437" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?w=1768&amp;ssl=1 1768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?resize=1024%2C531&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?resize=768%2C398&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703905116_2-1.png?resize=1536%2C796&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h2 class="wp-block-heading" id="step-3-creating-the-flip-side">Step 3: Creating the flip side</h2>



<p>Now that we’ve established the fold, it’s time to create the flip side. Start by selecting <code>::before</code>, then declare <code>content: ""</code> to create the element without content. The <code>background</code> can be inherited from the square, and the dimensions should leverage the coordinates that we saved. As you can see, I’ve also added a <code>box-shadow</code> where the blur radius scales with <code>--x-coord</code> and <code>--y-coord</code>, but you’re welcome to adapt the formula as you see fit.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Square */
div {
  /* Place coordinates */
  border-top-right-radius: var(--x-coord) var(--y-coord);

  /* Draw line between coordinates */
  corner-top-right-shape: bevel;

  /* Flip side */
  &amp;::before {
    /* Generate empty element */
    content: "";

    /* Inherit background */
    background: inherit;

    /* Same as coordinates */
    width: var(--x-coord);
    height: var(--y-coord);

    /* Scale blur radius with --x-coord and --y-coord */
    box-shadow: 0 0 calc((var(--x-coord) + var(--y-coord)) / 3) #00000050;
  }
}</code></pre>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1768" height="916" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?resize=1768%2C916" alt="White paper with s white rectangle in the top-left corner and a diagonal cut in the top-right corner." class="wp-image-393435" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?w=1768&amp;ssl=1 1768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?resize=1024%2C531&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?resize=768%2C398&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775703996642_3.png?resize=1536%2C796&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h2 class="wp-block-heading" id="step-4-positioning-the-flip-side-before-">Step 4: Positioning the flip side (<code>::before</code>)</h2>



<p>Next, we need to shift <code>::before</code> to the (top-)right corner. We’re avoiding <a href="https://css-tricks.com/css-anchor-positioning-guide/">anchor positioning</a>, because there’s no need for modern features if more supported features work well using the same amount of code. So, declare <code>position: relative</code> on the square and <code>position: absolute</code> on <code>::before</code>. This makes <code>::before</code> position relative to the square, and is a trick that only works for parent-child relationships. Actually, this shortcoming is why anchor positioning was invented, but we just don’t need it in this case.</p>



<p>In addition, declare <code>inset: 0 0 auto auto</code> on <code>::before</code> to align it to the top-right corner of the square, and <code>overflow: clip</code> <em>on the square</em> to clip the half of <code>::before</code> that overflows it.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Square */
div {
  /* Place coordinates */
  border-top-right-radius: var(--x-coord) var(--y-coord);

  /* Draw line between coordinates */
  corner-top-right-shape: bevel;

  /* Clip any overflow */
  overflow: clip;

  /* For alignment */
  position: relative;

  /* Flip side */
  &amp;::before {
    /* Generate empty element */
    content: "";

    /* Inherit background */
    background: inherit;

    /* Same as coordinates */
    width: var(--x-coord);
    height: var(--y-coord);

    /* Scale blur radius with --x-coord and --y-coord */
    box-shadow: 0 0 calc((var(--x-coord) + var(--y-coord)) / 3) #00000050;

    /* For alignment */
    position: absolute;

    /* Align to top-right */
    inset: 0 0 auto auto;
  }
}</code></pre>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1768" height="916" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?resize=1768%2C916&#038;ssl=1" alt="White paper with the top-right corner folded in." class="wp-image-393434" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?w=1768&amp;ssl=1 1768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?resize=1024%2C531&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?resize=768%2C398&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704182876_4.png?resize=1536%2C796&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>You can stop here if you want, but there’s room for improvement…</p>



<h2 class="wp-block-heading" id="step-5-sculpting-the-flip-side">Step 5: Sculpting the flip side</h2>



<p>To make the outcome look a bit more realistic, we’ll use <code>corner-bottom-left-shape: bevel</code> to make one more straight cut, this time to <code>::before</code>. There are, most likely, many ways to tackle this depending on how sharply we want to crease the fold, how elevated we want the flip side to be, and the angle from which we want to view the square, but I don’t think it matters as long as the effect looks decent, so we’re aiming for a sharp crease, the flip side sticking up, and an aerial view. If you’d rather something different, keep in mind that the shadow also impacts the outcome, and that you’d be facing a trickier implementation.</p>



<p>The only degree of complexity that I suggest is this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Ensure realistic fold */
@container style(--x-coord &lt; --y-coord) {
  border-bottom-left-radius: 100% calc(100% - var(--x-coord));
}

@container style(--x-coord >= --y-coord) {
  border-bottom-left-radius: calc(100% - var(--y-coord)) 100%;
}</code></pre>



<p>These are <a href="https://css-tricks.com/the-range-syntax-has-come-to-container-style-queries-and-if/">container style queries using the range syntax</a>, where if the value of <code>--x-coord</code> is less than the value of <code>--y-coord</code>, we subtract the value of <code>--x-coord</code> from <code>100%</code> and use it as the y-axis coordinate for the relevant border radius (<code>border-bottom-left-radius</code>, in this case). The other axis is set to <code>100%</code>. Adversely, if the value of <code>--x-coord</code> is <em>more</em> than (or equal to) the value of <code>--y-coord</code>, we subtract the value of <code>--y-coord</code> from <code>100%</code> and use it as the x-axis coordinate. Once again, the other axis is set to <code>100%</code>.</p>



<p>The result is that the crease, shadow, and now <em>perspective</em> of the fold is calculated using only <code>--x-coord</code> and <code>--y-coord</code> to look realistic (or realistic enough, anyway). Using the <a href="https://css-tricks.com/playing-with-codepen-slidevars/">slideVars</a> toggles in the top-right corner of the demo, you can see for yourself by testing various combinations of coordinates:</p>



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



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



<p>If you want to implement a failsafe to ensure that the coordinates don’t exceed the dimensions of the square, breaking the effect, you can use <a href="https://css-tricks.com/almanac/functions/m/min/"><code>min()</code></a>. The modified coordinate variables below set <code>--y-coord</code> to an impossible <code>999999999rem</code>, but caps it at the height of the square (although I can’t imagine that you’d actually need this, to be completely honest):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">--x-coord: min(--square-width, 9rem);
--y-coord: min(--square-height, 999999999rem);</code></pre>



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



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1768" height="916" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1768%2C916&#038;ssl=1" alt="White paper with the top-right corner folded in." class="wp-image-393432" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?w=1768&amp;ssl=1 1768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1024%2C531&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=768%2C398&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_4309AEC000A3E54B3EAE7D9665BB7D541E09DE1556E769AE721013146BE52BD2_1775704329444_6.png?resize=1536%2C796&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>All in all, we have not only a folded corner effect but a utility that builds the effect based on only two coordinates.</p>



<p>The full code:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  /* x-axis coordinate */
  --x-coord: 9rem;

  /* y-axis coordinate */
  --y-coord: 5rem;

  /* Square */
  div {
    /* Place coordinates */
    border-top-right-radius: var(--x-coord) var(--y-coord);

    /* Draw line between coordinates */
    corner-top-right-shape: bevel;

    /* Clip any overflow */
    overflow: clip;

    /* For alignment */
    position: relative;

    /* Flip side */
    &amp;::before {
      /* Generate empty element */
      content: "";

      /* Inherit background */
      background: inherit;

      /* Same as coordinates */
      width: var(--x-coord);
      height: var(--y-coord);

      /* Scale blur radius with --x-coord and --y-coord */
      box-shadow: 0 0 calc((var(--x-coord) + var(--y-coord)) / 3) #00000050;

      /* For alignment */
      position: absolute;

      /* Align to top-right */
      inset: 0 0 auto auto;

      /* Draw line between coordinates */
      corner-bottom-left-shape: bevel;

      /* Ensure realistic fold */
      @container style(--x-coord &lt; --y-coord) {
        border-bottom-left-radius: 100% calc(100% - var(--x-coord));
      }

      @container style(--x-coord >= --y-coord) {
        border-bottom-left-radius: calc(100% - var(--y-coord)) 100%;
      }
    }
  }
}</code></pre>



<p class="is-style-explanation"><strong>Note:</strong> We could <a href="https://css-tricks.com/the-range-syntax-has-come-to-container-style-queries-and-if/">swap container style queries for <code>if()</code> functions</a>, which are shorter but less readable.</p>



<h2 class="wp-block-heading" id="folded-corners-using-clip-path-vs-corner-shape-">Folded corners using <code>clip-path</code> vs. <code>corner-shape</code></h2>



<p><a href="https://codepen.io/KittyGiraudel/pen/raNoZLr" rel="noopener">Kitty’s Giraudel’s folded corners</a> work in all browsers, and because <code>clip-path</code> is used, which is a more versatile shaping feature, there are more ways to customize the shape. It’s also the more correct approach, for whatever that’s worth. However, my <code>corner-shape</code> approach is cleaner and likely wouldn’t require any further customization anyway, but lacks Safari and Firefox support for now. So unless you need folded corners today, I’d bookmark both:</p>



<ul class="wp-block-list">
<li><a href="https://codepen.io/KittyGiraudel/pen/raNoZLr" rel="noopener">Folded corners using CSS <code>clip-path</code></a></li>



<li><a href="https://codepen.io/mrdanielschwarz/pen/EagyxrX" rel="noopener">Folded corners using CSS <code>corner-shape</code></a></li>
</ul>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/using-css-corner-shape-for-folded-corners/">Using CSS corner-shape For Folded Corners</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/using-css-corner-shape-for-folded-corners/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/corner-shape-fold-5-1.mp4" length="350278" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">393431</post-id>	</item>
		<item>
		<title>A Scrollytelling Gift for Mum on Mother’s Day 2026</title>
		<link>https://css-tricks.com/a-scrollytelling-gift-for-mum-on-mothers-day-2026/</link>
					<comments>https://css-tricks.com/a-scrollytelling-gift-for-mum-on-mothers-day-2026/#comments</comments>
		
		<dc:creator><![CDATA[Lee Meyer]]></dc:creator>
		<pubDate>Thu, 07 May 2026 14:22:38 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[Scroll Driven Animation]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394649</guid>

					<description><![CDATA[<p>I will explain how my mum inspired this 2026 Mother’s Day scrollytelling experiment — but also, how she inspired my approach to dev and life.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/a-scrollytelling-gift-for-mum-on-mothers-day-2026/">A Scrollytelling Gift for Mum on Mother’s Day 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>My mum loved logic because she was born at a time when nothing made sense. She was born in 1945, the year World War II ended, so she dodged a literal bullet because we are Jewish. But from the first day of her life, she found that famine, racism, and misfortune kept trying to take her away. In 2011, cancer took her away from me forever. But on a lighter note, this Mother’s Day I’m bringing her back to life the only way I know how: UI mad science!</p>



<p>I will explain how my mum inspired this 2026 Mother’s Day scrollytelling experiment — but also, how she inspired my approach to dev and life. Along the way, I’ll discuss some of the tech involved in this virtual Mother’s Day gift. I normally write either <a href="https://css-tricks.com/author/leemeyer/">inspirational or technical posts</a> — but for Mother’s Day, you’re getting a twofer.</p>



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



<h2 class="wp-block-heading" id="try-the-interactive-mother-s-day-card">Try the interactive Mother’s Day card</h2>



<p>Here’s the CodePen, which uses <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap/Using_scroll_snap_events" rel="noopener">scroll-snap events</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Conditional_rules/Container_scroll-state_queries" rel="noopener">scroll-state queries</a>, so it will only work in Chromium-based browsers at the moment.</p>



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



<p>Alternatively, here’s a video demo with commentary by my eight-year-old. It was bittersweet to realise that this is the closest he has come to interacting with his nana, because she passed before he was born.</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="mothers day interactive card 2026" width="500" height="375" src="https://www.youtube.com/embed/gu2cj9zO00s?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading" id="why-i-made-this">Why I made this</h2>



<p>Mum was born in a hospital in Kazakhstan, where civilian patients shared wards with discharged soldiers suffering PTSD. They wandered in and out of the maternity rooms, terrifying the patients and making labour even harder for my grandmother.</p>



<p>When Mum was born, she wasn’t breathing. The staff immersed her in cold water, then hot, then cold water again — <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC2672845/" rel="noopener">a so-called remedy at the time based on no science</a>. This was the beginning of a larger pattern in her life: She kept surviving not because of the help she received, but despite chaos disguised as help.</p>



<p>So, as an adult, Mum learned to survive by finding patterns and sense in the unfathomable. She accomplished this by combining her three passions:</p>



<ul class="wp-block-list">
<li>In photography, she framed moments when the chaos of her surroundings temporarily harmonized into beauty.</li>



<li>In teaching, she used those images to help tell a story that broke the chaos into logical steps people could follow.</li>



<li>In computer programming, she encapsulated those illustrated teachable moments within interactive experiences. Unlike in real life, if a programmed interaction goes wrong, you can trace why and solve the problem.</li>
</ul>



<p>In other words, she educated me by using the skill set I now think of as web development—before the web existed.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="765" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/lee-mothers-day.jpg?resize=1024%2C765" alt="Preview of the interactive card. A woman with short brown hair at the right of the frame looking left into the lens of a large camera and standing in the middle of a lush flower garden. In loving memory of Anna Meyer, 1945-2011." class="wp-image-394650" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/lee-mothers-day.jpg?resize=1024%2C765&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/lee-mothers-day.jpg?resize=300%2C224&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/lee-mothers-day.jpg?resize=768%2C573&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/lee-mothers-day.jpg?w=1200&amp;ssl=1 1200w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h2 class="wp-block-heading" id="gamifying-the-experience-of-knowing-my-mum">Gamifying the experience of knowing my mum</h2>



<p>I drew inspiration from Roland Franke’s <a href="https://codepen.io/ROL4ND909/pen/MWMzbog" rel="noopener">deconstructed radial slice transition using scroll-snap events</a>. Roland’s Pen showcases eye-catching, scroll-triggered transitions between landscapes as a figure sits in the foreground watching. This made me think of the patience my mum put into observing the world—but then she’d encapsulate everything in short, interactive stories I could digest as a young child.</p>



<p>I’m symbolizing that experience in my Mother’s Day game with the scroll-triggered <a href="https://www.youtube.com/watch?v=kQM6Q9Axyx0" rel="noopener">time-lapse</a> animation of day to night, stylized with CSS shapes. Using a single scroll gesture, we grasp the gist of an entire day. That experience is like the way my mum could explain a big topic to me in a way that felt like play.</p>



<p>My mum taught me that video games don’t have to be about blowing things up. She once used <a href="https://en.wikipedia.org/wiki/QuickBASIC" rel="noopener">QuickBASIC</a> to build a photography game long before <a href="https://en.wikipedia.org/wiki/Pok%C3%A9mon_Snap" rel="noopener">Pokémon Snap</a> existed. I remember passing a shop in the 90s with <a href="https://www.youtube.com/watch?v=QHWHCOe4Vbk" rel="noopener">Armor Alley</a> playing in demo mode in the window. I was obviously fascinated, but my mum said, “I don’t like it. The helicopter started it,” then she went home and built her photography game for me to play instead.</p>



<p>She once told me a story about photography from her childhood in the Soviet Union. She remembered taking a photo of a government building just because it looked cool, but a soldier saw her and confiscated the roll of film from her camera. Maybe the lesson was that exposing the reality of something can be just as much of a threat as shooting things in the militaristic sense of the word.</p>



<p>The violence common in games is a metaphor for the uncertainty and randomness we face in life, but my mum’s photography game taught me that violence isn’t the only way of coping with that problem, even in a game.</p>



<h2 class="wp-block-heading" id="how-the-scrollytelling-mother-s-day-card-works">How the scrollytelling Mother&#8217;s Day card works</h2>



<p>My mum inspired the randomness of the UFOs in this experiment with her ability to use a camera to capture the fleeting moments of sense in a chaotic world.</p>



<p>The combination of deterministic scroll-triggered animations with the randomness of the UFOs and text physics is possible using alien technology I’ve not seen used much in the wild: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap/Using_scroll_snap_events" rel="noopener">scroll-snap events</a>. This emergent module — available <a href="https://caniuse.com/wf-scroll-snap-events" rel="noopener">only in Chrome and Opera at the time of writing</a> — provides a simple JavaScript API so that when we style the page to snap between the day and night scenes, we can trigger behavior that isn’t possible in CSS alone, like the random flight paths of the UFOs, and the <a href="https://www.youtube.com/watch?v=CUAuy5SWJcw" rel="noopener">Pretext</a>-inspired effect of the UFOs repelling letters as the spaceships fly through the text.</p>



<p class="is-style-explanation"><strong>Sidenote:</strong> <a href="https://css-tricks.com/the-importance-of-native-randomness-in-css/">Randomness in CSS is coming</a>, but it only works in Safari for now.</p>



<p>Here’s the CSS to enable scroll snapping:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* The scroll container */
body {
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}

/* Each snap target */
.snap-panel {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}</code></pre>



<p>&#8230;and the JavaScript to handle scroll snap events:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// scrollsnapchanging fires while the user is scrolling —
// snapTargetBlock is the panel they are heading toward.
snapScroller.addEventListener('scrollsnapchanging', ({ snapTargetBlock }) => {
  markPanelStates({ active: selectedPanel, incoming: snapTargetBlock });
  if (snapTargetBlock === dayPanel)   onScrollingTowardDay();
  if (snapTargetBlock === nightPanel) onScrollingTowardNight();
});

// scrollsnapchange fires once a panel has snapped into place —
// snapTargetBlock is the panel now fully in view.
snapScroller.addEventListener('scrollsnapchange', ({ snapTargetBlock }) => {
  selectedPanel = snapTargetBlock;
  markPanelStates({ active: selectedPanel, incoming: null });
  if (snapTargetBlock === dayPanel)   onLandedOnDay();
  if (snapTargetBlock === nightPanel) onLandedOnNight();
});</code></pre>



<p>You can see that handling these events lets us create context-aware transitions between the two scenes, depending on the state of the game logic when the user slides between day and night and back again. My mum would always give me another chance to get things right.</p>



<h2 class="wp-block-heading" id="in-case-you-found-this-while-googling-for-a-conventional-mother-s-day-gift-idea-">In case you found this while Googling for a conventional Mother’s Day gift idea…</h2>



<p>Parting words: In the unlikely event that your mum is not into avant-garde homemade virtual gifts that showcase emergent browser features, get her a Kindle or something. I bought my mum a Kindle when she was alive. We’d read the same novel separately on our Kindles, then we’d compare notes over the phone on the days I couldn’t visit.</p>



<p><em>Happy Mother’s Day, everyone!</em></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/a-scrollytelling-gift-for-mum-on-mothers-day-2026/">A Scrollytelling Gift for Mum on Mother’s Day 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/a-scrollytelling-gift-for-mum-on-mothers-day-2026/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394649</post-id>	</item>
		<item>
		<title>Google’s Prompt API</title>
		<link>https://css-tricks.com/googles-prompt-api/</link>
					<comments>https://css-tricks.com/googles-prompt-api/#comments</comments>
		
		<dc:creator><![CDATA[Geoff Graham]]></dc:creator>
		<pubDate>Wed, 06 May 2026 19:41:29 +0000</pubDate>
				<category><![CDATA[Links]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[browser]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394653</guid>

					<description><![CDATA[<p><a href="https://wil.to/posts/googles-prompt-api/" rel="noopener">Mat Marquis</a> on Google pulling the web standards equivalent of <a href="https://en.wikipedia.org/wiki/Songs_of_Innocence_(U2_album)#Release" rel="noopener">U2 album marketing</a>:</p>
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>As a Chrome user, you’ll have&#160;<a href="https://www.techspot.com/news/112309-google-chrome-has-silently-pushing-4gb-ai-model.html" rel="noopener">received Gemini Nano in the form of a 4GB transfer</a>&#160;recently; no permission asked or required. If you remove it, </p>
</blockquote>
<p>&#8230;</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/googles-prompt-api/">Google’s Prompt API</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><a href="https://wil.to/posts/googles-prompt-api/" rel="noopener">Mat Marquis</a> on Google pulling the web standards equivalent of <a href="https://en.wikipedia.org/wiki/Songs_of_Innocence_(U2_album)#Release" rel="noopener">U2 album marketing</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>As a Chrome user, you’ll have&nbsp;<a href="https://www.techspot.com/news/112309-google-chrome-has-silently-pushing-4gb-ai-model.html" rel="noopener">received Gemini Nano in the form of a 4GB transfer</a>&nbsp;recently; no permission asked or required. If you remove it, Chrome will re-download it. For&nbsp;<a href="https://gdpr-info.eu/issues/consent/" rel="noopener">reasons I can only guess at</a>, Gemini Nano is presumably now considered to be part of Chrome itself, despite being a standalone product that is included alongside but not integrated&nbsp;<em>into</em>&nbsp;the browser — the way a copy of&nbsp;<a href="https://en.wikipedia.org/wiki/BonziBuddy" rel="noopener">Bonzi Buddy</a>&nbsp;included in a browser update might be considered a part of said browser.</p>
</blockquote>



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



<p>It&#8217;s not exactly <em>new</em> news, as we&#8217;ve had <a href="https://developer.chrome.com/docs/ai/prompt-api" rel="noopener">published</a> <a href="https://github.com/webmachinelearning/prompt-api/blob/main/README.md" rel="noopener">explainers</a> on it for over a year now, as well as an <a href="https://groups.google.com/a/chromium.org/g/blink-dev/c/x3QEjLYx5Rg" rel="noopener">intent to prototype</a> for just as long.</p>



<p><a href="https://github.com/mozilla/standards-positions/issues/1213#issuecomment-4347988313" rel="noopener">Mozilla has already voiced its concerns/opposition</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><a href="https://developer.chrome.com/docs/ai/prompt-api#use_the_prompt_api:~:text=Before%20you%20use%20this%20API%2C%20acknowledge%20Google%27s%20Generative%20AI%20Prohibited%20Uses%20Policy%2E" rel="noopener">According to Chrome&#8217;s documentation</a>, to use the prompt API you must &#8216;acknowledge&#8217;&nbsp;<a href="https://policies.google.com/terms/generative-ai/use-policy" rel="noopener">Google&#8217;s Generative AI Prohibited Uses Policy</a>. Elements of this policy go beyond law. For example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Do not engage … generating or distributing content that facilitates … Sexually explicit content<br>Do not engage in misinformation, misrepresentation, or misleading activities. This includes … Facilitating misleading claims related to governmental or democratic processes</p>
</blockquote>



<p>This seems like a bad direction for an API on the web platform, and sets a worrying precedent for more APIs that have UA-specific rules around usage.</p>
</blockquote>



<p>I have nothing to add, only that this is the sort of thing that seems worth knowing. Mat&#8217;s take-home isn&#8217;t exactly comforting because, remember, <em>this has already shipped</em>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I’d like to say that something to the tune of “their whole argument hinges on ‘positive developer sentiment,’ so let’s show them that there isn’t any” — but there isn’t any; they&nbsp;<em>cited</em>&nbsp;places where there isn’t any. That’s not how it works for them. Google participates in the web standards process the way a bear participates in the “camping” process.</p>



<p>[&#8230;]</p>



<p>Remember this the next time Google announces an “exciting new standard” that they’re heroically championing — for you, for users, for good of the web — in language that has just a hint of inevitability about it.</p>
</blockquote>



<p>The <a href="https://css-tricks.com/the-ecological-impact-of-browser-diversity/">browser ecosystem</a> has historically provided us with plenty of concerns. <a href="https://infrequently.org/series/browser-choice-must-matter/" rel="noopener">Alex Russell&#8217;s writing</a> is a treasure trove of the current limits of browser choice. And things are especially murky when we need to be reminded that <em><a href="https://polypane.app/blog/not-all-browser-apis-are-web-apis/" rel="noopener">not all browser APIs are Web APIs</a></em>.</p>



<p>Maybe helpful, maybe not:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2218" height="1906" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=2218%2C1906" alt="Chrome browser settings with system tab open showing disabled on-device AI option." class="wp-image-394658" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?w=2218&amp;ssl=1 2218w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=300%2C258&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=1024%2C880&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=768%2C660&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=1536%2C1320&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/05/chrome-system-settings-ai.png?resize=2048%2C1760&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>More coverage, if you&#8217;d like:</p>



<ul class="wp-block-list">
<li><a href="https://support.google.com/gemini/answer/16283624?hl=en&amp;visit_id=639136889569119000-2199918715&amp;p=mws_gic_ga&amp;rd=1" rel="noopener">&#8220;Use Gemini in Chrome&#8221;</a> (Gemini Apps Help)</li>



<li><a href="https://www.engadget.com/2166113/chrome-downloads-a-4gb-ai-file-without-user-consent-researcher-alleges/" rel="noopener">&#8220;Chrome downloads a 4GB AI file without user consent, researcher alleges&#8221;</a> (Engadget)</li>



<li><a href="https://cybernews.com/security/google-chrome-ai-model-device-no-consent/" rel="noopener">&#8220;Guy finds Google Chrome is quietly installing a 4GB AI model on our devices&#8221;</a> (Cybernews)</li>



<li><a href="https://www.androidauthority.com/google-chrome-weights-bin-ai-model-download-explained-3664043/" rel="noopener">&#8220;Is Chrome&#8217;s 4GB &#8220;weights.bin&#8221; file spyware?&#8221;</a> (Android Authority)</li>
</ul>



<p></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/googles-prompt-api/">Google’s Prompt API</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/googles-prompt-api/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394653</post-id>	</item>
		<item>
		<title>Making Zigzag CSS Layouts With a Grid + Transform Trick</title>
		<link>https://css-tricks.com/zigzag-css-grid-layouts/</link>
					<comments>https://css-tricks.com/zigzag-css-grid-layouts/#comments</comments>
		
		<dc:creator><![CDATA[Durgesh Rajubhai Pawar]]></dc:creator>
		<pubDate>Wed, 06 May 2026 13:50:44 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[grid]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[transform]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393013</guid>

					<description><![CDATA[<p>Most grid layouts sit in neat rows, perfectly aligned, like soldiers in formation. But sometimes you want something with more rhythm like, say, a zigzag pattern. Here's how to do it with CSS Grid.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/zigzag-css-grid-layouts/">Making Zigzag CSS Layouts With a Grid + Transform Trick</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>Most grid layouts sit in neat rows, perfectly aligned, like soldiers in formation. But sometimes you want something with more rhythm — a layout where items cascade diagonally, like water flowing down a waterfall.</p>



<p>This is the zigzag layout. And building it requires a small trick that reveals something fascinating about how CSS transforms actually work.</p>



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



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



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



<p>Before writing a single line of CSS, let&#8217;s think about approach.</p>



<p>The first idea that comes to mind: set up a flex container with <code>flex-direction: column</code> and <code>flex-wrap: wrap</code>, so items flow down and then wrap into a second column. Usually we think of the <a href="https://css-tricks.com/almanac/properties/f/flex-wrap/"><code>flex-wrap</code></a> property in terms of rows, but the nice thing about flexbox is that it works in either orientation.</p>



<p>Two problems make this approach awkward:</p>



<ol class="wp-block-list">
<li><strong>You need a fixed height.</strong> You have to tell the container &#8220;you are <code>500px</code> tall&#8221; for wrapping to kick in. That&#8217;s brittle.</li>



<li><strong>The tab order breaks.</strong> Items flow down the first column (i.e., 1, 2, 3), then jump to the second column (i.e., 4, 5, 6). That&#8217;s not a waterfall. That&#8217;s two buckets.</li>
</ol>



<p>To be fair, the <a href="https://css-tricks.com/complete-guide-css-grid-layout/">CSS Grid</a> approach we&#8217;re about to build has its own hardcoded value. We&#8217;ll get to that. But it sidesteps the <kbd>Tab</kbd> order problem entirely, and that&#8217;s a meaningful win.</p>



<h2 class="wp-block-heading" id="the-grid-plan">The Grid Plan</h2>



<p>Here&#8217;s what I want to do instead:</p>



<ol class="wp-block-list">
<li>Create a two-column grid with items sitting side by side, nothing fancy.</li>



<li>Select every item in the second column, the even ones.</li>



<li>Shift them down by half of their own height to establish the staggered layout.</li>
</ol>



<p>That shift is where the magic happens. Let&#8217;s build it.</p>



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



<p>We start with a wrapper and five items. Nothing in the file yet, just a blank slate.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="wrapper">
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
&lt;/div></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">*,
*::before,
*::after {
  box-sizing: border-box;
}

.wrapper {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  max-width: 800px;
  margin: 0 auto;
}

.item {
  height: 100px;
  border: 2px solid;
}</code></pre>



<p>We&#8217;re applying <code>box-sizing: border-box</code> globally because without it, the items aren&#8217;t actually <code>100px</code> tall — they&#8217;re slightly taller once the border gets added. This will matter in a moment.</p>



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



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



<p>Now the fun part. Let&#8217;s grab every even item and translate it down:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.item:nth-child(even of .item) {
  transform: translateY(50%);
}</code></pre>



<p>A quick note on the selector. You might reach for <code>.item:nth-of-type(even)</code> here, and in this demo it would produce the same result since all the children are the same element type. But <code>nth-of-type</code> selects by tag name, not by class. So if you ever mix different element types inside the wrapper, it&#8217;ll match in ways you don&#8217;t expect. <code>:nth-child(even of .item)</code> is more precise because it explicitly filters by class, and it&#8217;s well-supported in modern browsers.</p>



<p>The zigzag emerges immediately. But let&#8217;s pause here, because something subtle is happening and it&#8217;s worth understanding.</p>



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



<h2 class="wp-block-heading" id="transform-percentages-are-different">Transform Percentages Are Different</h2>



<p>Percentages in transforms work completely differently than they do anywhere else in CSS.</p>



<p>In flow layout, positioned layout, or really any layout mode, a percentage refers to the parent&#8217;s available space. If you write <code>width: 50%</code> on an element inside a wrapper, you&#8217;re saying: <q>The container is this wide. Make me half of that.</q></p>



<p>Transforms don&#8217;t work this way. In a transform, percentages refer to the element itself. So <code>translateY(50%)</code> doesn&#8217;t mean &#8220;move down by half of the available space.&#8221; It means &#8220;move down by half of your own height.&#8221; If the element is <code>200px</code> tall, it moves down by <code>100px</code>.</p>



<p>This is actually the same coordinate-system behavior you see with the individual <code>translate()</code>, <code>scale()</code>, and <code>rotate()</code> CSS properties. All of them are applied in the element&#8217;s own coordinate space, post-layout. The browser finishes laying everything out first, including positions, sizes — basically the whole box model — and then applies the transform relative to the element itself. That&#8217;s why <code>scale(2)</code> grows outward from the element&#8217;s center, not from the top-left of the page.</p>



<p>This is exactly why the trick works. Each even item shifts down relative to its own size, not the container&#8217;s. The zigzag stays proportional no matter how tall the items are.</p>



<p>The result looks close. But it&#8217;s not quite right.</p>



<h2 class="wp-block-heading" id="the-gap-problem">The Gap Problem</h2>



<p>We can expose the imperfection by cranking the <a href="https://css-tricks.com/almanac/properties/g/gap/"><code>gap</code></a> up to something absurd — say, <code>100px</code>. When we do, the even items clearly aren&#8217;t sitting where they should. They need to travel a little further to account for the vertical space between rows.</p>



<p>Here&#8217;s the fix. First, let&#8217;s store the gap in a CSS custom property so we can reference it in multiple places:</p>



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

  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  max-width: 800px;
  margin: 0 auto;
}

.item:nth-child(even of .item) {
  transform: translateY(calc(50% + var(--gap) / 2));
}</code></pre>



<p>We translate by <code>50%</code> of the element&#8217;s height plus half of the gap. We divide the gap by <code>2</code> because we only need to cover half the distance between rows — the full value would push it too far.</p>



<p>Set the gap to <code>16px</code>, it looks great. Set it to <code>100px</code>, it still looks great. The math holds regardless of the value.</p>



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



<h2 class="wp-block-heading" id="the-overflow-surprise">The Overflow Surprise</h2>



<p>We&#8217;ve solved the core puzzle. But there&#8217;s a hidden problem waiting to surface.</p>



<p>Let&#8217;s add a border to the wrapper to see its boundaries:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.wrapper {
  border: 2px solid red;
}</code></pre>



<p>With five items, everything looks fine. The wrapper contains all of its children. No overflow. No issues.</p>



<p>Now add a sixth item:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line="7"><code markup="tt">&lt;div class="wrapper">
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
  &lt;div class="item">&lt;/div>
&lt;/div></code></pre>



<p>The sixth item is even. It gets translated down. And it spills right out of the container.</p>



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



<p>Why? Because transforms don&#8217;t affect layout. As far as the browser&#8217;s layout engine is concerned, that sixth item is still sitting in its original, untranslated position. The wrapper sizes itself based on that original position. The transform shifts pixels visually, but the parent has no idea anything moved.</p>



<p>We surprised the browser.</p>



<h2 class="wp-block-heading" id="the-fix-reserve-the-space">The Fix: Reserve the Space</h2>



<p>The simplest solution is to add <code>padding-bottom</code> (or <code>padding-block-end</code>) to the wrapper, enough to accommodate the overshoot. The padding needs to match the translation: half the item height plus half the gap.</p>



<p>Since padding percentages reference the parent&#8217;s width (not the child&#8217;s height), we can&#8217;t use the same <code>50%</code> trick here. Instead, we store the item height as a variable:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.wrapper {
  --gap: 16px;
  --item-height: 100px;
  
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  margin: 0 auto;
  max-width: 800px;
  padding-bottom: calc(var(--item-height) / 2 + var(--gap) / 2);
}

.item {
  border: 2px solid;
  height: var(--item-height);
}</code></pre>



<p>Now, I&#8217;ll be up front: <code>--item-height: 100px</code> is a hard-coded value. That&#8217;s the same kind of brittleness I flagged in the flexbox approach, where you need a fixed container height for wrapping to work. Both approaches ask you to know a dimension ahead of time. The difference here is that you&#8217;re locking down the item height rather than the container height, and the rest of the layout — column structure, gap math, source order — stays flexible. It&#8217;s a trade-off, not a deal-breaker, but it&#8217;s worth being honest about.</p>



<p>The wrapper now reserves exactly enough space at the bottom. No overflow. No surprises.</p>



<h2 class="wp-block-heading" id="a-note-on-accessibility">A Note on Accessibility</h2>



<p>This approach keeps items in their natural source order, and that matters more than it might seem at first glance.</p>



<p><strong>Screen readers are unaffected.</strong> Transforms are purely visual. The DOM order stays 1-6, and that&#8217;s exactly how assistive technology will announce them. No reordering surprises, unlike the flexbox column-wrap approach where the visual order and DOM order can diverge.</p>



<p><strong>Focus order stays intact, too.</strong> When someone tabs through the items, focus follows the source order, not where the items appear visually. In our zigzag, the visual flow and source order both cascade left-right, top-down, so they naturally agree. If your layout ever gets complex enough that visual and source order start to diverge, that&#8217;s when you&#8217;d need to think more carefully about focus management.</p>



<p><strong>Respect motion preferences.</strong> The zigzag itself is static — we&#8217;re not animating the transform. But if you ever decide to animate items into their staggered positions (say, on page load), wrap that animation in a <code>prefers-reduced-motion</code> check:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* animates when user has no motion preference */
@media (prefers-reduced-motion: no-preference) {
  .item {
    animation: slide-in 0.3s ease-out both;
  }
}</code></pre>



<p>In this case, we&#8217;ve set it up so that users who have no preference on motion are the only ones who get the animation. Typically, though, <a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/">you might do the inverse of that</a>. The layout still works either way.</p>



<h2 class="wp-block-heading" id="the-final-demo">The Final Demo</h2>



<p>Once again:</p>



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



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



<p>The zigzag layout is really just three ideas stacked on top of each other:</p>



<ol class="wp-block-list">
<li>A two-column grid gives us the foundation.</li>



<li><code>translateY(50%)</code> creates the stagger and works because transform percentages reference the element itself, not the parent.</li>



<li><code>padding-bottom</code> reserves space for the translated items because transforms move pixels without telling the layout engine.</li>
</ol>



<p>Change the gap. Change the item height. Add more items. The zigzag holds.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/zigzag-css-grid-layouts/">Making Zigzag CSS Layouts With a Grid + Transform Trick</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/zigzag-css-grid-layouts/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393013</post-id>	</item>
		<item>
		<title>Fixed-Height Cards: More Fragile Than They Look</title>
		<link>https://css-tricks.com/fixed-height-cards-more-fragile-than-they-look/</link>
					<comments>https://css-tricks.com/fixed-height-cards-more-fragile-than-they-look/#comments</comments>
		
		<dc:creator><![CDATA[Kevine Nzapdi]]></dc:creator>
		<pubDate>Mon, 04 May 2026 14:01:36 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[content]]></category>
		<category><![CDATA[language]]></category>
		<category><![CDATA[layout]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393102</guid>

					<description><![CDATA[<p>Getting a multi-column of cards to line up equally is is a headache we've all faced, and it gets even harder when working with fixed heights.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/fixed-height-cards-more-fragile-than-they-look/">Fixed-Height Cards: More Fragile Than They Look</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>Fixed-height cards often feel like a safe choice. A designer hands you a mockup where every card aligns perfectly in a grid. The titles are short, the excerpts fit neatly, and the layout looks stable across the entire page. So you implement the design exactly as specified and ship it.</p>



<p>Everything works until the content changes. An editor updates the copy, a translation adds longer words, and some users bump their default font size, especially those with low vision or digital eye strain, just to make things easier to read.</p>



<p>I ran into this while building a “Recent Articles” section for a blog. The design assumed relatively short English titles, so everything fit comfortably inside the fixed height.</p>



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



<p>The layout looked solid at first glance:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1464" height="924" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771412989046_file.png?resize=1464%2C924" alt="A three-column layout of cards. Each card is equal height, containing an image, heading, blurb, tags, and action button." class="wp-image-393125" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771412989046_file.png?w=1464&amp;ssl=1 1464w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771412989046_file.png?resize=300%2C189&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771412989046_file.png?resize=1024%2C646&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771412989046_file.png?resize=768%2C485&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Initial design</figcaption></figure>



<p>But once the content changed, the cracks started appearing:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1192" height="480" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415403004_file.png?resize=1192%2C480" alt="A three-column layout of cards. The content contained in the third card is longer than the first two cards, resulting in its content overlapping with elements below it." class="wp-image-393123" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415403004_file.png?w=1192&amp;ssl=1 1192w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415403004_file.png?resize=300%2C121&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415403004_file.png?resize=1024%2C412&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415403004_file.png?resize=768%2C309&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Translating the content to French made things worse:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1143" height="478" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415428852_file.png?resize=1143%2C478&#038;ssl=1" alt="" class="wp-image-393122" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415428852_file.png?w=1143&amp;ssl=1 1143w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415428852_file.png?resize=300%2C125&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415428852_file.png?resize=1024%2C428&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415428852_file.png?resize=768%2C321&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Language issues</figcaption></figure>



<p>German translations pushed the layout even further:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1134" height="460" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415437407_file.png?resize=1134%2C460" alt="A three-column layout of cards. The heading in the first card is longer than the headings in the other two cards, resulting in content below the headings overlapping with elements below them." class="wp-image-393120" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415437407_file.png?w=1134&amp;ssl=1 1134w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415437407_file.png?resize=300%2C122&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415437407_file.png?resize=1024%2C415&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771415437407_file.png?resize=768%2C312&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">More layout failures</figcaption></figure>



<p>What once looked like a stable component turned out to depend on a fragile assumption: that the content would always stay within a fixed height.</p>



<p>Here’s a demo of the layout:</p>



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



<h3 class="wp-block-heading" id="fixed-height-layouts-look-fragile">Fixed-Height Layouts Look Fragile</h3>



<p>In the design specifications, the pixel dimensions were exact, and you know that cards align more cleanly when they have the same vertical rhythm and equal size, which creates in our mind a sense of order that I and the designer kind of trusted.</p>



<p>So, I set:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__title {
  margin: 0 0 8px;
  font-size: 18px;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__excerpt {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}</code></pre>



<p>But surprisingly, the behavior changed as soon as the font settings changed. I increased the browser’s default text size and realized that it introduced pressure inside the cards. My text blocks grew, but the container remained the same, and elements began competing for the same space.</p>



<p>Normally, a block element simply grows with its content. But the moment I set that height, I broke that relationship. The browser doesn&#8217;t treat this as a problem; it just resolves the conflict the only way it can, by either letting content overflow or clipping it.</p>



<p>In the original version of the layout, I just bluntly hid those problems with <code>overflow: hidden</code>.</p>



<p>To make the problem visible, we can remove the safety net:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line="8"><code markup="tt">.card__title {
  display: -webkit-box;
  font-size: 18px;
  line-height: 1.2;
  margin: 0 0 8px;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  /* overflow: hidden; */
}

.card__excerpt {
  display: -webkit-box;
  font-size: 14px;
  line-height: 1.4;
  margin: 0 0 10px;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}</code></pre>



<p>Without <code>overflow: hidden</code>, the failure is no longer subtle. The content stops clipping and starts spilling out like groceries from a torn bag. Some excerpts sit right on the tags, and everything was breaking once we stopped hiding the pressure inside the card.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1132" height="421" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773162669651_image.png?resize=1132%2C421" alt="A three-column layout of cards. The first cards heading is much longer than the other two cards, causing the heading to overlap with the content beneath it." class="wp-image-393114" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773162669651_image.png?w=1132&amp;ssl=1 1132w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773162669651_image.png?resize=300%2C112&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773162669651_image.png?resize=1024%2C381&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773162669651_image.png?resize=768%2C286&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Removing <code>overflow: hidden</code> reveals the structural tension instead of masking it.</figcaption></figure>



<p>Unfortunately, the browser has no way to reconcile those competing instructions except by letting elements collide.</p>



<h3 class="wp-block-heading" id="removing-the-fixed-height">Removing the Fixed Height</h3>



<p>Removing the constraints that held this layout together reveals where the real problem lives. Fixed heights, absolute positioning, and grid alignment were all trying to control the same thing.</p>



<h4 class="wp-block-heading" id="absolutely-positioned-actions-removed-from-flow">Absolutely Positioned Actions: Removed From Flow</h4>



<p>Up to this point, the fixed height looks like the main culprit to me. But it isn’t acting alone; the actions at the bottom of the card were absolutely positioned:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__actions {
  position: absolute;
  inset: 0 14px 14px;
}</code></pre>



<p>This feels like a clean solution; the actions stay pinned to the bottom of the card no matter how long the content is.</p>



<p>In a typical block layout, a container’s height is determined by the combined contribution of its in-flow children.</p>



<p>I’m sure you have seen <a href="https://css-tricks.com/absolute-relative-fixed-positioining-how-do-they-differ/">how absolutely positioned elements behave</a>. The browser still renders them, even though they no longer contribute to the parent’s intrinsic height. Visually, the actions belong to the card, structurally, the layout ignores them.</p>



<p>To compensate, we reserved space manually:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__body {
  padding-block-end: 14px;
}</code></pre>



<p>This padding is really just an estimate. The moment the font size increases, buttons wrap, or translations make the text longer, the estimate stops being reliable.</p>



<p>Instead of trying to predict how much space the actions might need, we can let the browser calculate it.</p>



<p>Here is the same layout without absolute positioning:</p>



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



<p>The change is small, but the shift in behavior is quite noticeable. Even with the fixed height still in place, the internal tension shrinks because the layout is no longer working against itself.</p>



<p>This is the first structural improvement. The card still has an extrinsic height constraint, so the layout isn’t fully flexible yet.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1126" height="501" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771429260865_image.png?resize=1126%2C501" alt="A three-column layout of cards. The heading contained in the second card is shorter than the other two cards, resulting in the card bottom borders being uneven and overlapping the content." class="wp-image-393112" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771429260865_image.png?w=1126&amp;ssl=1 1126w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771429260865_image.png?resize=300%2C133&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771429260865_image.png?resize=1024%2C456&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771429260865_image.png?resize=768%2C342&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Removing absolute positioning reduces internal layout tension, even before removing the fixed height.</figcaption></figure>



<h4 class="wp-block-heading" id="there-is-an-illusion-of-control">There is an Illusion of Control</h4>



<p>If fixed heights act like ceilings, line clamping acts more like a mute button. In the original component, I clamped the title and the excerpt:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__title {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.card__excerpt {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
}</code></pre>



<p>Clamping feels reassuring to me at that time because it limits drift and keeps cards visually aligned. But in practice, that flips the relationship.</p>



<p>To really see this more clearly, let’s remove clamping while keeping everything else the same. This version is identical to the previous demo except that I have removed all clamping from <code>.card__title</code> and <code>.card__excerpt</code> but left the overflow so that we can clearly see what happens.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1149" height="607" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773164139142_image-1.png?resize=1149%2C607" alt="A three-column layout of cards. The first card's content is shorter than the other two cards, resulting in its border overlapping the content." class="wp-image-393111" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773164139142_image-1.png?w=1149&amp;ssl=1 1149w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773164139142_image-1.png?resize=300%2C158&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773164139142_image-1.png?resize=1024%2C541&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7CA1115BFA9387F857EF921E56E77A943A603D3C3D838C516571362CA9D1CAF2_1773164139142_image-1.png?resize=768%2C406&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Removing clamping exposes how much content the layout was suppressing.</figcaption></figure>



<p>Without clamping, the tension inside the component becomes obvious. You see how German card grows taller, and the excerpt wraps naturally. What this really shows us is that a stable layout shouldn&#8217;t rely on <code>overflow: hidden</code>. If a layout only works because content is being suppressed, it’s probably fragile.</p>



<p>Up to this point, almost every failure we’ve seen traces back to a single decision:</p>



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



<p>This one line may look innocent to you, but it overrides the browser’s default sizing behavior.</p>



<p>At some point, the simplest question becomes unavoidable: So what happens if we just&#8230; stop? Remove the height entirely and let the browser do its thing?</p>



<p>Let’s remove the fixed height while keeping the rest of the layout intact. Clamping can stay in place since we want to compare behaviors.</p>



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



<p>Once I restored intrinsic sizing inside the card, the alignment problem really became a grid issue, which brings us to our next refinement.</p>



<h4 class="wp-block-heading" id="let-the-grid-handle-equal-heights">Let the Grid Handle Equal Heights</h4>



<p>Fixed heights felt appealing. But having equal heights doesn’t actually mean fixing the heights manually. The grid can handle that alignment for us without me imposing hard boundaries on each component.</p>



<p>Sometimes, the fix is surprisingly small. Removing <code>align-items: start</code> lets the grid items stretch naturally, and switching to a more flexible column definition helps the layout adapt better across different screen sizes.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card-grid {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}</code></pre>



<p>See how the same layout uses intrinsic card heights and flexible grid tracks:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1150" height="493" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771432329324_image.png?resize=1150%2C493" alt="A three-column layout of cards. The content in the second card is shorter than the content in the first and third cards." class="wp-image-393106" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771432329324_image.png?w=1150&amp;ssl=1 1150w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771432329324_image.png?resize=300%2C129&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771432329324_image.png?resize=1024%2C439&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_9FDC20B5C845F6A072AD09BF1338C3363C6F91D4ADB2AA0EA629B08B8CDCC440_1771432329324_image.png?resize=768%2C329&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Grid normalizes alignment without imposing arbitrary height constraints.</figcaption></figure>



<p>To make the button nicely align like we had initially, instead of positioning and reserving space manually:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  padding: 14px;
  position: relative;
}</code></pre>



<p>We turn the card into a vertical layout:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  display: flex;
  flex-direction: column;
  padding: 14px;
}</code></pre>



<p>We&#8217;re not going to go deep on flexbox here, as <a href="https://css-tricks.com/equal-columns-with-flexbox-its-more-complicated-than-you-might-think/">Kevin Powell has a great article on exactly that</a>. But it&#8217;s worth knowing what&#8217;s happening. Turning the card into a flex container with <code>flex-direction: column</code> lines everything up vertically from top to bottom.</p>



<p>The next step is removing the artificial space that was holding room for the actions:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__body {
  padding-block-end: 56px;
  padding-block-start: 10px;
}</code></pre>



<p>That padding was a guess; it only worked as long as the content stayed predictable. Instead, we let the body expand naturally:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding-block-start: 10px;
}</code></pre>



<p>The <code>flex: 1</code> tells the body to take up whatever space is left after the image, and the actions have taken what they need.</p>



<p>If the tags need a bit of breathing room, a simple margin does the job:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__tags {
  margin-block-end: 10px;
}</code></pre>



<p>We get a card that looks just as aligned as in our original page, but now the alignment comes from layout flow, not from forcing the height.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1126" height="553" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_515F46CFDB4EE19FEB6AF9062D43AFC70FA23A7AB0325DB26247B7D2B9D34667_1773489039837_image.png?resize=1126%2C553&#038;ssl=1" alt="A three-column layout of cards that contain an image, heading, blurb, tags, and button." class="wp-image-393105" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_515F46CFDB4EE19FEB6AF9062D43AFC70FA23A7AB0325DB26247B7D2B9D34667_1773489039837_image.png?w=1126&amp;ssl=1 1126w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_515F46CFDB4EE19FEB6AF9062D43AFC70FA23A7AB0325DB26247B7D2B9D34667_1773489039837_image.png?resize=300%2C147&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_515F46CFDB4EE19FEB6AF9062D43AFC70FA23A7AB0325DB26247B7D2B9D34667_1773489039837_image.png?resize=1024%2C503&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_515F46CFDB4EE19FEB6AF9062D43AFC70FA23A7AB0325DB26247B7D2B9D34667_1773489039837_image.png?resize=768%2C377&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h4 class="wp-block-heading" id="using-clamp-for-fluid-typography">Using <code>clamp()</code> for Fluid Typography</h4>



<p>Fluid typography with <code>clamp()</code> can make titles scale more smoothly across viewport sizes:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card__title {
  font-size: clamp(1rem, 2vw, 1.25rem);
}</code></pre>



<p>If you want to know more about <code>clamp()</code>, <a href="https://css-tricks.com/linearly-scale-font-size-with-css-clamp-based-on-the-viewport/">Pedro Rodriguez’s article on scaling font size</a> with CSS <code>clamp()</code> is a good read.</p>



<p>Declaring <code>clamp(1rem, 2vw, 1.25rem)</code> allows the title to scale with the viewport while staying within a safe range. The font size can grow or shrink with the viewport (<code>2vw</code>) but will never go smaller than <code>1rem</code> or larger than <code>1.25rem</code>.</p>



<h3 class="wp-block-heading" id="designing-for-failure">Designing for Failure</h3>



<p>None of the problems I mentioned earlier in this layout appeared while I was building it. The problems appeared only when some conditions changed. Sometimes an image didn’t load, which changed the vertical balance of the card. And as the viewport narrowed, the text had to wrap more aggressively.</p>



<p>If you want to know whether a component will hold up with real content, try putting it under extreme conditions. A few simple tweaks are enough to reveal where the layout starts to break or fall apart:</p>



<ul class="wp-block-list">
<li>Increase the browser’s default font size to see how it behaves.</li>



<li>Enable text-only zoom instead of page zoom to observe the difference.</li>



<li>Replace a title with a single unbroken string or simulate other languages with longer words.</li>



<li>Simulate a missing image.</li>



<li>Shrink the viewport until the text starts wrapping aggressively.</li>
</ul>



<p>Rather than explaining things abstractly, we can introduce them directly into the intrinsic-height version of the card.</p>



<h3 class="wp-block-heading" id="stress-test-mode">Stress Test Mode</h3>



<p>From the intrinsic-height version, we can add a simple toggle that simulates a few content stress cases.</p>



<p>Add this button inside the <code>.demo-toolbar</code>:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;button type="button" id="toggleStress">
  Toggle stress test
&lt;/button></code></pre>



<p>Add the following script, too:</p>



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

stressBtn.addEventListener("click", () => {
  document.body.classList.toggle("stress");
});</code></pre>



<p>This script simply listens for clicks on the button and adds or removes a stress class on the <code>&lt;body&gt;</code>. That class acts as a switch that turns the stress-test styles on and off.</p>



<p>And add these styles:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body.stress .card:nth-child(1) .card__title::after {
  content: "ExtremelyLongUnbrokenStringWithoutAnySpacesToTestOverflowBehavior";
}

body.stress .card:nth-child(2) .card__excerpt {
  font-size: 1.1rem;
}

body.stress .card__media img {
  display: none;
}</code></pre>



<p>These styles simulate a few common layout stress cases. The first card gets an unbroken string to test overflow behavior. The second increases text size to mimic larger default font settings. The rule on <code>.card__media img</code> hides media entirely to simulate a missing or failed image load.</p>



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



<p>This stability isn&#8217;t coming from the defensive rules I added at the end. It comes from the earlier structural decisions. Once fixed heights and out-of-flow positioning were removed, the component could adapt naturally to whatever content it receives.</p>



<p>Once you start relying on intrinsic sizing, you stop worrying about every possible string length or font setting. If the content gets longer or the text size changes, the browser can handle it. Most layout problems start when we take that flexibility away.</p>



<h3 class="wp-block-heading" id="so-what-grows-and-what-doesn-t-">So, What Grows and What Doesn’t?</h3>



<p>The original card failed for a simple reason: <strong>it depended on assumptions that were never stated.</strong> The title was supposed to fit in two lines, the excerpt was supposed to fit in four and buttons were supposed to stay on one line. Translations were supposed to stay “about the same length” and users were supposed to keep default text settings. None of that was enforced. They were simply guesses.</p>



<p>Those assumptions quietly made their way into my CSS. As long as the content stayed within those boundaries, everything kind of looked stable. But the moment it drifted, the layout started responding badly to the conflict.</p>



<p>When I rebuilt this component, the first thing I did was remove those hidden dependencies. There’s no fixed pixel ceiling anymore, no padding buffer that needs me to constantly tweak, and no truncation acting as a safety net to keep the layout from breaking.</p>



<p><a href="https://css-tricks.com/almanac/properties/t/text-overflow/">Truncation can still be a deliberate design choice.</a> But you shouldn’t truncate just to keep the layout from collapsing. When that happens, the component is already under strain.</p>



<p>The final demo shows that idea in practice. It loads stressed content by default, with longer translated text, wrapped tags, and a missing image, so that you can see how the component behaves under real conditions rather than ideal ones.</p>



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



<p>Each card grows as needed, and the grid keeps alignment without hiding overflow or relying on defensive spacing.</p>



<h3 class="wp-block-heading" id="i-think-fixed-heights-are-still-useful">I Think Fixed Heights Are Still Useful</h3>



<p>Working through this layout changed how I think about fixed heights. I still use them when they make sense, and I still clamp text when truncation is intentional. But whenever I find myself trying to control how content flows inside a component, it’s usually a sign that the layout needs to be reconsidered. Most of the time, letting the browser handle the sizing leads to a more resilient result.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/fixed-height-cards-more-fragile-than-they-look/">Fixed-Height Cards: More Fragile Than They Look</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/fixed-height-cards-more-fragile-than-they-look/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393102</post-id>	</item>
		<item>
		<title>What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More</title>
		<link>https://css-tricks.com/whats-important-10/</link>
					<comments>https://css-tricks.com/whats-important-10/#comments</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 01 May 2026 13:43:26 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394456</guid>

					<description><![CDATA[<p>Developers have been experimenting with HTML-in-Canvas, a hexagonal world map-analytics feature, a web-based OS for e-ink devices, replacing image sources using the content property, and more. This is What’s !important #10.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-10/">What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, 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>Developers have been experimenting with HTML-in-Canvas, a hexagonal world map-analytics feature, a web-based OS for e-ink devices, replacing <code>img</code> <code>src</code>s using <code>content</code>, and more. This is <strong>What’s !important #10</strong>.</p>



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



<h3 id="htmlincanvas-experiments" class="wp-block-heading">HTML-in-Canvas experiments</h3>



<p>HTML-in-Canvas, a new API that enables us to render real semantic HTML in a <code>&lt;canvas&gt;</code> with visual effects, is the talk of the town right now, so let’s lead with that. <a href="https://css-tricks.com/author/amitsheen/">Amit Sheen</a> showed us <a href="https://frontendmasters.com/blog/the-web-is-fun-again-first-experiments-with-html-in-canvas/" rel="noopener">how the HTML-in-Canvas API works</a>, and also created some <a href="https://hicshowroom.com/" rel="noopener">demos over at the HiC Showroom</a>, like this one (requires Chrome 146 with the <code>chrome://flags/#canvas-draw-element</code> flag enabled):</p>



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



<h3 id="building-a-hexagonal-world-mapanalytics-feature" class="wp-block-heading">Building a hexagonal world map-analytics feature</h3>



<p>Ben Schwarz (awesome name, but no relation) talked about <a href="https://calibreapp.com/blog/building-our-beloved-hex-map" rel="noopener">building a hexagonal world map-analytics feature</a>. While it’s more of a retrospective than a developer walkthrough, it’s a <em>really</em> interesting read about analytics, design constraints, inspiration, engineering, and of course SVG and CSS.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1760" height="990" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1760%2C990&#038;ssl=1" alt="A world map composed of small hexagons colored in orange, green, and red." class="wp-image-394491" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?w=1760&amp;ssl=1 1760w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1536%2C864&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://calibreapp.com/blog/building-our-beloved-hex-map" rel="noopener">Calibre</a>.</figcaption></figure>



<h3 id="rekindle-a-webbased-os-for-eink-devices" class="wp-block-heading">Rekindle — a web-based OS for e-ink devices</h3>



<p><a href="https://rekindle.ink/" rel="noopener">Rekindle</a> is basically a web-based operating system for e-ink devices like Kindle, Kobo, and Boox, which are often low-powered with few features. Rekindle includes an insane number of features and apps, and is designed in black-and-white, with no animations, and no doubt with many more e-ink optimizations.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2560" height="1606" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=2560%2C1606&#038;ssl=1" alt="A black and white user interface for Rekindle that primarily shows a grid of app icons." class="wp-image-394492" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=768%2C482&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=1536%2C963&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=2048%2C1285&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>The takeaway isn’t a tutorial (unfortunately) or even some commentary (like with the world map retrospective above), it’s that we have a whole bunch of media queries that’d be so useful for e-ink devices if it weren’t for the fact that they’re shipping with low-powered, proprietary web browsers that don’t recognize them. <a href="https://www.w3.org/TR/mediaqueries-5/" rel="noopener">Media Queries Level 5</a> can query hover capability, the precision of pointers, display update frequency, color depth, monochromatic bit-depth, color index size, dynamic range, and more, probably.</p>



<p>Thoughts? Is e-ink optimization likely to break out in the coming years, or is low demand for these media queries why a dedicated service like Rekindle needs to exist? It’s worth noting that the browsers and many of the media queries are in active development, so I don’t know. Watch this space, maybe?</p>



<p>Either way, I’d love to see a dev deep dive on Rekindle!</p>



<h3 id="replacing-img-srcs-using-content" class="wp-block-heading">Replacing <code>img</code> <code>src</code>s using <code>content</code></h3>



<p><a href="https://bsky.app/profile/scrwd.mastodon.social.ap.brid.gy/post/3mjwgpvgy7k32" rel="noopener">Jon discovered</a> that CSS can be used to replace image sources, like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;img src="image.png" alt="Alt text"></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
  content: url(new-image.png) / "New alt text";
}</code></pre>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:wjkfbooeb4bmapfc2op7eeza/app.bsky.feed.post/3mjwgpvgy7k32" data-bluesky-cid="bafyreifcnpfsgjnbzi2x5kpbzrb7six3zsrupp2vqovedyrf2y3dvziizi" data-bluesky-embed-color-mode="system"><p lang="en">TIL! Who knew you could change the &quot;src&quot; of an #HTML &lt;img&gt; using #CSS:

img { content: url(whatever.png) }

NO PSEUDOS!

<iframe title="Untitled" id="cp_embed_MYjRQje" src="https://codepen.io/jon/embed/preview/MYjRQje?default-tabs=html%2Cresult&amp;height=300&amp;host=https%3A%2F%2Fcodepen.io&amp;slug-hash=MYjRQje" scrolling="no" frameborder="0" height="300" allowtransparency="true" allowfullscreen="true" allowpaymentrequest="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;"></iframe>

Seems to work in all current browsers too. How did I miss this?</p>&mdash; Jon (<a href="https://bsky.app/profile/did:plc:wjkfbooeb4bmapfc2op7eeza?ref_src=embed" rel="noopener">@scrwd.mastodon.social.ap.brid.gy</a>) <a href="https://bsky.app/profile/did:plc:wjkfbooeb4bmapfc2op7eeza/post/3mjwgpvgy7k32?ref_src=embed" rel="noopener">Apr 20, 2026 at 13:09</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>



<p>It’s really interesting to learn this about the <a href="https://css-tricks.com/almanac/properties/c/content/"><code>content</code></a> property, which has been Baseline for 11 years now. I experimented a bit more and discovered that this trick also works with the <a href="https://css-tricks.com/almanac/functions/i/image-set/"><code>image-set()</code></a> function:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
  content: image-set(
    url("image.png") 1x,
    url("image-2x.png") 2x
  );
}</code></pre>



<p>So if you’re working on a website with non-responsive <code>&lt;img&gt;</code>s and no way to change the markup, write the logic in CSS instead.</p>



<h3 id="implementing-responsive-images-with-sizesauto" class="wp-block-heading">Implementing responsive images with <code>sizes=auto</code></h3>



<p>Having said that, if you <em>do</em> have access to the HTML, you’ll want to serve responsive images using the <code>srcset</code> and <code>sizes</code> HTML attributes. <a href="https://css-tricks.com/author/wilto/">Mat Marquis</a> demonstrated <a href="https://piccalil.li/blog/the-end-of-responsive-images/" rel="noopener">how the new <code>sizes=auto</code> attribute-value combination replaces responsive breakpoints for images that are loaded lazily</a>.</p>



<p>If you’re interested, Amit Sheen also talked about <a href="https://frontendmasters.com/blog/building-a-ui-without-breakpoints/" rel="noopener">building layouts (not necessarily images) without breakpoints</a>.</p>



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



<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/150" rel="noopener">Firefox 150</a>
<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:muted" rel="noopener"><code>:muted</code></a> and all other media-based pseudo-classes (no Chrome support)</li>



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



<li><a href="https://piccalil.li/blog/the-end-of-responsive-images/" rel="noopener"><code>sizes=auto</code></a> (no Safari support)</li>



<li><a href="https://css-tricks.com/almanac/functions/l/light-dark/"><code>light-dark()</code></a> with image support (no Chrome/Safari support)</li>



<li><a href="https://css-tricks.com/almanac/functions/c/color-mix/"><code>color-mix()</code></a> with the syntax for two or more colors (no Chrome/Safari support)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaNotify" rel="noopener"><code>ariaNotify()</code></a> (no Chrome/Safari support)</li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes/stp-release-242" rel="noopener">Safari TP 242</a>
<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby" rel="noopener"><code>closedby</code></a></li>



<li>Advanced <a href="https://css-tricks.com/almanac/functions/a/attr/"><code>attr()</code></a> (no Firefox support)</li>
</ul>
</li>
</ul>



<p>If you’re keen for more content, here’s Wes Bos and Scott Tolinski of Syntax.fm discussing <a href="https://www.youtube.com/watch?v=unqPqGeJMck" rel="noopener">10 new CSS and HTML APIs</a>:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="10 New CSS and HTML APIs" width="500" height="281" src="https://www.youtube.com/embed/unqPqGeJMck?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Until next time!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-10/">What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, 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-10/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394456</post-id>	</item>
		<item>
		<title>The Importance of Native Randomness in CSS</title>
		<link>https://css-tricks.com/the-importance-of-native-randomness-in-css/</link>
					<comments>https://css-tricks.com/the-importance-of-native-randomness-in-css/#respond</comments>
		
		<dc:creator><![CDATA[Alvaro Montoro]]></dc:creator>
		<pubDate>Thu, 30 Apr 2026 15:26:18 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[CSS functions]]></category>
		<category><![CDATA[random]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393373</guid>

					<description><![CDATA[<p>We're getting new functions for generating random numbers in CSS! But the road to get here has been a long and winding one.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-importance-of-native-randomness-in-css/">The Importance of Native Randomness 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>Recently, I published a <a href="https://alvaromontoro.com/blog/68092/native-random-values-in-css" rel="noopener">story about the new random functions that have landed in CSS</a> and how they work. In this article, we’ll explore the challenges of randomness in CSS, how the concept has evolved over time, and why this native feature is a big deal.</p>



<p>One of the first things I wanted to do when I started developing websites was create unique experiences that changed from person to person. Just little things: a random background here, random colors there… Even small micro-interactions, like confetti or falling snow, needed some level of randomness to feel natural.</p>



<p>And I was not alone! I soon discovered that many web developers (“webmasters,” at the time) wanted to do things like that: adding wow factors and a sense of uniqueness to their sites. But we had a problem: CSS.</p>



<p>CSS is a declarative and deterministic language. Two characteristics that clash with the idea of natural variation:</p>



<ul class="wp-block-list">
<li><strong>Declarative</strong> means that it focuses on the <em>what</em>, not the <em>how</em>. In contrast to imperative languages, developers using CSS tell the browser what the expected result is, but not how to achieve it.</li>



<li><strong>Deterministic</strong> means that for a given input we will get the same output. Always the same. If you specify that a color will be red, that color will be red, not blue or yellow.</li>
</ul>



<p>This is by design, and it’s one of the things that makes CSS predictable and reliable. If you understand how the layout engine works, you can tell which styles will be applied at any given time. Which is great… but not so great if you want to generate random content.</p>



<p>And so began a challenging (and sometimes tortuous) journey for designers and developers to achieve natural variation from a deterministic system.</p>



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



<h3 class="wp-block-heading" id="the-long-and-winding-road-to-random-styles">The Long and Winding Road to Random Styles</h3>



<p>The path to random styles in CSS is paved with multiple attempts and shortcomings. But at every step along the way, developers found new solutions that improved on the previous ones. Even if only a little.</p>



<p><strong>Note:</strong> This timeline reflects logical progress more than a strict historical or chronological order.</p>



<h4 class="wp-block-heading" id="css-pseudo-randomness-and-patterns">CSS Pseudo-Randomness and Patterns</h4>



<p>We can simulate randomness in CSS by creating patterns. But this is not truly random. The results will always be the same, and sooner or later people will notice the pattern.</p>



<p>One way to create this simulation is by using <a href="https://css-tricks.com/almanac/pseudo-selectors/n/nth-child/"><code>:nth-child()</code></a> selectors or by playing with animations. The first method is easy but yields subpar results; the second may trick and impress some people.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Warning:</strong> Auto-playing media</summary>
<figure class="wp-block-image size-full ticss-3ca19010"><img data-recalc-dims="1" loading="lazy" decoding="async" width="400" height="460" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?resize=400%2C460" alt="Animated illustrated of five numbered cards swapping positions with the first card moving toward the back in succession." class="wp-image-393374" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?w=400&amp;ssl=1 400w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?resize=261%2C300&amp;ssl=1 261w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption"><strong>Credit:</strong> Alvaro Montoro</figcaption></figure>
</details>



<p>Needless to say, these methods are hacks that don’t provide randomization at any level. A human may not be able to precisely predict which value comes next — at least not without some effort — but a machine certainly can.</p>



<h4 class="wp-block-heading" id="pre-processors-to-the-rescue">Pre-Processors to the Rescue</h4>



<p>We turned to the next best thing: tooling. In particular, CSS preprocessors such as Sass, SCSS, Less, and the like. These tools include math modules that provide random functions we can use at compilation time.</p>



<p>The key phrase in the previous paragraph is “<strong>at compilation time.</strong>” Yes, we are generating random values for our CSS properties. But once those values are produced during compilation, they are frozen forever (or until the next compilation, to be more precise). Just like a mosquito stuck in amber.</p>



<p>The values will be random when the CSS is generated, but every time visitors visit or refresh the page, they will get the same ones. To produce new values, we would need to recompile the stylesheets.</p>



<p>This was a baby step toward styling randomization, but there was still a long way to go.</p>



<h4 class="wp-block-heading" id="server-side-randomness">Server-Side Randomness</h4>



<p>We moved to the next best thing: using other languages to generate random values and passing them to CSS through HTML. Server-side languages like PHP, Java, ASP, and others were perfect for this task while generating the HTML (or even the CSS itself).</p>



<p>This approach works well: we get new random values every time the page is generated, which usually means every time it is visited or refreshed. We also have full control over the randomization, since we can implement our own functions.</p>



<p>It has shortcomings, too. If new content is added dynamically to the page, it gets stuck with the “frozen” values generated during the initial page load. Better than patterns, better than preprocessors… but still not perfect.</p>



<p>This limitation became an even bigger problem with the rise and widespread adoption of single-page applications and client-side JavaScript architectures.</p>



<h4 class="wp-block-heading" id="and-javascript-finally-">And JavaScript&#8230; Finally!</h4>



<p>With the proliferation of web applications, it made sense to move randomness to JavaScript. The language is already heavily used, and adding a few random functions to the mix doesn’t seem like a big stretch.</p>



<p>And JavaScript finally solved it! For the first time, styles could actually behave with natural variation: random on creation, on refresh, and even on mutation.</p>



<p>It can be done in many ways, too: using frameworks, CSS-in-JS libraries, or plain vanilla JavaScript. The methods to incorporate styling through this language are vast and well supported. There are some performance and complexity concerns, but JavaScript gets the job done.</p>



<p>We finally had true randomization in web styles… just not in CSS itself.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1098" height="568" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=1098%2C568" alt="Summary of the different technologies and how they handle randomization" class="wp-image-393376" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?w=1098&amp;ssl=1 1098w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=1024%2C530&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=768%2C397&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<h3 class="wp-block-heading" id="a-web-problem-and-a-web-solution">A Web Problem, and a Web Solution</h3>



<p>That last part is important. We have randomization on the web (JavaScript gets the job done) but something feels off. Something doesn’t quite feel right. At its core, that discomfort comes from two things:</p>



<ul class="wp-block-list">
<li>We are applying an imperative solution to a declarative problem.</li>



<li>We are moving layout decisions from CSS to JavaScript.</li>
</ul>



<h4 class="wp-block-heading" id="an-imperative-solution-to-a-declarative-problem">An Imperative Solution to a Declarative Problem</h4>



<p>We mentioned earlier that CSS is a declarative language that focuses on the <em>what</em>, while JavaScript is an imperative language that focuses on the <em>how</em>.</p>



<p>By moving randomization to JavaScript, we are trying to answer a <em>what</em> question with a <em>how</em> answer. It works, but it’s not ideal.</p>



<p>Using JavaScript, we finally achieved style randomness at all levels: when the page is created, when it is refreshed, and when elements are added or changed (mutation). But in doing so, we are breaking the model.</p>



<p>CSS handles layout, and JavaScript handles logic. We solved a CSS limitation by moving layout decisions into JavaScript, creating a mismatch that produces that subtle “this isn’t quite right” feeling — even when everything technically works.</p>



<h4 class="wp-block-heading" id="the-css-solution">The CSS Solution</h4>



<p>The solution to this model mismatch is simple: <strong>move randomization to CSS</strong>. Solve a layout problem directly in the layout layer instead of delegating it to a different tool or language. And this happened with the introduction of two new random functions as part of the <a href="https://www.w3.org/TR/css-values-5/#randomness" rel="noopener">CSS Values and Units Module Level 5</a>:</p>



<ul class="wp-block-list">
<li><strong><code>random()</code>:</strong> generates a random value between a minimum and a maximum.</li>



<li><strong><code>random-item()</code>:</strong> selects a random value from a given list.</li>
</ul>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1324" height="596" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=1324%2C596&#038;ssl=1" alt="Showing a CSS code snippet of the random and random-item functions." class="wp-image-393378" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?w=1324&amp;ssl=1 1324w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=300%2C135&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=1024%2C461&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=768%2C346&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>This approach also aligns with the <a href="https://www.w3.org/2001/tag/doc/leastPower.html" rel="noopener"><strong>Rule of Least Power</strong></a>, which suggests choosing the least powerful language suitable for a given purpose. In practice, this means solving a problem using the least powerful language capable of expressing and solving it.</p>



<p>Usually, that language will be better suited to the task. Its features will be adapted to the level at which they are applied, making them simpler, more efficient, and better performing. While a more powerful language can certainly do the job, it often introduces an unnecessary layer of complexity and abstraction.</p>



<p>On the web platform, we have <strong>HTML</strong> for structure (least powerful), <strong>CSS</strong> for styling and layout (more powerful), and <strong>JavaScript</strong> (significantly more powerful). By implementing randomization in CSS, we move the solution to the appropriate layer while also following the Rule of Least Power.</p>



<p>And that’s one of the reasons the new random CSS features are such a big deal… and why they represent something much bigger than just <em>another feature</em>.</p>



<h3 class="wp-block-heading" id="the-big-deal">The Big Deal</h3>



<p>CSS has always been deterministic by design, and native randomness breaks with that tradition. It isn’t just another feature, it represents a shift in how we think about CSS as a language and about the web platform itself.</p>



<p>For the first time, CSS can model natural systems with variation directly: no hacks, no tools, no outsourcing layout decisions to other languages. Randomization takes an honored place in the styling layer, where it always belonged.</p>



<p>This unlocks creative possibilities: generative layouts, organic patterns, playful micro-interactions, and design systems that feel alive and unique. But it also restores architectural clarity: each layer of the web once again does the job it was designed for.</p>



<p>With this change, CSS moves from being purely a styling language toward becoming a generative layout system. It is no longer just a passive actor in web development; it becomes an active participant in the rendering process, defining a space of possible outcomes that the browser resolves into a concrete page.</p>



<p>And that’s the real big deal. Native randomness isn’t just about making things look different; it’s about making the platform more coherent and expressive.</p>



<p>It’s also a reminder that CSS is still evolving, and that sometimes the features people overlook can reshape how we think about a language, and what we imagine is possible on the web.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-importance-of-native-randomness-in-css/">The Importance of Native Randomness 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/the-importance-of-native-randomness-in-css/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393373</post-id>	</item>
		<item>
		<title>contrast()</title>
		<link>https://css-tricks.com/almanac/functions/c/contrast/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 14:58:19 +0000</pubDate>
				<category><![CDATA[filter]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392899</guid>

					<description><![CDATA[<p>The <code>contrast()</code> filter function increases or decreases the contrast of an element.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast/">contrast()</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>The CSS <code>contrast()</code> filter function increases or decreases the contrast of an element, either making colors pop out more or dulling them to gray. Unlike other <code><a href="https://css-tricks.com/almanac/properties/f/filter/">filter</a></code> functions like <code><a href="https://css-tricks.com/almanac/functions/b/brightness/">brightness()</a></code> or <code>saturate()</code>, <code>contrast()</code> affects both saturation and lightness, keeping only the color&#8217;s hue.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.low {
  filter: contrast(50%);
}

.normal {
  filter: contrast(100%);
}

.high {
  filter: contrast(200%);
}</code></pre>



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



<p>The&nbsp;<code>contrast()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/filter-effects/#funcdef-filter-contrast" rel="noopener">Filter Effects Module Level 1</a>&nbsp;specification.</p>



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



<p>The official syntax for the&nbsp;<code>contrast()</code>&nbsp;function is:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;contrast()> = contrast( [ &lt;number> | &lt;percentage> ]? )</code></pre>



<p>Or simply:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">filter: contrast(&lt;amount>);</code></pre>



<p>The <code>contrast()</code> function is only compatible with the CSS <code><a href="https://css-tricks.com/almanac/properties/f/filter/">filter</a></code> and <code><a href="https://css-tricks.com/almanac/properties/b/backdrop-filter/">backdrop-filter</a></code> properties.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Using percentages */
filter: contrast(0%); /* Totally grayed out */
filter: contrast(50%); /* Partially grayed out */
filter: contrast(100%); /* No change */
filter: contrast(150%); /* Element is 1.5 times more defined */

/* Using numbers (0–1 range) */
filter: contrast(0); /* Totally grayed out */
filter: contrast(0.5); /* Partially grayed out */
filter: contrast(1); /* No change */
filter: contrast(1.5); /* Element is 1.5 times more defined */

/* Using percentages */
filter: contrast(0%); /* Totally grayed out */
filter: contrast(50%); /* Partially grayed out */
filter: contrast(100%); /* No change */
filter: contrast(150%); /* Element is 1.5 times more defined */

/* Using numbers (0–1 range) */
filter: contrast(0); /* Totally grayed out */
filter: contrast(0.5); /* Partially grayed out */
filter: contrast(1); /* No change */
filter: contrast(1.5); /* Element is 1.5 times more defined */

/* Works with CSS variables */
--amount: 200%;
filter: contrast(--amount);

/* No argument */
filter: contrast(); /* No change */

/* Negative value */
filter: contrast(-1.5); /* No effect */
filter: contrast(--amount);

/* No argument */
filter: contrast(); /* No change */

/* Negative value */
filter: contrast(-1.5); /* No effect */</code></pre>



<p>The <code>contrast()</code> function takes a single argument, which can be a positive decimal or percentage value. The argument determines the new contrast for the element, where:</p>



<ul class="wp-block-list">
<li><code>0</code>&nbsp;or&nbsp;<code>0%</code>&nbsp;dries out all contrast from the element, resulting in a completely gray image.</li>



<li><code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;leaves the element completely unchanged.</li>



<li>Values above&nbsp;<code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;increase the contrast linearly.</li>
</ul>



<p>Negative values aren&#8217;t allowed. But CSS variables are:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.element {
  --filter-amount: 150%;
  filter: contrast(var(--filter-amount));
}</code></pre>



<h3 class="wp-block-heading" id="how-contrast-affects-color">How <code>contrast()</code> affects color</h3>



<p>Like other filter functions, the&nbsp;<code>contrast()</code>&nbsp;filter operates purely on RGB math. Specifically, given an&nbsp;<code>&lt;amount&gt;</code>&nbsp;it multiplies each RGB channel by that&nbsp;<code>&lt;amount&gt;</code>&nbsp;and then adds&nbsp;<code>255 * (0.5 - 0.5 * &lt;amount&gt;)</code>&nbsp;to the result. In practice, this affects colors in one of two ways:</p>



<ul class="wp-block-list">
<li>High contrast (greater than&nbsp;<code>1</code>) makes light pixels get lighter and dark pixels get darker, so colors become more vivid.</li>



<li>Low contrast (smaller than&nbsp;<code>1</code>) pulls all pixels toward a middle gray. This reduces the difference between light and dark areas, making the image look flat and muted.</li>
</ul>



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



<p>Some background images, usually in hero sections or carousels, can make the foreground text difficult to read. Especially if it has very bright and dark colors, which compete with any text color. To solve this, we can use&nbsp;contrast()&nbsp;to reduce the difference between the image&#8217;s whites and blacks, making text more readable against the whole image.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
    filter: contrast(70%) brightness(60%);
}</code></pre>



<p>The low contrast flattens the image, and as a plus, we can also reduce the image&#8217;s brightness to make the text pop regardless of its colors.</p>



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



<h3 class="wp-block-heading" id="making-product-card-images-pop-on-hover">Demo: Making product card images pop on hover</h3>



<p>Another useful application for&nbsp;<code>contrast()</code>&nbsp;is to highlight an image in a user&#8217;s interaction. For example, in a row of image cards, we could increase the image&#8217;s contrast and also scale it on hover</p>



<pre rel="SCSS" class="wp-block-csstricks-code-block language-scss" data-line=""><code markup="tt">.card img {
  transition:
    filter 0.4s ease,
    transform 0.4s ease;
}

.card:hover img {
  filter: contrast(125%);
  transform: scale(1.05);
}</code></pre>



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



<h3 class="wp-block-heading" id="is-contrast-the-same-as-contrast-color-">Is <code>contrast()</code> the same as <code>contrast-color()</code>?</h3>



<p>While both CSS functions have similar names, they are not to be confused with each other.</p>



<ul class="wp-block-list">
<li><strong><code>contrast()</code> is a filter function that makes an element more vivid</strong> by making whites lighter and blacks darker.</li>



<li><strong><a href="https://css-tricks.com/exploring-the-css-contrast-color-function-a-second-time/"><code>contrast-color()</code></a> returns the text color with the highest contrast to a solid background.</strong> Its resulting color is either white or black, depending on which color contrasts most with the background. It is also not a filter function.</li>
</ul>



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



<p>The&nbsp;<code>contrast()</code>&nbsp;function is currently supported across all modern browsers.</p>




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




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="filter"></baseline-status>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast/">contrast()</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">392899</post-id>	</item>
		<item>
		<title>contrast-color()</title>
		<link>https://css-tricks.com/almanac/functions/c/contrast-color/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 14:57:48 +0000</pubDate>
				<category><![CDATA[color]]></category>
		<category><![CDATA[contrast-color()]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392903</guid>

					<description><![CDATA[<p>The <code>contrast-color()</code> function takes a <code>&#60;color&#62;</code> and returns either black or white, whichever is the most contrasting color for that value.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast-color/">contrast-color()</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>The CSS <code>contrast-color()</code> function takes a <code>&lt;color></code> value (as well as a variable) and returns either black or white, whichever is the most contrasting color for that value.</p>



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



<p>In other words, <code>contrast-color()</code> is sort of an accessibility tool for conforming to <a href="https://w3c.github.io/wcag/guidelines/22/#contrast-minimum" rel="noopener">WCAG contrast requirements</a>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  background-color: var(--swatch);
  color: contrast-color(var(--swatch));
}</code></pre>



<p>For example, on the next demo update the background color to see the text color change automatically.</p>



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



<p>The&nbsp;<code>contrast-color()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener">CSS Color Module Level 5</a>&nbsp;specification.</p>



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



<p>The CSS <code>contrast-color()</code> function syntax is is formatted like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">contrast-color() = contrast-color( &lt;color> )</code></pre>



<p>Let&#8217;s break that down with examples.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Using a custom variable */
contrast-color(var(--base-background));

/* Passing a color directly */
contrast-color(#34cdf2);
contrast-color(green);</code></pre>



<p><code>contrast-color()</code>&nbsp;takes a&nbsp;<code>&lt;color&gt;</code>&nbsp;as its only argument and resolves to white or black, depending on which has the highest contrast. If both white and black have the same contrast level, the function defaults to white.</p>



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



<p>The&nbsp;<code>contrast-color()</code>&nbsp;give us a simple alternative to defining multiple background and text colors, while also ensuring they are contrasting enough. Imagine we had the following scenario:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --primary-text: #f1f8e9;
  --primary-bg: #2d5a27;
  --secondary-text: #311b92;
  --secondary-bg: #d1c4e9;
  --tertiary-text: #002b36;
  --tertiary-bg: #ff5722;
}

.primary {
  color: var(--primary-text);
  background-color: var(--primary-bg);
}

.secondary {
  color: var(--secondary-text);
  background-color: var(--secondary-bg);
}

.tertiary {
  color: var(--tertiary-text);
  background-color: var(--tertiary-bg);
}</code></pre>



<p>We defined a text color for each background color in our variables, and if we had more than three possible backgrounds, we&#8217;d have had to define them all. Instead, using&nbsp;<code>contrast-color()</code>, we could define only the background color for each theme and let the function return the appropriate contrasting color for the texts.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --primary: #2d5a27;
  --secondary: #d1c4e9;
  --tertiary: #ff5722;
}

.primary {
  color: contrast-color(var(--primary));
  background-color: var(--primary);
}

.secondary {
  color: contrast-color(var(--secondary));
  background-color: var(--secondary);
}

.tertiary {
  color: contrast-color(var(--tertiary-bg));
  background-color: var(--tertiary-bg);
}</code></pre>



<p><strong>It is important to note that <code>contrast-color()</code> is still a work in progress</strong> (at the time of this writing), and in some cases might not be appropriate from a design standpoint since it only returns black or white. Therefore, I recommend using it only in simple scenarios where either black or white make sense.</p>



<p>In fact, it has some shortcomings that are worth noting.</p>



<h3 class="wp-block-heading" id="-contrast-color-shortcomings"><code>contrast-color()</code> shortcomings</h3>



<p>While&nbsp;<code>contrast-color()</code>&nbsp;appears to improve web accessibility, it has&nbsp;<em>buts</em>&nbsp;we should be aware of before using it.</p>



<ul class="wp-block-list">
<li><strong>It resolves to only black or white texts.</strong> Although the draft promises more control in the future, we have to stick to those two colors for now.</li>



<li><strong>We&#8217;re stuck with white when using colors where neither black nor white is a sufficient contrast</strong>, or they both have the same contrast.</li>



<li><strong><code>contrast-color()</code> only works with colors for now.</strong> So, in cases where you&#8217;re working with text on background images or using font weights to increase contrast, you&#8217;ll have to find a different way to meet contrast requirements. And even if it can be technically used with gradients, these too can only go between black to white which might not provide enough contrast between the gradient colors.</li>



<li><strong><code>contrast-color()</code> doesn&#8217;t account for the <code>font-size</code></strong>, which is a defining criterion, in choosing a contrast color. Hopefully, this will be accounted for in the future.</li>
</ul>



<p>So, at the time of writing, it seems it&#8217;s better to manually define colors that are contrasting enough in our themes as&nbsp;<code>contrast-color()</code>&nbsp;isn&#8217;t really feasible right now.</p>



<h3 class="wp-block-heading" id="older-syntax">Older syntax</h3>



<p>Based on earlier <a href="https://css-tricks.com/exploring-color-contrast-for-the-first-time/">articles</a>, the <code>contrast-color()</code> function used to take multiple <code>color</code> arguments–the base color versus multiple contrasting color options to choose from:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">contrast-color(var(--bg) vs red, lightgreen, blue)</code></pre>



<p>This syntax no longer exists in the draft. It&#8217;s one color and one color only.</p>



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



<p>The&nbsp;<code>contrast-color()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener">CSS Color Module Level 5</a>&nbsp;specification.</p>



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




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



<p>While browser support is limited at the time of this writing, it&#8217;s a good idea to include a fallback if you&#8217;re planning to use it on a project. We can use the <a href="https://css-tricks.com/almanac/rules/s/supports/"><code>@supports</code></a> at-rule to detect if the browser understands the function:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  --bg-color: #2d5a27;
  background-color: var(--bg-color);

  /* Default Fallback */
  color: ghostwhite;
}

/* Use the function if supported */
@supports (color: contrast-color(red)) {
  .card {
    color: contrast-color(var(--bg-color));
  }
}</code></pre>



<h3 class="wp-block-heading" id="references-earlier-articles-">Further reading:</h3>



<ul class="wp-block-list">
<li><a href="https://www.smashingmagazine.com/2022/05/accessible-design-system-themes-css-color-contrast/" rel="noopener">Manage Accessible Design System Themes With CSS Color-Contrast()</a> (Daniel Yuschick)</li>
</ul>



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

      <div class="tags">
      <a href="https://css-tricks.com/tag/accessibility/" rel="tag">accessibility</a> <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/relative-color/" rel="tag">relative color</a>    </div>
  
  <time datetime="2026-02-11" title="Originally published Feb 11, 2026">
    <strong>
                
        Article
      </strong>

    on

    Feb 11, 2026  </time>

  <h3>
    <a href="https://css-tricks.com/approximating-contrast-color-with-other-css-features/">
      Approximating contrast-color() With Other CSS Features    </a>
  </h3>

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

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

      <div class="tags">
      <a href="https://css-tricks.com/tag/accessibility/" rel="tag">accessibility</a> <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/css-functions/" rel="tag">CSS functions</a>    </div>
  
  <time datetime="2025-06-05" title="Originally published Jun 5, 2025">
    <strong>
                
        Article
      </strong>

    on

    Jun 5, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/exploring-the-css-contrast-color-function-a-second-time/">
      Exploring the CSS contrast-color() Function… a Second Time    </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 links" id="mini-post-389698">

      <div class="tags">
      <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/css-functions/" rel="tag">CSS functions</a>    </div>
  
  <time datetime="2025-10-08" title="Originally published Oct 8, 2025">
    <strong>
                
        Link
      </strong>

    on

    Oct 8, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/the-thing-about-contrast-color/">
      The thing about contrast-color    </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></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast-color/">contrast-color()</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">392903</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=9143 metric#misses=14 metric#hit-ratio=99.9 metric#bytes=7068889 metric#prefetches=484 metric#store-reads=32 metric#store-writes=2 metric#store-hits=492 metric#store-misses=10 metric#sql-queries=30 metric#ms-total=503.21 metric#ms-cache=33.62 metric#ms-cache-avg=1.0187 metric#ms-cache-ratio=6.7 -->
