<?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 &#8211; COMPUTERCOURSESONLINE</title>
	<atom:link href="https://computercoursesonline.com/index.php/category/css/feed/" rel="self" type="application/rss+xml" />
	<link>http://computercoursesonline.com</link>
	<description></description>
	<lastBuildDate>Thu, 28 May 2026 21:06:07 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.9.2</generator>
	<item>
		<title>Algorithmic Theming Engines: Building Self-Correcting Color Systems With `contrast-color()`</title>
		<link>http://computercoursesonline.com/index.php/2026/05/28/algorithmic-theming-engines-building-self-correcting-color-systems-with-contrast-color/</link>
					<comments>http://computercoursesonline.com/index.php/2026/05/28/algorithmic-theming-engines-building-self-correcting-color-systems-with-contrast-color/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Thu, 28 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1227</guid>

					<description><![CDATA[Algorithmic Theming Engines: Building Self-Correcting Color Systems With `contrast-color()` Algorithmic Theming Engines: Building Self-Correcting Color Systems With `contrast-color()` Durgesh Pawar 2026-05-28T13:00:00+00:00 2026-05-28T20:50:06+00:00 The HTTP Archive Web Almanac has been tracking color contrast failures for years. The numbers have barely moved. After half a decade of design system tooling, accessibility linters, and entire JavaScript libraries dedicated...]]></description>
										<content:encoded><![CDATA[<p>              <title>Algorithmic Theming Engines: Building Self-Correcting Color Systems With `contrast-color()`</title></p>
<article>
<header>
<h1>Algorithmic Theming Engines: Building Self-Correcting Color Systems With `contrast-color()`</h1>
<address>Durgesh Pawar</address>
<p>                  2026-05-28T13:00:00+00:00<br />
                  2026-05-28T20:50:06+00:00<br />
                </header>
<p>The <a href="https://almanac.httparchive.org/">HTTP Archive Web Almanac</a> has been tracking color contrast failures for years. The numbers have barely moved. After half a decade of design system tooling, accessibility linters, and entire JavaScript libraries dedicated to computing readable text colors, <a href="https://almanac.httparchive.org/en/2025/accessibility#color-contrast">70% of websites still fail basic WCAG contrast checks in 2025</a>. The <a href="https://webaim.org/projects/million/">WebAIM Million</a> paints an even grimmer picture &mdash; 83.9% of homepages flagged for low contrast text in 2026, up from 79.1% in 2025. The rate improves by maybe a few percentage points per year on one benchmark and actually gets <em>worse</em> on another. That’s not progress &mdash; that’s proof that relying on runtime JavaScript for something this fundamental doesn’t scale across the open web. We didn’t need better libraries. We’ve needed better CSS.</p>
<p>The <code>contrast-color()</code> function is that better CSS. One declaration. The browser runs the contrast math during style computation, before the page paints, and hands you the right text color. No library, no build step, no hydration flash.</p>
<p><strong>Note</strong>: If you’ve seen it called <code>color-contrast()</code> in older articles and spec drafts &mdash; <a href="https://github.com/w3c/csswg-drafts/issues/7557">that name was changed</a>, and the old syntax no longer works in any browser.</p>
<h2 id="what-it-does-and-what-it-doesn-t">What It Does (And What It Doesn’t)</h2>
<p>The Level 5 version is simple. You give it a color. It gives you back <code>black</code> or <code>white</code>, whichever has more contrast against your input.</p>
<pre><code class="language-css">.button {
  background-color: var(--brand-color);
  color: contrast-color(var(--brand-color));
}
</code></pre>
<p>Change <code>--brand-color</code> to neon green, text goes black. Change it to midnight navy, text goes white. Swap themes at runtime via JavaScript and the text adapts instantly &mdash; no event listeners, no recalculation.</p>
<p>A few things to know about the current version:</p>
<ul>
<li>It returns a <code>&lt;color&gt;</code>, not a number. You get an actual color value (<code>black</code> or <code>white</code>), you can use anywhere CSS accepts a color.</li>
<li><strong>Black or white only</strong>, for now. Candidate color lists and target ratios are planned for Level 6.</li>
<li><strong>No keywords.</strong> If you’ve seen <code>max</code> in older blog posts, that was stripped from the spec. Using it will silently break your declaration.</li>
<li>As mentioned above, this function used to be called <code>color-contrast()</code> in early drafts. That name is dead &mdash; <a href="https://github.com/w3c/csswg-drafts/issues/7557">the CSSWG renamed it</a> to follow the convention that CSS functions are named for what they return. <code>color-mix()</code> returns a color. <code>contrast-color()</code> returns a color. The old <code>color-contrast()</code> name sounded like it returned a contrast <em>ratio</em> (a number like 4.5), which was misleading. Any tutorial from 2021&ndash;2023 showing <code>color-contrast()</code> syntax won’t work in current browsers.
<p></li>
</ul>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="the-spec-split-level-5-versus-level-6">The Spec Split: Level 5 Versus Level 6</h2>
<p>This function lives across two specifications. That’s unusual and worth understanding.</p>
<p><a href="https://www.w3.org/TR/css-color-5/#contrast-color"><strong>CSS Color Level 5</strong></a> defines what browsers ship today. One color in, black or white out. The algorithm is deliberately marked “UA-defined”, meaning the browser decides what math to use internally. Right now, every engine uses WCAG 2.x relative luminance. But that “UA-defined” label isn’t accidental &mdash; it’s a planned escape hatch.</p>
<p>You’ll see APCA (<strong>Accessible Perceptual Contrast Algorithm</strong>) mentioned a lot in this context. APCA models how human eyes actually perceive contrast, factoring in font weight, spatial frequency, and ambient light &mdash; a genuine improvement over the WCAG 2.x formula. By not locking <em>“use WCAG 2.x”</em> into the Level 5 spec, browser vendors <em>could</em> swap to APCA later without breaking any existing code. If the spec had shipped with a <code>wcag2()</code> keyword as the default, every site using it would’ve been stuck on the old math permanently.</p>
<p>But APCA’s future is far less certain than the hype suggests. Adrian Roselli’s “<a href="https://adrianroselli.com/2026/04/wcag3-contrast-as-of-april-2026.html">WCAG3 Contrast as of April 2026</a>” lays out the current situation clearly: APCA was pulled from the WCAG 3 working draft <a href="https://www.w3.org/TR/2023/WD-wcag-3.0-20230724/#color-and-contrast">in mid-2023</a> after failing to gain enough Working Group support. The WCAG 3 spec currently says the contrast algorithm is <em>“yet to be determined,”</em> and the standard itself <a href="https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/#timeline">may not be finalized until 2030 or later</a>. Roselli also <a href="https://issues.chromium.org/issues/341439947">filed a Chromium issue in May 2024</a> asking for the “Advanced Perceptual Contrast Algorithm” experiment flag to be removed from DevTools entirely, arguing that the implementation is outdated and risks misleading developers into thinking APCA is further along &mdash; or more official &mdash; than it actually is. That issue is still open.</p>
<p>None of this means APCA is dead. The research behind it is peer-reviewed and substantive, and its creator <a href="https://adrianroselli.com/2026/04/wcag3-contrast-as-of-april-2026.html#comment-408186">has noted</a> that colors passing APCA guidelines greatly exceed WCAG 2 minimums in the vast majority of cases. But right now, there is no guarantee APCA will be the algorithm that replaces WCAG 2.x &mdash; and that uncertainty matters for <code>contrast-color()</code>. If a different algorithm wins out, or if WCAG 3 adopts something entirely new, the “UA-defined” label means browsers can adapt without breaking your code. It also means the Level 6 features &mdash; candidate color lists, target ratios, the <code>tbd-fg</code>/<code>tbd-bg</code> keywords &mdash; are all designed around an algorithm that may or may not materialize in its current form.</p>
<p><strong>CSS Color Level 6</strong> adds the extended syntax &mdash; candidate color lists and target contrast ratios:</p>
<div class="break-out">
<pre><code class="language-css">/&#042; Level 6 future syntax — not shipping yet &#042;/
color: contrast-color(var(--bg) tbd-bg wcag2(aa), #1a1a2e, #e2e8f0, #fbbf24);
</code></pre>
</div>
<p>The browser would evaluate each candidate left to right and pick the first that meets the 4.5:1 AA threshold. The <code>tbd-fg</code> and <code>tbd-bg</code> keywords indicate whether the base color is foreground or background, which matters for directional contrast models like APCA. This is all Working Draft territory &mdash; doubly so given APCA’s uncertain status. Use the Level 5 version for now.</p>
<h2 id="browser-support">Browser Support</h2>
<p>This one’s in better shape than most new CSS features. All three major engines have shipped it in stable releases: <a href="https://chromestatus.com/feature/40142548">Chrome 147</a> (April 2026), <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/contrast-color">Firefox 146</a>, and <a href="https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes">Safari 26.0</a>. It reached <a href="https://web.dev/baseline/">Baseline Newly Available</a> status in April 2026. Check <a href="https://caniuse.com/mdn-css_types_color_contrast-color">caniuse</a> for the full version matrix. All three engines pass the Web Platform Tests for <code>contrast-color()</code>, which means the edge cases (e.g., tie-breaking logic, color space conversion, syntax parsing) behave the same across browsers.</p>
<p>The raw global support percentage on caniuse looks low, but that mostly reflects enterprise browsers and people who never update. If you’re reading this, your browser almost certainly supports it already.</p>
<p>Progressive enhancement is straightforward using <code>@supports</code>:</p>
<pre><code class="language-css">.card {
  background: var(--bg);
  color: &#035;fff;
  text-shadow: 0 0 4px rgb(0 0 0 / 0.8);
}

@supports (color: contrast-color(red)) {
  .card {
    color: contrast-color(var(--bg));
    text-shadow: none;
  }
}
</code></pre>
<p>Older browsers get white text with a dark shadow for legibility. Supporting browsers get the native calculation. Nobody sees broken text.</p>
<p>One thing to watch for: automated accessibility scanners (Lighthouse, Axe, etc.) can’t evaluate <code>text-shadow</code>. They only look at the computed <code>color</code> against <code>background-color</code>. So the fallback will still get flagged as a contrast failure in CI/CD pipelines, even if the shadow makes the text perfectly legible to human eyes. If your team runs automated a11y checks, you may need to allowlist that specific rule or add a comment explaining why the flag is a false positive.</p>
<blockquote><p><strong>A note on PostCSS</strong>:</p>
<p>There’s a plugin (<a href="https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-contrast-color-function"><code>@csstools/postcss-contrast-color-function</code></a>) that evaluates <code>contrast-color()</code> at build time. It works for static colors like <code>contrast-color(#ff0000)</code>. But the moment you use a custom property &mdash; <code>contrast-color(var(--bg))</code> &mdash; the plugin can’t help because it has no access to runtime values. If your theming is dynamic (which is the whole point of doing this), skip the polyfill and rely on <code>@supports</code>.</p></blockquote>
<div class="partners__lead-place"></div>
<h2 id="the-gotchas">The Gotchas</h2>
<h3 id="it-doesn-t-guarantee-perceptual-or-aaa-compliance">It Doesn’t Guarantee Perceptual or AAA Compliance</h3>
<p>This can trip people up: <em>“I used the contrast function, so my site passes accessibility checks now, right?”</em></p>
<p>Mathematically? Usually yes. There is a persistent myth that for certain “mid-tone” backgrounds, both black and white fail the standard WCAG 4.5:1 AA ratio. That’s mathematically false. Under the WCAG 2.x relative luminance formula, there is absolutely no background color where both pure black and pure white fail AA. One (or both) will always pass.</p>
<p>Take <code>#2277d3</code> (a medium blue). It sits right on a mathematical knife-edge where both black and white actually pass AA (both hit roughly 4.58:1). <code>contrast-color()</code> will hand you whichever has the slight mathematical edge.</p>
<p>But here is the actual gotcha: the WCAG 2.x math has known perceptual blind spots. That same <code>#2277d3</code> with black text mathematically passes AA, but to human eyes, it can be incredibly difficult to read. <code>contrast-color()</code> gives you <em>mathematical</em> compliance, which is great for automated audits, but that doesn’t always equal <em>perceptual</em> accessibility. (This is exactly why APCA exists and why the spec was designed to let browsers swap algorithms later.)</p>
<p>Furthermore, if you’re aiming for the stricter WCAG AAA standard (7.0:1), a true dead zone <em>does</em> exist. For backgrounds with a luminance between roughly 10% and 30%, neither black nor white will hit 7:1. In those cases, <code>contrast-color()</code> can’t save you &mdash; it just hands you the “least bad” failing option.</p>
<h3 id="transitions-snap-not-fade">Transitions Snap, Not Fade</h3>
<p>If you’re animating a background from <code>white</code> to <code>black</code> on hover:</p>
<pre><code class="language-css">.btn {
  background-color: &#035;fff;
  color: contrast-color(&#035;fff); /&#042; black &#042;/
  transition: background-color 1s, color 1s;
}
.btn:hover {
  background-color: &#035;000;
  color: contrast-color(&#035;000); /&#042; white &#042;/
}
</code></pre>
<p>The background fades smoothly over one second. But because the Level 5 output is a <a href="https://www.w3.org/TR/css-transitions-1/#discrete">discrete value</a> (black or white), the text color can’t be interpolated. It snaps.</p>
<p>And here is the visual gotcha: the snap doesn’t happen halfway through. If you’ve been building themes for a while, you probably have muscle memory from the old Sass days, where we checked if <code>lightness($bg) &gt; 50%</code>. That relied on HSL lightness, where 50% is the geometric midpoint.</p>
<p>But WCAG 2.x relative luminance is a non-linear scale. Under the WCAG formula, the mathematical tipping point &mdash; where black and white have identical contrast against the background &mdash; actually occurs at approximately 18% relative luminance (specifically ~17.9%).</p>
<p>Because of that, the visual behavior during a white-to-black fade is heavily skewed. The text doesn’t snap in the middle. It stays black for the vast majority of the animation, only snapping to white at the very tail-end of the transition when the background gets extremely dark. It’s a jarring, late hard cut.</p>
<p>You might assume <a href="https://css-tricks.com/almanac/properties/t/transition/transition-behavior/"><code>transition-behavior: allow-discrete</code></a> fixes this. It doesn’t. <code>allow-discrete</code> does not fix the jarring visual experience because it cannot interpolate a binary output; it only shifts the timing of the hard snap to the 50% mark of the animation duration. If you need smooth text color transitions, you’ll have to layer <code>color-mix()</code> or manage the crossfade yourself.</p>
<h3 id="tie-goes-to-white">Tie Goes To White</h3>
<p>If the background is a perfect middle gray where both black and white produce identical contrast ratios, <a href="https://www.w3.org/TR/css-color-5/#contrast-color">the spec has a hardcoded tiebreaker</a>: white wins. Not a big deal in practice, but worth knowing if you’re debugging gray palettes and the text isn’t doing what you expect.</p>
<h3 id="gradients-and-images-are-out">Gradients And Images Are Out</h3>
<p>The function takes a flat <code>&lt;color&gt;</code> value. You can’t pass it a gradient or a <code>url()</code>. <code>contrast-color(linear-gradient(...))</code> is a parse error. If your background is a photo or a complex gradient, you still need JavaScript or manually color-pick for overlay text.</p>
<h3 id="transparent-colors-are-composited-first">Transparent Colors Are Composited First</h3>
<p>Pass a semi-transparent color, and the browser blends it against an assumed opaque canvas (usually white) before running the contrast math. It’s not ignoring your alpha channel &mdash; it’s compositing it. But the result might surprise you if you expected the function to “see through” to whatever’s actually behind the element.</p>
<h3 id="windows-high-contrast-mode">Windows High Contrast Mode</h3>
<p>If a user enables Windows High Contrast, the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors"><code>forced-colors: active</code></a> media query kicks in and the browser aggressively overwrites author-defined colors. <code>contrast-color()</code> bows out &mdash; <a href="https://www.w3.org/TR/css-color-adjust-1/#forced-colors-mode">forced system colors</a> like <code>CanvasText</code> take over completely. You don’t need to write manual media queries to undo your contrast logic; the browser handles the hierarchy.</p>
<h2 id="combining-it-with-other-color-functions">Combining It With Other Color Functions</h2>
<p>Black or white sounds limiting, but once you feed that output into other CSS color functions, you can build an entire component palette off a single custom property.</p>
<h3 id="brand-tinted-contrast-with-relative-color-syntax">Brand-Tinted Contrast With Relative Color Syntax</h3>
<p>Pure black text on a vibrant card looks fine. Pure white on a coral card can feel flat. What if the contrast text was a very dark or very light <em>tint</em> of the background color instead?</p>
<p>Kevin Hamer explored related territory in his CSS-Tricks piece “<a href="https://css-tricks.com/approximating-contrast-color-with-other-css-features/">Approximating contrast-color() With Other CSS Features</a>”, where he used OKLCH lightness and <code>round()</code> to approximate the black/white switch <em>without</em> <code>contrast-color()</code> &mdash; essentially <code>oklch(from &lt;color&gt; round(1.21 - l) 0 0)</code>. That’s a polyfill strategy: get the binary light/dark decision working in browsers that don’t support the native function yet. What we’re doing here is different &mdash; we <em>start</em> with <code>contrast-color()</code>’s native output and then enrich it by injecting the background’s own hue:</p>
<pre><code class="language-css">.card {
  --bg-hue: 260; /&#042; Indigo &#042;/
  --bg: oklch(0.6 0.1 var(--bg-hue));
  background: var(--bg);

  /&#042; Pull L from the black/white contrast color,
     but inject subtle chroma and the background's hue &#042;/
  color: oklch(from contrast-color(var(--bg)) l 0.05 var(--bg-hue));
}
</code></pre>
<p>When <code>contrast-color()</code> returns white, <code>l</code> is 1 (full lightness). When it returns black, <code>l</code> is 0. By pulling the background’s hue back in and adding a touch of chroma, you get text that reads as a deep dark indigo or a pale icy indigo instead of generic black/white. Hamer’s approach gives you the black/white decision without browser support; this one takes the decision the browser already made and gives it personality.</p>
<p><strong>Fair warning</strong>: By tweaking the lightness and chroma of the black/white output, you can push a borderline contrast ratio into failing territory. Always run your tinted output through an accessibility linter before shipping.</p>
<p><strong>Also worth noting</strong>: This example chains two very modern features &mdash; <code>contrast-color()</code> and <code>oklch(from ...)</code>. If either one isn’t supported, the entire declaration fails silently. Your <code>@supports</code> block needs to test for both:</p>
<div class="break-out">
<pre><code class="language-css">@supports (color: contrast-color(red)) and (color: oklch(from red l c h)) {
  /&#042; Safe to use both &#042;/
}
</code></pre>
</div>
<h3 id="softened-contrast-with-color-mix">Softened Contrast With <code>color-mix()</code></h3>
<p>Similar idea, simpler API. Mix the sharp black/white output back into the background to soften it:</p>
<div class="break-out">
<pre><code class="language-css">.alert {
  --bg: var(--alert-color);
  background: var(--bg);

  /&#042; 80% contrast, 20% background = softer but readable &#042;/
  color: color-mix(in oklch, contrast-color(var(--bg)) 80%, var(--bg));

  /&#042; 40% contrast for a subtle border &#042;/
  border: 1px solid
    color-mix(in oklch, contrast-color(var(--bg)) 40%, var(--bg));
}
</code></pre>
</div>
<p>One custom property driving text, border, and potentially <code>box-shadow</code> or <code>outline</code>. Change <code>--alert-color</code> and the entire component recalculates.</p>
<p>This pattern also works well for <code>::placeholder</code> text, which is a common pain point in dynamic theming. Placeholder text should be readable but visually softer than the input’s main text &mdash; <code>color-mix()</code> with <code>contrast-color()</code> gets you there:</p>
<div class="break-out">
<pre><code class="language-css">input {
  --bg: var(--input-bg);
  background: var(--bg);
  color: contrast-color(var(--bg));
}

input::placeholder {
  color: color-mix(in oklch, contrast-color(var(--bg)) 50%, var(--bg));
}
</code></pre>
</div>
<p><code>50%</code> mix gives you a muted but legible placeholder that adapts automatically to whatever background the input sits on.</p>
<h3 id="theme-aware-contrast-with-light-dark">Theme-Aware Contrast With <code>light-dark()</code></h3>
<p>For apps that support system light/dark mode:</p>
<pre><code class="language-css">:root {
  color-scheme: light dark;
  --surface: light-dark(&#035;fff, #121212);
}

.component {
  background: var(--surface);
  color: contrast-color(var(--surface));
}
</code></pre>
<p>When the operating system switches to dark mode, <code>--surface</code> resolves to <code>#121212</code>, and <code>contrast-color()</code> returns white. No media queries, no JavaScript theme detection. The whole chain resolves natively.</p>
<div class="partners__lead-place"></div>
<h2 id="what-you-can-remove-from-your-bundle">What You Can Remove From Your Bundle</h2>
<p>The practical payoff: every one of these libraries existed because CSS couldn’t do contrast math. If you’re only using them for readable-text-color selection, you can pull them out of your runtime entirely:</p>
<table class="tablesaw break-out">
<thead>
<tr>
<th>Library</th>
<th>Size</th>
<th>What it did</th>
</tr>
</thead>
<tbody>
<tr>
<td>chroma-js</td>
<td>~14 kB</td>
<td>Color parsing, luminance calc, readable color selection</td>
</tr>
<tr>
<td>polished</td>
<td>~11 kB</td>
<td><code>readableColor()</code> for styled-components</td>
</tr>
<tr>
<td>tinycolor2</td>
<td>~5 kB</td>
<td>Hex parsing, WCAG contrast ratio math</td>
</tr>
</tbody>
</table>
<p>You might still need these for generating complex color scales, but the contrast-for-readability use case is now covered natively.</p>
<p>Beyond bundle size, there’s a performance angle that’s easy to overlook. Those JavaScript libraries don’t just cost you network bytes &mdash; they run on the main thread. Every time a theme changes or a component mounts with a dynamic background, your JS has to parse the color, compute luminance, decide black or white, and write the result back to the DOM. That’s main-thread work competing with layout, event handlers, and everything else your app is doing. <code>contrast-color()</code> moves all of that into the browser’s native style computation phase &mdash; heavily optimized C++ that runs before paint. For apps with lots of themed components, that’s a real difference in responsiveness.</p>
<p>There’s also a subtle bug that goes away: <strong>hydration flash</strong>. In React or Vue SSR apps, the server renders HTML without JavaScript. The client then hydrates, running JS to calculate contrast and inject the correct text color. For a brief window between initial paint and hydration, the text is either invisible or the wrong color. Moving contrast into CSS eliminates that entirely &mdash; the browser resolves the correct color during the initial paint, before JavaScript loads.</p>
<h2 id="what-we-used-to-do">What We Used To Do</h2>
<p>For context on what this replaces:</p>
<p><strong>Sass era.</strong> You’d write a function that checked <code>lightness($bg) &gt; 50%</code> and returned black or white at compile time. Worked for static themes. Completely useless for user-picked colors, CMS palettes, or dark mode, because the output was baked into the CSS file and could never change at runtime.</p>
<p><strong>The variable toggle hack.</strong> When CSS custom properties shipped, people got creative. GitHub used a version of this for their issue label picker &mdash; splitting colors into <code>--r</code>, <code>--g</code>, <code>--b</code> channels, calculating Rec.709 luminance inside <code>calc()</code>, multiplying by negative infinity, and clamping to <code>0</code> or <code>1</code>. It worked. It was also unreadable, unmaintainable, and would break silently if you got one parenthesis wrong. (Kevin Hamer’s <a href="https://css-tricks.com/approximating-contrast-color-with-other-css-features/">OKLCH-based approximation</a> is the most elegant version of this lineage &mdash; cleaner math, better perceptual alignment &mdash; but it’s still a workaround for a function that now ships natively.)</p>
<p><code>contrast-color()</code> replaces all of these approaches with a single function call. And because the spec lets browsers upgrade the underlying algorithm, your code won’t need to change if and when a successor to WCAG 2.x contrast math lands &mdash; whether that’s APCA or something else entirely.</p>
<hr />
<p></p>
<p>That <a href="https://almanac.httparchive.org/en/2025/accessibility#color-contrast">70% failure rate</a> was never about developers refusing to care about contrast. It was about the distance between caring and shipping &mdash; the library, the build step, the runtime calculation, the hydration flash, the one component someone forgot to wire up. Every gap in that chain was a spot where accessibility quietly dropped out.</p>
<p><code>contrast-color()</code> doesn’t make developers care more. It makes caring cost nothing.</p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/05/28/algorithmic-theming-engines-building-self-correcting-color-systems-with-contrast-color/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()`</title>
		<link>http://computercoursesonline.com/index.php/2026/05/21/advanced-tree-counting-mathematical-layouts-with-sibling-index-and-sibling-count/</link>
					<comments>http://computercoursesonline.com/index.php/2026/05/21/advanced-tree-counting-mathematical-layouts-with-sibling-index-and-sibling-count/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Thu, 21 May 2026 08:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1223</guid>

					<description><![CDATA[Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()` Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()` Durgesh Pawar 2026-05-21T08:00:00+00:00 2026-05-21T20:44:30+00:00 You know that thing where you have a grid of cards, and you want them to fade in one after another? That staggered cascade effect. Looks great. Should be simple. And yet every...]]></description>
										<content:encoded><![CDATA[<p>              <title>Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()`</title></p>
<article>
<header>
<h1>Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()`</h1>
<address>Durgesh Pawar</address>
<p>                  2026-05-21T08:00:00+00:00<br />
                  2026-05-21T20:44:30+00:00<br />
                </header>
<p>You know that thing where you have a grid of cards, and you want them to fade in one after another? That staggered cascade effect. Looks great. Should be simple. And yet every time I’ve built it, the implementation has made me feel like I’m doing something fundamentally stupid.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="zxowBog" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Dynamic Staggered Animations with CSS sibling-index() [forked]](https://codepen.io/smashingmag/pen/zxowBog) by <a href="https://codepen.io/durgeshpawar">Durgesh</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxowBog">Dynamic Staggered Animations with CSS sibling-index() [forked]</a> by <a href="https://codepen.io/durgeshpawar">Durgesh</a>.</figcaption></figure>
<p>Because the options were always the same. Say you want staggered animation delays on a list of 10 items. You either wrote a Sass loop that spat out a dozen <code>:nth-child()</code> rules, each one hardcoding a <code>--index</code> variable for that specific position:</p>
<pre><code class="language-css">/&#042; One rule per item. Hope the list never grows. &#042;/
li:nth-child(1) { --idx: 1; }
li:nth-child(2) { --idx: 2; }
li:nth-child(3) { --idx: 3; }
/&#042; ... eight more of these ... &#042;/
li:nth-child(10) { --idx: 10; }

li {
  animation-delay: calc(var(--idx) &#042; 100ms);
}
</code></pre>
<p>Ten items. Ten rules. If the list grows to 50? You cap it and hope for the best, or set up a Sass loop that generates hundreds of selectors at build time. Engineers like Roman Komarov have come up with <a href="https://kizu.dev/tree-counting-and-random/">O(âˆšN) strategies</a> &mdash; legitimately clever stuff &mdash; but you still end up with 63 rules to cover 1,023 elements.</p>
<p>Or you looped through elements in JavaScript and set inline styles. <code>style=&quot;--index: 3&quot;</code>. Right there in the DOM. Works fine. Also spreads layout concerns across your scripts and quietly breaks six months later when someone refactors the component without realizing the CSS depends on a JavaScript-injected variable.</p>
<p>Both approaches have always bugged me for the same reason: <strong>you’re telling the browser something it already knows</strong>. The browser <em>built</em> the DOM tree. It knows which element is the third child. It has the data. <strong>CSS just couldn’t access it.</strong></p>
<p>Well, now it can:</p>
<pre><code class="language-css">li {
  animation-delay: calc(sibling-index() * 100ms);
}
</code></pre>
<p>One line. Works for 5 items or 5,000. No event listeners. No mutation observers. No re-renders.</p>
<p><code>sibling-index()</code> and <code>sibling-count()</code> are part of the <a href="https://drafts.csswg.org/css-values-5/#tree-counting">CSS Values and Units Module Level 5</a> spec (Section 9, if you’re the type who reads W3C drafts for fun). The proposal was <a href="https://github.com/w3c/csswg-drafts/issues/4559">approved via CSSWG issue #4559</a> after substantial discussion. The functions themselves take no arguments &mdash; you just use them.</p>
<ul>
<li><strong><code>sibling-index()</code></strong> gives you the 1-based position of an element among its parent’s children. First child returns <code>1</code>. Fifth child returns <code>5</code>. It only counts element nodes &mdash; text nodes, comments, and whitespace are all invisible to it.</li>
<li><strong><code>sibling-count()</code></strong> gives you the total number of element children the parent has. Basically, the CSS equivalent of <code>element.parentElement.children.length</code> in JavaScript, but available in your stylesheet.</li>
</ul>
<p>Both functions resolve to <code>&lt;integer&gt;</code> &mdash; not <code>&lt;string&gt;</code>, an actual number. That means you can throw them into <code>calc()</code>, <code>min()</code>, <code>max()</code>, <code>round()</code>, <code>mod()</code>, trigonometric stuff like <a href="https://web.dev/articles/css-trig-functions"><code>sin()</code> and <code>cos()</code></a>. When you write <code>calc(sibling-index() * 100ms)</code>, CSS handles the type coercion and spits out a valid <code>&lt;time&gt;</code> value. No tricks needed. Compare that with <code>counter()</code>, which returns a string and can only live inside <code>content</code> on pseudo-elements &mdash; it’s a different thing entirely.</p>
<p>One clarification that trips people up: <code>:nth-child()</code> is a <em>selector</em>. It picks elements. It doesn’t produce a value. You can’t write <code>calc(:nth-child() * 10px)</code> &mdash; that’s not valid CSS. <code>sibling-index()</code> does the opposite. It sits inside your declarations and gives you a number you can calculate with. They solve different problems, and until now we’ve been duct-taping <code>:nth-child()</code> into a role it was never designed for.</p>
<h2 id="patterns-worth-stealing">Patterns Worth Stealing</h2>
<p>Once it clicks that these are just integers, ideas come fast.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h3 id="reverse-stagger">Reverse Stagger</h3>
<p>Want the last item to animate first? Subtract:</p>
<pre><code class="language-css">.card {
  animation: fade-in 0.4s ease both;
  animation-delay: calc((sibling-count() - sibling-index()) &#042; 80ms);
}
</code></pre>
<p>Last child gets <code>(N - N) * 80ms = 0ms</code> &mdash; it fires instantly. First child gets <code>(N - 1) * 80ms</code>. The animation kicks off the moment the page loads instead of pausing for an awkward beat.</p>
<h3 id="automatic-equal-widths">Automatic Equal Widths</h3>
<p>Stop counting children manually to set percentages:</p>
<pre><code class="language-css">.tab {
  width: calc(100% / sibling-count());
}
</code></pre>
<p>Five tabs? 20% each. Add a sixth? 16.66%. Remove two? 25%. No media queries, no resize observers, no JavaScript at all.</p>
<p>That said, you can imagine a scenario where too many items make for really narrow tabs, at which point you might want to go with something else, perhaps a Flexbox wrapping solution.</p>
<h3 id="hue-distribution">Hue Distribution</h3>
<p>Spread colors evenly across the color wheel:</p>
<pre><code class="language-css">.swatch {
  background-color: hsl(
    calc((360deg / sibling-count()) &#042; sibling-index()) 70% 50%
  );
}
</code></pre>
<p>Three items get hues 120Â° apart. Twelve items get 30Â° increments. The palette adapts to whatever’s in the DOM, which is the kind of thing you’d normally reach for a JavaScript color library to do.</p>
<h3 id="circular-menus">Circular Menus</h3>
<p>Distributing items in a circle used to mean calculating sine and cosine in JavaScript. CSS now has <a href="https://web.dev/articles/css-trig-functions"><code>sin()</code> and <code>cos()</code> natively</a> (Juan Diego RodrÃ­guez has a great <a href="https://css-tricks.com/the-most-hated-css-feature-cos-and-sin/">practical walkthrough</a> of these on CSS-Tricks), and combined with tree-counting, the whole thing collapses into pure CSS:</p>
<pre><code class="language-css">.radial-item {
  --angle: calc((360deg / sibling-count()) &#042; sibling-index());
  --radius: 120px;

  position: absolute;
  left: calc(50% + var(--radius) &#042; cos(var(--angle)));
  top: calc(50% + var(--radius) &#042; sin(var(--angle)));
  transform: rotate(calc(var(--angle) &#042; -1));
}
</code></pre>
<p>Six items? Hexagon. Eight? Octagon. Add or remove items, and the layout recalculates. No JavaScript computing coordinates.</p>
<h3 id="z-index-stacking">Z-Index Stacking</h3>
<p>Building a card fan? One line:</p>
<pre><code class="language-css">.card {
  z-index: calc(sibling-count() - sibling-index());
}
</code></pre>
<p>First card stacks highest, last card gets 0. Flip the math if you want the reverse.</p>
<div class="partners__lead-place"></div>
<h2 id="the-gotchas">The Gotchas</h2>
<p>These are worth going through individually because they’re not obvious from the spec.</p>
<h3 id="shadow-dom-scoping">Shadow DOM Scoping</h3>
<p><code>sibling-index()</code> and <code>sibling-count()</code> operate on the DOM tree, not the flattened visual tree. This distinction will absolutely bite you with <a href="https://www.smashingmagazine.com/2025/07/web-components-working-with-shadow-dom/">Web Components</a>.</p>
<p>Say you have a custom element with this shadow DOM:</p>
<pre><code class="language-html">&lt;section&gt;
  &lt;slot&gt;&lt;/slot&gt;
  &lt;div class="internal"&gt;&lt;/div&gt;
&lt;/section&gt;
</code></pre>
<p>If you style <code>.internal</code> with <code>sibling-index()</code>, it returns <code>2</code>. Always. Even if the <code>&lt;slot&gt;</code> projects 300 elements. The function sees two children of <code>&lt;section&gt;</code> in the shadow tree &mdash; the <code>&lt;slot&gt;</code> and the <code>.internal</code> div. Projected light DOM content doesn’t exist as far as the count is concerned.</p>
<p>There’s also a security thing going on here. If a light DOM stylesheet tries to reach into a component via <code>::part()</code> and use <code>sibling-index()</code>, the browser returns <code>0</code>. Flat zero. It’s a deliberate wall to prevent external CSS from probing the internal structure of third-party components. Honestly, I think that’s the right call.</p>
<h3 id="pseudo-elements-don-t-count">Pseudo-Elements Don’t Count</h3>
<p><code>::before</code> and <code>::after</code> aren’t siblings. They don’t show up in <code>sibling-count()</code> and they don’t have their own <code>sibling-index()</code>. But &mdash; and this is the part that’ll save you a debugging session &mdash; you <em>can</em> use these functions inside pseudo-element declarations. When you write <code>#target::before { width: calc(sibling-index() * 10px); }</code>, it evaluates <code>sibling-index()</code> against <code>#target</code>, not against the pseudo-element. The pseudo-element isn’t a real node, so the function traces back to its originating element. Same story with <code>::slotted(*)::before</code> &mdash; it checks the slotted element’s index in the light DOM.</p>
<h3 id="display-none-still-counts"><code>display: none</code> Still Counts</h3>
<p>This one burned me. Elements with <code>display: none</code> vanish from the layout tree. They take up no space. Screen readers don’t see them. But they’re still in the DOM.</p>
<p>Since <code>sibling-index()</code> reads the DOM tree, not the layout tree, hidden elements get counted:</p>
<pre><code class="language-html">&lt;ul&gt;
  &lt;!-- sibling-index() = 1 --&gt;
  &lt;li&gt;Apple&lt;/li&gt;
  &lt;!-- sibling-index() = 2, invisible --&gt;
  &lt;li style="display:none"&gt;Banana&lt;/li&gt;
  &lt;!-- sibling-index() = 3, NOT 2 --&gt;
  &lt;li&gt;Cherry&lt;/li&gt;       
&lt;/ul&gt;
</code></pre>
<p>Cherry is <code>3</code>, not <code>2</code>. The hidden banana still holds its spot.</p>
<p>This doesn’t matter for most layouts. But if you’re building something like a search filter that hides non-matching items with <code>display: none</code>, your staggered animations and circular layouts will develop gaps. The visible items keep their original, non-sequential indexes. For anything that depends on continuous counting &mdash; radial menus, proportional widths &mdash; you’ll need to actually remove filtered nodes from the DOM instead of just hiding them. Or fall back to JavaScript-managed indexes.</p>
<p><strong>Note:</strong> <code>visibility: hidden</code> and <code>opacity: 0</code> count too, but that feels more intuitive since those elements still take up space. <code>display: none</code> is the sneaky one because the element disappears visually but still occupies a DOM slot.</p>
<h4 id="custom-properties-evaluate-immediately">Custom Properties Evaluate Immediately</h4>
<p>This is subtle. If you try to centralize the index on a parent:</p>
<pre><code class="language-css">.parent {
  --idx: sibling-index();
}
</code></pre>
<p>&hellip;that <code>--idx</code> resolves right there on <code>.parent</code>. It grabs the parent’s own sibling index, locks it to that number, and every child inherits that single fixed value. Every child gets the same number. Almost certainly not what you want or expect.</p>
<p>The fix is simple &mdash; put the function on the elements that need it:</p>
<pre><code class="language-css">.child {
  --idx: sibling-index();
  animation-delay: calc(var(--idx) &#042; 100ms);
}
</code></pre>
<p>The CSSWG has discussed an <code>inherits: declaration</code> addition to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@property"><code>@property</code></a> that could theoretically fix this. If you haven’t used <code>@property</code>, it lets you define a custom property’s type, initial value, and inheritance behavior &mdash; way more control than a raw <code>--variable</code>. But the <code>inherits: declaration</code> idea is still in early CSSWG discussion, not baked into any spec draft. It could be years before it lands &mdash; or it might not land at all. Even with <code>@property</code> today, there’s no mechanism to say <em>“don’t evaluate yet, wait for the child.”</em> So for now, just apply directly.</p>
<h3 id="performance-at-scale">Performance at Scale</h3>
<p>Changing the DOM &mdash; i.e., adding, removing, reordering children &mdash; triggers style recalculation for affected siblings. The browser handles this during the cascade phase (before layout and paint), so it’s faster than the old approach of looping in JavaScript and stamping inline styles.</p>
<p>But there’s a real cost if you push it. Inserting an element at the beginning of a container with 10,000 children forces the engine to recalculate the sibling index for all 10,000 elements after it. For normal stuff &mdash; navigation, card grids, tab bars &mdash; you’ll never notice. For a live stock ticker or an infinite-scroll feed with thousands of nodes constantly churning, keep using JavaScript-managed indexes inside your virtualization window. These functions are fast. They’re not zero-cost.</p>
<h2 id="browser-support">Browser Support</h2>
<p>As of writing, <a href="https://developer.chrome.com/blog/new-in-chrome-138">Chrome/Edge 138</a> shipped these functions in stable releases (June 2025), and <a href="https://webkit.org/blog/17640/webkit-features-for-safari-26-2/">Safari 26.2</a> followed. Firefox hasn’t shipped them in stable yet, but Mozilla’s <a href="https://github.com/mozilla/standards-positions/issues/1194">spec position is positive</a> and implementation work is actively underway &mdash; tracked under <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1953973">Bugzilla issue #1953973</a>. Check <a href="https://caniuse.com/wf-sibling-count">caniuse</a> for the latest before you ship.</p>
<p>Chrome and Safari together cover roughly 75&ndash;80% of global traffic. That’s a strong majority, but Firefox’s absence means you still need a fallback.</p>
<p>For shipping today, <code>@supports</code> is your friend:</p>
<pre><code class="language-css">/&#042; Baseline that works everywhere &#042;/
.item {
  width: 25%;
  animation-delay: 0ms;
}

/&#042; Progressively enhance where supported &#042;/
@supports (z-index: sibling-index()) {
  .item {
    width: calc(100% / sibling-count());
    animation-delay: calc(sibling-index() &#042; 80ms);
  }
}
</code></pre>
<p>Static fallback for Firefox. Mathematical layout for everyone else. Nobody gets a broken page.</p>
<blockquote><p><strong>On polyfills:</strong></p>
<p>A JavaScript polyfill that loops through siblings and sets inline styles is the exact thing these functions exist to replace. But that doesn’t mean you’re stuck with hardcoded fallback values either. Juan Diego RodrÃ­guez wrote a solid piece on “<a href="https://css-tricks.com/how-to-wait-for-the-sibling-count-and-sibling-index-functions/">How to Wait for the <code>sibling-count()</code> and <code>sibling-index()</code> Functions</a>” that lays out the right model for progressive enhancement until native support hits Baseline. His approach uses existing CSS techniques (like Roman Komarov’s <a href="https://kizu.dev/tree-counting-and-random/">counting hacks</a>) as a bridge rather than a full JavaScript polyfill. Worth reading if you need to ship something production-ready today while Firefox catches up.</p></blockquote>
<div class="partners__lead-place"></div>
<h2 id="accessibility-notes">Accessibility Notes</h2>
<p>This needs saying because it’s easy to get excited and forget: <strong>these functions are purely visual</strong>. They change how things look. They don’t change what things <em>mean</em>.</p>
<p>If you use <code>sibling-index()</code> math to visually reorder a list &mdash; via <code>order</code> or grid placement &mdash; a screen reader still reads the DOM in source order. Keyboard tab order follows the DOM, too. Visual layout and semantic structure will contradict each other, and that’s an accessibility failure.</p>
<p>For interactive components like data grids, radial menus, or custom listboxes that lean on tree-counting for layout, you still need JavaScript to sync ARIA attributes. <code>aria-posinset</code> and <code>aria-setsize</code> have no idea what CSS is calculating. If your CSS says <em>“this is visually item 3 of 7”</em> but ARIA says something different (or nothing), assistive technology users get a broken experience.</p>
<p>On the debugging side, recent versions of Chrome DevTools let you inspect computed <code>sibling-index()</code> and <code>sibling-count()</code> values directly in the Elements panel, which helps when the math isn’t doing what you expect.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/mathematical-layouts-sibling-index-sibling-count/sibling-index-devtools.jpeg"></p>
<p>    <img loading="lazy" width="800" height="400" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="sibling-index devtools" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/mathematical-layouts-sibling-index-sibling-count/sibling-index-devtools.jpeg"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/mathematical-layouts-sibling-index-sibling-count/sibling-index-devtools.jpeg">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="what-s-coming">What’s Coming</h2>
<p>The current spec only counts <em>all</em> element siblings. But the CSSWG has documented a planned extension in <a href="https://github.com/w3c/csswg-drafts/issues/9572">issue #9572</a>: an <code>of &lt;selector&gt;</code> argument, matching what <code>:nth-child()</code> already supports.</p>
<p>Something like <code>sibling-index(of .active)</code> would let you count only siblings matching a specific selector. An element that’s the eighth child overall but the third <code>.active</code> child would return <code>3</code>. For dynamic UIs where you’re filtering or toggling visibility, that would keep the index sequential without requiring DOM manipulation.</p>
<p>There’s also been CSSWG discussion around <a href="https://github.com/w3c/csswg-drafts/issues/11068"><code>children-count()</code></a> and <a href="https://github.com/w3c/csswg-drafts/issues/11069"><code>descendant-count()</code></a> functions &mdash; the first would tell you how many children an element has (useful for parent-driven layouts), the second would count all descendants recursively. Both are still at the proposal stage, but they’d round out the tree-counting story: <code>sibling-index()</code> and <code>sibling-count()</code> give you the horizontal view (where am I among my peers?), while <code>children-count()</code> and <code>descendant-count()</code> would give you the vertical view (what’s below me?).</p>
<p><em>That feeling I mentioned at the top &mdash; writing ten <code>:nth-child()</code> rules for a staggered animation and wondering if you’re missing something obvious? You weren’t. The obvious thing just didn’t exist yet.</em></p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/05/21/advanced-tree-counting-mathematical-layouts-with-sibling-index-and-sibling-count/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Beyond `border-radius`: What The CSS `corner-shape` Property Unlocks For Everyday UI</title>
		<link>http://computercoursesonline.com/index.php/2026/03/12/beyond-border-radius-what-the-css-corner-shape-property-unlocks-for-everyday-ui/</link>
					<comments>http://computercoursesonline.com/index.php/2026/03/12/beyond-border-radius-what-the-css-corner-shape-property-unlocks-for-everyday-ui/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Thu, 12 Mar 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1164</guid>

					<description><![CDATA[Beyond `border-radius`: What The CSS `corner-shape` Property Unlocks For Everyday UI Beyond `border-radius`: What The CSS `corner-shape` Property Unlocks For Everyday UI Brecht De Ruyte 2026-03-12T10:00:00+00:00 2026-03-18T09:33:12+00:00 When I first started building websites, rounded corners required five background images, one for each corner, one for the body, and a prayer that the client wouldn’t ask...]]></description>
										<content:encoded><![CDATA[<p>              <title>Beyond `border-radius`: What The CSS `corner-shape` Property Unlocks For Everyday UI</title></p>
<article>
<header>
<h1>Beyond `border-radius`: What The CSS `corner-shape` Property Unlocks For Everyday UI</h1>
<address>Brecht De Ruyte</address>
<p>                  2026-03-12T10:00:00+00:00<br />
                  2026-03-18T09:33:12+00:00<br />
                </header>
<p>When I first started building websites, rounded corners required five background images, one for each corner, one for the body, and a prayer that the client wouldn’t ask for a different radius. Then the <code>border-radius</code> property landed, and the entire web collectively sighed with relief. That was over fifteen years ago, and honestly, we’ve been riding that same wave ever since. Just as then, I hope that we can look at this feature as a progressive enhancement slowly making its way to other browsers.</p>
<p>I like a good <code>border-radius</code> like any other guy, but the fact is that it only gives us one shape. Round. That’s it. Want beveled corners? Clip-path. Scooped ticket edges? SVG mask. Squircle app icons? A carefully tuned SVG that you hope nobody asks you to animate. We’ve been hacking around the limitations of <code>border-radius</code> for years, and those hacks come with real trade-offs: borders don’t follow clip-paths, shadows get cut off, and you end up with brittle code that breaks the moment someone changes a padding value.</p>
<p>Well, the new <strong><code>corner-shape</code></strong> changes all of that.</p>
<h2 id="what-is-corner-shape">What Is <code>corner-shape</code>?</h2>
<p>The <a href="https://css-tricks.com/almanac/properties/c/corner-shape/"><code>corner-shape</code></a> property is a companion to <code>border-radius</code>. It doesn’t replace it; it modifies the <em>shape</em> of the curve that <code>border-radius</code> creates. Without <code>border-radius</code>, <code>corner-shape</code> does nothing. But together, they’re a powerful pair.</p>
<p>The property accepts these values:</p>
<ul>
<li><strong><code>round</code></strong>: the default, same as regular <code>border-radius</code>,</li>
<li><strong><code>squircle</code></strong>: a superellipse, the smooth Apple-style rounded square,</li>
<li><strong><code>bevel</code></strong>: a straight line between the two radius endpoints (snipped corners),</li>
<li><strong><code>scoop</code></strong>: an inverted curve, creating concave corners,</li>
<li><strong><code>notch</code></strong>: sharp inward cuts,</li>
<li><strong><code>square</code></strong>: effectively removes the rounding, overriding <code>border-radius</code>.</li>
</ul>
<p>And you can set different values per corner, just like <code>border-radius</code>:</p>
<pre><code class="language-css">&#042;corner-shape: bevel round scoop squircle;
/&#042; top-left, top-right, bottom-right, bottom-left &#042;/
</code></pre>
<p>You can also use the <a href="https://css-tricks.com/almanac/functions/s/superellipse/"><code>superellipse()</code></a> function with a numeric parameter for fine-grained control.</p>
<pre><code class="language-css">.element { 
  border-radius: 25px;
  corner-shape: superellipse(0); /&#042; equal to 'bevel' &#042;/
}
</code></pre>
<p>So the question here might be: why not call this property “<code>border-shape</code>” instead? Well, first of all, that is <a href="https://una.im/border-shape">something completely different that we’ll get to play around with soon</a>. Second, it does apply to a bit more than borders, such as outlines, box shadows, and backgrounds. That’s the thing that the <code>clip-path</code> property could never do.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="why-progressive-enhancement-matters-here">Why Progressive Enhancement Matters Here</h2>
<p>At the time of writing (March 2026), <code>corner-shape</code> is only supported in Chrome 139+ and other Chromium-based browsers. That’s a significant chunk of users, but certainly not everyone. The temptation is to either ignore the property until it’s everywhere or to build demos that fall apart without it.</p>
<p>I don’t think either approach is right. The way I see it, <code>corner-shape</code> is the perfect candidate for progressive enhancement, just as <code>border-radius</code> was in the age of Internet Explorer 6. The baseline should use the techniques we already know, such as <code>border-radius</code>, <code>clip-path</code>, <code>radial-gradient</code> masks and look intentionally good. Then, for browsers that support <code>corner-shape</code>, we upgrade the experience. Sometimes this can be as simple as just providing a more basic default; sometimes it might need to be a bit more.</p>
<p><strong>Every demo in this article is created with that progressive enhancement idea.</strong> The structure for the demos looks like:</p>
<pre><code class="language-css">@layer base, presentation, demo;
</code></pre>
<p>The <code>presentation</code> layer contains the full polished UI using proven techniques. The <code>demo</code> layer wraps everything in <code>@supports</code>:</p>
<pre><code class="language-css">@layer demo {
  @supports (corner-shape: bevel) {
    /&#042; upgrade styles here &#042;/
  }
}
</code></pre>
<p>No fallback banners, no “your browser doesn’t support this” messages. Just two tiers of design: good and better. I thought it could be nice just to show some examples. There are a few out there already, but I hope I can add a bit of extra inspiration on top of those.</p>
<h2 id="demo-1-product-cards-with-ribbon-badges">Demo 1: Product Cards With Ribbon Badges</h2>
<p>Every e-commerce site has them: those little “New” or “Sale” badges pinned to the corner of a product card. Traditionally, getting that ribbon shape means reaching for <code>clip-path: polygon()</code> or a rotated pseudo-element, let&rsquo;s call it “fiddly code” that has the chance to fall apart the moment someone changes a padding value.</p>
<p>But here’s the thing: we don’t <em>need</em> the ribbon shape in the baseline. A simple badge with slightly rounded corners tells the same story and looks perfectly fine:</p>
<pre><code class="language-css">.product__badge {
  border-radius: 0 4px 4px 0;
  background-color: var(--badge-bg);
}
</code></pre>
<p>That’s it. A small, clean label sitting flush against the left edge of the card. Nothing fancy, nothing broken. It works in every browser.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/1-product-cards-corner-badges.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Product cards with colored corner badges like “New,” “–30%,” and “Limited.”" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/1-product-cards-corner-badges.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/1-product-cards-corner-badges.png">Large preview</a>)<br />
    </figcaption></figure>
<p>For browsers that support <code>corner-shape</code>, we enhance:</p>
<pre><code class="language-css">@layer demo {
  /&#042; If the browser supports `corner-shape` &#042;/
  @supports (corner-shape: bevel) {
    .product {
      border-radius: 40px;
      corner-shape: squircle;
    }

    .product&#095;&#095;badge {
      padding: 0.35rem 1.4rem 0.35rem 1rem;
      border-radius: 0 16px 16px 0;
      corner-shape: round bevel bevel round;
    }
  }
}
</code></pre>
<p>The <code>round bevel bevel round</code> combination creates a directional ribbon. Round where it meets the card edge, beveled to a point on the other side. No <code>clip-path</code>, no pseudo-element tricks. Borders, shadows, and backgrounds all follow the declared shape because it <em>is</em> the shape.</p>
<p>The cards themselves upgrade from <code>border-radius: 12px</code> to a larger size and the <code>squircle</code> corner-shape, that smooth superellipse curve that makes standard rounding look slightly off by comparison. Designers will notice immediately. Everyone else will just say it “feels more premium.”</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/2-product-cards-ribbon-badges.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Product cards with arrow-shaped corner badges labeled “New,” “–30%,” and “Limited,” pointing inward." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/2-product-cards-ribbon-badges.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/2-product-cards-ribbon-badges.png">Large preview</a>)<br />
    </figcaption></figure>
<p><strong>Hot tip:</strong> Using the <code>squircle</code> value on card components is one of those upgrades where the before-and-after difference can be subtle in isolation, but transformative across an entire page. It’s the iOS effect: once everything uses superellipse curves, plain circular arcs start looking out of place. In this demo, I did exaggerate a bit.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="GgjNwQE" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Corner-shape: Labels [forked]](https://codepen.io/smashingmag/pen/GgjNwQE) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgjNwQE">Corner-shape: Labels [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<div class="partners__lead-place"></div>
<h2 id="demo-2-buttons-tags-and-components">Demo 2: Buttons, Tags, And Components</h2>
<p>This is the “component library demo”, the one that shows <code>corner-shape</code> isn’t just for hero sections. It’s practical, everyday UI: solid buttons, outlined buttons, status tags, directional arrows, notification badges.</p>
<p>The set-up is intentionally clean. Standard <code>border-radius: 10px</code> buttons with a polished typeface. Everything works, everything looks professional. You could do this without hesitation.</p>
<p>The <code>corner-shape</code> layer turns it into a showcase. Each button type gets its own shape to demonstrate the range of what’s possible:</p>
<pre><code class="language-css">@layer demo {
  @supports (corner-shape: bevel) {
    .btn--primary {
      corner-shape: bevel;
      transition: corner-shape 0.3s ease;

      &amp;:hover {
        corner-shape: squircle;
      }
    }

    .btn--secondary {
      border-radius: 25px;
      corner-shape: superellipse(0.5);
    }

    .btn--danger {
      border-radius: 16px;
      corner-shape: squircle;
    }

    .btn--notch {
      border-radius: 12px;
      corner-shape: notch;
    }

    .btn--scoop {
      border-radius: 14px;
      corner-shape: scoop;
    }
  }
}
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/3-buttons-before.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Buttins and tags before" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/3-buttons-before.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Before. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/3-buttons-before.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/4-buttons-after.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Buttins and tags after" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/4-buttons-after.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      After. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/4-buttons-after.png">Large preview</a>)<br />
    </figcaption></figure>
<p>The primary button starts beveled, faceted, and gem-like, and softens to <code>squircle</code> on hover. Because <code>corner-shape</code> values animate via their <code>superellipse()</code> equivalents, the transition is smooth. It’s a fun interaction that used to be hard to achieve but is now a single property (used alongside <code>border-radius</code>, of course).</p>
<p>The secondary button uses <code>superellipse(0.5)</code>, a value that is <em>between</em> a standard circle and a squircle, combined with a larger <code>border-radius</code> for a distinctive pill-like shape. The danger button gets a more prominent <code>squircle</code> with a generous radius. And <code>notch</code> and <code>scoop</code> each bring their own sharp or concave personality.</p>
<p>Beyond buttons, the status tags get <code>corner-shape: notch</code>, those sharp inward cuts that give them a machine-stamped look. The directional arrow tags use <code>round bevel bevel round</code> (and its reverse for the back arrow), replacing what used to require <code>clip-path: polygon()</code>. Now borders and shadows work correctly across all states.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="gbwLQdG" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Corner-shape: Buttons &amp; Tags [forked]](https://codepen.io/smashingmag/pen/gbwLQdG) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/gbwLQdG">Corner-shape: Buttons &amp; Tags [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<h2 id="demo-3-testimonial-cards">Demo 3: Testimonial Cards</h2>
<p>This demo is probably my favourite one. At its foundation, these are just testimonial cards with serif typography, a sandy palette, and scooped corners on the featured card. The design language is intentionally different from the clean geometric buttons demo, and that’s the point. <code>corner-shape</code> merely adds that extra “edge”.</p>
<p>The basis is standard <code>border-radius: 16px</code> cards. The featured testimonial spans full width with a subtle gradient and a decorative open quote mark. Normal cards alternate in a two-column grid. It already looks like something from a premium marketing site.</p>
<p>The <code>corner-shape</code> layer adds character:</p>
<pre><code class="language-css">@layer demo {
  /&#042; Progressive enhancement &#042;/
  @supports (corner-shape: scoop) {
    .testimonial {
      border-radius: 20px;
      corner-shape: squircle;
    }

    .testimonial--featured {
      border-radius: 24px;
      corner-shape: scoop;
    }

    .testimonial:not(.testimonial--featured):nth-child(even) {
      corner-shape: scoop round;
    }

    .testimonial&#095;&#095;avatar {
      border-radius: 28%;
      corner-shape: squircle;
    }
  }
}
</code></pre>
<p>The featured card gets full <code>scoop</code> corners, concave on all four sides, creating an organic, almost hand-crafted feel that matches the serif typography. Even-numbered cards mix <code>scoop round</code>, giving each one a slightly different personality without any extra markup.</p>
<p>The author avatars switch from circles to <code>squircle</code>. A small touch that makes it a bit more “different”.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/5-testimonials-before.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Testimonials before" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/5-testimonials-before.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Fallback. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/5-testimonials-before.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/6-testimonials-after.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Testimonials after" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/6-testimonials-after.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Supported. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/6-testimonials-after.png">Large preview</a>)<br />
    </figcaption></figure>
<p><strong>Hot tip:</strong> <code>corner-shape: scoop</code> pairs beautifully with serif fonts and warm color palettes. The concave curves echo the organic shapes found in editorial design, calligraphy, and print layouts. For geometric sans-serif designs, stick with <code>squircle</code> or <code>bevel</code>.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="RNGoqvZ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Corner-shape: Testimonials [forked]](https://codepen.io/smashingmag/pen/RNGoqvZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/RNGoqvZ">Corner-shape: Testimonials [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<div class="partners__lead-place"></div>
<h2 id="demo-4-pricing-cards">Demo 4: Pricing Cards</h2>
<p>Every SaaS site needs a pricing page, and the visual hierarchy challenge is always the same: make one plan stand out without the others feeling neglected. This demo solves it with <code>corner-shape</code>.</p>
<p>This is quite similar to the last demo in that we once again have a nice baseline for browsers that don’t yet support <code>corner-shape</code>. We have three cards in a row, where the featured plan is distinguished by a warm gradient background, a stronger border, and a “Most Popular” badge.</p>
<p>The enhancement takes it further:</p>
<pre><code class="language-css">@layer demo {
  @supports (corner-shape: squircle) {
    .plan {
      border-radius: 20px;
      corner-shape: squircle;
    }

    .plan--featured {
      border-radius: 24px;
      corner-shape: scoop;
    }

    .plan&#095;&#095;badge {
      inset-inline-start: 50%;
      translate: -50% 0;
      padding-inline: 1.2rem;
      border-radius: 10px;
      corner-shape: bevel;
    }

    .plan&#095;&#095;cta {
      border-radius: 12px;
      corner-shape: squircle;
    }
  }
}
</code></pre>
<p>Regular plans get <code>squircle</code> for that premium feel. The featured plan gets <code>scoop</code>, concave corners that immediately set it apart from its neighbors. The “Most Popular” badge centers itself and takes on <code>corner-shape: bevel</code>, creating a gem-like, faceted shape that feels like a jewel pinned to the card. The CTA buttons get <code>squircle</code> to match the card language.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/7-pricing-before.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Pricing cards: before" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/7-pricing-before.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Before. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/7-pricing-before.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/8-pricing-after.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Pricing cards: after" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/8-pricing-after.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      After. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/8-pricing-after.png">Large preview</a>)<br />
    </figcaption></figure>
<p>What I like about this demo is how the shape hierarchy mirrors the content hierarchy. The most important element (featured plan) gets the most distinctive shape (<code>scoop</code>). The badge gets the sharpest shape (<code>bevel</code>). Everything else gets a simpler upgrade (<code>squircle</code>). Shape becomes a tool for visual emphasis, not just decoration.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="vEXyQMZ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Corner-shape: Pricing [forked]](https://codepen.io/smashingmag/pen/vEXyQMZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vEXyQMZ">Corner-shape: Pricing [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<h2 id="demo-5-music-player">Demo 5: Music Player</h2>
<p>The final demo is a warm dark UI for a music player with album art, playback controls, genre tags, and a listening queue. It’s the most visually complex demo, and it shows how <code>corner-shape</code> works across many different element types within a single cohesive design.</p>
<p>This time, I went for a dark warm palette built on <code>oklch(18% 0.015 40)</code>, and standard rounded corners throughout. The album art gets <code>border-radius: 12px</code>, queue items get <code>border-radius: 12px</code>, genre tags get <code>border-radius: 5px</code>. It looks good. It’s a complete, polished player.</p>
<p>And then once again, we add some enhancements:</p>
<pre><code class="language-css">@layer demo {
  @supports (corner-shape: squircle) {
    .now-playing {
      border-radius: 20px;
      corner-shape: squircle;
    }

    .now-playing&#095;&#095;art {
      border-radius: 16px;
      corner-shape: squircle;
    }

    .now-playing&#095;&#095;swatch {
      border-radius: 26%;
      corner-shape: squircle;
    }

    .queue-item {
      border-radius: 14px;
      corner-shape: scoop round;
    }

    .tag {
      border-radius: 8px;
      corner-shape: bevel;
    }
  }
}
</code></pre>
<p>The player card and album art get <code>squircle</code>, the same curves used for app icons and album thumbnails. Album art swatches go from <code>border-radius: 22%</code> to a proper <code>squircle</code> at <code>26%</code>, which is a subtle but meaningful difference in the visual elements you stare at while listening.</p>
<p>Queue items get <code>scoop round</code>, resulting in concave corners on the top-left and bottom-left, and round on the right. It gives each row a distinctive feel without overwhelming the layout. Genre tags get <code>bevel</code> for that sharp feeling.</p>
<p>The Play button also gets <code>corner-shape: squircle</code> on its existing <code>border-radius: 50%</code> to fit the album covers. On the surface, the difference is barely noticeable, but it contributes to the overall feel of the player.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="ogzYQRB" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Corner-shape: Music player [forked]](https://codepen.io/smashingmag/pen/ogzYQRB) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogzYQRB">Corner-shape: Music player [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/9-music-before.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Music player: before" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/9-music-before.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Before. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/9-music-before.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/10-music-after.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Music player: after" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/10-music-after.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      After. (<a href="https://files.smashing.media/articles/beyond-border-radius-css-corner-shape-property-ui/10-music-after.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="browser-support">Browser Support</h2>
<p>As of writing, <code>corner-shape</code> is available in Chrome 139+ and Chromium-based browsers. Firefox and Safari don’t support it yet. The spec lives in <a href="https://drafts.csswg.org/css-borders/#propdef-corner-shape">CSS Borders and Box Decorations Module Level 4</a>, which is a W3C Working Draft as of this writing.</p>
<p>For practical use, that’s fine. That’s the whole point of how these demos are built. The <code>presentation</code> layer delivers a polished, complete UI to every browser. The <code>demo</code> layer is a bonus for supporting browsers, wrapped in <code>@supports (corner-shape: ...)</code>. I lived through the time when <code>border-radius</code> was only available in Firefox. Somewhere along the line, it seems like we have forgotten that not every website needs to look exactly the same in every browser. What we really want is: no “broken” layouts and no “your browser doesn’t support this” messages, but rather a beautiful experience that just works, and can progressively enhance a bit of extra joy. In other words, we’re working with two tiers of design: good and better.</p>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>The approach I keep coming back to is: don’t design for <code>corner-shape</code>, and don’t design <em>around</em> the lack of it. Design a solid baseline with <code>border-radius</code> and then enhance it. The presentation layer in every demo looks intentionally good. It’s not a degraded version waiting for a better browser. It’s a <strong>complete design</strong>. The <code>demo</code> layer adds a dimension that <code>border-radius</code> alone can’t express.</p>
<p>What surprises me most about <code>corner-shape</code> is the <em>range</em> it offers &mdash; the amazing powerhouse we have with this single property: <code>squircle</code> for that premium, superellipse feel on cards and avatars; <code>bevel</code> for directional elements and gem-like badges; <code>scoop</code> for editorial warmth and visual hierarchy; <code>notch</code> for mechanical precision on tags; and <code>superellipse()</code> for fine control between <code>round</code> and <code>squircle</code>. And the ability to mix values per corner (<code>round bevel bevel round</code>, <code>scoop round</code>) opens up shapes that would have required SVG masks or <code>clip-path</code> hacks.</p>
<p>We went from five background images to <code>border-radius</code>, to <code>corner-shape</code>. Each step removed a category of workarounds. I’m excited to see what designers do with this one.</p>
<h3 id="further-reading">Further Reading</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/corner-shape"><code>corner-shape</code></a> (MDN)</li>
<li>“<a href="https://css-tricks.com/what-can-we-actually-do-with-corner-shape/">What Can We Actually Do With <code>corner-shape</code>?</a>”, Daniel Schwarz</li>
<li><a href="https://drafts.csswg.org/css-borders/#propdef-corner-shape">CSS Borders and Box Decorations Module Level 4</a> (W3C specification)</li>
<li><a href="https://codepen.io/bySebastian/pen/VYjPzYo">A fun demo for “eco-labels”</a>, Sebastian on CodePen</li>
</ul>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/03/12/beyond-border-radius-what-the-css-corner-shape-property-unlocks-for-everyday-ui/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Getting Started With The Popover API</title>
		<link>http://computercoursesonline.com/index.php/2026/03/02/getting-started-with-the-popover-api/</link>
					<comments>http://computercoursesonline.com/index.php/2026/03/02/getting-started-with-the-popover-api/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Mon, 02 Mar 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1166</guid>

					<description><![CDATA[Getting Started With The Popover API Getting Started With The Popover API Godstime Aburu 2026-03-02T10:00:00+00:00 2026-03-18T09:33:12+00:00 Tooltips feel like the smallest UI problem you can have. They’re tiny and usually hidden. When someone asks how to build one, the traditional answer almost always comes back using some JavaScript library. And for a long time, that...]]></description>
										<content:encoded><![CDATA[<p>              <title>Getting Started With The Popover API</title></p>
<article>
<header>
<h1>Getting Started With The Popover API</h1>
<address>Godstime Aburu</address>
<p>                  2026-03-02T10:00:00+00:00<br />
                  2026-03-18T09:33:12+00:00<br />
                </header>
<p>Tooltips feel like the smallest UI problem you can have. They’re tiny and usually hidden. When someone asks how to build one, the traditional answer almost always comes back using some JavaScript library. And for a long time, that was the sensible advice.</p>
<p>I followed it, too.</p>
<p>On the surface, a tooltip is simple. Hover or focus on an element, show a little box with some text, then hide it when the user moves away. But once you ship one to real users, the edges start to show. Keyboard users <code>Tab</code> into the trigger, but never see the tooltip. Screen readers announce it twice, or not at all. The tooltip flickers when you move the mouse too quickly. It overlaps content on smaller screens. Pressing <code>Esc</code> does not close it. Focus gets lost.</p>
<p>Over time, my tooltip code grew into something I didn’t really want to own anymore. Event listeners piled up. Hover and focus had to be handled separately. Outside clicks needed special cases. ARIA attributes had to be kept in sync by hand. Every small fix added another layer of logic.</p>
<p>Libraries helped, but they were also more like black boxes I worked around instead of fully understanding what was happening behind the scenes.</p>
<p>That was what pushed me to look at the newer <a href="https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute">Popover API</a>. I wanted to see what would happen if I rebuilt a single tooltip using the browser’s native model without the aid of a library.</p>
<p>As we start, it’s worth noting that, as with any new feature, there are some things with it that are still being ironed out. That said, it currently enjoys great browser support, although there are several pieces to the overall API that are in flux. It’s worth keeping an eye on <a href="https://caniuse.com/?search=popover+api">Caniuse</a> in the meantime.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="the-old-tooltip">The “Old” Tooltip</h2>
<p>Before the Popover API, using a tooltip library was not a shortcut. It was the default. Browsers didn’t have a native concept of a tooltip that worked across mouse, keyboard, and assistive technology. If you cared about correctness, your only option was to use a library, and that is exactly what I did.</p>
<p>At a high level, the pattern was always the same: a trigger element, a hidden tooltip element, and JavaScript to coordinate the two.</p>
<div class="break-out">
<pre><code class="language-html">&lt;button class="info"&gt;?&lt;/button&gt;
&lt;div class="tooltip" role="tooltip"&gt;Helpful text&lt;/div&gt;
</code></pre>
</div>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png"></p>
<p>    <img loading="lazy" width="800" height="257" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="The old approach with ~60 lines of JavaScript with five event listeners vs the new approach is about 10 lines of declarative HTML with zero event listeners." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      The old approach required ~60 lines of JavaScript with five event listeners and manual state management. The new approach is about 10 lines of declarative HTML with zero event listeners. (<a href="https://files.smashing.media/articles/getting-started-popover-api/1-popover-api.png">Large preview</a>)<br />
    </figcaption></figure>
<p>The library handled the wiring that allowed the element to show on hover or focus, hide on blur or mouse leave, and reposition/resize on scroll.</p>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div>
</figure>
<p>None of it was accidental. It was merely compensating for gaps in web platform features.</p>
<h2 id="why-i-used-a-library">Why I Used A Library</h2>
<p>The library was doing real work for me: positioning, flipping at viewport edges, event coordination across input types, and scroll awareness inside complex layouts. Positioning alone justified the dependency. Handling scroll containers, transforms, and responsive layouts correctly is not simple.</p>
<p>The real issues showed up in <strong>accessibility behavior</strong>, not visuals. The tooltip worked, but not all the time. Here’s where things started to fray at the seams:</p>
<ul>
<li>Tooltips sometimes appeared late or not at all.</li>
<li>Tabbing quickly could skip them entirely.</li>
<li>Escape dismissal was not reliable.</li>
</ul>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>Keyboard navigation with the old implementation: Tabbing quickly causes tooltips to be skipped entirely, and Escape dismissal is unreliable.</figcaption></figure>
<p>I also ran into issues trying to sync hover and focus behavior:</p>
<ul>
<li>Mouse users expect immediacy.</li>
<li>Keyboard users expect predictability.</li>
<li>Supporting both meant delays and edge cases.</li>
</ul>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>This timing mismatch creates an inconsistent experience across input methods.</figcaption></figure>
<p>Not to mention, there were issues with <strong>assistive technologies</strong>, particularly screen readers: Sometimes the tooltip was announced, sometimes it wasn’t, and sometimes it was announced twice.</p>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>Screen reader behavior with custom tooltips.</figcaption></figure>
<p>Keeping ARIA attributes in sync required manual updates. Miss one state change, and the tooltip became confusing or invisible to the accessibility tree.</p>
<h2 id="this-was-not-bad-code">This Was Not Bad Code</h2>
<p>The implementation was tested, the library was solid, and the behavior was reasonable given the tools available at the time.</p>
<blockquote><p>The core problem was not the code. It was that the web platform lacked proper affordances.</p></blockquote>
<p>For example, the browser has no real way of knowing that the element was a tooltip. Everything was built from conventions: generic elements, event listeners, manually-managed ARIA, and custom dismissal logic.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png"></p>
<p>    <img loading="lazy" width="800" height="436" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Event flow: before and after Popover API." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Before: A tangled web of event listeners, state management, and manual ARIA updates. After: The browser understands the relationship declaratively. (<a href="https://files.smashing.media/articles/getting-started-popover-api/2-before-after-popover-api.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Over time, the tooltip could become fragile. Small changes carried risk. Minor fixes caused regressions. Worse, adding new tooltips inherited the same complexity. Things technically worked, but never felt settled or complete.</p>
<p>That was the state of things when I decided to rebuild the tooltip using the browser’s native <a href="https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute">Popover API</a>.</p>
<div class="partners__lead-place"></div>
<h2 id="the-moment-i-tried-the-popover-api">The Moment I Tried The Popover API</h2>
<p>I didn’t switch to using the Popover API because I wanted to experiment with something new. I switched because I was tired of maintaining tooltip behavior that I believed the browser should have already understood.</p>
<p>I was skeptical at first. Most new web APIs promise simplicity, but still require glue, edge-case handling, or fallback logic that quietly recreates the same complexity that you were trying to escape.</p>
<p>So, I tried the Popover API in the smallest way possible. Here’s what that looked like:</p>
<div class="break-out">
<pre><code class="language-html">&lt;!-- popovertarget creates the connection to id="tip-1" --&gt;
&lt;button popovertarget="tip-1"&gt;?&lt;/button&gt;

&lt;!-- popover="manual": browser manages this as a popover --&gt;
&lt;!-- role="tooltip": tells assistive technology what this is --&gt;
&lt;div id="tip-1" popover="manual" role="tooltip"&gt;
  This button triggers a helpful tip.
&lt;/div&gt;
</code></pre>
</div>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>The complete tooltip implementation using the Popover API</figcaption></figure>
<p>No event listeners. No state tracking. No ARIA updates handled in JavaScript. I focused the button, and the tooltip appeared. I pressed the <code>Esc</code> key, and it disappeared.</p>
<h2 id="what-immediately-stood-out">What Immediately Stood Out</h2>
<p>A few things became obvious within minutes:</p>
<h3 id="i-didn-t-write-any-javascript-to-open-or-close-it">I Didn’t Write Any JavaScript To Open Or Close It</h3>
<p>The browser handled invocation entirely through HTML. The relationship between trigger and tooltip was explicit.</p>
<h3 id="the-esc-key-just-worked">The <code>Esc</code> Key Just Worked</h3>
<p>I didn’t add a key listener. Pressing the <code>Esc</code> key properly closed the tooltip because the browser understands that popovers should be dismissible.</p>
<h3 id="aria-state-automatically-synced">ARIA State Automatically Synced</h3>
<p>The <code>aria-expanded</code> attribute updated on its own when the popover opened and closed. There was no need for manual bookkeeping and no risk of stale state.</p>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>The browser’s DevTools showing <code>aria-expanded</code> automatically updating from <code>false</code> to <code>true</code> as the popover opens.</figcaption></figure>
<p>This was the moment that the Popover API stopped feeling like a convenience and more like true bona fide platform behavior.</p>
<p>What surprised me most was not the reduced code but the <strong>change in responsibility</strong>. Before, the tooltip existed because my JavaScript said so. Now, it exists because the browser understands what it is supposed to be and its role in the markup. The tooltip is no longer simply a box positioned near a button anymore, but participating in the browser’s focus model, the accessibility tree, and native dismissal rules.</p>
<p>That’s when my migration to the Popover API started.</p>
<h3 id="understanding-invoker-commands">Understanding Invoker Commands</h3>
<p>The <code>popovertarget</code> and <code>popovertargetaction</code> attributes are part of HTML’s invoker commands, a declarative way to control interactive elements without JavaScript.</p>
<ul>
<li><code>popovertarget=&quot;id&quot;</code>: Connects the button to a popover element.</li>
<li><code>popovertargetaction</code>: Specifies what should happen:
<ul>
<li><code>show</code>: Only opens the popover.</li>
<li><code>hide</code>: Only closes the popover.</li>
<li><code>toggle</code>(default): Opens the popover if closed and closes it if it’s open.</li>
</ul>
</li>
</ul>
<p>This means you can have multiple triggers for the same tooltip:</p>
<div class="break-out">
<pre><code class="language-html">&lt;button popovertarget="help-tip" popovertargetaction="show"&gt;
  Show Help
&lt;/button&gt;

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

&lt;div id="help-tip" popover="manual" role="tooltip"&gt;
  Help content
&lt;/div&gt;
</code></pre>
</div>
<p>The browser coordinates everything with no JavaScript needed for the basic interaction.</p>
<h2 id="free-accessibility-wins">Free Accessibility Wins</h2>
<p>This is the part that made me switch completely. I expected the Popover API to reduce code. I didn’t expect it to remove entire categories of accessibility bugs I had been chasing for years. Before the migration, my tooltip system looked fine at the very least. Keyboard support existed, ARIA attributes were present, and screen readers usually behaved accordingly. But “usually” did a lot of heavy lifting.</p>
<p>Once I swapped in native popovers, three things changed immediately.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png"></p>
<p>    <img loading="lazy" width="800" height="436" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Accessibility tree comparison: Custom vs Native Popover API." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Custom implementations use fragile JavaScript to connect triggers and tooltips. The Popover API creates a native browser connection that assistive technology can trust. (<a href="https://files.smashing.media/articles/getting-started-popover-api/3-accessibility-tree-comparison.png">Large preview</a>)<br />
    </figcaption></figure>
<h3 id="1-the-keyboard-just-works">1. The Keyboard “Just Works”</h3>
<p>Keyboard support depended on multiple layers lining up correctly: focus had to trigger the tooltip, blur had to hide it, <code>Esc</code> had to be wired manually, and timing mattered. If you missed one edge case, the tooltip would either stay open too long or disappear before it could be read.</p>
<p>With the <code>popover</code> attribute set to <code>auto</code> or <code>manual</code>, the browser takes over the basics: <code>Tab</code> and <code>Shift</code>+<code>Tab</code> behave normally, <code>Esc</code> closes the tooltip every time, and no extra listeners are required.</p>
<pre><code class="language-html">&lt;div popover="manual"&gt;
  Helpful explanation
&lt;/div&gt;
</code></pre>
<p>What disappeared from my codebase were global keydown handlers, <code>Esc</code>-specific cleanup logic, and state checks during keyboard navigation. The keyboard experience stopped being something I had to maintain, and it became a browser guarantee.</p>
<h3 id="2-screenreader-predictability">2. Screenreader Predictability</h3>
<p>This was the biggest improvement. Even with careful ARIA work, the behavior varied, as I outlined earlier. Every small change felt risky. Using a popover with a proper role looks and feels a lot more stable and predictable as far as what’s going to happen:</p>
<pre><code class="language-html">&lt;div popover="manual" role="tooltip"&gt;
  Helpful explanation
&lt;/div&gt;
</code></pre>
<p>And here’s another win: After the switch, <a href="https://www.smashingmagazine.com/2024/11/why-optimizing-lighthouse-score-not-enough-fast-website/">Lighthouse</a> stopped flagging incorrect ARIA state warnings for the interaction, largely because there are no longer custom ARIA states for me to accidentally get wrong.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png"></p>
<p>    <img loading="lazy" width="800" height="436" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="ARIA state warnings before migration, and 100% audit score after switching to the Popover API." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Before the migration, Lighthouse flagged accessibility warnings about incorrect ARIA state management. After switching to the Popover API, the audit score improved. (<a href="https://files.smashing.media/articles/getting-started-popover-api/4-manual-aria-popover-api.png">Large preview</a>)<br />
    </figcaption></figure>
<h3 id="3-focus-management">3. Focus Management</h3>
<p>Focus used to be fragile. Before, I had rules like: let focus trigger show tooltip, move focus into tooltip and don’t close, blur trigger when it’s too close, and close tooltip and restore focus manually. This worked until it didn’t.</p>
<p>With the Popover API, the browser enforces a simpler model where focus can more naturally move into the popover. Closing the popover returns focus to the trigger, and there are no invisible focus traps or lost focus moments. And I didn’t add focus restoration code; I removed it.</p>
<figure class="video-embed-container break-out">
<div class="video-embed-container--wrapper"></div><figcaption>Tab to focus the trigger, the tooltip appears, press <code>Escape</code> to dismiss, and focus automatically returns to the trigger.</figcaption></figure>
<h2 id="where-the-popover-api-maybe-still-isn-t-enough">Where The Popover API Maybe Still Isn’t Enough</h2>
<p>As much as the Popover API has simplified my code and improved semantics, it still has not completely eliminated JavaScript. That’s not totally a bad thing because what’s changed is that JavaScript is no longer a key dependency. I am no longer compensating for missing platform behavior anymore. I am much more focused on <em>intent</em>.</p>
<p>Here are a few places where I could see the API continue to improve.</p>
<h3 id="tooltip-timing-still-matters">Tooltip Timing Still Matters</h3>
<p>Native popovers open and close immediately. That is usually the expected behavior, but not always ideal for what we consider to be tooltips. In those cases, instant dismissal can feel unstable when you move your mouse a few pixels too quickly or accidentally brush past the trigger &mdash; the tooltip will flash, then disappear, which can be jarring.</p>
<p>I want to be able to control that timing and apply delays between hover or focus and opening the tooltip. So I still add small delays. What changed was how much of the interaction logic I actually needed to own. Before, even basic open and close behavior required JavaScript. With the Popover API, and especially with HTML invoker commands, that responsibility shifts back to the browser.</p>
<pre><code class="language-html">&lt;button
  popovertarget="help-tip"
  popovertargetaction="show"&gt;
  ?
&lt;/button&gt;

&lt;div id="help-tip" popover="manual" role="tooltip"&gt;
  This button triggers a helpful tip.
&lt;/div&gt;
</code></pre>
<p>At this point, the browser handles invocation, dismissal, and ARIA state on its own. There’s no JavaScript involved just to make the tooltip appear or disappear.</p>
<p>JavaScript only comes back in when I want intentional behavior. In this case, a short delay before hiding the tooltip, and cancelling if the pointer moves into it. This isn’t about accessibility fixes. It’s about human behavior.</p>
<p>It’s worth noting that CSS is beginning to explore this space as well. The emerging interest/invoker work introduces <a href="https://css-tricks.com/a-first-look-at-the-interest-invoker-api-for-hover-triggered-popovers/#aa-interest-delay-and-the-css-of-it-all">ways to express entry and exit delays directly in CSS</a>, which could remove this small bit of JavaScript entirely. For now, I still handle it imperatively, but the direction of the platform is clear.</p>
<pre><code class="language-javascript">let hideTimeout;

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

const hide = () =&gt; {
  hideTimeout = setTimeout(() =&gt; {
    tooltip.hidePopover();
  }, 200);
};
</code></pre>
<p>The difference is that this logic stays small and local. It no longer defines how the tooltip works. It simply refines how it feels.</p>
<h3 id="hover-intent-with-invoker-commands">Hover Intent With Invoker Commands</h3>
<p>The browser does not know why someone hovers over an element or focuses on it. Was it intentional, or was the pointer just passing through? That part has always required some judgment.</p>
<p>What changed is where that logic lives. With invoker commands handling the core open and close behavior, JavaScript no longer owns the interaction model. It only adds intent on top of it.</p>
<pre><code class="language-html">&lt;button
 popovertarget="help-tip"
 popovertargetaction="show"&gt;
  ?
&lt;/button&gt;
</code></pre>
<p>The platform manages invocation, dismissal, and ARIA state. JavaScript is only needed when we want behavior that the browser cannot infer, such as a short delay before hiding or cancelling dismissal if the pointer moves into the tooltip.</p>
<pre><code class="language-javascript">let hideTimeout;

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

const hide = () =&gt; {
  hideTimeout = setTimeout(() =&gt; {
    tooltip.hidePopover();
  }, 200);
};
</code></pre>
<p>And again, CSS is beginning to explore this space with new interaction primitives, which may reduce the need for custom hover intent code even further.</p>
<h3 id="manual-popovers-and-focus">Manual Popovers And Focus</h3>
<p>For <code>popover=&quot;manual&quot;</code>, the browser does not restore focus automatically the way it can for auto popovers. That responsibility remains explicit. When a tooltip opens on focus and closes on blur, I return focus deliberately to the trigger:</p>
<pre><code class="language-javascript">tooltip.hidePopover();
trigger.focus();
</code></pre>
<p>This is not a limitation but a clear boundary between platform behavior and person intent.</p>
<h3 id="the-honest-take">The Honest Take</h3>
<p>The Popover API does not magically solve tooltips. It stopped forcing me to rebuild fragile infrastructure. I still write JavaScript and think about edge cases, but now I am solving product problems instead of recreating UI primitives the browser should already understand.</p>
<div class="partners__lead-place"></div>
<h2 id="when-i-would-still-reach-for-a-tooltip-library">When I would Still Reach For A Tooltip Library</h2>
<p>Even after migrating my tooltips to the Popover API, I did not walk away thinking libraries were old and obsolete. They have earned their place, just in more specific situations.</p>
<h3 id="1-large-or-mature-design-systems">1. Large Or Mature Design Systems</h3>
<p>If you are maintaining a large design system used across multiple teams, a tooltip library can still make sense because centralized behavior, documented patterns, and consistent defaults across products. In those environments, changing the underlying interaction model is not just a technical decision; it is an organizational one. A well-maintained library gives teams guardrails, especially when not everyone is deeply familiar with accessibility nuances.</p>
<h3 id="2-complex-positioning-requirements">2. Complex Positioning Requirements</h3>
<p>For most tooltips, native positioning is enough, but if you need collision detection across nested scroll containers, custom flipping logic, or fine-grained control over offsets and boundaries, libraries like <a href="https://floating-ui.com">Floating UI</a> still shine. They are optimized for geometry problems that the platform is only beginning to address.</p>
<p>It is also worth mentioning <a href="https://css-tricks.com/css-anchor-positioning-guide/">CSS anchor positioning</a>, which is starting to cover many of the problems that tooltip libraries historically solved. Anchors allow a popover to be positioned relative to a trigger using pure CSS, including viewport-aware placement and edge flipping. This moves even more responsibility back to the platform instead of JavaScript.</p>
<p>That said, anchor positioning is still new and <a href="https://css-tricks.com/css-anchor-positioning-guide/#aa-known-bugs">there are known issues,</a> although the good news is that they are part of Interop, meaning <a href="https://webstatus.dev/features/popover?q=baseline_date%3A2025-01-01..2025-12-31&amp;start=25">we can look forward to full and consistent browser support</a>. For teams that need consistent cross-browser behavior today, libraries remain the practical choice. The direction is clear that the platform is steadily absorbing work that once required dedicated positioning engines.</p>
<h3 id="3-teams-without-accessibility-experience">3. Teams Without Accessibility Experience</h3>
<p>This one matters. If a team does not have strong accessibility knowledge, a good library can act as a safety net, though it will not guarantee perfect accessibility. It can, however, prevent the many common mistakes. The Popover API gives you better defaults, but it still assumes you know when to add roles, labels, focus management, and testing. Without that understanding, even native tools can be misused.</p>
<h2 id="the-decision-line">The Decision Line</h2>
<p>For me, the choice now looks like this:</p>
<blockquote class="pull-quote">
<p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aUse%20the%20Popover%20API%20for%20simplicity,%20clarity,%20and%20platform-aligned%20behavior.%20Use%20a%20library%20when%20scale,%20customization,%20or%20constraints%20demand%20it.%20It%e2%80%99s%20not%20about%20purity.%20It%e2%80%99s%20about%20choosing%20the%20right%20level%20of%20abstraction%20for%20the%20problem%20in%20front%20of%20you.%0a&amp;url=https://smashingmagazine.com%2f2026%2f03%2fgetting-started-popover-api%2f"></p>
<p>Use the Popover API for simplicity, clarity, and platform-aligned behavior. Use a library when scale, customization, or constraints demand it. It’s not about purity. It’s about choosing the right level of abstraction for the problem in front of you.</p>
<p>    </a>
  </p>
<div class="pull-quote__quotation">
<div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
</p></div>
</blockquote>
<p>And sometimes the right tool is still a library &mdash; just no longer by default.</p>
<figure class="
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png"></p>
<p>    <img loading="lazy" width="800" height="800" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/getting-started-popover-api/5-popover-api-browser-support.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="conclusion">Conclusion</h2>
<p>The Popover API means that tooltips are no longer something you simulate. They’re something the browser understands. Opening, closing, keyboard behavior, Escape handling, and a big chunk of accessibility now come from the platform itself, not from ad-hoc JavaScript.</p>
<p>That does not mean tooltip libraries are obsolete because they still make sense for complex design systems, heavy customization, or legacy constraints, but <strong>the default has shifted</strong>. For the first time, the simplest tooltip can also be the most correct one. If you are curious, try this experiment: Simply replace just one tooltip in your product with the Popover API, do not rewrite everything, do not migrate a whole system, and just pick one and see what disappears from your code.</p>
<p>When the platform gives you a better primitive, the win is not just fewer lines of JavaScript, but it is fewer things you have to worry about at all.</p>
<p>Check out the full source code in <a href="https://github.com/BboyGT/PopOver-API">my GitHub repo</a>.</p>
<h3 id="further-reading">Further Reading</h3>
<p>For deeper dives into popovers and related APIs:</p>
<ul>
<li>“<a href="https://css-tricks.com/poppin-in/">Poppin’ In</a>”, Geoff Graham</li>
<li>“<a href="https://css-tricks.com/clarifying-the-relationship-between-popovers-and-dialogs/">Clarifying the Relationship Between Popovers and Dialogs</a>”, Zell Liew</li>
<li>“<a href="https://una.im/popover-hint/">What is popover=hint?</a>”, Una Kravets</li>
<li>“<a href="https://css-tricks.com/invoker-commands-additional-ways-to-work-with-dialog-popover-and-more/">Invoker Commands</a>”, Daniel Schwarz</li>
<li>“<a href="https://css-tricks.com/creating-an-auto-closing-notification-with-an-html-popover/">Creating an Auto-Closing Notification with an HTML Popover</a>”, Preethi</li>
<li><a href="https://open-ui.org/components/popover.research.explainer/">Open UI Popover API Explainer</a></li>
<li>“<a href="https://css-tricks.com/popover-the-balloons/">Pop(over) the Balloons</a>”, John Rhea</li>
<li>“<a href="https://css-tricks.com/css-anchor-positioning-guide/">CSS Anchor Positioning</a>”, Juan Diego Rodríguez</li>
</ul>
<p>MDN also <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">offers comprehensive technical documentation</a> for the Popover API.</p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/03/02/getting-started-with-the-popover-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CSS @scope: An Alternative To Naming Conventions And Heavy Abstractions</title>
		<link>http://computercoursesonline.com/index.php/2026/02/05/css-scope-an-alternative-to-naming-conventions-and-heavy-abstractions/</link>
					<comments>http://computercoursesonline.com/index.php/2026/02/05/css-scope-an-alternative-to-naming-conventions-and-heavy-abstractions/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Thu, 05 Feb 2026 08:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1152</guid>

					<description><![CDATA[CSS &#60;code&#62;@scope&#60;/code&#62;: An Alternative To Naming Conventions And Heavy Abstractions CSS &#60;code&#62;@scope&#60;/code&#62;: An Alternative To Naming Conventions And Heavy Abstractions Blake Lundquist 2026-02-05T08:00:00+00:00 2026-02-05T20:32:14+00:00 When learning the principles of basic CSS, one is taught to write modular, reusable, and descriptive styles to ensure maintainability. But when developers become involved with real-world applications, it often feels...]]></description>
										<content:encoded><![CDATA[<p>              <title>CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions</title></p>
<article>
<header>
<h1>CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions</h1>
<address>Blake Lundquist</address>
<p>                  2026-02-05T08:00:00+00:00<br />
                  2026-02-05T20:32:14+00:00<br />
                </header>
<p>When learning the principles of basic CSS, one is taught to write modular, reusable, and descriptive styles to ensure maintainability. But when developers become involved with real-world applications, it often feels impossible to add UI features without styles leaking into unintended areas.</p>
<p>This issue often snowballs into a self-fulfilling loop; styles that are theoretically scoped to one element or class start showing up where they don’t belong. This forces the developer to create even more specific selectors to override the leaked styles, which then accidentally override global styles, and so on.</p>
<p>Rigid class name conventions, such as <a href="https://getbem.com/introduction/">BEM</a>, are one theoretical solution to this issue. The <strong>BEM (Block, Element, Modifier) methodology</strong> is a <a href="https://www.smashingmagazine.com/2012/04/a-new-front-end-methodology-bem/">systematic way of naming CSS classes</a> to ensure reusability and structure within CSS files. Naming conventions like this can <a href="https://www.smashingmagazine.com/2018/06/bem-for-beginners/">reduce cognitive load by leveraging domain language to describe elements and their state</a>, and if implemented correctly, <a href="https://www.smashingmagazine.com/2025/06/css-cascade-layers-bem-utility-classes-specificity-control/">can make styles for large applications easier to maintain</a>.</p>
<p>In the real world, however, it doesn’t always work out like that. Priorities can change, and with change, implementation becomes inconsistent. Small changes to the HTML structure can require many CSS class name revisions. With highly interactive front-end applications, class names following the BEM pattern can become long and unwieldy (e.g., <code>app-user-overview__status--is-authenticating</code>), and not fully adhering to the naming rules breaks the system’s structure, thereby negating its benefits.</p>
<p>Given these challenges, it’s no wonder that developers have turned to frameworks, Tailwind being <a href="https://2024.stateofcss.com/en-US/tools/">the most popular CSS framework</a>. Rather than trying to fight what seems like an unwinnable specificity war between styles, it is easier to give up on the <a href="https://css-tricks.com/the-c-in-css-the-cascade/">CSS Cascade</a> and use tools that guarantee complete isolation.</p>
<h2 id="developers-lean-more-on-utilities">Developers Lean More On Utilities</h2>
<p>How do we know that some developers are keen on avoiding cascaded styles? It’s the rise of “modern” front-end tooling &mdash; like <a href="https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx/">CSS-in-JS frameworks</a> &mdash; designed specifically for that purpose. Working with isolated styles that are tightly scoped to specific components can seem like a breath of fresh air. It removes the need to name things &mdash; <a href="https://24ways.org/2014/naming-things/">still one of the most hated and time-consuming front-end tasks</a> &mdash; and allows developers to be productive without fully understanding or leveraging the benefits of CSS inheritance.</p>
<p>But ditching the CSS Cascade comes with its own problems. For instance, composing styles in JavaScript requires heavy build configurations and often leads to styles awkwardly intermingling with component markup or HTML. Instead of carefully considered naming conventions, we allow build tools to autogenerate selectors and identifiers for us (e.g., <code>.jsx-3130221066</code>), requiring developers to keep up with yet another pseudo-language in and of itself. (As if the cognitive load of understanding what all your component’s <code>useEffect</code>s do weren’t already enough!)</p>
<p>Further abstracting the job of naming classes to tooling means that basic debugging is often constrained to specific application versions compiled for development, rather than leveraging native browser features that support live debugging, such as Developer Tools.</p>
<blockquote class="pull-quote">
<p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIt%e2%80%99s%20almost%20like%20we%20need%20to%20develop%20tools%20to%20debug%20the%20tools%20we%e2%80%99re%20using%20to%20abstract%20what%20the%20web%20already%20provides%20%e2%80%94%20all%20for%20the%20sake%20of%20running%20away%20from%20the%20%e2%80%9cpain%e2%80%9d%20of%20writing%20standard%20CSS.%0a&amp;url=https://smashingmagazine.com%2f2026%2f02%2fcss-scope-alternative-naming-conventions%2f"></p>
<p>It’s almost like we need to develop tools to debug the tools we’re using to abstract what the web already provides — all for the sake of running away from the “pain” of writing standard CSS.</p>
<p>    </a>
  </p>
<div class="pull-quote__quotation">
<div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
</p></div>
</blockquote>
<p>Luckily, modern CSS features not only make writing standard CSS more flexible but also give developers like us a great deal more power to manage the cascade and make it work for us. <a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/">CSS Cascade Layers</a> are a great example, but there’s another feature that gets a surprising lack of attention &mdash; although that is changing now that it has recently become <strong>Baseline compatible</strong>.</p>
<h2 id="the-css-scope-at-rule">The CSS <code>@scope</code> At-Rule</h2>
<p>I consider the <strong>CSS <code>@scope</code> at-rule</strong> to be a potential cure for the sort of style-leak-induced anxiety we’ve covered, one that does not force us to compromise native web advantages for abstractions and extra build tooling.</p>
<blockquote><p>“The <code>@scope</code> CSS at-rule enables you to select elements in specific DOM subtrees, targeting elements precisely without writing overly-specific selectors that are hard to override, and without coupling your selectors too tightly to the DOM structure.”</p>
<p>&mdash; <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope">MDN</a></p></blockquote>
<p>In other words, we can work with isolated styles in specific instances <strong>without sacrificing inheritance, cascading, or even the basic separation of concerns</strong> that has been a long-running guiding principle of front-end development.</p>
<p>Plus, it has <a href="https://caniuse.com/css-cascade-scope">excellent browser coverage</a>. In fact, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/146">Firefox 146</a> added support for <code>@scope</code> in December, making it <a href="https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility">Baseline compatible</a> for the first time. Here is a simple comparison between a button using the BEM pattern versus the <code>@scope</code> rule:</p>
<pre><code class="language-html">&lt;!-- BEM --&gt; 
&lt;button class="button button--primary"&gt;
  &lt;span class="button&#095;&#095;text"&gt;Click me&lt;/span&gt;
  &lt;span class="button&#095;&#095;icon"&gt;→&lt;/span&gt;
&lt;/button&gt;

&lt;style&gt;
  .button .button&#095;&#095;text { /&#042; button text styles &#042;/ }
  .button .button&#095;&#095;icon { /&#042; button icon styles &#042;/ }
  .button--primary { primary button styles &#042;/ }
&lt;/style&gt;
</code></pre>
<pre><code class="language-html">&lt;!-- @scope --&gt; 
&lt;button class="primary-button"&gt;
  &lt;span&gt;Click me&lt;/span&gt;
  &lt;span&gt;→&lt;/span&gt;
&lt;/button&gt;

&lt;style&gt;
  @scope (.primary-button) {
    span:first-child { /&#042; button text styles &#042;/ }
    span:last-child { /&#042; button icon styles &#042;/ }
  }
&lt;/style&gt;
</code></pre>
<p>The <code>@scope</code> rule allows for <strong>precision with less complexity</strong>. The developer no longer needs to create boundaries using class names, which, in turn, allows them to write selectors based on native HTML elements, thereby eliminating the need for prescriptive CSS class name patterns. By simply removing the need for class name management, <code>@scope</code> can alleviate the fear associated with CSS in large projects.</p>
<h2 id="basic-usage">Basic Usage</h2>
<p>To get started, add the <code>@scope</code> rule to your CSS and insert a root selector to which styles will be scoped:</p>
<pre><code class="language-css">@scope (&lt;selector&gt;) {
  /&#042; Styles scoped to the &lt;selector&gt; &#042;/
}
</code></pre>
<p>So, for example, if we were to scope styles to a <code>&lt;nav&gt;</code> element, it may look something like this:</p>
<div class="break-out">
<pre><code class="language-css">@scope (nav) {
  a { /&#042; Link styles within nav scope &#042;/ }

  a:active { /&#042; Active link styles &#042;/ }

  a:active::before { /&#042; Active link with pseudo-element for extra styling &#042;/ }

  @media (max-width: 768px) {
    a { /&#042; Responsive adjustments &#042;/ }
  }
}
</code></pre>
</div>
<p>This, on its own, is not a groundbreaking feature. However, a second argument can be added to the scope to create a <strong>lower boundary</strong>, effectively defining the scope’s start and end points.</p>
<div class="break-out">
<pre><code class="language-css">/&#042; Any `a` element inside `ul` will not have the styles applied &#042;/
@scope (nav) to (ul) {
  a {
    font-size: 14px;
  }
}
</code></pre>
</div>
<p>This practice is called <strong>donut scoping</strong>, and <a href="https://css-tricks.com/solved-by-css-donuts-scopes/">there are several approaches</a> one could use, including a series of similar, highly specific selectors coupled tightly to the DOM structure, a <code>:not</code> pseudo-selector, or assigning specific class names to <code>&lt;a&gt;</code> elements within the <code>&lt;nav&gt;</code> to handle the differing CSS.</p>
<p>Regardless of those other approaches, the <code>@scope</code> method is much more concise. More importantly, it prevents the risk of broken styles if classnames change or are misused or if the HTML structure were to be modified. Now that <code>@scope</code> is Baseline compatible, we no longer need workarounds!</p>
<p>We can take this idea further with multiple end boundaries to create a “style figure eight”:</p>
<div class="break-out">
<pre><code class="language-css">/&#042; Any &lt;a&gt; or &lt;p&gt; element inside &lt;aside&gt; or &lt;nav&gt; will not have the styles applied &#042;/
@scope (main) to (aside, nav) {
  a {
    font-size: 14px;
  }
  p {
    line-height: 16px;
    color: darkgrey;
  }
}
</code></pre>
</div>
<p>Compare that to a version handled without the <code>@scope</code> rule, where the developer has to “reset” styles to their defaults:</p>
<div class="break-out">
<pre><code class="language-css">main a {
  font-size: 14px;
}

main p {
  line-height: 16px;
  color: darkgrey;
}

main aside a,
main nav a {
  font-size: inherit; /&#042; or whatever the default should be &#042;/
}

main aside p,
main nav p {
  line-height: inherit; /&#042; or whatever the default should be &#042;/
  color: inherit; /&#042; or a specific color &#042;/
}
</code></pre>
</div>
<p>Check out the following example. Do you notice how simple it is to target some nested selectors while exempting others?</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="wBWXggN" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [@scope example [forked]](https://codepen.io/smashingmag/pen/wBWXggN) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/wBWXggN">@scope example [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption></figure>
<p>Consider a scenario where unique styles need to be applied to slotted content within <a href="https://www.smashingmagazine.com/2025/07/web-components-working-with-shadow-dom/">web components</a>. When slotting content into a web component, that content becomes part of the Shadow DOM, but still inherits styles from the parent document. The developer might want to implement different styles depending on which web component the content is slotted into:</p>
<pre><code class="language-html">&lt;!-- Same &lt;user-card&gt; content, different contexts --&gt;
&lt;product-showcase&gt;
  &lt;user-card slot="reviewer"&gt;
    &lt;img src="avatar.jpg" slot="avatar"&gt;
    &lt;span slot="name"&gt;Jane Doe&lt;/span&gt;
  &lt;/user-card&gt;
&lt;/product-showcase&gt;

&lt;team-roster&gt;
  &lt;user-card slot="member"&gt;
    &lt;img src="avatar.jpg" slot="avatar"&gt;
    &lt;span slot="name"&gt;Jane Doe&lt;/span&gt;
  &lt;/user-card&gt;
&lt;/team-roster&gt;
</code></pre>
<p>In this example, the developer might want the <code>&lt;user-card&gt;</code> to have distinct styles only if it is rendered inside <code>&lt;team-roster&gt;</code>:</p>
<pre><code class="language-css">@scope (team-roster) {
  user-card {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }
  
  user-card img {
    border-radius: 50%;
    width: 40px;
    height: 40px;
  }
}
</code></pre>
<h2 id="more-benefits">More Benefits</h2>
<p>There are additional ways that <code>@scope</code> can remove the need for class management without resorting to utilities or JavaScript-generated class names. For example, <code>@scope</code> opens up the possibility to easily <strong>target descendants of any selector</strong>, not just class names:</p>
<div class="break-out">
<pre><code class="language-css">/&#042; Only div elements with a direct child button are included in the root scope &#042;/
@scope (div:has(&gt; button)) {
  p {
    font-size: 14px;
  }
}
</code></pre>
</div>
<p>And they <strong>can be nested</strong>, creating scopes within scopes:</p>
<pre><code class="language-css">@scope (main) {
  p {
    font-size: 16px;
    color: black;
  }
  @scope (section) {
    p {
      font-size: 14px;
      color: blue;
    }
    @scope (.highlight) {
      p {
        background-color: yellow;
        font-weight: bold;
      }
    }
  }
}
</code></pre>
<p>Plus, the root scope can be easily referenced within the <code>@scope</code> rule:</p>
<div class="break-out">
<pre><code class="language-css">/&#042; Applies to elements inside direct child `section` elements of `main`, but stops at any direct `aside` that is a direct chiled of those sections &#042;/
@scope (main &gt; section) to (:scope &gt; aside) {
  p {
    background-color: lightblue;
    color: blue;
  }
  /&#042; Applies to ul elements that are immediate siblings of root scope  &#042;/
  :scope + ul {
    list-style: none;
  }
}
</code></pre>
</div>
<p>The <code>@scope</code> at-rule also introduces a new <strong>proximity</strong> dimension to CSS specificity resolution. In traditional CSS, when two selectors match the same element, the selector with the higher specificity wins. With <code>@scope</code>, when two elements have equal specificity, the one whose scope root is closer to the matched element wins. This eliminates the need to override parent styles by manually increasing an element’s specificity, since inner components naturally supersede outer element styles.</p>
<div class="break-out">
<pre><code class="language-html">&lt;style&gt;
  @scope (.container) {
    .title { color: green; } 
  }
  &lt;!-- The &lt;h2&gt; is closer to .container than to .sidebar so "color: green" wins. --&gt;
  @scope (.sidebar) {
    .title { color: red; }
  }
&lt;/style&gt;

&lt;div class="sidebar"&gt;
  &lt;div class="container"&gt;
    &lt;h2 class="title"&gt;Hello&lt;/h2&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>Utility-first CSS frameworks, such as Tailwind, work well for prototyping and smaller projects. Their benefits quickly diminish, however, when used in larger projects involving more than a couple of developers.</p>
<p>Front-end development has become increasingly overcomplicated in the last few years, and CSS is no exception. While the <code>@scope</code> rule isn’t a cure-all, it can reduce the need for complex tooling. When used in place of, or alongside strategic class naming, <code>@scope</code> can make it easier and more fun to write maintainable CSS.</p>
<h3 id="further-reading">Further Reading</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope">CSS <code>@scope</code></a> (MDN)</li>
<li>“<a href="https://css-tricks.com/almanac/rules/s/scope/">CSS <code>@scope</code></a>”, Juan Diego Rodríguez (CSS-Tricks)</li>
<li><a href="https://www.firefox.com/en-US/firefox/146.0/releasenotes/">Firefox 146 Release Notes</a> (Firefox)</li>
<li><a href="https://caniuse.com/css-cascade-scope">Browser Support</a> (CanIUse)</li>
<li><a href="https://2024.stateofcss.com/en-US/tools/">Popular CSS Frameworks</a> (State of CSS 2024)</li>
<li>“<a href="https://css-tricks.com/the-c-in-css-the-cascade/">The “C” in CSS: Cascade</a>”, Thomas Yip (CSS-Tricks)</li>
<li><a href="https://getbem.com/introduction/">BEM Introduction</a> (Get BEM)</li>
</ul>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/02/05/css-scope-an-alternative-to-naming-conventions-and-heavy-abstractions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Unstacking CSS Stacking Contexts</title>
		<link>http://computercoursesonline.com/index.php/2026/01/27/unstacking-css-stacking-contexts/</link>
					<comments>http://computercoursesonline.com/index.php/2026/01/27/unstacking-css-stacking-contexts/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Tue, 27 Jan 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1150</guid>

					<description><![CDATA[Unstacking CSS Stacking Contexts Unstacking CSS Stacking Contexts Gabriel Shoyombo 2026-01-27T10:00:00+00:00 2026-01-29T20:32:35+00:00 Have you ever set z-index: 99999 on an element in your CSS, and it doesn’t come out on top of other elements? A value that large should easily place that element visually on top of anything else, assuming all the different elements are...]]></description>
										<content:encoded><![CDATA[<p>              <title>Unstacking CSS Stacking Contexts</title></p>
<article>
<header>
<h1>Unstacking CSS Stacking Contexts</h1>
<address>Gabriel Shoyombo</address>
<p>                  2026-01-27T10:00:00+00:00<br />
                  2026-01-29T20:32:35+00:00<br />
                </header>
<p>Have you ever set <code>z-index: 99999</code> on an element in your CSS, and it doesn’t come out on top of other elements? A value that large should easily place that element visually on top of anything else, assuming all the different elements are set at either a lower value or not set at all.</p>
<p>A webpage is usually represented in a two-dimensional space; however, by applying specific CSS properties, an imaginary z-axis plane is introduced to convey depth. This plane is perpendicular to the screen, and from it, the user perceives the order of elements, one on top of the other. The idea behind the imaginary z-axis, the user’s perception of stacked elements, is that the CSS properties that create it combine to form what we call a <strong>stacking context</strong>.</p>
<p>We’re going to talk about how elements are “stacked” on a webpage, what controls the stacking order, and practical approaches to “unstack” elements when needed.</p>
<h2 id="about-stacking-contexts">About Stacking Contexts</h2>
<p>Imagine your webpage as a desk. As you add HTML elements, you’re laying pieces of paper, one after the other, on the desk. The last piece of paper placed is equivalent to the most recently added HTML element, and it sits on top of all the other papers placed before it. This is the normal document flow, even for nested elements. The desk itself represents the root stacking context, formed by the <code>&lt;html&gt;</code> element, which contains all other folders.</p>
<p>Now, specific CSS properties come into play.</p>
<p>Properties like <code>position</code> (with <code>z-index</code>), <code>opacity</code>, <code>transform</code>, and <code>contain</code>) act like a folder. This folder takes an element and all of its children, extracts them from the main stack, and groups them into a separate sub-stack, creating what we call a <strong>stacking context</strong>. For positioned elements, this happens when we declare a <code>z-index</code> value other than <code>auto</code>. For properties like <code>opacity</code>, <code>transform</code>, and <code>filter</code>, the stacking context is created automatically when specific values are applied.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png"></p>
<p>    <img loading="lazy" width="800" height="436" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Before (global stacking order) and after (stacking context order)" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      When the browser decides what goes on top, it stacks the folders first, not the individual papers inside them. This is “The Golden Rule” of stacking contexts that many developers miss. (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/1-stacking-context-order.png">Large preview</a>)<br />
    </figcaption></figure>
<blockquote><p>Try to understand this: Once a piece of paper (i.e., a child element) is inside a folder (i.e., the parent’s stacking context), it can never exit that folder or be placed between papers in a different folder. Its <code>z-index</code> is now only relevant inside its own folder.</p></blockquote>
<p>In the illustration below, Paper B is now within the stacking context of Folder B, and can only be ordered with other papers in the folder.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png"></p>
<p>    <img loading="lazy" width="800" height="436" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Before (global stacking order) and after (stacking context order)" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/2-stacking-contexts.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Imagine, if you will, that you have two folders on your desk:</p>
<pre><code class="language-html">&lt;div class="folder-a"&gt;Folder A&lt;/div&gt;
&lt;div class="folder-b"&gt;Folder B&lt;/div&gt;
</code></pre>
<pre><code class="language-css">.folder-a { z-index: 1; }
.folder-b { z-index: 2; }
</code></pre>
<p>Let’s update the markup a bit. Inside Folder A is a special page, <code>z-index: 9999</code>. Inside Folder B is a plain page, <code>z-index: 5</code>.</p>
<pre><code class="language-html">&lt;div class="folder-a"&gt;
   &lt;div class="special-page"&gt;Special Page&lt;/div&gt;
&lt;/div&gt;

&lt;div class="folder-b"&gt;
  &lt;div class="plain-page"&gt;Plain Page&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-css">.special-page { z-index: 9999; }
.plain-page { z-index: 5; }
</code></pre>
<p>Which page is on top?</p>
<p>It’s the <code>.plain-page</code> in Folder B. The browser ignores the child papers and stacks the two folders first. It sees Folder B (<code>z-index: 2</code>) and places it on top of Folder A (<code>z-index: 1</code>) because we know that two is greater than one. Meanwhile, the <code>.special-page</code> set to <code>z-index: 9999</code> page is at the bottom of the stack even though its <code>z-index</code> is set to the highest possible value.</p>
<p>Stacking contexts can also be nested (folders inside folders), creating a “family tree.” The same principle applies: a child can never escape its parents’ folder.</p>
<p>Now that you get how stacking contexts behave like folders that group and reorder layers, it’s worth asking: why do certain properties &mdash; like <code>transform</code> and <code>opacity</code> &mdash; create new stacking contexts?</p>
<p>Here’s the thing: these properties don’t create stacking contexts because of how they look; they do it because of how the browser works under the hood. When you apply <code>transform</code>, <code>opacity</code>, <code>filter</code>, or <code>perspective</code>, you’re telling the browser, <em>“Hey, this element might move, rotate, or fade, so be ready!”</em></p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png"></p>
<p>    <img loading="lazy" width="800" height="533" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram illustrating the main document layout with an applied transform that creates a new stacking context, which in turn, runs on the GPU to handle the transformation. It indicates that a new stacking context is promoted for performance." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/3-diagram-stacking-context.png">Large preview</a>)<br />
    </figcaption></figure>
<p>When you use these properties, the browser creates a new stacking context to manage rendering more efficiently. This allows the browser to handle animations, transforms, and visual effects independently, reducing the need to recalculate how these elements interact with the rest of the page. Think of it as the browser saying, <em>“I’ll handle this folder separately so I don’t have to reshuffle the entire desk every time something inside it changes.”</em></p>
<p>But there’s a side effect. Once the browser lifts an element into its own layer, it must “flatten” everything within it, creating a new stacking context. It’s like taking a folder off the desk to handle it separately; everything inside that folder gets grouped, and the browser now treats it as a single unit when deciding what sits on top of what.</p>
<p>So even though the <code>transform</code> and <code>opacity</code> properties might not appear to affect the way that elements stack visually, they do, and it’s for performance optimisation. Several other CSS properties can also create stacking contexts for similar reasons. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts">MDN provides a complete list</a> if you want to dig deeper. There are quite a few, which only illustrates how easy it is to inadvertently create a stacking context without knowing it.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="the-unstacking-problem">The “Unstacking” Problem</h2>
<p>Stacking issues can arise for many reasons, but some are more common than others. Modal components are a classic pattern because they require toggling the component to “open” on a top layer above all other elements, then removing it from the top layer when it is “closed.”</p>
<p>I’m pretty confident that all of us have run into a situation where we open a modal and, for whatever reason, it doesn’t appear. It’s not that it didn’t open properly, but that it is out of view in a lower layer of the stacking context.</p>
<p>This leaves you to wonder “how come?” since you set:</p>
<div class="break-out">
<pre><code class="language-css">.overlay {
  position: fixed; /&#042; creates the stacking context &#042;/
  z-index: 1; /&#042; puts the element on a layer above everything else &#042;/
  inset: 0; 
  width: 100%; 
  height: 100vh; 
  overflow: hidden;
  background-color: &#035;00000080;
}
</code></pre>
</div>
<p>This looks correct, but if the parent element containing the modal trigger is a child element within another parent element that’s also set to <code>z-index: 1</code>, that technically places the modal in a sublayer obscured by the main folder. Let’s look at that specific scenario and a couple of other common stacking-context pitfalls. I think you’ll see not only how easy it is to inadvertently create stacking contexts, but also how to mismanage them. Also, how you return to a managed state depends on the situation.</p>
<h3 id="scenario-1-the-trapped-modal">Scenario 1: The Trapped Modal</h3>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="pvbddjd" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 1: The Trapped Modal (Problem) [forked]](https://codepen.io/smashingmag/pen/pvbddjd) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvbddjd">Scenario 1: The Trapped Modal (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<p>If you click the “Open Modal” button in the header, you’ll notice that the overlay and modal appear behind the main content. This is because the modal is a child of the header container, which has a lower stacking context order (<code>z-index: 1</code>) than the main container (<code>z-index</code> of <code>2</code>). Despite the modal overlay and the modal having <code>z-index</code> values of <code>9998</code> and <code>9999</code>, respectively, the main container with a <code>z-index: 2</code> still sits right above them.</p>
<h3 id="scenario-2-the-submerged-dropdown">Scenario 2: The Submerged Dropdown</h3>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="zxBPPvm" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 2: The Submerged Dropdown (Problem) [forked]](https://codepen.io/smashingmag/pen/zxBPPvm) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/zxBPPvm">Scenario 2: The Submerged Dropdown (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<p>Here, we have a similar issue with the first scenario. When you hover over the “services” link, the dropdown shows, but behind the main container. I intentionally set the main container’s <code>margin-top</code> to <code>20px</code> to make the dropdown visible enough for you to see it appear, but keep it just behind the main container. This is another popular issue front-end developers encounter, stemming from context stacking. While it is similar to the first scenario, there’s another approach to resolving it, which will be explored soon.</p>
<h3 id="scenario-3-the-clipped-tooltip">Scenario 3: The Clipped Tooltip</h3>
<p>Now, this is an interesting one. It’s not about which element has the higher <code>z-index</code>. It’s about <code>overflow: hidden</code> doing <a href="https://www.smashingmagazine.com/2021/04/css-overflow-issues/">what it’s designed to do</a>: preventing content from visually escaping its container, even when that content has <code>z-index: 1000</code>.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="GgqOOoo" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 3: The Clipped Tooltip (Problem) [forked]](https://codepen.io/smashingmag/pen/GgqOOoo) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GgqOOoo">Scenario 3: The Clipped Tooltip (Problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<p>Who would have thought <code>overflow: hidden</code> could stop a <code>z-index: 1000</code>? Well, it did stop it, as you can see in the Codepen above.</p>
<p>I think developers trust <code>z-index</code> so much that they expect it to pull them out of any obscurity issue, but in reality, it doesn’t work that way. Not that it isn’t powerful, it’s just that other factors determine its ability to push your element to the top.</p>
<p>Before you slap <code>z-index</code> on that element, remember that while this might get you out of the current jam, it might also throw you into a greater one <a href="https://www.matuzo.at/blog/2025/never-lose-a-z-index-battle-again">that even <code>z-index: infinity</code> won’t get you out of</a>.</p>
<p>Let’s try to understand the problem before attempting to fix it.</p>
<div class="partners__lead-place"></div>
<h2 id="identifying-the-trapped-layer">Identifying The Trapped Layer</h2>
<p>When you encounter an issue such as those listed above, it is helpful to know that the element isn’t possessed; instead, an ancestor has sinned, and the child is paying the debt. In non-spiritual English terms, the obscured element isn’t the problem; an ancestor element has created a lower-level stacking context that has led the children to be below the children of a parent with a higher-level stacking context.</p>
<p>A good way to track and find that parent is to descend into the browser’s devtools to inspect the element and make your way up, checking each parent level to see which has a property or properties that trigger a stacking context, and find out its position in the order compared to sibling elements. Let’s create a checklist to order our steps.</p>
<h3 id="your-debugging-checklist">Your Debugging Checklist</h3>
<ol>
<li><strong>Inspect the Problem Element.</strong><br />
Right-click your hidden element (the modal, the dropdown menu, the tooltip) and click “Inspect.”</li>
<li><strong>Check its Styles.</strong><br />
In the “Styles” or “Computed” pane, verify that it has the expected high <code>z-index</code> (e.g., <code>z-index: 9999;</code>).</li>
<li><strong>Climb the DOM Tree.</strong><br />
In the “Elements” panel, look at the element’s immediate parent. Click on it.</li>
<li><strong>Investigate the Parent’s Styles.</strong><br />
Look at the parent’s CSS in the “Styles” pane. You are now hunting for any property that creates a new stacking context. Look for any properties related to positioning, visual effects, and containment.</li>
<li><strong>Repeat.</strong><br />
If the immediate parent is clean, click on its parent (the grandparent of your element). Repeat Step 4. Keep climbing the DOM tree, one parent at a time, until you find the culprit.</li>
</ol>
<p>Now, let’s apply this checklist to our three scenarios.</p>
<h3 id="problem-1-the-trapped-modal">Problem 1: The Trapped Modal</h3>
<ol>
<li><strong>Inspect:</strong> We inspect the <code>.modal-content</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 9999</code>. That’s not the problem.</li>
<li><strong>Climb:</strong> We look at its parent, <code>.modal-container</code>. It has no trapping properties.</li>
<li><strong>Climb Again:</strong> We look at its parent, the <strong><code>.header</code></strong>.</li>
<li><strong>Investigate:</strong> We check the styles for <code>.header</code> and find the culprit: <code>position: absolute</code> and <code>z-index: 1</code>. This element is creating a stacking context. We’ve seen our trap! The modal’s <code>z-index: 9999</code> is “trapped” inside a <code>z-index: 1</code> folder.</li>
</ol>
<h3 id="problem-2-the-submerged-dropdown">Problem 2: The Submerged Dropdown</h3>
<ol>
<li><strong>Inspect:</strong> We inspect the <code>.dropdown-menu</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 100</code>.</li>
<li><strong>Climb:</strong> We check its parent <code>li</code>, then its parent <code>ul</code>, then its parent <strong><code>.navbar</code></strong>.</li>
<li>Investigate: We find <code>.navbar</code> has <code>position: relative</code> and <code>z-index: 1</code>. This creates Stacking Context A.</li>
<li><strong>Analyse Siblings:</strong> This isn’t the whole story. Why is it under the content? We now inspect the sibling of <code>.navbar</code>, which is <code>.content</code>. We find it has <code>position: relative</code> and <code>z-index: 2</code> (Stacking Context B). The browser is stacking the “folders”: <code>.content</code> (2) on top of <code>.navbar</code> (1). We’ve found the root cause.</li>
</ol>
<h3 id="problem-3-the-clipped-tooltip">Problem 3: The Clipped Tooltip</h3>
<ol>
<li><strong>Inspect:</strong> We inspect the <code>.tooltip</code>.</li>
<li><strong>Check Styles:</strong> We see <code>z-index: 1000</code>.</li>
<li><strong>Climb:</strong> We check its parent, <code>.tooltip-trigger</code>. It’s fine.</li>
<li><strong>Climb Again:</strong> We check its parent, the <strong><code>.card-container</code></strong>.</li>
<li><strong>Investigate:</strong> We scan its styles and find the culprit: <code>overflow: hidden</code>. This is a special type of trap. It clips any child that tries to render outside its boundaries, regardless of <code>z-index</code> values.</li>
</ol>
<h3 id="advanced-tooling">Advanced Tooling</h3>
<p>While climbing the DOM tree works, it can be slow. Here are tools that speed things up.</p>
<h4 id="devtools-3d-view">DevTools 3D View</h4>
<p>Some browsers, such as Microsoft Edge (in the “More Tools” menu) and Firefox (in the “Inspector” tab), include a “3D View” or “Layers” panel. This tool is a lifesaver. It visually explodes the webpage into its different layers, showing you exactly how the stacking contexts are grouped.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png"></p>
<p>    <img loading="lazy" width="800" height="534" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Microsoft Edge 3D Stacking Context View" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Microsoft Edge 3D Stacking Context View. (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/4-microsoft-edge-stacking-context.png">Large preview</a>)<br />
    </figcaption></figure>
<p>You can immediately see your modal trapped in a low-level layer and identify the parent.</p>
<h4 id="browser-extensions">Browser Extensions</h4>
<p>Smart developers have built extensions to help. Tools like this <a href="https://chrome.google.com/webstore/detail/z-context/jigamimbjojkdgnlldajknogfgncplbhhttps://chrome.google.com/webstore/detail/z-context/jigamimbjojkdgnlldajknogfgncplbh">“CSS Stacking Context Inspector” Chrome extension</a> add an extra <code>z-index</code> tab to your DevTools to show you information about elements that create a stacking context.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png"></p>
<p>    <img loading="lazy" width="800" height="341" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="CSS Stacking Context Inspector Chrome extension" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/5-browser-extensions.png">Large preview</a>)<br />
    </figcaption></figure>
<h4 id="ide-extensions">IDE Extensions</h4>
<p>You can even spot issues during development with an extension <a href="https://marketplace.visualstudio.com/items?itemName=mikerheault.vscode-better-css-stacking-contexts">like this one for VS Code</a>, which highlights potential stacking context issues directly in your editor.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png"></p>
<p>    <img loading="lazy" width="800" height="468" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Better CSS Stacking Contexts Extension" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Better CSS Stacking Contexts Extension. (<a href="https://files.smashing.media/articles/unstacking-css-stacking-contexts/6-ide-extensions.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="unstacking-and-regaining-control">Unstacking And Regaining Control</h2>
<p>After we’ve identified the root cause, the next step is to deal with it. There are several approaches you can take to tackle this problem, and I’ll list them in order. You can choose anyone at any level, though; no one can complain or obstruct another.</p>
<h3 id="change-the-html-structure">Change The HTML Structure</h3>
<p>This is considered the optimal fix. For you to run into a stacking context issue, you must have placed some elements in funny positions within your HTML. Restructuring the page will help you reshape the DOM and eliminate the stacking context problem. Find the problematic element and remove it from the trapping element in the HTML markup. For instance, we can solve the first scenario, “The Trapped Modal,” by moving the <code>.modal-container</code> out of the header and placing it in the <code>&lt;body&gt;</code> element by itself.</p>
<div class="break-out">
<pre><code class="language-html">&lt;header class="header"&gt;
  &lt;h2&gt;Header&lt;/h2&gt;
  &lt;button id="open-modal"&gt;Open Modal&lt;/button&gt;
  &lt;!-- Former position --&gt;
&lt;/header&gt;
&lt;main class="content"&gt;
  &lt;h1&gt;Main Content&lt;/h1&gt;
  &lt;p&gt;This content has a z-index of 2 and will still not cover the modal.&lt;/p&gt;
&lt;/main&gt;

&lt;!-- New position  --&gt;
&lt;div id="modal-container" class="modal-container"&gt;
  &lt;div class="modal-overlay"&gt;&lt;/div&gt;
  &lt;div class="modal-content"&gt;
    &lt;h3&gt;Modal Title&lt;/h3&gt;
    &lt;p&gt;Now, I'm not behind anything. I've gotten a better position as a result of DOM restructuring.&lt;/p&gt;
    &lt;button id="close-modal"&gt;Close&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
</div>
<p>When you click the “Open Modal” button, the modal is positioned in front of everything else as it’s supposed to be.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="azZVVNP" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 1: The Trapped Modal (Solution) [forked]](https://codepen.io/smashingmag/pen/azZVVNP) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/azZVVNP">Scenario 1: The Trapped Modal (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<h3 id="adjust-the-parent-stacking-context-in-css">Adjust The Parent Stacking Context In CSS</h3>
<p>What if the element is one you can’t move without breaking the layout? It’s better to address the issue: <strong>the parent establishes the context.</strong> Find the CSS property (or properties) responsible for triggering the context and remove it. If it has a purpose and cannot be removed, give the parent a higher <code>z-index</code> value than its sibling elements to lift the entire container. With a higher <code>z-index</code> value, the parent container moves to the top, and its children appear closer to the user.</p>
<p>Based on what we learned in “<a href="-The-Submerged-Dropd">The Submerged Dropdown</a>” scenario, we can’t move the dropdown out of the navbar; it wouldn’t make sense. However, we can increase the <code>z-index</code> value of the <code>.navbar</code> container to be greater than the <code>.content</code> element’s <code>z-index</code> value.</p>
<pre><code class="language-css">.navbar {
  background: &#035;333;
  /&#042; z-index: 1; &#042;/
  z-index: 3;
  position: relative;
}
</code></pre>
<p>With this change, the <code>.dropdown-menu</code> now appears in front of the content without any issue.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="YPWEEWz" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 2: The Submerged Dropdown (Solution) [forked]](https://codepen.io/smashingmag/pen/YPWEEWz) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YPWEEWz">Scenario 2: The Submerged Dropdown (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<h3 id="try-portals-if-using-a-framework">Try Portals, If Using A Framework</h3>
<p>In frameworks like <a href="https://react.dev/reference/react-dom/createPortal">React</a> or <a href="https://www.digitalocean.com/community/tutorials/vuejs-portal-vue">Vue</a>, a Portal is a feature that lets you render a component outside its normal parent hierarchy in the DOM. Portals are like a teleportation device for your components. They let you render a component’s HTML anywhere in the document (typically right into <code>document.body</code>) while keeping it logically connected to its original parent for props, state, and events. This is perfect for escaping stacking context traps since the rendered output literally appears outside the problematic parent container.</p>
<pre><code class="language-javascript">ReactDOM.createPortal(
  &lt;ToolTip /&gt;,
  document.body
);
</code></pre>
<p>This ensures your dropdown content isn’t hidden behind its parent, even if the parent has <code>overflow: hidden</code> or a lower <code>z-index</code>.</p>
<p>In the “The Clipped Tooltip” scenario we looked at earlier, I used a Portal to rescue the tooltip from the <code>overflow: hidden</code> clip by placing it in the document body and positioning it above the trigger within the container.</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="myEqqEe" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scenario 3: The Clipped Tooltip (Solution) [forked]](https://codepen.io/smashingmag/pen/myEqqEe) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myEqqEe">Scenario 3: The Clipped Tooltip (Solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<div class="partners__lead-place"></div>
<h2 id="introducing-stacking-context-without-side-effects">Introducing Stacking Context Without Side Effects</h2>
<p>All the approaches explained in the previous section are aimed at “unstacking” elements from problematic stacking contexts, but there are some situations where you’ll actually need or want to create a stacking context.</p>
<p>Creating a new stacking context is easy, but all approaches come with a side effect. That is, except for using <a href="https://css-tricks.com/almanac/properties/i/isolation/"><code>isolation: isolate</code></a>. When applied to an element, the stacking context of that element’s children is determined relative to each child and within that context, rather than being influenced by elements outside of it. A classic example is assigning that element a negative value, such as <code>z-index: -1</code>.</p>
<p>Imagine you have a <code>.card</code> component. You want to add a decorative shape that sits behind the <code>.card</code>’s text, but on top of the card’s background. Without a stacking context on the card, <code>z-index: -1</code> sends the shape to the bottom of the root stacking context (the whole page). This makes it disappear behind the <code>.card</code>’s white background:</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="QwEOOEM" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Negative z-index (problem) [forked]](https://codepen.io/smashingmag/pen/QwEOOEM) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/QwEOOEM">Negative z-index (problem) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<p>To solve this, we declare <code>isolation: isolate</code> on the parent <code>.card</code>:</p>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="MYeOOeG" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Negative z-index (solution) [forked]](https://codepen.io/smashingmag/pen/MYeOOeG) by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/MYeOOeG">Negative z-index (solution) [forked]</a> by <a href="https://codepen.io/drprime01">Shoyombo Gabriel Ayomide</a>.</figcaption></figure>
<p>Now, the <code>.card</code> element itself becomes a stacking context. When its child element &mdash; the decorative shape created on the <code>:before</code> pseudo-element &mdash; has <code>z-index: -1</code>, it goes to the very bottom of the parent’s stacking context. It sits perfectly behind the text and on top of the card’s background, as intended.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Remember: the next time your <code>z-index</code> seems out of control, it’s a trapped stacking context.</p>
<h3 id="references">References</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context">Stacking context</a> (MDN)</li>
<li><a href="https://web.dev/learn/css/z-index">Z-index and stacking contexts</a> (web.dev)</li>
<li>“<a href="https://www.freecodecamp.org/news/the-css-isolation-property/">How to Create a New Stacking Context with the Isolation Property in CSS</a>”, Natalie Pina</li>
<li>“<a href="https://www.joshwcomeau.com/css/stacking-contexts/">What The Heck, z-index??</a>”, Josh Comeau</li>
</ul>
<h3 id="further-reading-on-smashingmag">Further Reading On SmashingMag</h3>
<ul>
<li>“<a href="https://www.smashingmagazine.com/2021/02/css-z-index-large-projects/">Managing CSS Z-Index In Large Projects</a>”, Steven Frieson</li>
<li>“<a href="https://www.smashingmagazine.com/2024/09/sticky-headers-full-height-elements-tricky-combination/">Sticky Headers And Full-Height Elements: A Tricky Combination</a>”, Philip Braunen</li>
<li>“<a href="https://www.smashingmagazine.com/2019/04/z-index-component-based-web-application/">Managing Z-Index In A Component-Based Web Application</a>”, Pavel Pomerantsev</li>
<li>“<a href="https://www.smashingmagazine.com/2009/09/the-z-index-css-property-a-comprehensive-look/">The Z-Index CSS Property: A Comprehensive Look</a>”, Louis Lazaris</li>
</ul>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/01/27/unstacking-css-stacking-contexts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Smashing Animations Part 8: Theming Animations Using CSS Relative Colour</title>
		<link>http://computercoursesonline.com/index.php/2026/01/14/smashing-animations-part-8-theming-animations-using-css-relative-colour/</link>
					<comments>http://computercoursesonline.com/index.php/2026/01/14/smashing-animations-part-8-theming-animations-using-css-relative-colour/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Wed, 14 Jan 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1146</guid>

					<description><![CDATA[Smashing Animations Part 8: Theming Animations Using CSS Relative Colour Smashing Animations Part 8: Theming Animations Using CSS Relative Colour Andy Clarke 2026-01-14T10:00:00+00:00 2026-01-15T23:02:56+00:00 I’ve recently refreshed the animated graphics on my website with a new theme and a group of pioneering characters, putting into practice plenty of the techniques I shared in this series....]]></description>
										<content:encoded><![CDATA[<p>              <title>Smashing Animations Part 8: Theming Animations Using CSS Relative Colour</title></p>
<article>
<header>
<h1>Smashing Animations Part 8: Theming Animations Using CSS Relative Colour</h1>
<address>Andy Clarke</address>
<p>                  2026-01-14T10:00:00+00:00<br />
                  2026-01-15T23:02:56+00:00<br />
                </header>
<p>I’ve recently refreshed the animated graphics on <a href="https://stuffandnonsense.co.uk/">my website</a> with a new theme and a group of pioneering characters, putting into practice plenty of the techniques I shared in <a href="https://www.smashingmagazine.com/author/andy-clarke/">this series</a>. A few of my animations change appearance when someone interacts with them or at different times of day.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://stuffandnonsense.co.uk/blog"></p>
<p>    <img loading="lazy" width="800" height="341" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Graphics from Andy’s website" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      View this animated SVG on <a href="https://stuffandnonsense.co.uk/blog">my website</a>. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/1-andy-website-animated-graphics.png">Large preview</a>)<br />
    </figcaption></figure>
<p>The colours in the graphic atop <a href="https://stuffandnonsense.co.uk/blog">my blog pages</a> change from morning until night every day. Then, there’s the <a href="https://stuffandnonsense.co.uk/blog/let-it-snow">snow mode</a>, which adds chilly colours and a wintery theme, courtesy of an overlay layer and a blending mode.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png"></p>
<p>    <img loading="lazy" width="800" height="359" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Snow mode applied to the town background" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Snow mode allows my pioneer town background to adapt throughout the day. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/2-snow-mode.png">Large preview</a>)<br />
    </figcaption></figure>
<p>While working on this, I started to wonder whether CSS relative colour values could give me more control while also simplifying the process.</p>
<p><strong>Note</strong>: <em>In this tutorial, I’ll focus on relative colour values and the OKLCH colour space for theming graphics and animations. If you want to dive deep into relative colour, Ahmad Shadeed created a superb <a href="https://ishadeed.com/article/css-relative-colors/">interactive guide</a>. As for colour spaces, gamuts, and OKLCH, our own Geoff Graham <a href="https://www.smashingmagazine.com/2023/08/oklch-color-spaces-gamuts-css/">wrote</a> about them.</em></p>
<div class="refs">
<ul>
<li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/"><strong>Smashing Animations Part 1</strong>: How Classic Cartoons Inspire Modern CSS</a></li>
<li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-2-css-masking-add-extra-dimension/"><strong>Smashing Animations Part 2</strong>: How CSS Masking Can Add An Extra Dimension</a></li>
<li><a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-3-smil-not-dead/"><strong>Smashing Animations Part 3</strong>: SMIL’s Not Dead Baby, SMIL’s Not Dead</a></li>
<li><a href="https://www.smashingmagazine.com/2025/06/smashing-animations-part-4-optimising-svgs/"><strong>Smashing Animations Part 4</strong>: Optimising SVGs</a></li>
<li><a href="https://www.smashingmagazine.com/2025/10/smashing-animations-part-5-building-adaptive-svgs/"><strong>Smashing Animations Part 5</strong>: Building Adaptive SVGs With <code>&lt;symbol&gt;</code>, <code>&lt;use&gt;</code>, And CSS Media Queries</a></li>
<li><a href="https://www.smashingmagazine.com/2025/11/smashing-animations-part-6-svgs-css-custom-properties/"><strong>Smashing Animations Part 6</strong>: Magnificent SVGs With <code>&lt;use&gt;</code> And CSS Custom Properties</a></li>
<li><a href="https://www.smashingmagazine.com/2025/12/smashing-animations-part-7-recreating-toon-text-css-svg/"><strong>Smashing Animations Part 7</strong>: Recreating Toon Text With CSS And SVG</a></li>
</ul>
</div>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="how-cartoon-animation-taught-me-to-reuse-everything">How Cartoon Animation Taught Me To Reuse Everything</h2>
<p>The <a href="https://en.wikipedia.org/wiki/Hanna-Barbera">Hanna-Barbera</a> animated series I grew up watching had budgets far lower than those available when William Hanna and Joseph Barbera produced <em>Tom and Jerry</em> shorts at MGM Cartoons. This meant the animators needed to develop techniques to work around their cost restrictions.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/3-yogi-bear-show.png"></p>
<p>    <img loading="lazy" width="800" height="196" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Repeated use of elements in the Yogi Bear Show" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/3-yogi-bear-show.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      The Yogi Bear Show, copyright Warner Bros. Entertainment Inc. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/3-yogi-bear-show.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Repeated use of elements was key. Backgrounds were reused whenever possible, with zooms and overlays helping construct new scenes from the same artwork. It was born of necessity, but it also encouraged thinking in terms of series rather than individual scenes.</p>
<h2 id="the-problem-with-manually-updating-colour-palettes">The problem With Manually Updating Colour Palettes</h2>
<p>Let’s get straight to my challenge. In Toon Titles like this one &mdash; based on the 1959 Yogi Bear Show episode “Lullabye-Bye Bear” &mdash; and my work generally, palettes are limited to a select few colours.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/4-yogi-bear.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Illustration of Yogi Bear asleep in a hammock tied between two thin, white trees. Andy Clarke’s Toon Titles is displayed above Yogi in cartoon-style typography." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/4-yogi-bear.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      View this on my <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html">Toon Titles website</a>. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/4-yogi-bear.png">Large preview</a>)<br />
    </figcaption></figure>
<p>I create shades and tints from what I call my “foundation” colour to expand the palette without adding more hues.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Colour palette of a foundation colour" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Colour palette with shades and tints of a foundation colour. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/5-colour-palette.png">Large preview</a>)<br />
    </figcaption></figure>
<p>In <a href="https://www.sketch.com">Sketch</a>, I work in the <a href="https://www.smashingmagazine.com/2021/07/hsl-colors-css/">HSL colour space</a>, so this process involves increasing or decreasing the lightness value of my foundation colour. Honestly, it’s not an arduous task &mdash; but choosing a different foundation colour requires creating a whole new set of shades and tints. Doing that manually, again and again, quickly becomes laborious.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Shades and tints of a different foundation colour." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Shades and tints of a different foundation colour. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/6-foundation-colour.png">Large preview</a>)<br />
    </figcaption></figure>
<p>I mentioned the HSL &mdash; <strong>H</strong> (hue), S (saturation), and <strong>L</strong> (lightness) &mdash; colour space, but that’s just one of several ways to describe colour.</p>
<p>RGB &mdash; <strong>R</strong> (red), <strong>G</strong> (green), <strong>B</strong> (blue) &mdash; is probably the most familiar, at least in its Hex form.</p>
<p>There’s also LAB &mdash; <strong>L</strong> (lightness), <strong>A</strong> (green–red), <strong>B</strong> (blue–yellow) &mdash; and the newer, but now widely supported LCH &mdash; <strong>L</strong> (lightness), <strong>C</strong> (chroma), <strong>H</strong> (hue) &mdash; model in its OKLCH form. With LCH &mdash; specifically OKLCH in CSS &mdash; I can adjust the lightness value of my foundation colour.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png"></p>
<p>    <img loading="lazy" width="800" height="334" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Lightness changes to the foundation colour." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Lightness changes to my foundation colour. Chroma and Hue remain the same. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/7-lightness-change-foundation-colour.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Or I can alter its <em>chroma</em>. LCH chroma and HSL saturation both describe the intensity or richness of a colour, but they do so in different ways. LCH gives me a wider range and more predictable blending between colours.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png"></p>
<p>    <img loading="lazy" width="800" height="334" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Chroma changes to the foundation colour" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Chroma changes to my foundation colour. Lightness and Hue remain the same. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/8-chroma-changes-foundation-colour.png">Large preview</a>)<br />
    </figcaption></figure>
<p>I can also alter the hue to create a palette of colours that share the same lightness and chroma values. In both HSL and LCH, the hue spectrum starts at red, moves through green and blue, and returns to red.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png"></p>
<p>    <img loading="lazy" width="800" height="334" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Hue changes to the foundation colour." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Hue changes to my foundation colour. Lightness and Chrome remain the same. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/9-hue-changes-foundation-colour.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="why-oklch-changed-how-i-think-about-colour">Why OKLCH Changed How I Think About Colour</h2>
<p>Browser support for the OKLCH colour space <a href="https://caniuse.com/wf-oklab">is now widespread</a>, even if design tools &mdash; including Sketch &mdash; haven’t caught up. Fortunately, that shouldn’t stop you from using OKLCH. Browsers will happily convert Hex, HSL, LAB, and RGB values into OKLCH for you. You can define a CSS custom property with a foundation colour in any space, including Hex:</p>
<pre><code class="language-css">/&#042; Foundation colour &#042;/
--foundation: &#035;5accd6;
</code></pre>
<p>Any colours derived from it will be converted into OKLCH automatically:</p>
<pre><code class="language-css">--foundation-light: oklch(from var(--foundation) [...]; }
--foundation-mid: oklch(from var(--foundation) [...]; }
--foundation-dark: oklch(from var(--foundation) [...]; }
</code></pre>
<h2 id="relative-colour-as-a-design-system">Relative Colour As A Design System</h2>
<p>Think of relative colour as saying: <em>“Take this colour, tweak it, then give me the result.”</em> There are two ways to adjust a colour: absolute changes and proportional changes. They look similar in code, but behave very differently once you start swapping foundation colours. Understanding that difference is what can turn using relative colour into a system.</p>
<pre><code class="language-css">/&#042; Foundation colour &#042;/
--foundation: &#035;5accd6;
</code></pre>
<p>For example, the lightness value of my foundation colour is <code>0.7837</code>, while a darker version has a value of <code>0.5837</code>. To calculate the difference, I subtract the lower value from the higher one and apply the result using a <code>calc()</code> function:</p>
<pre><code class="language-css">--foundation-dark: 
  oklch(from var(--foundation)
  calc(l - 0.20) c h);
</code></pre>
<p>To achieve a lighter colour, I add the difference instead:</p>
<pre><code class="language-css">--foundation-light:
  oklch(from var(--foundation)
  calc(l + 0.10) c h);
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png"></p>
<p>    <img loading="lazy" width="800" height="334" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Calculations of the difference between the foundation colour and Lightness, Chroma, and Hue-adjusted colours." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Calculating the difference between my foundation colour and Lightness, Chroma, and Hue-adjusted colours. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/10-calculating-colour-difference.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Chroma adjustments follow the same process. To reduce the intensity of my foundation colour from <code>0.1035</code> to <code>0.0035</code>, I subtract one value from the other:</p>
<pre><code class="language-css">oklch(from var(--foundation)
l calc(c - 0.10) h);
</code></pre>
<p>To create a palette of hues, I calculate the difference between the hue value of my foundation colour (<code>200</code>) and my new hue (<code>260</code>):</p>
<pre><code class="language-css">oklch(from var(--foundation)
l c calc(h + 60));
</code></pre>
<p>Those calculations are absolute. When I subtract a fixed amount, I’m effectively saying, <em>“Always subtract this much.”</em> The same applies when adding fixed values:</p>
<pre><code class="language-css">calc(c - 0.10)
calc(c + 0.10)
</code></pre>
<p>I learned the limits of this approach the hard way. When I relied on subtracting fixed chroma values, colours collapsed towards grey as soon as I changed the foundation. A palette that worked for one colour fell apart for another.</p>
<p>Multiplication behaves differently. When I multiply chroma, I’m telling the browser: <em>“Reduce this colour’s intensity by a proportion.”</em> The relationship between colours remains intact, even when the foundation changes:</p>
<pre><code class="language-css">calc(c &#042; 0.10)
</code></pre>
<div class="partners__lead-place"></div>
<h2 id="my-move-it-scale-it-rotate-it-rules">My Move It, Scale It, Rotate It Rules</h2>
<ul>
<li><strong>Move</strong> lightness (add or subtract),</li>
<li><strong>Scale</strong> chroma (multiply),</li>
<li><strong>Rotate</strong> hue (add or subtract degrees).</li>
</ul>
<p>I scale chroma because I want intensity changes to stay proportional to the base colour. Hue relationships are rotational, so multiplying hue makes no sense. Lightness is perceptual and absolute &mdash; multiplying it often produces odd results.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png"></p>
<p>    <img loading="lazy" width="800" height="334" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Lightness: Move it. Chroma: Scale it. Hue: Rotate it" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Lightness: Move it. Chroma: Scale it. Hue: Rotate it. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/11-move-scale-rotate.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="from-one-colour-to-an-entire-theme">From One Colour To An Entire Theme</h2>
<p>Relative colour allows me to define a foundation colour and generate every other colour I need &mdash; fills, strokes, gradient stops, shadows &mdash; from it. At that point, colour stops being a palette and starts being a system.</p>
<blockquote><p>SVG illustrations tend to reuse the same few colours across fills, strokes, and gradients. Relative colour lets you define those relationships once and reuse them everywhere &mdash; much like animators reused backgrounds to create new scenes.</p></blockquote>
<p>Change the foundation colour once, and every derived colour updates automatically, without recalculating anything by hand. Outside of animated graphics, I could use this same approach to define colours for the states of interactive elements such as buttons and links.</p>
<p>The foundation colour I used in my “Lullabye-Bye Bear” Toon Title is a cyan-looking blue. The background is a radial gradient between my foundation and a darker version.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="“Lullabye-Bye Bear” Toon Title" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      View this on my <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html">Toon Titles website</a>. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/12-toon-titles-website.png">Large preview</a>)<br />
    </figcaption></figure>
<p>To create alternative versions with entirely different moods, I only need to change the foundation colour:</p>
<pre><code class="language-css">--foundation: &#035;5accd6;
--grad-end: var(--foundation);
--grad-start: oklch(from var(--foundation)
  calc(l - 0.2357) calc(c &#042; 0.833) h);
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html"></p>
<p>    <img loading="lazy" width="800" height="171" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Three alternative versions of the “Lullabye-Bye Bear” Toon Title" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Use the colour picker on my <a href="https://stuffandnonsense.co.uk/toon-titles/24b.html">Toon Titles website</a> to see this in action. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/13-toon-titles-website.png">Large preview</a>)<br />
    </figcaption></figure>
<p>To bind those custom properties to my SVG gradient without duplicating colour values, I replaced hard-coded <code>stop-color</code> values with inline styles:</p>
<div class="break-out">
<pre><code class="language-svg">&lt;defs&gt;
  &lt;radialGradient id="bg-grad" […]&gt;
    &lt;stop offset="0%" style="stop-color: var(--grad-end);" /&gt;
    &lt;stop offset="100%" style="stop-color: var(--grad-start);" /&gt;
  &lt;/radialGradient&gt;
&lt;/defs&gt;
</code></pre>
</div>
<pre><code class="language-svg">&lt;path fill="url(#bg-grad)" fill="#5DCDD8" d="[...]"/&gt;
</code></pre>
<p>Next, I needed to ensure that my <a href="https://stuffandnonsense.co.uk/toon-text/index.html">Toon Text</a> always contrasts with whatever foundation colour I choose. A <code>180deg</code> hue rotation produces a complementary colour that certainly pops &mdash; but can vibrate uncomfortably:</p>
<pre><code class="language-css">.text-light {
  fill: oklch(from var(--foundation)
    l c calc(h + 180));
}
</code></pre>
<p>A <code>90°</code> shift produces a vivid secondary colour without being fully complementary:</p>
<pre><code class="language-css">.text-light {
  fill: oklch(from var(--foundation)
    l c calc(h - 90));
}
</code></pre>
<p>My recreation of Quick Draw McGraw’s 1959 Toon Title “El Kabong“ uses the same techniques but with a more varied palette. For example, there’s another radial gradient between the foundation colour and a darker shade.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="An animated still of Quick Draw McGraw swinging from a rope going from left to right against a purple gradient background. Andy Clarke’s Toon Titles is displayed above him in cartoon-style typography. A silhouetted building and palm tree are positioned in the bottom-right corner." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      View this on my <a href="https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html">Toon Titles website</a>. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/14-quick-draw-mcgraw.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html"></p>
<p>    <img loading="lazy" width="800" height="167" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Three alternative versions of Quick Draw McGraw’s 1959 Toon Title “El Kabong“" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/15-quick-draw-mcgraw-toon-titles.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Use the colour picker on my <a href="https://stuffandnonsense.co.uk/toon-titles/quick-draw-4b.html">Toon Titles website</a> to see this in action. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/15-quick-draw-mcgraw-toon-titles.png">Large preview</a>)<br />
    </figcaption></figure>
<p>The building and tree in the background are simply different shades of the same foundation colour. For those paths, I needed two additional <code>fill</code> colours:</p>
<pre><code class="language-css">.bg-mid {
  fill: oklch(from var(--foundation)
    calc(l - 0.04) calc(c &#042; 0.91) h);
}

.bg-dark {
  fill: oklch(from var(--foundation)
    calc(l - 0.12) calc(c &#042; 0.64) h);
}
</code></pre>
<h2 id="when-the-foundations-start-to-move">When The Foundations Start To Move</h2>
<p>So far, everything I’ve shown has been static. Even when someone uses a colour picker to change the foundation colour, that change happens instantly. But animated graphics rarely stand still &mdash; the clue is in the name. So, if colour is part of the system, there’s no reason it can’t animate, too.</p>
<p>To animate the foundation colour, I first need to split it into its OKLCH channels &mdash; lightness, chroma, and hue. But there’s an important extra step: I need to register those values as <em>typed</em> custom properties. But what does that mean?</p>
<p>By default, a browser doesn’t know whether a CSS custom property value represents a colour, length, number, or something else entirely. That often means <a href="https://css-tricks.com/what-you-need-to-know-about-css-color-interpolation/">they can’t be interpolated smoothly during animation</a>, and jump from one value to the next.</p>
<p>Registering a custom property tells the browser the type of value it represents and how it should behave over time. In this case, I want the browser to treat my colour channels as numbers so they can be animated smoothly.</p>
<pre><code class="language-css">@property --f-l {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.40;
}

@property --f-c {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.11;
}

@property --f-h {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 305;
}
</code></pre>
<p>Once registered, these custom properties behave like native CSS. The browser can interpolate them frame-by-frame. I then rebuild the foundation colour from those channels:</p>
<pre><code class="language-css">--foundation: oklch(var(--f-l) var(--f-c) var(--f-h));
</code></pre>
<p>This makes the foundation colour become animatable, just like any other numeric value. Here’s a simple “breathing” animation that gently shifts lightness over time:</p>
<pre><code class="language-css">@keyframes breathe {
  0%, 100% { --f-l: 0.36; }
  50% { --f-l: 0.46; }
}

.toon-title {
  animation: breathe 10s ease-in-out infinite;
}
</code></pre>
<p>Because every other colour in fills, gradients, and strokes is derived from <code>--foundation</code>, they all animate together, and nothing needs to be updated manually.</p>
<div class="partners__lead-place"></div>
<h2 id="one-animated-colour-many-effects">One Animated Colour, Many Effects</h2>
<p>At the start of this process, I wondered whether CSS relative colour values could offer more possibilities while also making them simpler to implement. I recently added a new gold mine background to my website’s <a href="https://stuffandnonsense.co.uk/contact">contact page</a>, and the first iteration included oil lamps that glow and swing.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://stuffandnonsense.co.uk/contact"></p>
<p>    <img loading="lazy" width="800" height="305" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A group of seven illustrated western characters in an underground gold mine scene." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      View this animated SVG on <a href="https://stuffandnonsense.co.uk/contact">my website</a>. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/16-gold-mine-scene.png">Large preview</a>)<br />
    </figcaption></figure>
<p>I wanted to explore how animating CSS relative colours could make the mine interior more realistic by tinting it with colours from the lamps. I wanted them to affect the world around them, the way real light does. So, rather than animating multiple colours, I built a tiny lighting system that animates just one colour.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png"></p>
<p>    <img loading="lazy" width="800" height="305" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Overlay layer applied to the SVG" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Adding an overlay layer to my SVG. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/17-overlay-layer-svg.png">Large preview</a>)<br />
    </figcaption></figure>
<p>My first task was to slot an overlay layer between the background and my lamps:</p>
<pre><code class="language-svg">&lt;path 
  id="overlay"
  fill="var(--overlay-tint)" 
  [...] 
  style="mix-blend-mode: color"
/&gt;
</code></pre>
<p>I used <code>mix-blend-mode: color</code> because that tints what’s beneath it while preserving the underlying luminance. As I only want the overlay to be visible when animations are turned on, I made the overlay opt-in:</p>
<pre><code class="language-css">.svg-mine &#035;overlay {
  display: none;
}
  
@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] &#035;overlay {
    display: block;
    opacity: 0.5;
  }
}
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png"></p>
<p>    <img loading="lazy" width="800" height="305" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="An overlay applied to the gold mine scene illuminates the background, making it brighter than the foreground." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      The overlay layer tints what’s beneath it. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/18-overlay-gold-mine-scene.png">Large preview</a>)<br />
    </figcaption></figure>
<p>The overlay was in place, but not yet connected to the lamps. I needed a light source. My lamps are simple, and each one contains a <code>circle</code> element that I blurred with a filter. The <code>filter</code> produces a very soft blur over the entire circle.</p>
<div class="break-out">
<pre><code class="language-svg">&lt;filter id="lamp-glow-1" x="-120%" y="-120%" width="340%" height="340%"&gt;
  &lt;feGaussianBlur in="SourceGraphic" stdDeviation="56"/&gt;
&lt;/filter&gt;
</code></pre>
</div>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png"></p>
<p>    <img loading="lazy" width="800" height="305" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Added oil lamps to the gold mine scene" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Adding oil lamps to my scene. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/19-oil-lamps.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Instead of animating the overlay and lamps separately, I animate a single “flame” colour token and derive everything else from that. First, I register three typed custom properties for OKLCH channels:</p>
<pre><code class="language-css">@property --fl-l {
  syntax: "&lt;number&gt;"; 
  inherits: true;
  initial-value: 0.86;
}
@property --fl-c {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 0.12;
}
@property --fl-h {
  syntax: "&lt;number&gt;";
  inherits: true;
  initial-value: 95;
}
</code></pre>
<p>I animated those channels, deliberately pushing a few frames towards orange so the flicker reads clearly as firelight:</p>
<div class="break-out">
<pre><code class="language-css">@keyframes flame {
  0%, 100% { --fl-l: 0.86; --fl-c: 0.12; --fl-h: 95; }
  6% { --fl-l: 0.91; --fl-c: 0.10; --fl-h: 92; }
  12% { --fl-l: 0.83; --fl-c: 0.14; --fl-h: 100; }
  18% { --fl-l: 0.88; --fl-c: 0.11; --fl-h: 94; }
  24% { --fl-l: 0.82; --fl-c: 0.16; --fl-h: 82; }
  30% { --fl-l: 0.90; --fl-c: 0.12; --fl-h: 90; }
  36% { --fl-l: 0.79; --fl-c: 0.17; --fl-h: 76; }
  44% { --fl-l: 0.87; --fl-c: 0.12; --fl-h: 96; }
  52% { --fl-l: 0.81; --fl-c: 0.15; --fl-h: 102; }
  60% { --fl-l: 0.89; --fl-c: 0.11; --fl-h: 93; }
  68% { --fl-l: 0.83; --fl-c: 0.16; --fl-h: 85; }
  76% { --fl-l: 0.91; --fl-c: 0.10; --fl-h: 91; }
  84% { --fl-l: 0.85; --fl-c: 0.14; --fl-h: 98; }
  92% { --fl-l: 0.80; --fl-c: 0.17; --fl-h: 74; }
}
</code></pre>
</div>
<p>Then I scoped that animation to the SVG, so the shared variables are available to both the lamps and my overlay:</p>
<div class="break-out">
<pre><code class="language-css">@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] {
    animation: flame 3.6s infinite linear;
    isolation: isolate;

    /&#042; Build a flame colour from animated channels &#042;/
    --flame: oklch(var(--fl-l) var(--fl-c) var(--fl-h));

    /&#042; Lamp colour derived from flame &#042;/
    --lamp-core: oklch(from var(--flame) calc(l + 0.05) calc(c &#042; 0.70) h);
  
    /&#042; Overlay tint derived from the same flame &#042;/
    --overlay-tint: oklch(from var(--flame)
      calc(l + 0.06) calc(c &#042; 0.65) calc(h - 10));
  }
}
</code></pre>
</div>
<p>Finally, I applied those derived colours to the glowing lamps and the overlay they affect:</p>
<pre><code class="language-css">@media (prefers-reduced-motion: no-preference) {
  .svg-mine[data-animations=on] &#035;mine-lamp-1 &gt; circle,
  .svg-mine[data-animations=on] &#035;mine-lamp-2 &gt; circle {
    fill: var(--lamp-core);
  }
  
  .svg-mine[data-animations=on] &#035;overlay {
    display: block;
    fill: var(--overlay-tint);
    opacity: 0.5;
  }
}
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png"></p>
<p>    <img loading="lazy" width="800" height="305" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="The lamps and overlay are connected." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      The lamps and overlay are connected. (<a href="https://files.smashing.media/articles/smashing-animations-part-8-css-relative-colour/20-lamps-overlay-connected.png">Large preview</a>)<br />
    </figcaption></figure>
<p>When the flame shifts toward orange, the lamps warm up, and the scene warms with them. When the flame cools, everything settles together. The best part is that nothing is written manually. If I change the foundation colour or tweak the flame animation ranges, the entire lighting system updates simultaneously.</p>
<p>You can see <a href="https://stuffandnonsense.co.uk/contact">the final result on my website</a>.</p>
<h2 id="reuse-repurpose-revisited">Reuse, Repurpose, Revisited</h2>
<p>Those Hanna-Barbera animators were forced to repurpose elements out of necessity, but I reuse colours because it makes my work <strong>more consistent</strong> and <strong>easier to maintain</strong>. CSS relative colour values allow me to:</p>
<ul>
<li>Define a single foundation colour,</li>
<li>Describe how other colours relate to it,</li>
<li>Reuse those relationships everywhere, and</li>
<li>Animate the system by changing one value.</li>
</ul>
<blockquote class="pull-quote">
<p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aRelative%20colour%20doesn%e2%80%99t%20just%20make%20theming%20easier.%20It%20encourages%20a%20way%20of%20thinking%20where%20colour,%20like%20motion,%20is%20intentional%20%e2%80%94%20and%20where%20changing%20one%20value%20can%20transform%20an%20entire%20scene%20without%20rewriting%20the%20work%20beneath%20it.%0a&amp;url=https://smashingmagazine.com%2f2026%2f01%2fsmashing-animations-part-8-css-relative-colour%2f"></p>
<p>Relative colour doesn’t just make theming easier. It encourages a way of thinking where colour, like motion, is intentional — and where changing one value can transform an entire scene without rewriting the work beneath it.</p>
<p>    </a>
  </p>
<div class="pull-quote__quotation">
<div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
</p></div>
</blockquote>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2026/01/14/smashing-animations-part-8-theming-animations-using-css-relative-colour/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Smashing Animations Part 7: Recreating Toon Text With CSS And SVG</title>
		<link>http://computercoursesonline.com/index.php/2025/12/17/smashing-animations-part-7-recreating-toon-text-with-css-and-svg/</link>
					<comments>http://computercoursesonline.com/index.php/2025/12/17/smashing-animations-part-7-recreating-toon-text-with-css-and-svg/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Wed, 17 Dec 2025 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1135</guid>

					<description><![CDATA[Smashing Animations Part 7: Recreating Toon Text With CSS And SVG Smashing Animations Part 7: Recreating Toon Text With CSS And SVG Andy Clarke 2025-12-17T10:00:00+00:00 2025-12-23T01:42:02+00:00 After finishing a project that required me to learn everything I could about CSS and SVG animations, I started writing this series about Smashing Animations and “How Classic Cartoons...]]></description>
										<content:encoded><![CDATA[<p>              <title>Smashing Animations Part 7: Recreating Toon Text With CSS And SVG</title></p>
<article>
<header>
<h1>Smashing Animations Part 7: Recreating Toon Text With CSS And SVG</h1>
<address>Andy Clarke</address>
<p>                  2025-12-17T10:00:00+00:00<br />
                  2025-12-23T01:42:02+00:00<br />
                </header>
<p>After finishing a project that required me to learn everything I could about CSS and SVG animations, I started writing this series about Smashing Animations and “<a href="https://www.smashingmagazine.com/2025/05/smashing-animations-part-1-classic-cartoons-inspire-css/">How Classic Cartoons Inspire Modern CSS</a>.” To round off this year, I want to show you how to use modern CSS to create that element that makes Toon Titles so impactful: their typography.</p>
<h2 id="title-artwork-design">Title Artwork Design</h2>
<p>In the silent era of the 1920s and early ’30s, the typography of a film’s title card created a mood, set the scene, and reminded an audience of the type of film they’d paid to see.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png"></p>
<p>    <img loading="lazy" width="800" height="156" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Typographic title cards from the early years of cinema" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Typographic title cards from the early years of cinema. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/1-typographic-title-cards.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Cartoon title cards were also branding, mood, and scene-setting, all rolled into one. In the early years, when major studio budgets were bigger, these title cards were often illustrative and painterly.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png"></p>
<p>    <img loading="lazy" width="800" height="300" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Top: William Hanna and Joseph Barbera’s 1940s Tom &amp; Jerry title cards. Bottom: Colour versions released in 1957. © Warner Bros. Entertainment Inc." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Top: William Hanna and Joseph Barbera’s 1940s Tom &amp; Jerry title cards. Bottom: Colour versions released in 1957. © Warner Bros. Entertainment Inc. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/2-tom-jerry-title-cards.png">Large preview</a>)<br />
    </figcaption></figure>
<p>But when television boomed during the 1950s, budgets dropped, and cards designed by artists like Lawrence “Art” Goble adopted a new visual language, becoming more graphic, stylised, and less intricate.</p>
<p><strong>Note:</strong> <em>Lawrence “Art” Goble is one of the often overlooked heroes of mid-century American animation. He primarily worked for Hanna-Barbera during its most influential years of the 1950s and 1960s.</em></p>
<p>Goble wasn’t a character animator. His role was to create atmosphere, so he designed environments for <em>The Flintstones</em>, <em>Huckleberry Hound</em>, <em>Quick Draw McGraw</em>, and <em>Yogi Bear</em>, as well as the opening title cards that set the tone. His title cards, featuring paintings with a logo overlaid, helped define the iconic look of Hanna-Barbera.</p>
<p>Goble’s artwork for characters such as Quick Draw McGraw and Yogi Bear was effective on smaller TV screens. Rather than reproducing a still from the cartoon, he focused on presenting a single, strong idea &mdash; often in silhouette &mdash; that captured its essence. In “The Buzzin’ Bear,” Yogi buzzes by in a helicopter. He bounces away, pic-a-nic basket in hand, in “Bear on a Picnic,” and for his “Prize Fight Fright,” Yogi boxes the title text.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.png"></p>
<p>    <img loading="lazy" width="800" height="300" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Title cards for Hanna-Barbera’s Yogi Bear." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Yogi Bear. © Warner Bros. Entertainment Inc. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/3-title-cards-yogi-bear.png">Large preview</a>)<br />
    </figcaption></figure>
<p>With little or no motion to rely on, Goble’s single frames had to create a mood, set the scene, and describe a story. They did this using flat colours, graphic shapes, and typography that was frequently integrated into the artwork.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png"></p>
<p>    <img loading="lazy" width="800" height="225" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Title cards for Hanna-Barbera’s Quick Draw McGraw." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Quick Draw McGraw. © Warner Bros. Entertainment Inc. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/4-title-cards-quick-draw-mcgraw.png">Large preview</a>)<br />
    </figcaption></figure>
<p>As designers who work on the web, toon titles can teach us plenty about how to convey a brand’s personality, make a first impression, and set expectations for someone’s experience using a product or website. We can learn from the artists’ techniques to create effective banners, landing-page headers, and even good ol’ fashioned splash screens.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="toon-title-typography">Toon Title Typography</h2>
<p>Cartoon title cards show how merging type with imagery delivers the punch a header or hero needs. With a handful of <code>text-shadow</code>, <code>text-stroke</code>, and <code>transform</code> tricks, modern CSS lets you tap into that same energy.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png"></p>
<p>    <img loading="lazy" width="800" height="455" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Title cards for Hanna-Barbera’s Augie Doggie." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Title cards for Hanna-Barbera’s Augie Doggie. © Warner Bros. Entertainment Inc. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/5-title-cards-augie-doggie.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="the-toon-text-title-generator">The Toon Text Title Generator</h2>
<p>Partway through writing this article, I realised it would be useful to have a tool for generating text styled like the cartoon titles I love so much. <a href="https://stuffandnonsense.co.uk/toon-text/tool.html">So I made one.</a></p>
<p>My Toon Text Title Generator lets you experiment with colours, strokes, and multiple text shadows. You can adjust paint order, apply letter spacing, preview your text in a selection of sample fonts, and then copy the generated CSS straight to your clipboard to use in a project.</p>
<h2 id="toon-title-css">Toon Title CSS</h2>
<p>You can simply copy-paste the CSS that the Toon Text Title Generator provides you. But let’s look closer at what it does.</p>
<h3 id="text-shadow">Text shadow</h3>
<p>Look at the type in this title from Augie Doggie’s episode “Yuk-Yuk Duck,” with its pale yellow letters and dark, hard, offset shadow that lifts it off the background and creates the illusion of depth.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/6-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>You probably already know that <code>text-shadow</code> accepts four values: (1) horizontal and (2) vertical offsets, (3) blur, and (4) a colour which can be solid or semi-transparent. Those offset values can be positive or negative, so I can replicate “Yuk-Yuk Duck” using a hard shadow pulled down and to the right:</p>
<pre><code class="language-css">color: &#035;f7f76d;
text-shadow: 5px 5px 0 &#035;1e1904;
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/7-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>On the other hand, this “Pint Giant” title has a different feel with its negative semi-soft shadow:</p>
<pre><code class="language-css">color: &#035;c2a872;
text-shadow:
  -7px 5px 0 &#035;b100e,
  0 -5px 10px &#035;546c6f;
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/8-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>To add extra depth and create more interesting effects, I can layer multiple shadows. For “Let’s Duck Out,” I combine four shadows: the first a solid shadow with a negative horizontal offset to lift the text off the background, followed by progressively softer shadows to create a blur around it:</p>
<pre><code class="language-css">color: &#035;6F4D80;
text-shadow:
  -5px 5px 0 &#035;260e1e, /&#042; Shadow 1 &#042;/
  0 0 15px &#035;e9ce96,   /&#042; Shadow 2 &#042;/
  0 0 30px &#035;e9ce96,   /&#042; Shadow 3 &#042;/
  0 0 30px &#035;e9ce96;   /&#042; Shadow 4 &#042;/
</code></pre>
<p>These shadows show that using <code>text-shadow</code> isn’t just about creating lighting effects, as they can also be decorative and add personality.</p>
<h3 id="text-stroke">Text Stroke</h3>
<p>Many cartoon title cards feature letters with a bold outline that makes them stand out from the background. I can recreate this effect using <code>text-stroke</code>. For a long time, this property was only available via a <code>-webkit-</code> prefix, but that also means it’s now supported across modern browsers.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/9-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p><code>text-stroke</code> is a shorthand for two properties. The first, <code>text-stroke-width</code>, draws a contour around individual letters, while the second, <code>text-stroke-color</code>, controls its colour. For “Whatever Goes Pup,” I added a <code>4px</code> blue stroke to the yellow text:</p>
<pre><code class="language-css">color: &#035;eff0cd;
-webkit-text-stroke: 4px &#035;7890b5;
text-stroke: 4px &#035;7890b5;
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/10-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Strokes can be especially useful when they’re combined with shadows, so for “Growing, Growing, Gone,” I added a thin <code>3px</code> stroke to a barely blurred <code>1px</code> shadow to create this three-dimensional text effect:</p>
<pre><code class="language-css">color: &#035;fbb999;
text-shadow: 3px 5px 1px &#035;5160b1;
-webkit-text-stroke: 3px &#035;984336;
text-stroke: 3px &#035;984336;
</code></pre>
<h3 id="paint-order">Paint Order</h3>
<p>Using <code>text-stroke</code> doesn’t always produce the expected result, especially with thinner letters and thicker strokes, because by default the browser draws a stroke over the fill. Sadly, CSS still does not permit me to adjust stroke placement as I often do in Sketch. However, the <code>paint-order</code> property has values that allow me to place the stroke behind, rather than in front of, the fill.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Left: paint-order: stroke; Right: paint-order: fill." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Left: <code>paint-order: stroke</code>. Right: <code>paint-order: fill</code>. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/11-paint-order.png">Large preview</a>)<br />
    </figcaption></figure>
<p><code>paint-order: stroke</code> paints the stroke first, then the fill, whereas <code>paint-order: fill</code> does the opposite:</p>
<pre><code class="language-css">color: &#035;fbb999;
paint-order: fill;
text-shadow: 3px 5px 1px &#035;5160b1;
text-stroke-color:&#035;984336;
text-stroke-width: 3px;
</code></pre>
<p>An effective stroke keeps letters readable, adds weight, and &mdash; when combined with shadows and paint order &mdash; gives flat text real presence.</p>
<div class="partners__lead-place"></div>
<h2 id="backgrounds-inside-text">Backgrounds Inside Text</h2>
<p>Many cartoon title cards go beyond flat colour by adding texture, gradients, or illustrated detail to the lettering. Sometimes that’s a texture, other times it might be a gradient with a subtle tonal shift. On the web, I can recreate this effect by using a background image or gradient behind the text, and then clipping it to the shape of the letters. This relies on two properties working together: <code>background-clip: text</code> and <code>text-fill-color: transparent</code>.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/12-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>First, I apply a background behind the text. This can be a bitmap or vector image or a CSS gradient. For this example from the Quick Draw McGraw episode “Baba Bait,” the title text includes a subtle top–bottom gradient from dark to light:</p>
<pre><code class="language-css">background: linear-gradient(0deg, &#035;667b6a, &#035;1d271a);
</code></pre>
<p>Next, I clip that background to the glyphs and make the text transparent so the background shows through:</p>
<pre><code class="language-css">-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
</code></pre>
<p>With just those two lines, the background is no longer painted behind the text; instead, it’s painted within it. This technique works especially well when combined with strokes and shadows. A clipped gradient provides the lettering with colour and texture, a stroke keeps its edges sharp, and a shadow elevates it from the background. Together, they recreate the layered look of hand-painted title cards using nothing more than a little CSS. As always, test clipped text carefully, as browser quirks can sometimes affect shadows and rendering.</p>
<h3 id="splitting-text-into-individual-characters">Splitting Text Into Individual Characters</h3>
<p>Sometimes I don’t want to style a whole word or heading. I want to style individual letters &mdash; to nudge a character into place, give one glyph extra weight, or animate a few letters independently.</p>
<p>In plain HTML and CSS, there’s only one reliable way to do that: wrap each character in its own <code>span</code> element. I could do that manually, but that would be fragile, hard to maintain, and would quickly fall apart when copy changes. Instead, when I need per-letter control, I use a text-splitting library like <a href="https://www.spltjs.com">splt.js</a> (although other solutions are available). This takes a text node and automatically wraps words or characters, giving me extra hooks to animate and style without messing up my markup.</p>
<p>It’s an approach that keeps my HTML readable and semantic, while giving me the fine-grained control I need to recreate the uneven, characterful typography you see in classic cartoon title cards. However, this approach comes with accessibility caveats, as most screen readers read text nodes in order. So this:</p>
<pre><code class="language-html">&lt;h2&gt;Hum Sweet Hum&lt;/h2&gt;
</code></pre>
<p>…reads as you’d expect:</p>
<blockquote><p>Hum Sweet Hum</p></blockquote>
<p>But this:</p>
<pre><code class="language-html">&lt;h2&gt;
&lt;span&gt;H&lt;/span&gt;
&lt;span&gt;u&lt;/span&gt;
&lt;span&gt;m&lt;/span&gt;
&lt;!-- etc. --&gt;
&lt;/h2&gt;
</code></pre>
<p>…can be interpreted differently depending on the browser and screen reader. Some will concatenate the letters and read the words correctly. Others may pause between letters, which in a worst-case scenario might sound like:</p>
<blockquote><p>“H…” “U…” “M…”</p></blockquote>
<p>Sadly, some splitting solutions don’t deliver an always accessible result, so I’ve written my own text splitter, <a href="https://stuffandnonsense.co.uk/toon-text/splinter.html#section-install">splinter.js</a>, which is currently in beta.</p>
<h3 id="transforming-individual-letters">Transforming Individual Letters</h3>
<p>To activate my Toon Text Splitter, I add a <code>data-</code> attribute to the element I want to split:</p>
<pre><code class="language-html">&lt;h2 data-split="toon"&gt;Hum Sweet Hum&lt;/h2&gt;
</code></pre>
<p>First, my script separates each word into individual letters and wraps them in a <code>span</code> element with class and ARIA attributes applied:</p>
<pre><code class="language-html">&lt;span class="toon-char" aria-hidden="true"&gt;H&lt;/span&gt;
&lt;span class="toon-char" aria-hidden="true"&gt;u&lt;/span&gt;
&lt;span class="toon-char" aria-hidden="true"&gt;m&lt;/span&gt;
</code></pre>
<p>The script then takes the initial content of the split element and adds it as an aria attribute to help maintain accessibility:</p>
<div class="break-out">
<pre><code class="language-html">&lt;h2 data-split="toon" aria-label="Hum Sweet Hum"&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;H&lt;/span&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;u&lt;/span&gt;
  &lt;span class="toon-char" aria-hidden="true"&gt;m&lt;/span&gt;
&lt;/h2&gt;
</code></pre>
</div>
<p>With those class attributes applied, I can then style individual characters as I choose.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/13-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>For example, for “Hum Sweet Hum,” I want to replicate how its letters shift away from the baseline. After using my Toon Text Splitter, I applied four different <code>translate</code> values using several <code>:nth-child</code> selectors to create a semi-random look:</p>
<pre><code class="language-css">/&#042; 4th, 8th, 12th... &#042;/
.toon-char:nth-child(4n) { translate: 0 -8px; }
/&#042; 1st, 5th, 9th... &#042;/
.toon-char:nth-child(4n+1) { translate: 0 -4px; }
/&#042; 2nd, 6th, 10th... &#042;/
.toon-char:nth-child(4n+2) { translate: 0 4px; }
/&#042; 3rd, 7th, 11th... &#042;/
.toon-char:nth-child(4n+3) { translate: 0 8px; }
</code></pre>
<p>But <code>translate</code> is only one property I can use to <code>transform</code> my toon text.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/14-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<p>I could also rotate those individual characters for an even more chaotic look:</p>
<pre><code class="language-css">/&#042; 4th, 8th, 12th... &#042;/
.toon-line .toon-char:nth-child(4n) { rotate: -4deg; }
/&#042; 1st, 5th, 9th... &#042;/
.toon-char:nth-child(4n+1) { rotate: -8deg; }
/&#042; 2nd, 6th, 10th... &#042;/
.toon-char:nth-child(4n+2) { rotate: 4deg; }
/&#042; 3rd, 7th, 11th... &#042;/
.toon-char:nth-child(4n+3) { rotate: 8deg; }
</code></pre>
<p>But <code>translate</code> is only one property I can use to <code>transform</code> my toon text. I could also <code>rotate</code> those individual characters for an even more chaotic look:</p>
<pre><code class="language-css">/&#042; 4th, 8th, 12th... &#042;/
.toon-line .toon-char:nth-child(4n) {
rotate: -4deg; }

/&#042; 1st, 5th, 9th... &#042;/
.toon-char:nth-child(4n+1) {
rotate: -8deg; }

/&#042; 2nd, 6th, 10th... &#042;/
.toon-char:nth-child(4n+2) {
rotate: 4deg; }

/&#042; 3rd, 7th, 11th... &#042;/
.toon-char:nth-child(4n+3) {
rotate: 8deg; }
</code></pre>
<p>And, of course, I could add animations to jiggle those characters and bring my toon text style titles to life. First, I created a keyframe animation that rotates the characters:</p>
<div class="break-out">
<pre><code class="language-css">@keyframes jiggle {
0%, 100% { transform: rotate(var(--base-rotate, 0deg)); }
25% { transform: rotate(calc(var(--base-rotate, 0deg) + 3deg)); }
50% { transform: rotate(calc(var(--base-rotate, 0deg) - 2deg)); }
75% { transform: rotate(calc(var(--base-rotate, 0deg) + 1deg)); }
}
</code></pre>
</div>
<p>Before applying it to the <code>span</code> elements created by my Toon Text Splitter:</p>
<pre><code class="language-css">.toon-char {
animation: jiggle 3s infinite ease-in-out;
transform-origin: center bottom; }
</code></pre>
<p>And finally, setting the rotation amount and a delay before each character begins to jiggle:</p>
<pre><code class="language-css">.toon-char:nth-child(4n) { --base-rotate: -2deg; }
.toon-char:nth-child(4n+1) { --base-rotate: -4deg; }
.toon-char:nth-child(4n+2) { --base-rotate: 2deg; }
.toon-char:nth-child(4n+3) { --base-rotate: 4deg; }

.toon-char:nth-child(4n) { animation-delay: 0.1s; }
.toon-char:nth-child(4n+1) { animation-delay: 0.3s; }
.toon-char:nth-child(4n+2) { animation-delay: 0.5s; }
.toon-char:nth-child(4n+3) { animation-delay: 0.7s; }
</code></pre>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png"></p>
<p>    <img loading="lazy" width="800" height="317" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Example from Andy&#039;s Toon Text collection." class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      See this example in my Toon Text collection. (<a href="https://files.smashing.media/articles/smashing-animations-part-7-recreating-toon-text-css-svg/15-toon-text-collection.png">Large preview</a>)<br />
    </figcaption></figure>
<div class="partners__lead-place"></div>
<h2 id="one-frame-to-make-an-impression">One Frame To Make An Impression</h2>
<p>Cartoon title artists had one frame to make an impression, and their typography was as important as the artwork they painted. The same is true on the web.</p>
<blockquote class="pull-quote">
<p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aA%20well-designed%20header%20or%20hero%20area%20needs%20clarity,%20character,%20and%20confidence%20%e2%80%94%20not%20simply%20a%20faded%20full-width%20background%20image.%0a&amp;url=https://smashingmagazine.com%2f2025%2f12%2fsmashing-animations-part-7-recreating-toon-text-css-svg%2f"></p>
<p>A well-designed header or hero area needs clarity, character, and confidence — not simply a faded full-width background image.</p>
<p>    </a>
  </p>
<div class="pull-quote__quotation">
<div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
</p></div>
</blockquote>
<p>With a few carefully chosen CSS properties &mdash; shadows, strokes, clipped backgrounds, and some restrained animation &mdash; we can recreate that same impact. I love toon text not because I’m nostalgic, but because its design is intentional. Make deliberate choices, and let a little toon text typography add punch to your designs.</p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2025/12/17/smashing-animations-part-7-recreating-toon-text-with-css-and-svg/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>State, Logic, And Native Power: CSS Wrapped 2025</title>
		<link>http://computercoursesonline.com/index.php/2025/12/09/state-logic-and-native-power-css-wrapped-2025/</link>
					<comments>http://computercoursesonline.com/index.php/2025/12/09/state-logic-and-native-power-css-wrapped-2025/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1122</guid>

					<description><![CDATA[State, Logic, And Native Power: CSS Wrapped 2025 State, Logic, And Native Power: CSS Wrapped 2025 Brecht De Ruyte 2025-12-09T10:00:00+00:00 2025-12-11T21:02:30+00:00 If I were to divide CSS evolutions into categories, we have moved far beyond the days when we simply asked for border-radius to feel like we were living in the future. We are currently...]]></description>
										<content:encoded><![CDATA[<p>              <title>State, Logic, And Native Power: CSS Wrapped 2025</title></p>
<article>
<header>
<h1>State, Logic, And Native Power: CSS Wrapped 2025</h1>
<address>Brecht De Ruyte</address>
<p>                  2025-12-09T10:00:00+00:00<br />
                  2025-12-11T21:02:30+00:00<br />
                </header>
<p>If I were to divide CSS evolutions into categories, we have moved far beyond the days when we simply asked for <code>border-radius</code> to feel like we were living in the future. We are currently living in a moment where the platform is handing us tools that don’t just tweak the visual layer, but fundamentally redefine how we architect interfaces. I thought the number of features announced in 2024 couldn’t be topped. I’ve never been so happily wrong.</p>
<p>The Chrome team’s “<a href="https://chrome.dev/css-wrapped-2025/"><strong>CSS Wrapped 2025</strong></a>” is not just a list of features; it is a manifesto for a dynamic, native web. As someone who has spent a couple of years documenting these evolutions &mdash; from <a href="https://www.smashingmagazine.com/2024/08/css5-era-evolution/">defining “CSS5” eras</a> to the intricacies of <a href="https://www.smashingmagazine.com/2024/05/modern-css-layouts-no-framework/">modern layout utilities</a> &mdash; I find myself looking at this year’s wrap-up with a huge sense of excitement. We are seeing a shift towards “Optimized Ergonomics” and “Next-gen interactions” that allow us to stop fighting the code and start sculpting interfaces in their natural state.</p>
<p>In this article, you can find <strong>a comprehensive look at the standout features from Chrome’s report</strong>, viewed through the lens of my recent experiments and hopes for the future of the platform.</p>
<h2 id="the-component-revolution-finally-a-native-customizable-select">The Component Revolution: Finally, A Native Customizable Select</h2>
<p>For years, we have relied on heavy JavaScript libraries to style dropdowns, a “decades-old problem” that the platform has finally solved. As I detailed in <a href="https://utilitybend.com/blog/the-customizable-select-part-one-history-trickery-and-styling-the-select-with-css">my deep dive into the history of the customizable select</a> (and related articles), this has been a long road involving <a href="https://open-ui.org/">Open UI</a>, bikeshedding names like <code>&lt;selectmenu&gt;</code> and <code>&lt;selectlist&gt;</code>, and finally landing on a solution that re-uses the existing <code>&lt;select&gt;</code> element.</p>
<p>The introduction of <code>appearance: base-select</code> is a strong foundation. It allows us to fully customize the <code>&lt;select&gt;</code> element &mdash; including the button and the dropdown list (via <code>::picker(select)</code>) &mdash; using standard CSS. Crucially, this is built with progressive enhancement in mind. By wrapping our styles in a feature query, we ensure a seamless experience across all browsers.</p>
<p>We can opt in to this new behavior without breaking older browsers:</p>
<pre><code class="language-css">select {
  /&#042; Opt-in for the new customizable select &#042;/
  @supports (appearance: base-select) {
    &amp;, &amp;::picker(select) {
      appearance: base-select;
    }
  }
}
</code></pre>
<p>The fantastic addition to allow rich content inside options, such as images or flags, is a lot of fun. We can create all sorts of selects nowadays:</p>
<ul>
<li><strong>Demo:</strong> I created a <a href="https://codepen.io/utilitybend/pen/ByawgNN">Poké-adventure demo</a> showing how the new <code>&lt;selectedcontent&gt;</code> element can clone rich content (like a Pokéball icon) from an option directly into the button.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="JoXwwoZ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [A customizable select with images inside of the options and the selectedcontent [forked]](https://codepen.io/smashingmag/pen/JoXwwoZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/JoXwwoZ">A customizable select with images inside of the options and the selectedcontent [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<ul>
<li><strong>Demo:</strong> A comprehensive look at <a href="https://codepen.io/utilitybend/pen/GgRrLWb">styling the select with only pseudo-elements</a>.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="pvyqqJR" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [A customizable select with only pseudo-elements [forked]](https://codepen.io/smashingmag/pen/pvyqqJR) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/pvyqqJR">A customizable select with only pseudo-elements [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<ul>
<li><strong>Demo:</strong> Or you can kick it up a notch with this <a href="https://codepen.io/utilitybend/pen/ByoBMBm">Menu selection demo using optgroups</a>.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="myPaaJZ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [An actual Select Menu with optgroups [forked]](https://codepen.io/smashingmag/pen/myPaaJZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/myPaaJZ">An actual Select Menu with optgroups [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<p>This feature alone signals a massive shift in how we will build forms, reducing dependencies and technical debt.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="scroll-markers-and-the-death-of-the-javascript-carousel">Scroll Markers And The Death Of The JavaScript Carousel</h2>
<p>Creating carousels has historically been a friction point between developers and clients. Clients love them, developers dread the JavaScript required to make them accessible and performant. The arrival of <code>::scroll-marker</code> and <code>::scroll-button()</code> pseudo-elements changes this dynamic entirely.</p>
<p>These features allow us to create navigation dots and scroll buttons purely with CSS, linked natively to the scroll container. As I wrote on my blog, this was <a href="https://utilitybend.com/blog/love-at-first-slide-creating-a-carousel-purely-out-of-css">Love at first slide</a>. The ability to create a fully functional, accessible slider without a single line of JavaScript is not just convenient; it is a triumph for performance. There are some accessibility concerns around this feature, and even though these are valid, there might be a way for us developers to make it work. The good thing is, all these UI changes are making it a lot easier than custom DOM manipulation and dragging around aria tags, but I digress…</p>
<p>We can now group markers automatically using <code>scroll-marker-group</code> and style the buttons using anchor positioning to place them exactly where we want.</p>
<div class="break-out">
<pre><code class="language-css">.carousel {
  overflow-x: auto;
  scroll-marker-group: after; /&#042; Creates the container for dots &#042;/

  /&#042; Create the buttons &#042;/
  &amp;::scroll-button(inline-end),
  &amp;::scroll-button(inline-start) {
    content: " ";
    position: absolute;
    /&#042; Use anchor positioning to center them &#042;/
    position-anchor: --carousel;
    top: anchor(center);
  }

  /&#042; Create the markers on the children &#042;/
  div {
    &amp;::scroll-marker {
      content: " ";
      width: 24px;
      border-radius: 50%;
      cursor: pointer;
    }
    /&#042; Highlight the active marker &#042;/
    &amp;::scroll-marker:target-current {
      background: white;
    }
  }
}
</code></pre>
</div>
<ul>
<li><strong>Demo:</strong> My experiment creating a <a href="https://codepen.io/utilitybend/pen/vEBQxNb">carousel purely out of HTML and CSS</a>, using anchor positioning to place the buttons.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="ogxJJjQ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Carousel Pure HTML and CSS [forked]](https://codepen.io/smashingmag/pen/ogxJJjQ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogxJJjQ">Carousel Pure HTML and CSS [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/bNbXZWb">Webshop slick slider remake</a> using <code>attr()</code> to pull background images dynamically into the markers.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="gbrZZPY" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Webshop slick slider remake in CSS [forked]](https://codepen.io/smashingmag/pen/gbrZZPY) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/gbrZZPY">Webshop slick slider remake in CSS [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<h2 id="state-queries-sticky-thing-stuck-snappy-thing-snapped">State Queries: Sticky Thing Stuck? Snappy Thing Snapped?</h2>
<p>For a long time, we have lacked the ability to know if a <a href="https://utilitybend.com/blog/is-the-sticky-thing-stuck-is-the-snappy-item-snapped-a-look-at-state-queries-in-css">“sticky thing is stuck” or if a “snappy item is snapped”</a> without relying on IntersectionObserver hacks. Chrome 133 introduced scroll-state queries, allowing us to query these states declaratively.</p>
<p>By setting <code>container-type: scroll-state</code>, we can now style children based on whether they are stuck, snapped, or overflowing. This is a massive “quality of life” improvement that I have been eagerly waiting for since CSS Day 2023. It has even evolved a lot since we can also see the direction of the scroll, lovely!</p>
<p>For a simple example: we can finally apply a shadow to a header <em>only</em> when it is actually sticking to the top of the viewport:</p>
<pre><code class="language-css">.header-container {
  container-type: scroll-state;
  position: sticky;
  top: 0;

  header {
    transition: box-shadow 0.5s ease-out;
    /&#042; The query checks the state of the container &#042;/
    @container scroll-state(stuck: top) {
      box-shadow: rgba(0, 0, 0, 0.6) 0px 12px 28px 0px;
    }
  }
}
</code></pre>
<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/XWLQPOe">sticky header</a> that only applies a shadow when it is actually stuck.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="raeooxY" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Sticky headers with scroll-state query, checking if the sticky element is stuck [forked]](https://codepen.io/smashingmag/pen/raeooxY) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/raeooxY">Sticky headers with scroll-state query, checking if the sticky element is stuck [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<ul>
<li><strong>Demo:</strong> A <a href="https://codepen.io/utilitybend/pen/MWMZoqp">Pokémon-themed list</a> that uses scroll-state queries combined with anchor positioning to move a frame over the currently snapped character.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="vEGvvLM" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Scroll-state query to check which item is snapped with CSS, Pokemon version [forked]](https://codepen.io/smashingmag/pen/vEGvvLM) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vEGvvLM">Scroll-state query to check which item is snapped with CSS, Pokemon version [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<div class="partners__lead-place"></div>
<h2 id="optimized-ergonomics-logic-in-css">Optimized Ergonomics: Logic In CSS</h2>
<p>The “Optimized Ergonomics” section of CSS Wrapped highlights features that make our workflows more intuitive. Three features stand out as transformative for how we write logic:</p>
<ol>
<li><strong><code>if()</code> Statements</strong><br />
We are finally getting conditionals in CSS. The <code>if()</code> function acts like a ternary operator for stylesheets, allowing us to apply values based on media, support, or style queries inline. This reduces the need for verbose <code>@media</code> blocks for single property changes.</li>
<li><strong><code>@function</code> functions</strong><br />
We can finally move some logic to a different place, resulting in some cleaner files, a real quality of life feature.</li>
<li><strong><code>sibling-index()</code> and <code>sibling-count()</code></strong><br />
These tree-counting functions solve the issue of staggering animations or styling items based on list size. As I explored in <a href="https://utilitybend.com/blog/styling-siblings-with-css-has-never-been-easier-experimenting-with-sibling-count-and-sibling-index">Styling siblings with CSS has never been easier</a>, this eliminates the need to hard-code custom properties (like <code>--index: 1</code>) in our HTML.</li>
</ol>
<h3 id="example-calculating-layouts">Example: Calculating Layouts</h3>
<p>We can now write concise mathematical formulas. For example, staggering an animation for cards entering the screen becomes trivial:</p>
<pre><code class="language-css">.card-container &gt; &#042; {
  animation: reveal 0.6s ease-out forwards;
  /&#042; No more manual --index variables! &#042;/
  animation-delay: calc(sibling-index() &#042; 0.1s);
}
</code></pre>
<p>I even experimented with using these functions along with trigonometry to place items in a perfect circle without any JavaScript.</p>
<ul>
<li><strong>Demo:</strong> <a href="https://codepen.io/utilitybend/pen/wBKQPLr">Staggering card animations dynamically</a>.</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="RNaEERz" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [Stagger cards using sibling-index() [forked]](https://codepen.io/smashingmag/pen/RNaEERz) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/RNaEERz">Stagger cards using sibling-index() [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<ul>
<li><strong>Demo:</strong> Placing items in a <a href="https://codepen.io/utilitybend/pen/VYvVXLN">perfect circle</a> using <code>sibling-index</code>, <code>sibling-count</code>, and the new CSS <code>@function</code> feature.<br />
</li>
</ul>
<figure class="break-out">
<p data-height="480" data-theme-id="light" data-slug-hash="XJdoojZ" data-user="smashingmag" data-default-tab="result" class="codepen">See the Pen [The circle using sibling-index, sibling-count and functions [forked]](https://codepen.io/smashingmag/pen/XJdoojZ) by <a href="https://codepen.io/utilitybend">utilitybend</a>.</p><figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/XJdoojZ">The circle using sibling-index, sibling-count and functions [forked]</a> by <a href="https://codepen.io/utilitybend">utilitybend</a>.</figcaption></figure>
<h2 id="my-css-to-do-list-features-i-can-t-wait-to-try">My CSS To-Do List: Features I Can’t Wait To Try</h2>
<p>While I have been busy sculpting selects and transitions, the “CSS Wrapped 2025” report is packed with other goodies that I haven’t had the chance to fire up in CodePen yet. These are high on my list for my next experiments:</p>
<h3 id="anchored-container-queries">Anchored Container Queries</h3>
<p>I used CSS Anchor Positioning for the buttons in my carousel demo, but “CSS Wrapped” highlights an evolution of this: <strong>Anchored Container Queries</strong>. This solves a problem we’ve all had with tooltips: if the browser flips the tooltip from top to bottom because of space constraints, the “arrow” often stays pointing the wrong way. With anchored container queries (<code>@container anchored(fallback: flip-block)</code>), we can style the element based on which fallback position the browser actually chose.</p>
<h3 id="nested-view-transition-groups">Nested View Transition Groups</h3>
<p>View Transitions have been a revolution, but they came with a specific trade-off: they flattened the element tree, which often broke 3D transforms or overflow: clip. I always had a feeling that it was missing something, and this might just be the answer. By using <code>view-transition-group: nearest</code>, we can finally nest transition groups within each other.</p>
<p>This allows us to maintain clipping effects or 3D rotations during a transition &mdash; something that was previously impossible because the elements were hoisted up to the top level.</p>
<pre><code class="language-css">.card img {
  view-transition-name: photo;
  view-transition-group: nearest; /&#042; Keep it nested! &#042;/
}
</code></pre>
<h3 id="typography-and-shapes">Typography and Shapes</h3>
<p>Finally, the ergonomist in me is itching to try <strong>Text Box Trim</strong>, which promises to remove that annoying extra whitespace above and below text content (the leading) to finally achieve perfect vertical alignment. And for the creative side, <code>corner-shape</code> and the <code>shape()</code> function are opening up non-rectangular layouts, allowing for “squaricles” and complex paths that respond to CSS variables. That being said, I can’t wait to have a design full of squircles!</p>
<div class="partners__lead-place"></div>
<h2 id="a-hopeful-future">A Hopeful Future</h2>
<p>We are witnessing a world where <strong>CSS is becoming capable of handling logic, state, and complex interactions that previously belonged to JavaScript</strong>. Features like <code>moveBefore</code> (preserving DOM state for iframes/videos) and <code>attr()</code> (using types beyond strings for colors and grids) further cement this reality.</p>
<p>While some of these features are currently experimental or specific to Chrome, the momentum is undeniable. We must hope for continued support across all browsers through initiatives like Interop to ensure these capabilities become the baseline. That being said, having browser engines is just as important as having all these awesome features in “Chrome first”. These new features need to be discussed, tinkered with, and tested before ever landing in browsers.</p>
<blockquote class="pull-quote">
<p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aIt%20is%20a%20fantastic%20moment%20to%20get%20into%20CSS.%20We%20are%20no%20longer%20just%20styling%20documents;%20we%20are%20crafting%20dynamic,%20ergonomic,%20and%20robust%20applications%20with%20a%20native%20toolkit%20that%20is%20more%20powerful%20than%20ever.%0a&amp;url=https://smashingmagazine.com%2f2025%2f12%2fstate-logic-native-power-css-wrapped-2025%2f"></p>
<p>It is a fantastic moment to get into CSS. We are no longer just styling documents; we are crafting dynamic, ergonomic, and robust applications with a native toolkit that is more powerful than ever.</p>
<p>    </a>
  </p>
<div class="pull-quote__quotation">
<div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
</p></div>
</blockquote>
<p>Let’s get going with this new era and spread the word.</p>
<p>This is <a href="https://chrome.dev/css-wrapped-2025/">CSS Wrapped</a>!</p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(gg, yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2025/12/09/state-logic-and-native-power-css-wrapped-2025/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Masonry: Things You Won’t Need A Library For Anymore</title>
		<link>http://computercoursesonline.com/index.php/2025/12/02/masonry-things-you-wont-need-a-library-for-anymore/</link>
					<comments>http://computercoursesonline.com/index.php/2025/12/02/masonry-things-you-wont-need-a-library-for-anymore/#respond</comments>
		
		<dc:creator><![CDATA[.]]></dc:creator>
		<pubDate>Tue, 02 Dec 2025 10:00:00 +0000</pubDate>
				<category><![CDATA[Css]]></category>
		<guid isPermaLink="false">http://computercoursesonline.com/?p=1120</guid>

					<description><![CDATA[Masonry: Things You Won’t Need A Library For Anymore Masonry: Things You Won’t Need A Library For Anymore Patrick Brosset 2025-12-02T10:00:00+00:00 2025-12-04T20:33:04+00:00 About 15 years ago, I was working at a company where we built apps for travel agents, airport workers, and airline companies. We also built our own in-house framework for UI components and...]]></description>
										<content:encoded><![CDATA[<p>              <title>Masonry: Things You Won’t Need A Library For Anymore</title></p>
<article>
<header>
<h1>Masonry: Things You Won’t Need A Library For Anymore</h1>
<address>Patrick Brosset</address>
<p>                  2025-12-02T10:00:00+00:00<br />
                  2025-12-04T20:33:04+00:00<br />
                </header>
<p>About 15 years ago, I was working at a company where we built apps for travel agents, airport workers, and airline companies. We also built our own in-house framework for UI components and single-page app capabilities.</p>
<p>We had components for everything: fields, buttons, tabs, ranges, datatables, menus, datepickers, selects, and multiselects. We even had a div component. Our div component was great by the way, it allowed us to do rounded corners on all browsers, which, believe it or not, wasn&rsquo;t an easy thing to do at the time.</p>
<figure class="
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png"></p>
<p>    <img loading="lazy" width="800" height="407" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Div component, which allows to do rounded corners" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/1-div-component-example.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Our work took place at a point in our history when JS, Ajax, and dynamic HTML were seen as a revolution that brought us into the future. Suddenly, we could update a page dynamically, get data from a server, and avoid having to navigate to other pages, which was seen as slow and flashed a big white rectangle on the screen between the two pages.</p>
<p>There was a phrase, made popular by Jeff Atwood (the founder of StackOverflow), which read:</p>
<blockquote><p>“Any application that can be written in JavaScript will eventually be written in JavaScript.”</p>
<p>&mdash; <a href="https://blog.codinghorror.com/all-programming-is-web-programming/#:~:text=any%20application%20that%C2%A0can%C2%A0be%20written%20in%20JavaScript%2C%C2%A0will%C2%A0eventually%20be%20written%20in%20JavaScript">Jeff Atwood</a></p></blockquote>
<p>To us at the time, this felt like a dare to actually go and create those apps. It felt like a blanket approval to do everything with JS.</p>
<p>So we did everything with JS, and we didn’t really take the time to research other ways of doing things. We didn’t really feel the incentive to properly learn what HTML and CSS could do. We didn’t really perceive the web as an evolving app platform in its entirety. We mostly saw it as something we needed to work around, especially when it came to browser support. We could just throw more JS at it to get things done.</p>
<p>Would taking the time to learn more about how the web worked and what was available on the platform have helped me? Sure, I could probably have shaved a bunch of code that wasn’t truly needed. But, at the time, maybe not that much.</p>
<p>You see, browser differences were pretty significant back then. This was a time when Internet Explorer was still the dominant browser, with Firefox being the close second, but starting to lose market share due to Chrome rapidly gaining popularity. Although Chrome and Firefox were quite good at agreeing on web standards, the environments in which our apps were running meant that we had to support IE6 for a long time. Even when we were allowed to support IE8, we still had to deal with a lot of differences between browsers. Not only that, but the web of the time just didn&rsquo;t have that many capabilities built right into the platform.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://gs.statcounter.com/browser-market-share/all/worldwide/2010"></p>
<p>    <img loading="lazy" width="800" height="492" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://gs.statcounter.com/browser-market-share/all/worldwide/2010">statcounter</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/2-browser-market-share.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Fast forward to today. Things have changed tremendously. Not only do we have more of these capabilities than ever before, but the rate at which they become available has increased as well.</p>
<p>Let me ask the question again, then: Would taking the time to learn more about how the web works and what is available on the platform help you today? Absolutely yes. Learning to understand and use the web platform today puts you at a huge advantage over other developers.</p>
<p>Whether you work on performance, accessibility, responsiveness, all of them together, or just shipping UI features, if you want to do it as a responsible engineer, knowing the tools that are available to you helps you reach your goals faster and better.</p>
<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">
<aside class="feature-panel">
<div class="feature-panel-left-col">
<div class="feature-panel-description">
<p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<p><a data-instant href="smashing-workshops" class="btn btn--green btn--large">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link"></p>
<div class="feature-panel-image">
<img loading="lazy" class="feature-panel-image-img lazyload" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Feature Panel" width="257" height="355" data-src="/images/smashing-cat/cat-scubadiving-panel.svg"></p>
</div>
<p></a>
</div>
</aside>
</div>
<h2 id="some-things-you-might-not-need-a-library-for-anymore">Some Things You Might Not Need A Library For Anymore</h2>
<p>Knowing what browsers support today, the question, then, is: What can we ditch? Do we need a div component to do rounded corners in 2025? Of course, we don’t. The <code>border-radius</code> property has been supported by all currently used browsers for more than 15 years at this point. And <code>corner-shape</code> is also coming soon, for even fancier corners.</p>
<p>Let’s take a look at relatively recent features that are now available in all major browsers, and which you can use to replace existing dependencies in your codebase.</p>
<p>The point isn&rsquo;t to immediately ditch all your beloved libraries and rewrite your codebase. As for everything else, you’ll need to take browser support into account first and decide based on other factors specific to your project. The following features are implemented in the three main browser engines (Chromium, WebKit, and Gecko), but you might have different browser support requirements that prevent you from using them right away. Now is still a good time to learn about these features, though, and perhaps plan to use them at some point.</p>
<h3 id="popovers-and-dialogs">Popovers And Dialogs</h3>
<p>The <a href="https://developer.mozilla.org/docs/Web/API/Popover_API">Popover API</a>, the <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/dialog"><code>&lt;dialog&gt;</code> HTML element</a>, and the <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/::backdrop"><code>::backdrop</code> pseudo-element</a> can help you get rid of dependencies on popup, tooltip, and dialog libraries, such as <a href="https://floating-ui.com/">Floating UI</a>, <a href="https://atomiks.github.io/tippyjs/">Tippy.js</a>, <a href="https://tetherjs.dev/docs/welcome/">Tether</a>, or <a href="https://react-tooltip.com/">React Tooltip</a>.</p>
<p>They handle accessibility and focus management for you, out of the box, are highly customizable by using CSS, and can easily be animated.</p>
<h3 id="accordions">Accordions</h3>
<p>The <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/details"><code>&lt;details&gt;</code> element</a>, its <a href="https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/details#name"><code>name</code> attribute</a> for mutually exclusive elements, and the <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/::details-content"><code>::details-content</code></a> pseudo-element remove the need for accordion components like the <a href="https://getbootstrap.com/docs/5.3/components/accordion/">Bootstrap Accordion</a> or the <a href="https://mui.com/material-ui/react-accordion/">React Accordion component</a>.</p>
<p>Just using the platform here means it’s easier for folks who know HTML/CSS to understand your code without having to first learn to use a specific library. It also means you’re immune to breaking changes in the library or the discontinuation of that library. And, of course, it means less code to download and run. Mutually exclusive details elements don’t need JS to open, close, or animate.</p>
<h3 id="css-syntax">CSS Syntax</h3>
<p><a href="https://developer.mozilla.org/docs/Web/CSS/@layer">Cascade layers</a>, for a more organized CSS codebase, <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/Nesting_selector">CSS nesting</a>, for more compact CSS, new color functions, <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_colors/Relative_colors">relative colors</a>, and <a href="https://developer.mozilla.org/docs/Web/CSS/color_value/color-mix"><code>color-mix</code></a>, new Maths functions like <a href="https://developer.mozilla.org/docs/Web/CSS/abs"><code>abs()</code></a>, <a href="https://developer.mozilla.org/docs/Web/CSS/sign"><code>sign()</code></a>, <a href="https://developer.mozilla.org/docs/Web/CSS/pow"><code>pow()</code></a> and others help reduce dependencies on <a href="https://css-tricks.com/is-it-time-to-un-sass/">CSS pre-processors</a>, utility libraries like Bootstrap and Tailwind, or even runtime CSS-in-JS libraries.</p>
<p>The game changer <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Selectors/:has"><code>:has()</code></a>, one of the most requested features for a long time, removes the need for more complicated JS-based solutions.</p>
<h3 id="js-utilities">JS Utilities</h3>
<p>Modern Array methods like <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast"><code>findLast()</code></a>, or <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/at"><code>at()</code></a>, as well as Set methods like <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/difference"><code>difference()</code></a>, <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection"><code>intersection()</code></a>, <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set/union"><code>union()</code></a> and others can reduce dependencies on libraries like <a href="https://lodash.com/">Lodash</a>.</p>
<h3 id="container-queries">Container Queries</h3>
<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_containment/Container_queries">Container queries</a> make UI components respond to things other than the viewport size, and therefore make them more reusable across different contexts.</p>
<p>No need to use a JS-heavy UI library for this anymore, and no need to use a <a href="https://github.com/GoogleChromeLabs/container-query-polyfill">polyfill</a> either.</p>
<h3 id="layout">Layout</h3>
<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout">Grid</a>, <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout/Subgrid">subgrid</a>, <a href="https://developer.mozilla.org/docs/Learn_web_development/Core/CSS_layout/Flexbox">flexbox</a>, or <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/columns">multi-column</a> have been around for a long time now, but looking at the <a href="https://2025.stateofcss.com/en-US">results of the State of CSS surveys</a>, it’s clear that developers tend to be very cautious with adopting new things, and wait for a very long time before they do.</p>
<p>These features have been <a href="https://web-platform-dx.github.io/web-features/">Baseline</a> for a long time and you could use them to get rid of dependencies on things like the <a href="https://getbootstrap.com/docs/5.3/layout/grid/">Bootstrap’s grid system</a>, <a href="https://get.foundation/sites/docs/flexbox-utilities.html">Foundation Framework’s flexbox utilities</a>, <a href="https://bulma.io/documentation/grid/fixed-grid/">Bulma fixed grid</a>, <a href="https://materializecss.com/grid.html">Materialize grid</a>, or <a href="https://tailwindcss.com/docs/columns">Tailwind columns</a>.</p>
<p>I’m not saying you should drop your framework. Your team adopted it for a reason, and removing it might be a big project. But looking at what the web platform can offer without a third-party wrapper on top comes with a lot of benefits.</p>
<h2 id="things-you-might-not-need-anymore-in-the-near-future">Things You Might Not Need Anymore In The Near Future</h2>
<p>Now, let’s take a quick look at some of the things you will not need a library for in the near future. That is to say, the things below are not quite ready for mass adoption, but being aware of them and planning for potential later use can be helpful.</p>
<h3 id="anchor-positioning">Anchor Positioning</h3>
<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_anchor_positioning">CSS anchor positioning</a> handles the positioning of popovers and tooltips relative to other elements, and takes care of keeping them in view, even when moving, scrolling, or resizing the page.</p>
<p>This is a great complement to the Popover API mentioned before, which will make it even easier to migrate away from more performance-intensive JS solutions.</p>
<h3 id="navigation-api">Navigation API</h3>
<p>The <a href="https://developer.mozilla.org/docs/Web/API/Navigation_API">Navigation API</a> can be used to handle navigation in single-page apps and might be a great complement, or even a replacement, to <a href="https://reactrouter.com/">React Router</a>, <a href="https://nextjs.org/docs/routing/introduction">Next.js routing</a>, or <a href="https://angular.io/guide/router">Angular routing tasks</a>.</p>
<h3 id="view-transitions-api">View Transitions API</h3>
<p>The <a href="https://developer.mozilla.org/docs/Web/API/View_Transition_API">View Transitions API</a> can animate between the different states of a page. On a single-page application, this makes smooth transitions between states very easy, and can help you get rid of animation libraries such as <a href="https://animejs.com/">Anime.js</a>, <a href="https://greensock.com/gsap/">GSAP</a>, or <a href="https://motion.dev/">Motion.dev</a>.</p>
<p>Even better, the API can also be used with multiple-page applications.</p>
<p>Remember earlier, when I said that the reason we built single-page apps at the company where I worked 15 years ago was to avoid the white flash of page reloads when navigating? Had that API been available at the time, we would have been able to achieve beautiful page transition effects without a single-page framework and without a huge initial download of the entire app.</p>
<h3 id="scroll-driven-animations">Scroll-driven Animations</h3>
<p><a href="https://developer.mozilla.org/docs/Web/CSS/CSS_scroll-driven_animations">Scroll-driven animations</a> run on the user’s scroll position, rather than over time, making them a great solution for storytelling and product tours.</p>
<p>Some people <a href="https://gt-era.com/">have gone a bit over the top</a> with it, but when used well, this can be a very effective design tool, and can help get rid of libraries like: <a href="https://scrollrevealjs.org/">ScrollReveal</a>, <a href="https://gsap.com/scroll/">GSAP Scroll</a>, or <a href="https://wowjs.uk/">WOW.js</a>.</p>
<h3 id="customizable-selects">Customizable Selects</h3>
<p>A <a href="https://developer.mozilla.org/docs/Learn_web_development/Extensions/Forms/Customizable_select">customizable select</a> is a normal <code>&lt;select&gt;</code> element that lets you fully customize its appearance and content, while ensuring accessibility and performance benefits.</p>
<p>This has been a long time coming, and a highly requested feature, and it’s amazing to see it come to the web platform soon. With a built-in customizable select, you can finally ditch all this hard-to-maintain JS code for your custom select components.</p>
<h3 id="css-masonry">CSS Masonry</h3>
<p><a href="https://developer.chrome.com/blog/masonry-update">CSS Masonry</a> is another upcoming web platform feature that I want to spend more time on.</p>
<p>With CSS Masonry, you can achieve layouts that are very hard, or even impossible, with flex, grid, or other built-in CSS layout primitives. Developers often resort to using third-party libraries to achieve Masonry layouts, such as the <a href="https://masonry.desandro.com/">Masonry JS library</a>.</p>
<p>But, more on that later. Let’s wrap this point up before moving on to Masonry.</p>
<div class="partners__lead-place"></div>
<h2 id="why-you-should-care">Why You Should Care</h2>
<p>The job market is full of web developers with experience in JavaScript and the latest frameworks of the day. So, really, what’s the point in learning to use the web platform primitives more, if you can do the same things with the libraries, utilities, and frameworks you already know today?</p>
<p>When an entire industry relies on these frameworks, and you can just pull in the right library, shouldn’t browser vendors just work with these libraries to make them load and run faster, rather than trying to convince developers to use the platform instead?</p>
<p>First of all, we do work with library authors, and we do make frameworks better by learning about what they use and improving those areas.</p>
<p>But secondly, “just using the platform” can bring pretty significant benefits.</p>
<h3 id="sending-less-code-to-devices">Sending Less Code To Devices</h3>
<p>The main benefit is that you end up sending far less code to your clients’ devices.</p>
<p>According to the <a href="https://almanac.httparchive.org/en/2024/">2024 Web Almanac</a>, the average number of HTTP requests is around 70 per site, <a href="https://almanac.httparchive.org/en/2024/javascript#how-many-javascript-requests-per-page">most of which is due to JavaScript with 23 requests</a>. In 2024, JS overtook images as the dominant file type too. The median number of page requests for JS files is 23, up 8% since 2022.</p>
<p>And page size continues to grow year over year. The median page weight is around 2MB now, which is 1.8MB more than it was 10 years ago.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://almanac.httparchive.org/en/2024/page-weight"></p>
<p>    <img loading="lazy" width="800" height="462" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Median page weight" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://almanac.httparchive.org/en/2024/page-weight">Web Almanac</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/3-median-page-weight.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Sure, your internet connection speed has probably increased, too, but that’s not the case for everyone. And not everyone has the same device capabilities either.</p>
<p>Pulling in third-party code for things you can do with the platform, instead, most probably means you ship more code, and therefore reach fewer customers than you normally would. On the web, bad loading performance leads to large abandonment rates and hurts brand reputation.</p>
<h3 id="running-less-code-on-devices">Running Less Code On Devices</h3>
<p>Furthermore, the code you do ship on your customers’ devices likely runs faster if it uses fewer JavaScript abstractions on top of the platform. It’s also probably more responsive and more accessible by default. All of this leads to more and happier customers.</p>
<p>Check my colleague Alex Russell’s <a href="https://infrequently.org/2024/01/performance-inequality-gap-2024/">yearly performance inequality gap blog</a>, which shows that premium devices are largely absent from markets with billions of users due to wealth inequality. And this gap is only growing over time.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://infrequently.org/2024/01/performance-inequality-gap-2024/#device-performance"></p>
<p>    <img loading="lazy" width="800" height="452" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Device performance scores" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://infrequently.org/2024/01/performance-inequality-gap-2024/#device-performance">Infrequently Noted</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/4-device-performance.png">Large preview</a>)<br />
    </figcaption></figure>
<h2 id="built-in-masonry-layout">Built-in Masonry Layout</h2>
<p>One web platform feature that’s coming soon and which I’m very excited about is CSS Masonry.</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png"></p>
<p>    <img loading="lazy" width="800" height="459" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="CSS Masonry" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/5-css-masonry.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Let me start by explaining what Masonry is.</p>
<h3 id="what-is-masonry">What Is Masonry</h3>
<blockquote><p>Masonry is a type of layout that was made popular by Pinterest years ago. It creates independent tracks of content within which items pack themselves up as close to the start of the track as they can.</p></blockquote>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png"></p>
<p>    <img loading="lazy" width="800" height="604" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      <a href="pinterest.com">Pinterest</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/6-pinterest-portfolio.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Many people see Masonry as a great option for portfolios and photo galleries, which it certainly can do. But Masonry is <strong>more flexible</strong> than what you see on Pinterest, and it’s <strong>not limited to just waterfall-like layouts</strong>.</p>
<p>In a Masonry layout:</p>
<ul>
<li>Tracks can be columns or rows:</li>
</ul>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png"></p>
<p>    <img loading="lazy" width="800" height="450" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout with columns and rows" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/7-layout-columns-rows.png">Large preview</a>)<br />
    </figcaption></figure>
<ul>
<li>Tracks of content don’t all have to be the same size:</li>
</ul>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png"></p>
<p>    <img loading="lazy" width="800" height="664" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout with tracks of different sizes" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/8-layout-different-sizes.png">Large preview</a>)<br />
    </figcaption></figure>
<ul>
<li>Items can span multiple tracks:</li>
</ul>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png"></p>
<p>    <img loading="lazy" width="800" height="565" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout with multiple tracks" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/9-layout-multiple-tracks.png">Large preview</a>)<br />
    </figcaption></figure>
<ul>
<li>Items can be placed on specific tracks; they don’t have to always follow the automatic placement algorithm:</li>
</ul>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png"></p>
<p>    <img loading="lazy" width="800" height="628" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout with items on specific tracks" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/10-layout-items-specific-tracks.png">Large preview</a>)<br />
    </figcaption></figure>
<h3 id="demos">Demos</h3>
<p>Here are a few simple demos I made by using the upcoming implementation of CSS Masonry in Chromium.</p>
<p><a href="https://microsoftedge.github.io/Demos/css-masonry/new-york.html">A photo gallery demo</a>, showing how items (the title in this case) can span multiple tracks:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png"></p>
<p>    <img loading="lazy" width="800" height="560" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A photo gallery demo, showing items on multiple tracks" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/11-photo-gallery-different-sizes.png">Large preview</a>)<br />
    </figcaption></figure>
<p>Another <a href="https://microsoftedge.github.io/Demos/css-masonry/nature.html">photo gallery showing tracks of different sizes</a>:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png"></p>
<p>    <img loading="lazy" width="800" height="437" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A photo gallery showing tracks of different sizes" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/12-photo-gallery-different-tracks.png">Large preview</a>)<br />
    </figcaption></figure>
<p>A <a href="https://microsoftedge.github.io/Demos/css-masonry/the-daily-oddity.html">news site layout</a> with some tracks wider than others, and some items spanning the entire width of the layout:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png"></p>
<p>    <img loading="lazy" width="800" height="607" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A news site layout with some tracks wider than others" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/13-news-site-layout.png">Large preview</a>)<br />
    </figcaption></figure>
<p>A <a href="https://microsoftedge.github.io/Demos/css-masonry/kanban.html">kanban board</a> showing that items can be placed onto specific tracks:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png"></p>
<p>    <img loading="lazy" width="800" height="320" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A kanban board with items on specific tracks" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/14-kanban-board.png">Large preview</a>)<br />
    </figcaption></figure>
<p><strong>Note</strong>: <em>The previous demos were made with a version of Chromium that’s not yet available to most web users, because CSS Masonry is only just starting to be implemented in browsers.</em></p>
<p>However, web developers have been happily using libraries to create Masonry layouts for years already.</p>
<h3 id="sites-using-masonry-today">Sites Using Masonry Today</h3>
<p>Indeed, Masonry is pretty common on the web today. Here are a few examples I found besides Pinterest:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png"></p>
<p>    <img loading="lazy" width="800" height="458" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Erik Johansson&#039;s photography site" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://www.erikjo.com/work">Erik Johansson</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/15-site-masonry.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png"></p>
<p>    <img loading="lazy" width="800" height="456" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Kristian Hammerstad&#039;s site" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://www.kristianhammerstad.com/">Kristian Hammerstad</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/16-masonry-site.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png"></p>
<p>    <img loading="lazy" width="800" height="479" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="L&#039;usine a Gouzou&#039;s catalogue" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://lusineagouzou.fr/catalogue">L&#8217;usine a Gouzou</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/17-masonry-site.png">Large preview</a>)<br />
    </figcaption></figure>
<p>And a few more, less obvious, examples:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png"></p>
<p>    <img loading="lazy" width="800" height="428" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout from Agora" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      A row-direction Masonry layout from <a href="http://agora.io/en/">www.agora.io</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/18-masonry-layout.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png"></p>
<p>    <img loading="lazy" width="800" height="633" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Different size tracks from The Free Dictionary" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Different size tracks from <a href="https://www.thefreedictionary.com/">www.thefreedictionary.com</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/19-different-size-tracks.png">Large preview</a>)<br />
    </figcaption></figure>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png"></p>
<p>    <img loading="lazy" width="800" height="605" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Masonry layout of OneSignal" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      Image source: <a href="https://onesignal.com/">OneSignal</a>. (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/20-masonry-layout.png">Large preview</a>)<br />
    </figcaption></figure>
<p>So, how were these layouts created?</p>
<h2 id="workarounds">Workarounds</h2>
<p>One trick that I’ve seen used is using a Flexbox layout instead, changing its direction to column, and setting it to wrap.</p>
<p>This way, you can place items of different heights in multiple, independent columns, giving the impression of a Masonry layout:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png"></p>
<p>    <img loading="lazy" width="800" height="578" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Flexbox layout" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/21-flexbox-layout.png">Large preview</a>)<br />
    </figcaption></figure>
<p>There are, however, two limitations with this workaround:</p>
<ol>
<li>The order of items is different from what it would be with a real Masonry layout. With Flexbox, items fill the first column first and, when it’s full, then go to the next column. With Masonry, items would stack in whichever track (or column in this case) has more space available.</li>
<li>But also, and perhaps more importantly, this workaround requires that you set a fixed height to the Flexbox container; otherwise, no wrapping would occur.</li>
</ol>
<h2 id="third-party-masonry-libraries">Third-party Masonry Libraries</h2>
<p>For more advanced cases, developers have been using libraries.</p>
<p>The most well-known and popular library for this is simply called <a href="https://masonry.desandro.com/">Masonry</a>, and it gets downloaded about 200,000 times per week <a href="https://www.npmjs.com/package/masonry-layout">according to NPM</a>.</p>
<p>Squarespace also provides a <a href="https://www.beyondspace.studio/blog/squarespace-masonry-gallery-layout-guide#method-2-using-gallery-section">layout component that renders a Masonry layout</a>, for a no-code alternative, and many sites use it.</p>
<p>Both of these options use JavaScript code to place items in the layout.</p>
<div class="partners__lead-place"></div>
<h2 id="built-in-masonry">Built-in Masonry</h2>
<p>I’m really excited that Masonry is now starting to appear in browsers as a built-in CSS feature. Over time, you will be able to use Masonry just like you do Grid or Flexbox, that is, without needing any workarounds or third-party code.</p>
<p>My team at Microsoft has been implementing built-in Masonry support in the Chromium open source project, which Edge, Chrome, and many other browsers are based on. Mozilla was actually the first browser vendor to <a href="https://github.com/w3c/csswg-drafts/issues/4650">propose an experimental implementation of Masonry</a> back in 2020. And <a href="https://webkit.org/blog/15269/help-us-invent-masonry-layouts-for-css-grid-level-3/">Apple has also been very interested</a> in making this new web layout primitive happen.</p>
<p>The work to standardize the feature is also moving ahead, with agreement within the CSS working group about the general direction and even a new display type <a href="https://github.com/w3c/csswg-drafts/issues/12022#issuecomment-3525043825"><code>display: grid-lanes</code></a>.</p>
<p>If you want to learn more about Masonry and track progress, check out my <a href="https://patrickbrosset.com/lab/css-masonry-resources/">CSS Masonry resources</a> page.</p>
<p>In time, when Masonry becomes a Baseline feature, just like Grid or Flexbox, we’ll be able to simply use it and benefit from:</p>
<ul>
<li>Better performance,</li>
<li>Better responsiveness,</li>
<li>Ease of use and simpler code.</li>
</ul>
<p>Let’s take a closer look at these.</p>
<h3 id="better-performance">Better Performance</h3>
<p>Making your own Masonry-like layout system, or using a third-party library instead, means you’ll have to run JavaScript code to place items on the screen. This also means that this code will be <em>render blocking</em>. Indeed, either nothing will appear, or things won’t be in the right places or of the right sizes, until that JavaScript code has run.</p>
<p>Masonry layout is often used for the main part of a web page, which means the code would be making your main content appear later than it could otherwise have, degrading your <a href="https://web.dev/articles/lcp#what-is-lcp">LCP, or Largest Contentful Paint metric</a>, which plays a big role in perceived performance and search engine optimization.</p>
<p>I tested the Masonry JS library with a simple layout and by simulating a slow 4G connection in DevTools. The library is not very big (24KB, 7.8KB gzipped), but it took 600ms to load under my test conditions.</p>
<p>Here is a performance recording showing that long 600ms load time for the Masonry library, and that no other rendering activity happened while that was happening:</p>
<figure class="
  
    break-out article__image
  
  
  "></p>
<p>    <a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png"></p>
<p>    <img loading="lazy" width="800" height="541" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="A performance recording showing 600ms load time for the Masonry library" class="lazyload" data-src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png"></p>
<p>    </a><figcaption class="op-vertical-bottom">
      (<a href="https://files.smashing.media/articles/masonry-things-you-wont-need-library-anymore/22-performance-recording.png">Large preview</a>)<br />
    </figcaption></figure>
<p>In addition, after the initial load time, the downloaded script then needed to be parsed, compiled, and then run. All of which, as mentioned before, was blocking the rendering of the page.</p>
<p>With a built-in Masonry implementation in the browser, we won’t have a script to load and run. The browser engine will just do its thing during the initial page rendering step.</p>
<h3 id="better-responsiveness">Better Responsiveness</h3>
<p>Similar to when a page first loads, resizing the browser window leads to rendering the layout in that page again. At this point, though, if the page is using the Masonry JS library, there’s no need to load the script again, because it’s already here. However, the code that moves items in the right places needs to run.</p>
<p>Now this particular library seems to be pretty fast at doing this when the page loads. However, it animates the items when they need to move to a different place on window resize, and this makes a big difference.</p>
<p>Of course, users don’t spend time resizing their browser windows as much as we developers do. But this animated resizing experience can be pretty jarring and adds to the perceived time it takes for the page to adapt to its new size.</p>
<h3 id="ease-of-use-and-simpler-code">Ease Of Use And Simpler Code</h3>
<p>How easy it is to use a web feature and how simple the code looks are important factors that can make a big difference for your team. They can’t ever be as important as the final user experience, of course, but developer experience impacts maintainability. Using a built-in web feature comes with important benefits on that front:</p>
<ul>
<li>Developers who already know HTML, CSS, and JS will most likely be able to use that feature easily because it’s been designed to integrate well and be consistent with the rest of the web platform.</li>
<li>There’s no risk of breaking changes being introduced in how the feature is used.</li>
<li>There’s almost zero risk of that feature becoming deprecated or unmaintained.</li>
</ul>
<p>In the case of built-in Masonry, because it’s a layout primitive, you use it from CSS, just like Grid or Flexbox, no JS involved. Also, other layout-related CSS properties, such as gap, work as you’d expect them to. There are no tricks or workarounds to know about, and the things you do learn are documented on MDN.</p>
<p>For the Masonry JS lib, initialization is a bit complex: it requires a data attribute with a specific syntax, along with hidden HTML elements to set the column and gap sizes.</p>
<p>Plus, if you want to span columns, you need to include the gap size yourself to avoid problems:</p>
<div class="break-out">
<pre><code class="language-html">&lt;script src="https://unpkg.com/masonry-layout@4.2.2/dist/masonry.pkgd.min.js"&gt;&lt;/script&gt;
&lt;style&gt;
  .track-sizer,
  .item {
    width: 20%;
  }
  .gutter-sizer {
    width: 1rem;
  }
  .item {
    height: 100px;
    margin-block-end: 1rem;
  }
  .item:nth-child(odd) {
    height: 200px;
  }
  .item--width2 {
    width: calc(40% + 1rem);
  }
&lt;/style&gt;

&lt;div class="container"
  data-masonry='{ "itemSelector": ".item", "columnWidth": ".track-sizer", "percentPosition": true, "gutter": ".gutter-sizer" }'&gt;
  &lt;div class="track-sizer"&gt;&lt;/div&gt;
  &lt;div class="gutter-sizer"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  &lt;div class="item item--width2"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  ...
&lt;/div&gt;
</code></pre>
</div>
<p>Let’s compare this to what a built-in Masonry implementation would look like:</p>
<pre><code class="language-html">&lt;style&gt;
  .container {
    display: grid-lanes;
    grid-lanes: repeat(4, 20%);
    gap: 1rem;
  }
  .item {
    height: 100px;
  }
  .item:nth-child(odd) {
    height: 200px;
  }
  .item--width2 {
    grid-column: span 2;
  }
&lt;/style&gt;

&lt;div class="container"&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  &lt;div class="item item--width2"&gt;&lt;/div&gt;
  &lt;div class="item"&gt;&lt;/div&gt;
  ...
&lt;/div&gt;
</code></pre>
<p>Simpler, more compact code that can just use things like <code>gap</code> and where spanning tracks is done with <code>span 2</code>, just like in grid, and doesn’t require you to calculate the right width that includes the gap size.</p>
<h2 id="how-to-know-what-s-available-and-when-it-s-available">How To Know What’s Available And When It’s Available?</h2>
<p>Overall, the question isn’t really if you should use built-in Masonry over a JS library, but rather <em>when</em>. The Masonry JS library is amazing and has been filling a gap in the web platform for many years, and for many happy developers and users. It has a few drawbacks if you compare it to a built-in Masonry implementation, of course, but those are not important if that implementation isn’t ready.</p>
<p>It’s easy for me to list these cool new web platform features because I work at a browser vendor, and I therefore tend to know what’s coming. But developers often share, survey after survey, that keeping track of new things is hard. <strong>Staying informed is difficult</strong>, and companies don’t always prioritize learning anyway.</p>
<p>To help with this, here are a few resources that provide updates in simple and compact ways so you can get the information you need quickly:</p>
<ul>
<li><a href="https://web-platform-dx.github.io/web-features-explorer/">The Web platform features explorer site</a>:
<ul>
<li>You might be interested in its <a href="https://web-platform-dx.github.io/web-features-explorer/release-notes/october-2025/">release notes</a> page.</li>
<li>And, if you like RSS, check out <a href="https://web-platform-dx.github.io/web-features-explorer/release-notes.xml">the release notes feed</a>, as well as the Baseline <a href="https://web-platform-dx.github.io/web-features-explorer/newly-available.xml">Newly Available</a> and <a href="https://web-platform-dx.github.io/web-features-explorer/widely-available.xml">Widely Available</a> feeds.</li>
</ul>
</li>
<li><a href="https://webstatus.dev/">The Web Platform Status dashboard</a>:
<ul>
<li>You might like its various <a href="https://webstatus.dev/?q=baseline_date%3A2025-01-01..2025-12-31">Baseline year</a> pages.</li>
</ul>
</li>
<li><a href="https://chromestatus.com/roadmap">Chrome Platform Status’ roadmap page</a>.</li>
</ul>
<p>If you have a bit more time, you might also be interested in browser vendors’ release notes:</p>
<ul>
<li><a href="https://developer.chrome.com/release-notes">Chrome</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-edge/web-platform/release-notes/">Edge</a></li>
<li><a href="https://www.firefox.com/en-US/releases/">Firefox</a></li>
<li><a href="https://developer.apple.com/documentation/safari-release-notes">Safari</a></li>
</ul>
<p>For even more resources, check out my <a href="https://patrickbrosset.com/lab/navigating-the-web-platform/">Navigating the Web Platform Cheatsheet</a>.</p>
<h2 id="my-thing-is-still-not-implemented">My Thing Is Still Not Implemented</h2>
<p>That’s the other side of the problem. Even if you do find the time, energy, and ways to keep track, there’s still frustration with getting your voice heard and your favorite features implemented.</p>
<p>Maybe you’ve been waiting for years for a specific bug to be resolved, or a specific feature to ship in a browser where it’s still missing.</p>
<p>What I’ll say is <strong>browser vendors do listen</strong>. I’m part of several cross-organization teams where we discuss developer signals and feedback all the time. We look at many different sources of feedback, both internal at each browser vendor and external/public on forums, open source projects, blogs, and surveys. And, we’re always trying to create better ways for developers to share their specific needs and use cases.</p>
<p>So, if you can, please demand more from browser vendors and pressure us to implement the features you need. I get that it takes time, and can also be intimidating (not to mention a high barrier to entry), but it also works.</p>
<p>Here are a few ways you can get your (or your company’s) voice heard: Take the annual <a href="https://stateofjs.com/">State of JS</a>, <a href="https://stateofcss.com/">State of CSS</a>, and <a href="https://stateofhtml.com/">State of HTML</a> surveys. They play a big role in how browser vendors prioritize their work.</p>
<p>If you need a specific standard-based API to be implemented consistently across browsers, consider submitting a proposal at the next <a href="https://github.com/web-platform-tests/interop/">Interop project</a> iteration. It requires more time, but consider how <a href="https://docs.google.com/document/d/1ICqlNtdRXlhIlRuXFr1BRgy68R6Q5AwPv2b4hsIWUMY/edit">Shopify</a> and <a href="https://www.rumvision.com/blog/interop-2026-key-apis-for-sitespeed-and-rum/">RUMvision</a> shared their wish lists for Interop 2026. Detailed information like this can be very useful for browser vendors to prioritize.</p>
<p>For more useful links to influence browser vendors, check out my <a href="https://patrickbrosset.com/lab/navigating-the-web-platform/">Navigating the Web Platform Cheatsheet</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>To close, I hope this article has left you with a few things to think about:</p>
<ul>
<li>Excitement for Masonry and other upcoming web features.</li>
<li>A few web features you might want to start using.</li>
<li>A few pieces of custom or 3rd-party code you might be able to remove in favor of built-in features.</li>
<li>A few ways to keep track of what’s coming and influence browser vendors.</li>
</ul>
<p>More importantly, I hope I’ve convinced you of the benefits of using the web platform to its full potential.</p>
<div class="signature">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Smashing Editorial" width="35" height="46" loading="lazy" class="lazyload" data-src="https://www.smashingmagazine.com/images/logo/logo--red.png"><br />
  <span>(yk)</span>
</div>
</article>
]]></content:encoded>
					
					<wfw:commentRss>http://computercoursesonline.com/index.php/2025/12/02/masonry-things-you-wont-need-a-library-for-anymore/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
