<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: richi.codes</title>
    <description>The latest articles on DEV Community by richi.codes (@richicodes).</description>
    <link>https://dev.to/richicodes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F10997%2Fdibuix_rixi.jpg</url>
      <title>DEV Community: richi.codes</title>
      <link>https://dev.to/richicodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richicodes"/>
    <language>en</language>
    <item>
      <title>CSS style queries: one variable, every style</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:30:43 +0000</pubDate>
      <link>https://dev.to/richicodes/css-style-queries-one-variable-every-style-3geo</link>
      <guid>https://dev.to/richicodes/css-style-queries-one-variable-every-style-3geo</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-28-css-style-queries-one-variable-every-style" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now write &lt;code&gt;@container style(--status: warn)&lt;/code&gt; and have CSS react to the value of a custom property, in every major browser. Container style queries became Baseline on May 19, 2026, when Firefox 151 shipped them, joining Chrome and Edge 111 and Safari 18.&lt;/p&gt;

&lt;p&gt;Size container queries got all the attention. Style queries are the quieter half of the same feature, and I think they're the more surprising one: instead of asking "how wide is my container?", a rule asks "what value does my container have for this variable?". That turns a custom property into a switch that CSS can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem it solves
&lt;/h2&gt;

&lt;p&gt;Think of a status card with three states: operational, degraded, down. Each state changes the border, the glow, the icon, the badge text and the colour of a chart. The usual answer is a modifier class or a &lt;code&gt;data-status&lt;/code&gt; attribute, plus one selector per state for every piece of the card.&lt;/p&gt;

&lt;p&gt;With a style query, the state lives in one custom property, and every descendant can ask about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"--status: warn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Degraded&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;212&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;170&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;251&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;191&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;36&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole trick in the demo above. Each card carries &lt;code&gt;style="--status: ok"&lt;/code&gt; and the only thing the JavaScript does on click is &lt;code&gt;card.style.setProperty('--status', next)&lt;/code&gt;. The colour, the &lt;code&gt;▲&lt;/code&gt;/&lt;code&gt;✕&lt;/code&gt; icon, the badge text (via &lt;code&gt;content: var(--badge)&lt;/code&gt;), the latency figure, the progress bar and the pulsing ring are all CSS reacting to that value. The board view uses a second variable, &lt;code&gt;--view: list&lt;/code&gt;, on the board element, and even the highlighted line in the code panel is a style query reading &lt;code&gt;--last&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What makes this different from an attribute selector is that custom properties inherit and cascade. The value can come from a parent, a theme class higher up, a media query or an inline style, and every element below it can query it without knowing where it was set.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A few rules that I checked against MDN and the spec, and then in the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;container-type&lt;/code&gt; needed.&lt;/strong&gt; Every element is a style container. You only need &lt;code&gt;container-type&lt;/code&gt; for size queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An element queries its ancestors, not itself.&lt;/strong&gt; The query is evaluated against an ancestor container, so in the example above &lt;code&gt;.badge&lt;/code&gt; can react to &lt;code&gt;--status&lt;/code&gt;, but a rule that tries to style &lt;code&gt;.card&lt;/code&gt; itself from its own &lt;code&gt;--status&lt;/code&gt; won't match. That's why every card in the demo has an inner &lt;code&gt;.body&lt;/code&gt; that carries the visual styles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stick to custom properties.&lt;/strong&gt; The spec also allows normal properties, as in &lt;code&gt;style(color: red)&lt;/code&gt;, but MDN's guide says custom properties are the only style feature currently supported, and that query didn't match in the Chromium I tested with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A query without a value checks "is it set?".&lt;/strong&gt; &lt;code&gt;style(--flag)&lt;/code&gt; is true when the computed value differs from the initial value. For an unregistered custom property, that means any value at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gotcha: plain matching compares tokens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;style(--status: warn)&lt;/code&gt; compares the computed value of the property with the value you wrote. For an unregistered custom property, that computed value is basically the text you wrote, so &lt;code&gt;--accent: #00f&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; match &lt;code&gt;style(--accent: blue)&lt;/code&gt;, and &lt;code&gt;--n: calc(6/2)&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; match &lt;code&gt;style(--n: 3)&lt;/code&gt;. I tested both in Chromium and neither matches.&lt;/p&gt;

&lt;p&gt;If you need equivalent values to match, register the property with a type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="l"&gt;--accent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;color&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Now --accent: #00f matches too */&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For keyword-like states such as &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt;, plain matching is exactly what you want, and you don't need &lt;code&gt;@property&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support and other gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Support:&lt;/strong&gt; Chrome and Edge 111, Safari 18, Firefox 151. Baseline "newly available" since May 19, 2026, so check your audience before you drop a fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari and &lt;code&gt;:root&lt;/code&gt;:&lt;/strong&gt; MDN's compatibility data notes that in Safari the document element can't be a container. If you set a variable on &lt;code&gt;:root&lt;/code&gt; and query it, the children of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; are fine, because &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; inherits the value and acts as the container. A rule that styles &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; itself from it won't match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature detection:&lt;/strong&gt; &lt;code&gt;@supports&lt;/code&gt; can't test an at-rule like this. In the demo I use a tiny probe: a hidden element sets &lt;code&gt;--sq: yes&lt;/code&gt;, a style query gives its child a 7 px width, and JavaScript checks the computed width. If it doesn't match, the page adds a class and falls back to &lt;code&gt;data-status&lt;/code&gt; attribute selectors that set the same variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range syntax isn't Baseline.&lt;/strong&gt; MDN documents a newer form with comparison operators, such as &lt;code&gt;style(--columns &amp;gt;= 3)&lt;/code&gt;, which compares values numerically. MDN's compatibility data lists Chrome 142 and no Safari support yet, so I left it out of the demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Range syntax across engines.&lt;/strong&gt; Numeric comparisons like &lt;code&gt;style(0 &amp;lt; --n &amp;lt; 10)&lt;/code&gt; are where style queries get really expressive. Once Safari ships them, a lot of "set a class from JavaScript when the number crosses a threshold" code can go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Querying normal properties.&lt;/strong&gt; &lt;code&gt;style(display: flex)&lt;/code&gt; or &lt;code&gt;style(font-weight: bold)&lt;/code&gt; are in the spec, but you can't count on them in every browser yet. When they arrive, components will be able to adapt to how their parent is styled, not only to variables you set on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design systems.&lt;/strong&gt; A single &lt;code&gt;--variant&lt;/code&gt; or &lt;code&gt;--tone&lt;/code&gt; variable on a component, with style queries inside it, is a clean alternative to stacks of modifier classes. I expect to see that pattern a lot more now that it works everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_size_and_style_queries" rel="noopener noreferrer"&gt;Using container size and style queries | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container" rel="noopener noreferrer"&gt;@container | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/151" rel="noopener noreferrer"&gt;Firefox 151 release notes for developers | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-conditional-5/#style-container" rel="noopener noreferrer"&gt;CSS Conditional Rules Module Level 5: Style Container Features&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-28-css-style-queries-one-variable-every-style" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>containerqueries</category>
      <category>customproperties</category>
      <category>baseline</category>
    </item>
    <item>
      <title>CSS light-dark() now swaps images, not just colors</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/css-light-dark-now-swaps-images-not-just-colors-53g2</link>
      <guid>https://dev.to/richicodes/css-light-dark-now-swaps-images-not-just-colors-53g2</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-27-css-light-dark-now-swaps-images-not-just-colors" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;light-dark()&lt;/code&gt; has been swapping colors for a while. Now it swaps images too: give it two gradients, two &lt;code&gt;url()&lt;/code&gt;s, or an image and &lt;code&gt;none&lt;/code&gt;, and the browser picks one based on the element's color scheme. As of September 14, 2026 that works in Chrome, Edge, Firefox and Safari, which makes it Baseline newly available.&lt;/p&gt;

&lt;p&gt;The landscape in the demo above has no image files and no theming JavaScript. The sky, the sun that turns into a moon, the stars, the clouds, the hills and the lit windows are all &lt;code&gt;background-image: light-dark(day, night)&lt;/code&gt;. The JavaScript in the demo only does two things: it changes the scene's &lt;code&gt;color-scheme&lt;/code&gt; when you click Day or Night, and it writes the code panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Dark mode always had a gap. Colors were easy with &lt;code&gt;light-dark()&lt;/code&gt; or custom properties, but any time an image had to change (a logo, a hero gradient, a list bullet, a decorative background) you went back to &lt;code&gt;@media (prefers-color-scheme: dark)&lt;/code&gt; and duplicated the rule.&lt;/p&gt;

&lt;p&gt;A media query only knows what the user picked in their system or browser. It doesn't know that one section of your page is forced to &lt;code&gt;color-scheme: dark&lt;/code&gt;. &lt;code&gt;light-dark()&lt;/code&gt; resolves against the color scheme of the element it's applied to, so the same rule does the right thing in a dark sidebar sitting inside a light page. The three logo tiles at the bottom of the demo share one CSS rule: one tile is &lt;code&gt;color-scheme: light&lt;/code&gt;, one is &lt;code&gt;dark&lt;/code&gt;, and one is &lt;code&gt;light dark&lt;/code&gt;, which follows your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The spec now defines two forms. The color form you already know, and an image form where each argument is an &lt;code&gt;&amp;lt;image&amp;gt;&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;#38bdf8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;#fde68a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;/* light */&lt;/span&gt;
    &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;#050816&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;#4c1d95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;/* dark */&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument is used when the element's color scheme is light. The second is used when it's dark. As MDN notes, you have to opt in with &lt;code&gt;color-scheme: light dark&lt;/code&gt; (usually on &lt;code&gt;:root&lt;/code&gt;). Without it, the page stays light and you always get the first image.&lt;/p&gt;

&lt;p&gt;To force one area to a scheme, set &lt;code&gt;color-scheme&lt;/code&gt; on that element. That's exactly what the demo's switch does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.scene&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"light"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.scene&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;none&lt;/code&gt; is valid on either side. The spec defines it as a fully transparent image with no natural size, and in Chrome the computed value reads &lt;code&gt;image(rgba(0, 0, 0, 0))&lt;/code&gt;. It's handy for layers that should only appear in one scheme, like the stars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.stars&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;#fff&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;51%&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works anywhere an image goes, not only on backgrounds. The spec's own example uses it for list bullets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="nc"&gt;.fancy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;list-style-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"light.svg"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"dark.svg"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked all of these snippets in Chromium by reading &lt;code&gt;getComputedStyle()&lt;/code&gt; under a light and a dark scheme. Each time the browser returned the image I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha that cost me a layer
&lt;/h2&gt;

&lt;p&gt;My first version of the demo had no stars. I had written this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Does NOT work: three images on the dark side */&lt;/span&gt;
&lt;span class="nc"&gt;.stars&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each side of &lt;code&gt;light-dark()&lt;/code&gt; takes a single image. A comma-separated list makes the whole value invalid, and the property falls back to &lt;code&gt;none&lt;/code&gt;. The fix is one &lt;code&gt;light-dark()&lt;/code&gt; per layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.stars&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--star1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--star2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--star3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;var()&lt;/code&gt; it's harder to spot. The browser can't reject the declaration while parsing because it doesn't know what the variables contain yet, but in my test the result was still &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support
&lt;/h2&gt;

&lt;p&gt;According to MDN's browser compatibility data, the image form shipped in Chrome and Edge 150, Firefox 150 and Safari 27. The color form has been around much longer: Chrome 123, Firefox 120 and Safari 17.5. If you test only with colors in an older browser, you may wrongly assume images work as well.&lt;/p&gt;

&lt;p&gt;The demo checks support with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light-dark(none, linear-gradient(red, blue))&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns false, the demo shows a notice and switches to a plan B: the same gradients chosen with a &lt;code&gt;data-resolved&lt;/code&gt; attribute. In plain CSS, the fallback can be an &lt;code&gt;@supports&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-dark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fade you see when you switch between day and night isn't part of &lt;code&gt;light-dark()&lt;/code&gt;. It comes from a View Transition (&lt;code&gt;document.startViewTransition&lt;/code&gt;), and the demo skips it when &lt;code&gt;prefers-reduced-motion&lt;/code&gt; is on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Older browsers in your traffic.&lt;/strong&gt; This is newly available, not widely available. Keep a fallback for anything important, like a logo that disappears on a dark background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image loading.&lt;/strong&gt; Before you swap two heavy raster images this way, check in your own DevTools Network panel what each browser downloads and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where you use it.&lt;/strong&gt; Logos, illustrations and decorative gradients are the obvious first targets. Anything that used to need a duplicated rule inside a &lt;code&gt;prefers-color-scheme&lt;/code&gt; query is worth reviewing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/light-dark" rel="noopener noreferrer"&gt;light-dark() CSS function — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-color-5/#typedef-light-dark-image" rel="noopener noreferrer"&gt;CSS Color Module Level 5: light-dark() — W3C Editor's Draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme" rel="noopener noreferrer"&gt;color-scheme CSS property — MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-27-css-light-dark-now-swaps-images-not-just-colors" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>darkmode</category>
      <category>baseline</category>
      <category>images</category>
    </item>
    <item>
      <title>View Transitions: animate any DOM change with one call</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:30:37 +0000</pubDate>
      <link>https://dev.to/richicodes/view-transitions-animate-any-dom-change-with-one-call-4nod</link>
      <guid>https://dev.to/richicodes/view-transitions-animate-any-dom-change-with-one-call-4nod</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-26-view-transitions-animate-any-dom-change" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You change the DOM however you like, wrap that change in &lt;code&gt;document.startViewTransition()&lt;/code&gt;, and the browser animates from the old state to the new one. No FLIP math, no animation library, no keeping two copies of the UI alive. Same-document view transitions have been Baseline since October 14, 2025, and the parts that make them scale (&lt;code&gt;view-transition-class&lt;/code&gt; and transition types) joined them by January 2026.&lt;/p&gt;

&lt;p&gt;The demo above is a bento grid where nothing is animated by hand. The grid simply gets reordered, filtered or switched to a list, and a card turns into a full panel. Turn on &lt;strong&gt;Slow motion&lt;/strong&gt; to watch it, or untick &lt;strong&gt;View Transitions&lt;/strong&gt; to see the same DOM changes without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is and why it matters
&lt;/h2&gt;

&lt;p&gt;Animating a layout change used to mean measuring every element before and after, then faking the motion with transforms. The View Transition API flips that around. When you call &lt;code&gt;startViewTransition(update)&lt;/code&gt;, the browser takes a snapshot of the current page, runs your &lt;code&gt;update&lt;/code&gt; callback, captures the new state, and animates between the two with a tree of pseudo-elements that sits on top of the page.&lt;/p&gt;

&lt;p&gt;By default the whole page is one snapshot called &lt;code&gt;root&lt;/code&gt;, and you get a cross-fade. The spec's user agent stylesheet sets it to a quarter of a second. Give an element its own &lt;code&gt;view-transition-name&lt;/code&gt; and it gets its own group, which the browser animates from its old position and size to its new ones. That's where the "wow" comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the demo works
&lt;/h2&gt;

&lt;p&gt;Everything goes through one function. This is a trimmed version of the one in the demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Plan B: same change, no animation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// If another transition replaces this one before it starts animating,&lt;/span&gt;
  &lt;span class="c1"&gt;// ready rejects with an AbortError. Handle it or the console will complain.&lt;/span&gt;
  &lt;span class="nx"&gt;vt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;vt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Shuffling is just reordering DOM nodes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shuffled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shuffle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nf"&gt;shuffled&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;])));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each card gets a unique name and a shared class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewTransitionName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`tile-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.tile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Don't capture the rest of the page: the toolbar updates instantly */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* One rule for all twelve cards, thanks to the class */&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.tile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.55s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation-timing-function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;view-transition-class&lt;/code&gt; you'd write a rule for &lt;code&gt;tile-0&lt;/code&gt;, another for &lt;code&gt;tile-1&lt;/code&gt;, and so on. A class doesn't need to be unique and doesn't create a group; it's only a styling hook for elements that already have a name.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;types&lt;/code&gt; array is what lets one API do several choreographies. In CSS, &lt;code&gt;:active-view-transition-type()&lt;/code&gt; matches the root element while a transition of that type is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root:active-view-transition-type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;&amp;amp;::view-transition-new(.tile):only-child&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pop-in&lt;/span&gt; &lt;span class="m"&gt;.45s&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.tile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;:only-child&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pop-out&lt;/span&gt; &lt;span class="m"&gt;.3s&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* pop-in and pop-out are ordinary @keyframes (scale + opacity) */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:only-child&lt;/code&gt; trick comes from the spec. When a card is filtered out it only exists in the old state, so its group has an old image and no new one. Cards that come back only have a new image. That's how the demo pops cards in and out while the rest slide into place.&lt;/p&gt;

&lt;p&gt;The card-to-panel morph doesn't need anything new. The name moves from one element to another inside the update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewTransitionName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewTransitionName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// same name, different element&lt;/span&gt;
  &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same name in the old and new state means same group, so the browser animates the card's box into the panel's box and cross-fades the content. I also give that pair a &lt;code&gt;hero&lt;/code&gt; class with &lt;code&gt;::view-transition-group(.hero) { z-index: 1; }&lt;/code&gt;. Without it, in my first version the growing panel was painted underneath the cards that come after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I hit or checked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Names must be unique among rendered elements.&lt;/strong&gt; If two rendered elements share a &lt;code&gt;view-transition-name&lt;/code&gt;, &lt;code&gt;ready&lt;/code&gt; rejects and the transition is skipped. In Chromium I got an &lt;code&gt;InvalidStateError&lt;/code&gt;. Hidden elements (&lt;code&gt;display: none&lt;/code&gt;) don't count, which is why filtering works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interrupting a transition.&lt;/strong&gt; Calling &lt;code&gt;startViewTransition()&lt;/code&gt; while another is active skips the old one with an &lt;code&gt;AbortError&lt;/code&gt;. If that happens before it starts animating, its &lt;code&gt;ready&lt;/code&gt; promise rejects, and with no handler Chromium logs an uncaught "Transition was skipped" error. If it happens mid-animation, &lt;code&gt;ready&lt;/code&gt; has already resolved and &lt;code&gt;finished&lt;/code&gt; simply resolves early. Press &lt;strong&gt;Shuffle&lt;/strong&gt; twice quickly in the demo and the log shows both cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clicks during the animation.&lt;/strong&gt; The &lt;code&gt;::view-transition&lt;/code&gt; overlay covers the page. In Chromium, a click during the animation hit &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, not the button underneath, until I added &lt;code&gt;::view-transition { pointer-events: none; }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old state is a picture.&lt;/strong&gt; The old snapshot is a static image and the new one is live, so stretched text during a size change is normal. MDN's trick of setting &lt;code&gt;height: 100%&lt;/code&gt; on &lt;code&gt;::view-transition-old()&lt;/code&gt; and &lt;code&gt;::view-transition-new()&lt;/code&gt; smooths over the different aspect ratios, and the demo uses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced motion is on you.&lt;/strong&gt; With reduced motion emulated in Chromium, the transitions still ran. The demo sets &lt;code&gt;animation: none&lt;/code&gt; on the view transition pseudo-elements inside &lt;code&gt;@media (prefers-reduced-motion: reduce)&lt;/code&gt;, so changes apply instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Browser support
&lt;/h2&gt;

&lt;p&gt;According to MDN's compat data, &lt;code&gt;document.startViewTransition()&lt;/code&gt; works in Chrome 111, Firefox 144 and Safari 18. The options object with &lt;code&gt;types&lt;/code&gt;, and &lt;code&gt;:active-view-transition-type()&lt;/code&gt;, arrived in Chrome 125, Firefox 147 and Safari 18.2. &lt;code&gt;view-transition-class&lt;/code&gt; is in Chrome 125, Firefox 144 and Safari 18.2. I ran the demo in Chromium 153 and WebKitGTK 2.52 with no console errors; I couldn't run Firefox in my environment. When the method doesn't exist, the demo shows a notice and applies the same changes instantly, which is exactly how the API was designed to degrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Element-scoped transitions.&lt;/strong&gt; MDN already documents &lt;code&gt;Element.startViewTransition()&lt;/code&gt; for running transitions inside a subtree, but &lt;code&gt;ViewTransition.transitionRoot&lt;/code&gt; is still marked experimental and Chromium-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;match-element&lt;/code&gt;.&lt;/strong&gt; It gives every selected element an automatic unique name, so you can skip the &lt;code&gt;tile-${i}&lt;/code&gt; step for same-document transitions. Its support is in Chrome 137, Firefox 144 and Safari 18.4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-document transitions.&lt;/strong&gt; The &lt;code&gt;@view-transition&lt;/code&gt; rule for multi-page sites isn't Baseline yet; web-features still lists it without Firefox support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/api/document/startviewtransition/index.md" rel="noopener noreferrer"&gt;Document: startViewTransition() method (MDN source)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/api/view_transition_api/using_types/index.md" rel="noopener noreferrer"&gt;Using view transition types (MDN source)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/css/reference/properties/view-transition-class/index.md" rel="noopener noreferrer"&gt;view-transition-class (MDN source)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/w3c/csswg-drafts/main/css-view-transitions-1/Overview.bs" rel="noopener noreferrer"&gt;CSS View Transitions Module Level 1 (spec source)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/web-platform-dx/web-features/main/features/view-transitions.yml.dist" rel="noopener noreferrer"&gt;web-features: view transitions Baseline status&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-26-view-transitions-animate-any-dom-change" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>animation</category>
      <category>viewtransitions</category>
    </item>
    <item>
      <title>CSS field-sizing: inputs that grow as you type</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/css-field-sizing-inputs-that-grow-as-you-type-283i</link>
      <guid>https://dev.to/richicodes/css-field-sizing-inputs-that-grow-as-you-type-283i</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-25-css-field-sizing-inputs-that-grow-as-you-type" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A textarea that grows as you type used to need a script that measured &lt;code&gt;scrollHeight&lt;/code&gt; on every keystroke. Now it takes one line of CSS: &lt;code&gt;field-sizing: content&lt;/code&gt;. With Firefox 152 shipping it on June 16, 2026, the property is Baseline, so it works in Chrome, Edge, Firefox and Safari.&lt;/p&gt;

&lt;p&gt;Type in the demo above, or press "Type it for me": the blanks in the sentence widen with every letter, the dropdown shrinks to the option you picked, and the chat box gains a line at a time until it hits its limit. Flip the switch to &lt;code&gt;fixed&lt;/code&gt; and you get the old behaviour back on the same markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is and why it matters
&lt;/h2&gt;

&lt;p&gt;Form controls have always had a size that ignores what's inside them. An &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; is just as wide whether it holds "Al" or a full street address, a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; is however many &lt;code&gt;rows&lt;/code&gt; and &lt;code&gt;cols&lt;/code&gt; it gets, and a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; is as wide as its longest option. The spec calls these "elements with default preferred size".&lt;/p&gt;

&lt;p&gt;&lt;code&gt;field-sizing&lt;/code&gt; takes two values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fixed&lt;/code&gt;: the default. The control keeps that default preferred size.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content&lt;/code&gt;: the browser works out the control's size from its content and ignores the default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole API. What makes it worth knowing is how many small annoyances it removes: chat composers, comment boxes, inline "fill in the blank" forms, tag inputs, compact filters with a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; that shouldn't reserve space for its longest option. All of that used to mean JavaScript that measured text, ran on every &lt;code&gt;input&lt;/code&gt; event and fought with fonts that hadn't loaded yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;This is the CSS behind the demo, trimmed to the parts that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* The technique: fields take the size of their content */&lt;/span&gt;
&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;textarea&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;field-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Inline blanks: never narrower than 3 characters,
   never wider than their container */&lt;/span&gt;
&lt;span class="nc"&gt;.sentence&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.sentence&lt;/span&gt; &lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Chat: fixed width, the height grows up to a limit and then scrolls */&lt;/span&gt;
&lt;span class="nc"&gt;.composer&lt;/span&gt; &lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.75em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three behaviours are going on there:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Text inputs shrink-wrap their text.&lt;/strong&gt; The &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;tel&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; types grow and shrink with the value. If there's a &lt;code&gt;placeholder&lt;/code&gt;, the field is wide enough to show it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A textarea grows sideways first, then down.&lt;/strong&gt; With nothing stopping it, a textarea with &lt;code&gt;field-sizing: content&lt;/code&gt; behaves like a single-line field. Once it can't get any wider, because it has a fixed &lt;code&gt;width&lt;/code&gt; or hits a &lt;code&gt;max-width&lt;/code&gt;, it starts adding lines. When it reaches &lt;code&gt;max-height&lt;/code&gt;, a scrollbar shows up. That's why the chat in the demo has &lt;code&gt;width: 100%&lt;/code&gt;: the width stays put and only the height changes. The spec uses exactly this case as its example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dropdown fits the selected option.&lt;/strong&gt; A regular &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; changes width as you pick different options. A list box (&lt;code&gt;multiple&lt;/code&gt;, or &lt;code&gt;size&lt;/code&gt; above 1) shows every option instead of scrolling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the demo the only JavaScript that touches sizes is the &lt;code&gt;ResizeObserver&lt;/code&gt; that prints the pixel readouts, plus the fallback I'll get to below. The growing itself is all CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;I checked each of these in Chromium with a small test page before writing them down. They match what MDN describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always set a &lt;code&gt;min-width&lt;/code&gt;.&lt;/strong&gt; An empty input with &lt;code&gt;field-sizing: content&lt;/code&gt; and no minimum is as wide as the text cursor. In my test it measured 9 pixels. That's why the demo uses &lt;code&gt;min-width: 3ch&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;size&lt;/code&gt;, &lt;code&gt;rows&lt;/code&gt; and &lt;code&gt;cols&lt;/code&gt; stop doing anything.&lt;/strong&gt; They set the default preferred size, and &lt;code&gt;content&lt;/code&gt; ignores that. An &lt;code&gt;&amp;lt;input size="40"&amp;gt;&lt;/code&gt; with &lt;code&gt;field-sizing: content&lt;/code&gt; was exactly as narrow as one without the attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fixed &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; cancel the effect.&lt;/strong&gt; If you set both, the control has a fixed size again. Use &lt;code&gt;min-*&lt;/code&gt; and &lt;code&gt;max-*&lt;/code&gt; to keep it within bounds, or fix only the width, as in the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxlength&lt;/code&gt; also caps the size&lt;/strong&gt;, because the field stops growing when no more characters fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the container.&lt;/strong&gt; In a CSS grid, an item's automatic minimum size can let a growing field push the whole card wider than the screen. That's exactly what my first version of this demo did at 375 px. &lt;code&gt;grid-template-columns: minmax(0, 1fr)&lt;/code&gt; on the parent fixes it, and so does &lt;code&gt;min-width: 0&lt;/code&gt; on the item.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Browser support
&lt;/h2&gt;

&lt;p&gt;According to MDN's compatibility data, Chrome and Edge 123 (March 2024) were first, followed by Safari 26.2 (December 2025) and Firefox 152 (June 2026). That makes it Baseline newly available from June 16, 2026. For the next few months, some people will still be on browsers without it.&lt;/p&gt;

&lt;p&gt;The good news is that the fallback is free: a browser that doesn't understand &lt;code&gt;field-sizing&lt;/code&gt; ignores the declaration and draws the form exactly as it always has. If the old size is acceptable, you don't need anything else. If it isn't, check for support and only load the old trick when the property is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;field-sizing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Old trick: measure and set the size by hand&lt;/span&gt;
  &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo does exactly that, and shows a notice when it's running on the fallback. In CSS you can use &lt;code&gt;@supports (field-sizing: content)&lt;/code&gt; for the same check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spec home has changed.&lt;/strong&gt; &lt;code&gt;field-sizing&lt;/code&gt; now lives in CSS Form Control Styling Level 1 (it moved there from CSS UI 4). That draft also covers &lt;code&gt;appearance: base&lt;/code&gt; and fully styleable form controls, so expect more news in this area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long fields in real layouts.&lt;/strong&gt; A field that grows changes the layout around it. In flex and grid that's fine, but a &lt;code&gt;max-width&lt;/code&gt; is always worth having.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing old code.&lt;/strong&gt; If you have a dependency or a hook whose only job is to auto-size textareas, it's a good candidate to delete once your audience is on browsers from the last few months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/field-sizing" rel="noopener noreferrer"&gt;field-sizing - MDN Web Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-forms-1/#field-sizing" rel="noopener noreferrer"&gt;CSS Form Control Styling Module Level 1: the field-sizing property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/152" rel="noopener noreferrer"&gt;Firefox 152 release notes for developers - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-25-css-field-sizing-inputs-that-grow-as-you-type" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>forms</category>
      <category>baseline</category>
      <category>ux</category>
    </item>
    <item>
      <title>CSS contrast-color(): text that picks its own color</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Sun, 25 Oct 2026 08:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/css-contrast-color-text-that-picks-its-own-color-o3h</link>
      <guid>https://dev.to/richicodes/css-contrast-color-text-that-picks-its-own-color-o3h</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-24-css-contrast-color-text-that-picks-its-own-color" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You give CSS a background color and it hands back white or black, whichever reads better on it. That's &lt;code&gt;contrast-color()&lt;/code&gt;, and since April 2026 it works in Chrome, Firefox and Safari.&lt;/p&gt;

&lt;p&gt;Play with the demo above for a minute. The big card has no text color set anywhere: drag the lightness slider, or hit &lt;strong&gt;Sweep&lt;/strong&gt;, and watch the text flip from white to black at the point where black starts winning. The strip of twelve tiles below is the same idea at a glance: same lightness and chroma, twelve hues, and each tile flips at a different spot because yellow and blue don't have the same luminance at the "same" lightness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is and why it matters
&lt;/h2&gt;

&lt;p&gt;Any time a color comes from data (a tag color from a CMS, a user-picked theme, a chart series, a team's brand color) you need a text color that stays legible on top of it. Until now that meant a JavaScript helper, a Sass function at build time, or a lookup table of hand-checked pairs. &lt;code&gt;contrast-color()&lt;/code&gt; moves that decision into the browser, at computed-value time, so it also follows custom properties, &lt;code&gt;:hover&lt;/code&gt; states and theme switches.&lt;/p&gt;

&lt;p&gt;There's a real example in this very repo. The active tag chip in the notes explorer on this site uses &lt;code&gt;bg-richi-orange text-white&lt;/code&gt;, which is white on &lt;code&gt;#FF5200&lt;/code&gt;. By the WCAG 2.1 formula that pair gives 3.25:1; black on the same orange gives 6.46:1. &lt;code&gt;contrast-color(#FF5200)&lt;/code&gt; returns black. The demo starts on that exact orange so you can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The minimal version is three lines, and it's what the demo card uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;#FF5200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contrast-color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener noreferrer"&gt;CSS Color Level 5 spec&lt;/a&gt; is short on purpose. The function takes one &lt;code&gt;&amp;lt;color&amp;gt;&lt;/code&gt; and resolves to either &lt;code&gt;white&lt;/code&gt; or &lt;code&gt;black&lt;/code&gt;, whichever produces maximum contrast with the input used as a solid background. If both tie, it returns &lt;code&gt;white&lt;/code&gt;. That's it: no target ratio, no list of candidates, no custom colors. The spec's changelog records that the function was simplified by a working group resolution, and a note says future versions are expected to add more control over the algorithm and the returned color.&lt;/p&gt;

&lt;p&gt;Two details from the spec are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The algorithm is up to the browser.&lt;/strong&gt; The spec says the exact contrast algorithm is UA-defined and advises browsers not to simply use the WCAG 2.1 contrast ratio, because of its known issues. It does require that the result still meets WCAG 2.1 AA for large text (3:1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It decides; it doesn't guarantee.&lt;/strong&gt; It picks the better of two options. It can't make a bad background good.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Out of curiosity I checked whether today's engines follow WCAG anyway. In Chromium 153 and in WebKitGTK 2.52, I compared &lt;code&gt;contrast-color()&lt;/code&gt; against the WCAG 2.1 "pick the higher ratio" rule on 2,000 random sRGB colors, and the picks matched every time in both. That's a test, not a promise: the spec allows it to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mid-tone trap
&lt;/h2&gt;

&lt;p&gt;Here's the catch MDN warns about: on mid-tones, neither white nor black is great. MDN's own example is &lt;code&gt;#2277d3&lt;/code&gt;, a royal blue that gets black text. By the WCAG formula that's 4.65:1 for black and 4.52:1 for white: both technically pass AA for normal text, both are uncomfortable for small type.&lt;/p&gt;

&lt;p&gt;There's a hard floor here too. Doing the math with the WCAG luminance formula, the best of black or white can never drop below √21 ≈ 4.58:1. So &lt;code&gt;contrast-color()&lt;/code&gt; will never fail AA outright, but on mid-tones it will hover right on that line. The demo shows both ratios live and tells you when you're above 7:1 (AAA) or only just in AA territory. The fix is still a design one: push the background lighter or darker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support, fallbacks and one gotcha
&lt;/h2&gt;

&lt;p&gt;Support according to MDN's browser compat data: Chrome 147, Firefox 146 and Safari 26. web-features marks it Baseline newly available since April 10, 2026. I ran the demo in Chromium 153 and WebKitGTK 2.52; I couldn't run Firefox in my environment.&lt;/p&gt;

&lt;p&gt;For older browsers, the classic "declare a fallback first" trick has a trap when you use a custom property. This doesn't work the way you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contrast-color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c"&gt;/* parses fine because of var() */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the value contains &lt;code&gt;var()&lt;/code&gt;, the browser accepts the second declaration at parse time without checking the function. If it then can't resolve &lt;code&gt;contrast-color()&lt;/code&gt;, the property becomes invalid at computed-value time and falls back to the inherited color, not to your &lt;code&gt;black&lt;/code&gt;. I tested this in Chromium with a made-up function name and that's exactly what happens. Use &lt;code&gt;@supports&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contrast-color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;red&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contrast-color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you need the same decision in JavaScript (the demo does this as plan B when &lt;code&gt;CSS.supports('color', 'contrast-color(red)')&lt;/code&gt; is false), the WCAG 2.1 version is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.04045&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;lin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Same rule as contrast-color(): the one with more contrast wins, ties go to white&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickTextColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;pickTextColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FF5200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "black"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The next level of the function.&lt;/strong&gt; The spec already says more control over the algorithm and the returned colors is expected. If you need brand colors instead of pure black and white, that's where it would show up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The algorithm itself.&lt;/strong&gt; Since it's UA-defined and the spec discourages plain WCAG 2.1, a browser could switch to a perceptual model. Mid-tone backgrounds are where you'd notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own audits.&lt;/strong&gt; Automated checkers compute WCAG 2.1 ratios. If a browser ever picks differently from WCAG on a mid-tone, the checker and the screen could disagree.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/contrast-color" rel="noopener noreferrer"&gt;contrast-color() - MDN Web Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener noreferrer"&gt;CSS Color Module Level 5: contrast-color()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/WCAG21/#contrast-minimum" rel="noopener noreferrer"&gt;WCAG 2.1: Success Criterion 1.4.3 Contrast (Minimum)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-24-css-contrast-color-text-that-picks-its-own-color" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>color</category>
      <category>a11y</category>
      <category>contrastcolor</category>
    </item>
    <item>
      <title>llms.txt v2: what an agent actually reads</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/llmstxt-v2-what-an-agent-actually-reads-3c43</link>
      <guid>https://dev.to/richicodes/llmstxt-v2-what-an-agent-actually-reads-3c43</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-23-llms-txt-v2-what-an-agent-actually-reads" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The llms.txt proposal got its first big revision in August 2026, and the changes answer the obvious question nobody could answer before: given a page, how does an agent find the llms.txt that describes it? I read the new text, ran this site's own &lt;code&gt;/llms.txt&lt;/code&gt; through the small reference parser that lives in the proposal's repository, and it crashed. The demo above shows both things: what a parser really gets out of the file, and how an agent walks from a page to its llms.txt and its Markdown version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llms.txt is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;It's a Markdown file that gives agents a curated entry point into a site: a name, a short summary, some context and lists of links to LLM-friendly content. Jeremy Howard published the original in September 2024. It's still a proposal "open for community input", not a W3C or IETF standard. The text is clear about what it's for: robots.txt says what automated access is acceptable, while llms.txt "is instead used on demand, when an agent needs information about a topic while assisting a user". It's meant for inference, not training.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in v2
&lt;/h2&gt;

&lt;p&gt;The Changes page lists four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any path, most specific wins.&lt;/strong&gt; A file at &lt;code&gt;/docs/llms.txt&lt;/code&gt; covers everything under &lt;code&gt;/docs/&lt;/code&gt;, and "where more than one file applies, agents should use the most specific one". This is what lets a GitHub Pages project site, which can't touch the host root, take part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two Markdown URL forms.&lt;/strong&gt; v1 only had &lt;code&gt;.md&lt;/code&gt; appended (&lt;code&gt;page.html.md&lt;/code&gt;). v2 also allows replacing the extension (&lt;code&gt;page.md&lt;/code&gt;). URLs without a file name use &lt;code&gt;index.html.md&lt;/code&gt; or &lt;code&gt;index.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery through link relations.&lt;/strong&gt; &lt;code&gt;rel="alternate" type="text/markdown"&lt;/code&gt; points to a page's Markdown version and &lt;code&gt;rel="describedby"&lt;/code&gt; points to the llms.txt that covers it, either as &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; elements or as an HTTP header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;## Optional&lt;/code&gt; lost its special meaning.&lt;/strong&gt; The context-expansion tool from v1 is no longer part of the proposal, so &lt;code&gt;Optional&lt;/code&gt; is now just a convention for secondary links.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The header form is the interesting one, because you can add it in your server or CDN config without touching a single page, and it also works on non-HTML files. This is the proposal's own example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Link: &amp;lt;/docs/page.html.md&amp;gt;; rel="alternate"; type="text/markdown", &amp;lt;/docs/llms.txt&amp;gt;; rel="describedby"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the equivalent in the page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/markdown"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs/page.html.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"describedby"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs/llms.txt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "How it gets found" tab in the demo builds that header for each page of a small example site. These are the two functions behind it, which I ran in Node with the outputs shown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The llms.txt files that cover a path, most specific first.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;coveringLlms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// page.html -&amp;gt; page.html.md ('append') or page.md ('replace').&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markdownUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;append&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;append&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.[^&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;/]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/llms.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs/llms.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nf"&gt;coveringLlms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs/api/fetch.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ['/docs/llms.txt', '/llms.txt']&lt;/span&gt;
&lt;span class="nf"&gt;markdownUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs/api/fetch.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// '/docs/api/fetch.md'&lt;/span&gt;
&lt;span class="nf"&gt;markdownUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;append&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                &lt;span class="c1"&gt;// '/docs/index.html.md'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Untick "/docs/llms.txt exists" and the docs pages fall back to the root file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format is stricter than it looks
&lt;/h2&gt;

&lt;p&gt;The proposal says the file is "in a precise format allowing fixed processing methods (i.e. classical programming techniques such as parsers and regex)". In order: an H1 with the name (the only required part), a blockquote with a short summary, zero or more paragraphs or lists with no headings, then H2 sections whose lists hold &lt;code&gt;[name](url)&lt;/code&gt; links, optionally followed by &lt;code&gt;:&lt;/code&gt; and notes.&lt;/p&gt;

&lt;p&gt;The repository still ships a parser of about twenty lines, &lt;code&gt;llms_txt/miniparse.py&lt;/code&gt;, and the demo's first tab is a line-for-line JavaScript port of it. I ran both on the same seven files, this site's llms.txt and the proposal's own included, and they return the same result in every case. Here is what I found by feeding it real input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;miniparse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_llms_txt&lt;/span&gt;

&lt;span class="nf"&gt;parse_llms_txt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# My site&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; What it is.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;One line of details.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;## Docs&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;- [Guide](https://example.com/guide.md): start here&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'title': 'My site', 'summary': 'What it is.', 'info': 'One line of details.',
#  'sections': {'Docs': [{'title': 'Guide', 'url': 'https://example.com/guide.md', 'desc': 'start here'}]}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every non-empty line in a section must contain a &lt;code&gt;[name](url)&lt;/code&gt; link.&lt;/strong&gt; A line like &lt;code&gt;- GitHub: https://github.com/me&lt;/code&gt; raises &lt;code&gt;AttributeError&lt;/code&gt;. So does a long list item wrapped onto a second, indented line, because the parser splits sections line by line. This site's &lt;code&gt;/llms.txt&lt;/code&gt; does both, which is why the "richi.codes today" example opens in red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the first line of the summary counts.&lt;/strong&gt; A blockquote spread over several &lt;code&gt;&amp;gt;&lt;/code&gt; lines keeps its first line as the summary and dumps the rest into the details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The summary needs details after it.&lt;/strong&gt; With nothing between the blockquote and the first &lt;code&gt;##&lt;/code&gt;, the summary comes back as &lt;code&gt;None&lt;/code&gt; and the blockquote ends up in &lt;code&gt;info&lt;/code&gt;. With only an H1 before the first &lt;code&gt;##&lt;/code&gt;, the parser raises &lt;code&gt;AttributeError&lt;/code&gt;. Even the &lt;code&gt;llms.txt&lt;/code&gt; in the proposal's own repository comes back with &lt;code&gt;summary: None&lt;/code&gt; for this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;###&lt;/code&gt; also splits.&lt;/strong&gt; The pattern is &lt;code&gt;^##&lt;/code&gt;, so &lt;code&gt;### Getting started&lt;/code&gt; becomes a section called &lt;code&gt;# Getting started&lt;/code&gt;. The spec doesn't allow headings before the first H2 anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent reading the file as plain Markdown won't care about any of this. But if a tool parses llms.txt the way the proposal invites it to, these details decide whether your links survive. The "richi.codes fixed" example shows the cheap fix: one line per link, real Markdown links, and a short paragraph after the summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who actually consumes it.&lt;/strong&gt; The proposal lists documentation platforms that generate the file and says the AI labs publish llms.txt files for their own developer docs. Publishing a file isn't the same as reading one, so check what the agents you care about document before treating llms.txt as a ranking factor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Link header.&lt;/strong&gt; It's the cheapest v2 change to adopt: one line of server config, and agents can go from any page to its Markdown version and its llms.txt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This site's file.&lt;/strong&gt; The demo makes it obvious that &lt;code&gt;/llms.txt&lt;/code&gt; here has plain-URL list items and wrapped lines. That's worth fixing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/AnswerDotAI/llms-txt/main/nbs/index.qmd" rel="noopener noreferrer"&gt;The /llms.txt file, v2 (proposal source, AnswerDotAI/llms-txt)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/AnswerDotAI/llms-txt/main/nbs/changes.qmd" rel="noopener noreferrer"&gt;Changes: v2 (August 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/AnswerDotAI/llms-txt/main/llms_txt/miniparse.py" rel="noopener noreferrer"&gt;llms_txt/miniparse.py, the reference parser&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-23-llms-txt-v2-what-an-agent-actually-reads" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llmstxt</category>
      <category>ai</category>
      <category>seo</category>
      <category>markdown</category>
    </item>
    <item>
      <title>CSS shape(): responsive shapes without SVG</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:30:40 +0000</pubDate>
      <link>https://dev.to/richicodes/css-shape-responsive-shapes-without-svg-48b2</link>
      <guid>https://dev.to/richicodes/css-shape-responsive-shapes-without-svg-48b2</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-22-css-shape-responsive-shapes-without-svg" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now draw a star, a blob or a speech bubble in &lt;code&gt;clip-path&lt;/code&gt; with percentages, &lt;code&gt;px&lt;/code&gt;, &lt;code&gt;calc()&lt;/code&gt; and custom properties, and morph between shapes with a single &lt;code&gt;transition&lt;/code&gt;: the CSS &lt;code&gt;shape()&lt;/code&gt; function has been Baseline since February 2026.&lt;/p&gt;

&lt;p&gt;For years, any &lt;code&gt;clip-path&lt;/code&gt; more interesting than &lt;code&gt;circle()&lt;/code&gt; or &lt;code&gt;polygon()&lt;/code&gt; meant &lt;code&gt;path()&lt;/code&gt;, and &lt;code&gt;path()&lt;/code&gt; takes an SVG path string where every number is a pixel. That's fine for a fixed-size icon, but it breaks the moment the element resizes: the shape doesn't scale, and you end up recalculating the string in JavaScript. &lt;code&gt;shape()&lt;/code&gt; uses the same drawing model as SVG paths, but written in CSS syntax, so each coordinate can be whatever CSS value makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shape() is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;shape()&lt;/code&gt; is a &lt;code&gt;&amp;lt;basic-shape&amp;gt;&lt;/code&gt; function, and MDN lists it for &lt;code&gt;clip-path&lt;/code&gt;, &lt;code&gt;offset-path&lt;/code&gt; and the newer &lt;code&gt;border-shape&lt;/code&gt;. It takes a starting point and a comma-separated list of commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.triangle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands mirror SVG path commands, with words instead of letters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;move&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;hline&lt;/code&gt; and &lt;code&gt;vline&lt;/code&gt; for jumps and straight lines.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curve ... with ...&lt;/code&gt; for Bézier curves: one control point gives a quadratic curve, two (separated by &lt;code&gt;/&lt;/code&gt;) a cubic one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;smooth&lt;/code&gt; for a curve that continues the previous one, &lt;code&gt;arc&lt;/code&gt; for elliptical arcs, and &lt;code&gt;close&lt;/code&gt; to go back to the start.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;to&lt;/code&gt; means coordinates measured from the top-left corner of the reference box; &lt;code&gt;by&lt;/code&gt; means relative to where the previous command ended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Percentages are relative to the width and height of the reference box. And according to MDN, this is the real difference from &lt;code&gt;path()&lt;/code&gt;: &lt;code&gt;path()&lt;/code&gt; is a single string limited to &lt;code&gt;px&lt;/code&gt;, while &lt;code&gt;shape()&lt;/code&gt; accepts percentages, &lt;code&gt;rem&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt; and math functions such as &lt;code&gt;calc()&lt;/code&gt;, &lt;code&gt;max()&lt;/code&gt; or &lt;code&gt;abs()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixing units: the responsive bubble
&lt;/h2&gt;

&lt;p&gt;The second tab in the demo above is a speech bubble. The trick is deciding what scales and what doesn't: the corners and the tail are fixed in &lt;code&gt;px&lt;/code&gt;, while the box and the tail's position are percentages. Because &lt;code&gt;shape()&lt;/code&gt; is just CSS, the values come straight from custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.bubble&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--arrow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hline&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;curve&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vline&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;curve&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;hline&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--arrow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--arrow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--arrow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;hline&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;curve&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--tail&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;vline&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;curve&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a walk around the edge: top edge, a quadratic curve for the top-right corner (the control point is the corner itself), down the right side, the bottom-right corner, along the bottom to the tail, down to the tip at &lt;code&gt;100%&lt;/code&gt; and back up, and the same on the left. Drag the width slider in the demo and the corners stay round instead of turning into ellipses, because their radius is in &lt;code&gt;px&lt;/code&gt;. The spec itself uses a speech bubble like this one to explain why mixing units matters.&lt;/p&gt;

&lt;p&gt;The sliders don't rebuild the string: they only change &lt;code&gt;--r&lt;/code&gt;, &lt;code&gt;--arrow&lt;/code&gt; and &lt;code&gt;--tail&lt;/code&gt; with &lt;code&gt;style.setProperty()&lt;/code&gt;. The browser redraws the shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Morphing for free
&lt;/h2&gt;

&lt;p&gt;The first tab is the part that catches the eye. The specification says that two &lt;code&gt;shape()&lt;/code&gt; values (or a &lt;code&gt;shape()&lt;/code&gt; and a &lt;code&gt;path()&lt;/code&gt;) can be interpolated if their command lists are the same length and have the same commands in order. A command counts as "the same" if it uses the same keyword and the same &lt;code&gt;by&lt;/code&gt;/&lt;code&gt;to&lt;/code&gt;, and for &lt;code&gt;curve&lt;/code&gt; and &lt;code&gt;smooth&lt;/code&gt; the same number of control points.&lt;/p&gt;

&lt;p&gt;So if you design every shape with the same skeleton, a &lt;code&gt;transition&lt;/code&gt; does the morph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clip-path&lt;/span&gt; &lt;span class="m"&gt;.6s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.badge&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the demo, the circle, the star, the flower and the random blob are all twelve &lt;code&gt;curve to X Y with CX CY&lt;/code&gt; commands generated from polar coordinates. The star simply puts each control point halfway along the segment, so the curve looks straight. Since the skeleton never changes, any shape morphs into any other.&lt;/p&gt;

&lt;p&gt;I checked the flip side in Chromium too: if I replace one of the &lt;code&gt;line&lt;/code&gt; commands in the hover state with a &lt;code&gt;curve&lt;/code&gt;, the transition stops interpolating and the shape jumps straight to the final value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support and gotchas
&lt;/h2&gt;

&lt;p&gt;According to MDN's compatibility data, &lt;code&gt;shape()&lt;/code&gt; is supported in Chrome and Edge 135, Firefox 148 and Safari 18.4. It became Baseline newly available on 24 February 2026, so it's not yet "widely available". For older browsers, feature detection is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;polygon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo uses the same check in JavaScript with &lt;code&gt;CSS.supports()&lt;/code&gt;. If it fails, it falls back to &lt;code&gt;path()&lt;/code&gt; with pixel values computed from the element's size and recalculated with a &lt;code&gt;ResizeObserver&lt;/code&gt;. That's exactly the work &lt;code&gt;shape()&lt;/code&gt; saves you. You can see the fallback by adding &lt;code&gt;?fallback&lt;/code&gt; to the demo's URL.&lt;/p&gt;

&lt;p&gt;Things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;clip-path&lt;/code&gt; only clips. Shadows and borders are clipped too, so the glow in the demo is a &lt;code&gt;filter: drop-shadow()&lt;/code&gt; on the parent, which follows the clipped shape.&lt;/li&gt;
&lt;li&gt;Computed values keep the units you wrote: &lt;code&gt;getComputedStyle()&lt;/code&gt; returns something like &lt;code&gt;shape(from 0px 0px, hline to calc(100% - 20px), ...)&lt;/code&gt;, which helps when you're debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;border-shape&lt;/code&gt;&lt;/strong&gt;: it accepts &lt;code&gt;shape()&lt;/code&gt; too, but in MDN's compatibility data it only appears in Chrome 147. It's the piece that would let you shape the border as well, not just clip the element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative control points&lt;/strong&gt;: the spec defines &lt;code&gt;from start&lt;/code&gt;, &lt;code&gt;from end&lt;/code&gt; and &lt;code&gt;from origin&lt;/code&gt; for control points. MDN's examples use them behind their own &lt;code&gt;@supports&lt;/code&gt; check, so test them in every browser before relying on them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converting existing paths&lt;/strong&gt;: if you already have an SVG &lt;code&gt;d&lt;/code&gt; string, the translation to &lt;code&gt;shape()&lt;/code&gt; is mechanical (&lt;code&gt;M&lt;/code&gt; → &lt;code&gt;from&lt;/code&gt;/&lt;code&gt;move&lt;/code&gt;, &lt;code&gt;L&lt;/code&gt; → &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;Q&lt;/code&gt;/&lt;code&gt;C&lt;/code&gt; → &lt;code&gt;curve&lt;/code&gt;, &lt;code&gt;Z&lt;/code&gt; → &lt;code&gt;close&lt;/code&gt;), and that's when you can swap pixels for percentages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/basic-shape/shape" rel="noopener noreferrer"&gt;shape() CSS function — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-shapes-1/#shape-function" rel="noopener noreferrer"&gt;CSS Shapes Module Level 1: the shape() function — CSSWG Editor's Draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/clip-path" rel="noopener noreferrer"&gt;clip-path — MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-22-css-shape-responsive-shapes-without-svg" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>clippath</category>
      <category>shape</category>
      <category>animation</category>
    </item>
    <item>
      <title>CSS Custom Highlight API: style text without spans</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:30:40 +0000</pubDate>
      <link>https://dev.to/richicodes/css-custom-highlight-api-style-text-without-spans-4o47</link>
      <guid>https://dev.to/richicodes/css-custom-highlight-api-style-text-without-spans-4o47</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-21-css-custom-highlight-api-style-text-without-spans" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can paint any piece of text on a page without wrapping it in a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; or a &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt;: the CSS Custom Highlight API has been Baseline since March 2026, when Firefox 149 shipped the last missing piece, the &lt;code&gt;::highlight()&lt;/code&gt; pseudo-element.&lt;/p&gt;

&lt;p&gt;Type something in the demo above. Every match turns orange, but keep an eye on the middle counter: the number of DOM nodes inside the text doesn't move. Start the read-along or flag the typos and it still doesn't move. In a browser without the API, the demo falls back to real &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; elements and you can watch that counter grow and turn orange.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;The classic way to highlight search results is to rewrite the HTML: split text nodes, wrap the matches in elements, and undo it all on the next keystroke. It means extra elements in the DOM on every keystroke, and it gets messy fast when two highlights need to overlap.&lt;/p&gt;

&lt;p&gt;MDN describes the API as a way to style arbitrary text ranges by "using JavaScript to create the ranges, and CSS to style them". It extends the idea behind the highlight pseudo-elements the browser already had (&lt;code&gt;::selection&lt;/code&gt;, &lt;code&gt;::spelling-error&lt;/code&gt;, &lt;code&gt;::grammar-error&lt;/code&gt;, &lt;code&gt;::target-text&lt;/code&gt;) to ranges you define yourself. Typical uses: search in page, spell and grammar checkers, code editors, collaborative editors with one colour per user.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Four steps: create &lt;code&gt;Range&lt;/code&gt; objects, group them in a &lt;code&gt;Highlight&lt;/code&gt;, register it by name in &lt;code&gt;CSS.highlights&lt;/code&gt;, and style that name with &lt;code&gt;::highlight()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstChild&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a text node&lt;/span&gt;

&lt;span class="c1"&gt;// 1. A Range for every match&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ranges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ranges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ranges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ranges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2 and 3. One Highlight holds them all; the registry gives it a name&lt;/span&gt;
&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 4. Only the name is shared between JS and CSS */&lt;/span&gt;
&lt;span class="nd"&gt;::highlight&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#FF5200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole trick in the demo's search box: on each &lt;code&gt;input&lt;/code&gt; event it walks the text nodes with a &lt;code&gt;TreeWalker&lt;/code&gt;, builds the ranges and calls &lt;code&gt;CSS.highlights.set()&lt;/code&gt; again. &lt;code&gt;CSS.highlights&lt;/code&gt; is a &lt;code&gt;Map&lt;/code&gt;-like registry, so &lt;code&gt;delete(name)&lt;/code&gt; and &lt;code&gt;clear()&lt;/code&gt; remove highlights, and a &lt;code&gt;Highlight&lt;/code&gt; is &lt;code&gt;Set&lt;/code&gt;-like, with &lt;code&gt;add()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The read-along button uses the same idea with a single range that moves to the next word every 280 ms. The typos button registers a third highlight and sets its &lt;code&gt;type&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;typos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;typoRanges&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;typos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spelling-error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// default is 'highlight'; 'grammar-error' also exists&lt;/span&gt;
&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;typos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to MDN, &lt;code&gt;type&lt;/code&gt; lets assistive technologies include that meaning when they expose the highlight, although support "may vary across platforms".&lt;/p&gt;

&lt;h3&gt;
  
  
  When highlights overlap
&lt;/h3&gt;

&lt;p&gt;In the demo the search can overlap the read-along word. Which colour wins is not decided by CSS order or &lt;code&gt;!important&lt;/code&gt;: it's decided by &lt;code&gt;Highlight.priority&lt;/code&gt;. With equal priorities, the most recently registered highlight wins for the conflicting properties. The demo gives the search &lt;code&gt;priority = 1&lt;/code&gt;, so orange stays on top. Properties that don't conflict are all applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can style
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;::highlight()&lt;/code&gt; only accepts a short list of properties: &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;background-color&lt;/code&gt;, &lt;code&gt;text-decoration&lt;/code&gt; and its longhands, &lt;code&gt;text-shadow&lt;/code&gt;, and &lt;code&gt;-webkit-text-stroke-color&lt;/code&gt;, &lt;code&gt;-webkit-text-fill-color&lt;/code&gt; and &lt;code&gt;-webkit-text-stroke-width&lt;/code&gt;. &lt;code&gt;background-image&lt;/code&gt; is ignored, so no gradients. The wavy red underline in the demo is plain &lt;code&gt;text-decoration: underline wavy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Support:&lt;/strong&gt; Chrome and Edge 105, Safari 17.2, Firefox 149 for the full feature. Firefox had the JavaScript side (&lt;code&gt;Highlight&lt;/code&gt;, &lt;code&gt;CSS.highlights&lt;/code&gt;) from 140, but &lt;code&gt;::highlight()&lt;/code&gt; couldn't use &lt;code&gt;text-decoration&lt;/code&gt; before 146 or &lt;code&gt;text-shadow&lt;/code&gt; before 149.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect it:&lt;/strong&gt; &lt;code&gt;'highlights' in CSS&lt;/code&gt; is the basic check. The demo checks that and &lt;code&gt;typeof Highlight === 'function'&lt;/code&gt;, shows a warning and falls back to &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari and &lt;code&gt;user-select: none&lt;/code&gt;:&lt;/strong&gt; browser-compat-data notes that in Safari the highlight style is ignored when combined with &lt;code&gt;user-select: none&lt;/code&gt; (WebKit bug 278455). Don't put highlights on text you've made unselectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No semantics by default:&lt;/strong&gt; MDN is clear that custom highlights don't add semantic meaning to the document; &lt;code&gt;type&lt;/code&gt; only covers spelling and grammar errors, and support for it varies. If the highlight carries meaning a screen reader user needs, MDN suggests semantic HTML such as &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; or another accessible cue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranges point at text nodes.&lt;/strong&gt; If your framework re-renders and replaces those nodes, recompute the ranges. The demo's fallback does exactly that after each reset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Older Firefox:&lt;/strong&gt; until your audience is on 149+, test &lt;code&gt;text-shadow&lt;/code&gt; and &lt;code&gt;text-decoration&lt;/code&gt; there, or stick to &lt;code&gt;color&lt;/code&gt; and &lt;code&gt;background-color&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility:&lt;/strong&gt; use &lt;code&gt;type&lt;/code&gt; where it fits, and don't rely on colour alone for meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editors and search:&lt;/strong&gt; in-page search, spell checking and collaborative cursors are the obvious places where this replaces a lot of DOM juggling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API" rel="noopener noreferrer"&gt;CSS Custom Highlight API - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::highlight" rel="noopener noreferrer"&gt;::highlight() - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Highlight/priority" rel="noopener noreferrer"&gt;Highlight: priority property - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-highlight-api-1/" rel="noopener noreferrer"&gt;CSS Custom Highlight API Module Level 1 - W3C Editor's Draft&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-21-css-custom-highlight-api-style-text-without-spans" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>highlight</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CSS progress(): one number drives the whole layout</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/css-progress-one-number-drives-the-whole-layout-2nb6</link>
      <guid>https://dev.to/richicodes/css-progress-one-number-drives-the-whole-layout-2nb6</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-20-css-progress-one-number-drives-the-whole-layout" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CSS can now tell you &lt;em&gt;how far along&lt;/em&gt; a value is between two others, as a plain number from 0 to 1. That's the &lt;code&gt;progress()&lt;/code&gt; function, and since 1 September 2026 it's Baseline newly available: Chrome and Edge 138, Safari 26 and Firefox 155 all ship it.&lt;/p&gt;

&lt;p&gt;It sounds modest. In practice it's the missing piece for fluid components: you compute one ratio from, say, the width of a container, and then every property that should "grow with the space" reads from that same number. Drag the card in the demo above and watch the cover, the corners, the gradient, the shadow, the equaliser bars and the extra text all move together. The only JavaScript in it changes the width of the box and prints the value; the morphing is pure CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What progress() returns
&lt;/h2&gt;

&lt;p&gt;The definition in CSS Values and Units Level 5 is short. &lt;code&gt;progress()&lt;/code&gt; takes three calculations — the progress value, a start and an end — and returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(progress value - start) / (end - start)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;clamped to the &lt;code&gt;[0, 1]&lt;/code&gt; range. So &lt;code&gt;progress(5, 0, 10)&lt;/code&gt; is &lt;code&gt;0.5&lt;/code&gt;, &lt;code&gt;progress(15, 0, 10)&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;progress(-5, 0, 10)&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The three arguments can be numbers, dimensions or percentages, and they can even mix units, as long as they're the same &lt;em&gt;type&lt;/em&gt;. &lt;code&gt;progress(3em, 0px, 100px)&lt;/code&gt; is fine (all lengths, with &lt;code&gt;3em&lt;/code&gt; resolved to its computed size); &lt;code&gt;progress(3s, 0px, 100px)&lt;/code&gt; is invalid. Whatever goes in, what comes out is a unitless &lt;code&gt;&amp;lt;number&amp;gt;&lt;/code&gt;, which is exactly what you want to multiply other values by.&lt;/p&gt;

&lt;p&gt;If you've written fluid sizes with &lt;code&gt;clamp()&lt;/code&gt; and a slope you worked out on paper, this is the same idea with the maths moved into the browser. The spec itself notes that &lt;code&gt;progress()&lt;/code&gt; is essentially syntactic sugar for a particular &lt;code&gt;calc()&lt;/code&gt; pattern — but it's sugar that makes the intent readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern from the demo
&lt;/h2&gt;

&lt;p&gt;Here's the core of the demo's CSS, trimmed down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;number&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.stage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.player&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* 0 when the container is 220px wide, 1 at 640px */&lt;/span&gt;
  &lt;span class="py"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;cqi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;220px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;640px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;18px&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;18px&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;36px&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.cover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;52px&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;110px&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* progress() of progress(): only shows up in the last stretch */&lt;/span&gt;
&lt;span class="nc"&gt;.extra&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--reveal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--reveal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* each bar fills over its own slice of --p */&lt;/span&gt;
&lt;span class="nc"&gt;.eq&lt;/span&gt; &lt;span class="nt"&gt;i&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12%&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;88%&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;.35&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things are going on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;100cqi&lt;/code&gt; is the input.&lt;/strong&gt; &lt;code&gt;.stage&lt;/code&gt; is a size container, so &lt;code&gt;100cqi&lt;/code&gt; inside &lt;code&gt;.player&lt;/code&gt; is the stage's full inline size. &lt;code&gt;progress(100cqi, 220px, 640px)&lt;/code&gt; turns that width into a ratio. Swap &lt;code&gt;100cqi&lt;/code&gt; for &lt;code&gt;100vw&lt;/code&gt; and the same component follows the viewport instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else is &lt;code&gt;start + range * var(--p)&lt;/code&gt;.&lt;/strong&gt; Padding, gap, radius and cover size are all linear interpolations off one number. In the full demo the background also uses &lt;code&gt;color-mix(in oklch, var(--teal) calc(100% - var(--p) * 100%), var(--orange))&lt;/code&gt;, so the colour shifts from teal to orange as the card grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;progress()&lt;/code&gt; can take &lt;code&gt;--p&lt;/code&gt; as its own input.&lt;/strong&gt; &lt;code&gt;progress(var(--p), .55, .9)&lt;/code&gt; remaps the 0.55–0.9 stretch of &lt;code&gt;--p&lt;/code&gt; to a fresh 0–1, so the extra line fades in late instead of all at once. Giving each bar a slightly later &lt;code&gt;--start&lt;/code&gt; turns the same trick into a staggered reveal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked these numbers in Chromium 141: with the stage at 430px, &lt;code&gt;--p&lt;/code&gt; computes to &lt;code&gt;0.5&lt;/code&gt;, the cover to &lt;code&gt;107px&lt;/code&gt; and the radius to &lt;code&gt;29px&lt;/code&gt;, which is what the formula predicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the @property matters
&lt;/h2&gt;

&lt;p&gt;You could write &lt;code&gt;--p: progress(...)&lt;/code&gt; without registering it, and in a flat component it would mostly work. But an unregistered custom property is just a stream of tokens: &lt;code&gt;var(--p)&lt;/code&gt; pastes &lt;code&gt;progress(100cqi, 220px, 640px)&lt;/code&gt; into every place that uses it, and &lt;code&gt;100cqi&lt;/code&gt; is then resolved against &lt;em&gt;that&lt;/em&gt; element's nearest container.&lt;/p&gt;

&lt;p&gt;I tested it with a nested container 100px wide: the unregistered version gave &lt;code&gt;0&lt;/code&gt; there (100px is below the 220px start), while the version registered as &lt;code&gt;&amp;lt;number&amp;gt;&lt;/code&gt; kept the &lt;code&gt;0.5&lt;/code&gt; computed on &lt;code&gt;.player&lt;/code&gt;. Registering &lt;code&gt;--p&lt;/code&gt; with &lt;code&gt;@property&lt;/code&gt; makes the browser compute the number once, on the element that declares it, and pass that number down. It's also what lets the demo read &lt;code&gt;--p&lt;/code&gt; with &lt;code&gt;getComputedStyle()&lt;/code&gt; and show it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline newly available, not widely available.&lt;/strong&gt; It works in current versions of all major engines, but older browsers don't understand it: a regular property that uses &lt;code&gt;progress()&lt;/code&gt; is thrown away at parse time, and a registered &lt;code&gt;--p&lt;/code&gt; ends up with its &lt;code&gt;initial-value&lt;/code&gt; (or whatever an ancestor sets). The demo checks &lt;code&gt;CSS.supports('width', 'calc(progress(1, 0, 2) * 1px)')&lt;/code&gt; and, if that fails, computes the same ratio in JavaScript and writes it to &lt;code&gt;--p&lt;/code&gt;. If you'd rather avoid JS, choose an &lt;code&gt;initial-value&lt;/code&gt; that looks fine on its own and treat the fluid behaviour as an enhancement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;no-clamp&lt;/code&gt; isn't there yet.&lt;/strong&gt; The spec allows &lt;code&gt;progress(no-clamp 15, 0, 10)&lt;/code&gt; to return &lt;code&gt;1.5&lt;/code&gt;, but according to MDN's compatibility data only Firefox 155 supports the keyword (Safari has it in Technology Preview). Chromium 141, which I tested with, rejects it in &lt;code&gt;CSS.supports()&lt;/code&gt;. For now, assume the result is always clamped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types have to match.&lt;/strong&gt; Mixing a time and a length, or a length and a plain number (&lt;code&gt;progress(3em, 0, 100)&lt;/code&gt;), makes the function invalid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start equal to end.&lt;/strong&gt; The spec defines the clamped result as &lt;code&gt;0&lt;/code&gt; in that case, so a container query where both bounds collapse won't blow up, but it won't animate either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discrete changes are still a job for queries.&lt;/strong&gt; &lt;code&gt;progress()&lt;/code&gt; interpolates; it doesn't switch layouts. Changing from a column to a row is still a container query. The two combine well: queries for the jumps, &lt;code&gt;progress()&lt;/code&gt; for everything in between.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-clamp&lt;/code&gt; reaching Chromium and stable Safari, which would make it practical to extrapolate past the bounds.&lt;/li&gt;
&lt;li&gt;The spec still has open questions on &lt;code&gt;progress()&lt;/code&gt;, including whether it should clamp to 0–100% (issue 11825). Earlier drafts had &lt;code&gt;media-progress()&lt;/code&gt; and &lt;code&gt;container-progress()&lt;/code&gt;; they were dropped in favour of using units like &lt;code&gt;vw&lt;/code&gt; or &lt;code&gt;cqi&lt;/code&gt; directly inside &lt;code&gt;progress()&lt;/code&gt;, which is the approach the demo uses.&lt;/li&gt;
&lt;li&gt;The rest of CSS Values 5: the spec describes &lt;code&gt;progress()&lt;/code&gt; as an input for other calculations, including the &lt;code&gt;*-mix()&lt;/code&gt; notations defined in the same draft.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/progress" rel="noopener noreferrer"&gt;progress() CSS function — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-values-5/#progress" rel="noopener noreferrer"&gt;CSS Values and Units Module Level 5: the progress() notation — W3C Editor's Draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property" rel="noopener noreferrer"&gt;@property CSS at-rule — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries#container_query_length_units" rel="noopener noreferrer"&gt;Container query length units — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webstatus.dev/features/progress-function" rel="noopener noreferrer"&gt;progress() — Web Platform Status&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-20-css-progress-one-number-drives-the-whole-layout" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>progress</category>
      <category>containerqueries</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Say no to AI training, not to AI search: robots.txt per bot</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Tue, 20 Oct 2026 07:30:40 +0000</pubDate>
      <link>https://dev.to/richicodes/say-no-to-ai-training-not-to-ai-search-robotstxt-per-bot-2ob0</link>
      <guid>https://dev.to/richicodes/say-no-to-ai-training-not-to-ai-search-robotstxt-per-bot-2ob0</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-19-say-no-to-ai-training-not-to-ai-search-robots-txt" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Block the AI bots" sounds like one switch, but it isn't. Anthropic uses three different crawlers, one for training, one for search and one for requests a person makes in Claude, and robots.txt can treat each one differently. That means you can keep your content out of training without disappearing from the answers. There's one rule in how robots.txt groups work that can quietly undo the whole setup, and the demo above opens right on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bots, three jobs
&lt;/h2&gt;

&lt;p&gt;Anthropic's help article on crawling lists them with what happens when you disable each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClaudeBot&lt;/strong&gt; collects web content that could contribute to training. Restricting it "signals that the site's future materials should be excluded from our AI model training datasets".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude-SearchBot&lt;/strong&gt; analyses content to improve search results. Blocking it "prevents our system from indexing your content for search optimization", which may reduce your visibility in user search results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude-User&lt;/strong&gt; fetches pages when someone asks Claude something. Blocking it prevents Claude from retrieving your content in response to a user query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if what you want is "don't train on my site, but do cite it", the file is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Training: no. Search and user requests: yes.
&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: &lt;span class="n"&gt;ClaudeBot&lt;/span&gt;
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /

&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;private&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the "No training, yes search" example in the demo. ClaudeBot has its own group and gets nothing. Claude-SearchBot and Claude-User aren't named, so they fall back to the &lt;code&gt;*&lt;/code&gt; group and can read everything except &lt;code&gt;/private/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same article has a few practical notes. The file has to go in the top-level directory of every subdomain you want to opt out. Blocking Anthropic's IP addresses instead "may not work correctly or persistently guarantee an opt-out", because it stops them from reading your robots.txt. They also support the non-standard &lt;code&gt;Crawl-delay&lt;/code&gt; directive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: naming a bot makes it forget &lt;code&gt;*&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now suppose you want Claude-SearchBot to focus on your blog and you add a small group for it. That's the "The * trap" example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt;-agent: *
Disallow: /private/

&lt;span class="nc"&gt;User&lt;/span&gt;-agent: ClaudeBot
Disallow: /

&lt;span class="c"&gt;# Looks harmless...&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;-agent: Claude-SearchBot
&lt;span class="nc"&gt;Allow&lt;/span&gt;: /blog/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;/private/x&lt;/code&gt; column in the demo. Claude-SearchBot is now &lt;strong&gt;allowed&lt;/strong&gt; there, and the cell gets an orange &lt;code&gt;*&lt;/code&gt; badge. The reason is that a crawler follows one group only: the one that names it. If such a group exists, the &lt;code&gt;*&lt;/code&gt; group is ignored completely, not merged in. Its only rule says &lt;code&gt;Allow: /blog/&lt;/code&gt;, nothing in it matches &lt;code&gt;/private/x&lt;/code&gt;, and a path that no rule matches is allowed.&lt;/p&gt;

&lt;p&gt;This isn't a quirk of my demo. Google's open-source robots.txt parser, which its README describes as the production code Googlebot uses, has a test for exactly this: with &lt;code&gt;Disallow: /x/&lt;/code&gt; under &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;Disallow: /y/&lt;/code&gt; under &lt;code&gt;FooBot&lt;/code&gt;, FooBot is allowed into &lt;code&gt;/x/page&lt;/code&gt;. In the code, once a group for the agent has been seen, the matcher never looks at the global rules again.&lt;/p&gt;

&lt;p&gt;The fix is boring: repeat in each named group the &lt;code&gt;*&lt;/code&gt; rules that should still apply ("Trap fixed" in the demo).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt;-agent: Claude-SearchBot
&lt;span class="nc"&gt;Allow&lt;/span&gt;: /blog/
Disallow: /private/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How a rule wins
&lt;/h2&gt;

&lt;p&gt;Inside a group, order doesn't matter. Every &lt;code&gt;Allow&lt;/code&gt; and &lt;code&gt;Disallow&lt;/code&gt; whose pattern matches the path is a candidate, and the longest pattern wins. If an &lt;code&gt;Allow&lt;/code&gt; and a &lt;code&gt;Disallow&lt;/code&gt; are the same length, &lt;code&gt;Allow&lt;/code&gt; wins. Patterns are anchored at the start of the path, &lt;code&gt;*&lt;/code&gt; matches any sequence of characters, and &lt;code&gt;$&lt;/code&gt; only means "end of the URL" when it's the last character.&lt;/p&gt;

&lt;p&gt;This is the matching function the demo uses, ported line by line from Google's &lt;code&gt;robots.cc&lt;/code&gt;. It keeps a list of positions in the path that can still match the pattern read so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rtMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;rtMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fish.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true: rules are prefixes&lt;/span&gt;
&lt;span class="nf"&gt;rtMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/catfish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// false: anchored at the start&lt;/span&gt;
&lt;span class="nf"&gt;rtMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/x/y.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/*.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;rtMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/foo/bar/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/foo/bar$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// false: $ ends the URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule's priority is simply the length of its pattern. To check that the port behaves like the original, I translated the assertions from the upstream test file (&lt;code&gt;robots_test.cc&lt;/code&gt;, which says it tests against RFC 9309) to JavaScript: 151 checks on groups, case, longest match, encoding and special characters, plus the URL and escaping helpers. They all pass against the code shipped in the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The user-agent token is case-insensitive, the path isn't.&lt;/strong&gt; &lt;code&gt;User-agent: claudebot&lt;/code&gt; applies to ClaudeBot, but &lt;code&gt;Disallow: /Private/&lt;/code&gt; doesn't block &lt;code&gt;/private/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only letters, &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; count in the token.&lt;/strong&gt; Google's parser stops at anything else, so &lt;code&gt;User-agent: Foo Bar&lt;/code&gt; is read as &lt;code&gt;Foo&lt;/code&gt;. The demo's last row lets you type a token and flags invalid ones like &lt;code&gt;Googlebot/2.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several &lt;code&gt;User-agent&lt;/code&gt; lines in a row share one group.&lt;/strong&gt; That's the tidy way to give Claude-SearchBot and Claude-User the same rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An empty &lt;code&gt;Disallow:&lt;/code&gt; allows everything&lt;/strong&gt; for that group, and it still counts as a named group, so the &lt;code&gt;*&lt;/code&gt; rules are still ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt is a request, not a lock.&lt;/strong&gt; It works when the crawler honours it, which Anthropic says its bots do. It doesn't protect private content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your own named groups.&lt;/strong&gt; Every time someone adds a &lt;code&gt;User-agent:&lt;/code&gt; block for a specific bot, check which &lt;code&gt;*&lt;/code&gt; rules it just dropped. The demo is a quick way to see it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New tokens.&lt;/strong&gt; Vendors split and rename crawlers. Read their own documentation for the exact names rather than copying a list from someone else's robots.txt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This site.&lt;/strong&gt; richi.codes currently has a single &lt;code&gt;User-agent: *&lt;/code&gt; group with &lt;code&gt;Allow: /&lt;/code&gt; (the "richi.codes today" example), so every bot that respects the file can read everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Does Anthropic crawl data from the web, and how can site owners block the crawler? — Claude Help Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/google/robotstxt/master/robots.cc" rel="noopener noreferrer"&gt;google/robotstxt: robots.cc, Google's robots.txt parser and matcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/google/robotstxt/master/robots_test.cc" rel="noopener noreferrer"&gt;google/robotstxt: robots_test.cc, tests against RFC 9309&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-19-say-no-to-ai-training-not-to-ai-search-robots-txt" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>robotstxt</category>
      <category>ai</category>
      <category>seo</category>
      <category>crawlers</category>
    </item>
    <item>
      <title>CSS can count now: sibling-index() and sibling-count()</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:30:40 +0000</pubDate>
      <link>https://dev.to/richicodes/css-can-count-now-sibling-index-and-sibling-count-11b6</link>
      <guid>https://dev.to/richicodes/css-can-count-now-sibling-index-and-sibling-count-11b6</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-18-css-can-count-now-sibling-index-and-sibling-count" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CSS can now tell an element where it sits among its siblings and how many siblings there are, as plain numbers you can use in &lt;code&gt;calc()&lt;/code&gt;. &lt;code&gt;sibling-index()&lt;/code&gt; and &lt;code&gt;sibling-count()&lt;/code&gt; became Baseline Newly available on 18 August 2026, when Firefox 154 shipped them.&lt;/p&gt;

&lt;p&gt;That sounds small until you remember the workarounds. A staggered entrance meant one &lt;code&gt;:nth-child()&lt;/code&gt; rule per item, or JavaScript that wrote &lt;code&gt;style="--i: 3"&lt;/code&gt; on every element and rewrote it whenever the list changed. A circular menu meant calculating angles in a script. Now the stylesheet can do it, and it stays correct when items are added or removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they return
&lt;/h2&gt;

&lt;p&gt;From the CSS Values and Units Level 5 spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sibling-index()&lt;/code&gt;&lt;/strong&gt; is the element's position among its siblings, as an &lt;code&gt;&amp;lt;integer&amp;gt;&lt;/code&gt;. Like &lt;code&gt;:nth-child()&lt;/code&gt;, it starts at 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sibling-count()&lt;/code&gt;&lt;/strong&gt; is the total number of child elements of the parent, including the element itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither takes arguments. They count elements only, so text nodes and comments between items don't change the numbers. MDN also points out the difference from &lt;code&gt;counter()&lt;/code&gt;: a counter gives you a string you can only print with &lt;code&gt;content&lt;/code&gt;, while these return integers you can do maths with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal version
&lt;/h2&gt;

&lt;p&gt;This is the classic stagger. Each item waits a little longer than the one before it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt; &lt;span class="m"&gt;0.4s&lt;/span&gt; &lt;span class="n"&gt;backwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sibling-index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60ms&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no class per item and no script. If you insert an element in the middle, everything after it moves up one position on its own.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sibling-count()&lt;/code&gt; comes in when the layout depends on the total. To spread items around a circle, each one takes its share of 360 degrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;360deg&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sibling-count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sibling-index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--a&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-128px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-1&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--a&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;rotate()&lt;/code&gt; turns the element towards its slot, &lt;code&gt;translateY()&lt;/code&gt; pushes it out to the radius, and the second &lt;code&gt;rotate()&lt;/code&gt; turns it back so the label stays upright. That's the "Ring" layout in the demo above. Remove a ball and the remaining ones get a new index and a new angle, and the &lt;code&gt;transition&lt;/code&gt; on &lt;code&gt;transform&lt;/code&gt; animates them into place.&lt;/p&gt;

&lt;p&gt;The other two layouts use the same idea. "Fan" centres the indices around zero with &lt;code&gt;var(--i) - (var(--n) + 1) / 2&lt;/code&gt; and multiplies by a small angle. "Sunflower" rotates every item by the golden angle (137.5°) times its index and pushes it out by &lt;code&gt;sqrt(index / count)&lt;/code&gt;, which gives that seed-head spiral. The panel under the demo shows the CSS for whichever layout is active.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha: custom properties and children
&lt;/h2&gt;

&lt;p&gt;Writing &lt;code&gt;--i: sibling-index()&lt;/code&gt; is handy, but an ordinary custom property doesn't store the number. It stores the tokens &lt;code&gt;sibling-index()&lt;/code&gt;, and those get evaluated wherever you use &lt;code&gt;var(--i)&lt;/code&gt;. If a child of the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; reads &lt;code&gt;var(--i)&lt;/code&gt;, it gets the child's own index, not the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;'s. I checked this in Chromium: a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; that was the second child inside the first &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; got 2, not 1.&lt;/p&gt;

&lt;p&gt;The fix is to register the property as an integer. It then resolves on the element where you declare it, and children inherit the number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--i&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;integer&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;integer&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sibling-index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="py"&gt;--n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sibling-count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;counter-reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's how the demo prints each ball's number: the integer becomes a counter, and the counter becomes text. Pseudo-elements are less of a worry: according to the spec, on &lt;code&gt;::before&lt;/code&gt; and friends these functions resolve against the originating element anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support and fallback
&lt;/h2&gt;

&lt;p&gt;According to MDN's compatibility data, support starts at Chrome and Edge 138, Safari 26.2 and Firefox 154. That's recent, so a fallback still makes sense for a while.&lt;/p&gt;

&lt;p&gt;Feature detection is one line, and the old approach of writing the numbers from JavaScript works as the plan B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;calc(1px * sibling-index())&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo does exactly that and shows a notice when it does. Because the rest of the CSS only reads &lt;code&gt;var(--i)&lt;/code&gt; and &lt;code&gt;var(--n)&lt;/code&gt;, nothing else has to change. Inline styles beat the &lt;code&gt;sibling-index()&lt;/code&gt; declaration in the stylesheet, so the JavaScript numbers win when they're present.&lt;/p&gt;

&lt;p&gt;Two more things from the spec worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;These functions work on the DOM tree, not the flattened tree, just like &lt;code&gt;:nth-child()&lt;/code&gt;. Slotted content is counted by its position in the light DOM.&lt;/li&gt;
&lt;li&gt;If a rule tries to count elements inside a shadow tree from outside, for example through &lt;code&gt;::part()&lt;/code&gt;, the function returns 0. That's deliberate, so shadow DOM structure doesn't leak out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wider support.&lt;/strong&gt; Baseline Newly available means current versions of every major browser support them. Users on older versions still need the fallback until the feature becomes Widely available, which is 30 months after the newly available date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer &lt;code&gt;:nth-child()&lt;/code&gt; ladders.&lt;/strong&gt; Long lists of &lt;code&gt;:nth-child(1)&lt;/code&gt;, &lt;code&gt;:nth-child(2)&lt;/code&gt;… rules in design systems are the obvious thing to replace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pairing with &lt;code&gt;@property&lt;/code&gt;.&lt;/strong&gt; If you plan to reuse the index in child elements, register the custom property from the start. It avoids the most confusing bug these functions can cause.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-values-5/#tree-counting" rel="noopener noreferrer"&gt;CSS Values and Units Module Level 5: tree-counting functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-index" rel="noopener noreferrer"&gt;sibling-index() — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-count" rel="noopener noreferrer"&gt;sibling-count() — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webstatus.dev/features/sibling-count" rel="noopener noreferrer"&gt;sibling-count() and sibling-index() — Web Platform Status&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-18-css-can-count-now-sibling-index-and-sibling-count" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>layout</category>
      <category>animation</category>
      <category>baseline</category>
    </item>
    <item>
      <title>Invoker Commands API: Declarative UI Control</title>
      <dc:creator>richi.codes</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:30:39 +0000</pubDate>
      <link>https://dev.to/richicodes/invoker-commands-api-declarative-ui-control-15ej</link>
      <guid>https://dev.to/richicodes/invoker-commands-api-declarative-ui-control-15ej</guid>
      <description>&lt;p&gt;&lt;strong&gt;This note has an interactive demo.&lt;/strong&gt; &lt;a href="https://richi.codes/article/2026-10-17-invoker-commands-api-declarative-ui-control" rel="noopener noreferrer"&gt;Try it in the original&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opening a modal or a popover from a button no longer needs a single line of JavaScript: the Invoker Commands API has been Baseline since December 2025. This API provides a declarative way to control interactive elements with buttons, reducing JavaScript boilerplate for common UI patterns like dialogs and popovers.&lt;/p&gt;

&lt;p&gt;Historically, if you wanted a button to open a modal dialog or toggle a popover, you'd reach for JavaScript. You'd grab the button, add an event listener, find the target element, and then call its &lt;code&gt;showModal()&lt;/code&gt; or &lt;code&gt;togglePopover()&lt;/code&gt; method. It worked, but it meant waiting for your JavaScript to download and execute, and it added to your bundle size. The web platform is now giving us a more declarative way to handle this, moving that responsibility back to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Invoker Commands?
&lt;/h2&gt;

&lt;p&gt;The Invoker Commands API introduces two new HTML attributes, &lt;code&gt;commandfor&lt;/code&gt; and &lt;code&gt;command&lt;/code&gt;, that you apply directly to a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element. These attributes allow a button to control other interactive elements on the page without needing any JavaScript. This means improved initial page interactivity, as the user does not have to wait for JavaScript to download and execute to make these buttons interactive.&lt;/p&gt;

&lt;p&gt;Think of it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;commandfor&lt;/code&gt;&lt;/strong&gt;: This attribute turns your &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; into a "command invoker" and tells the browser &lt;em&gt;which&lt;/em&gt; element it should control. Its value is the &lt;code&gt;id&lt;/code&gt; of the target element.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;command&lt;/code&gt;&lt;/strong&gt;: This attribute specifies &lt;em&gt;what action&lt;/em&gt; the button should perform on the &lt;code&gt;commandfor&lt;/code&gt; element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This declarative approach can complement and in many cases, replace older methods like &lt;code&gt;popovertargetaction&lt;/code&gt; and &lt;code&gt;popovertarget&lt;/code&gt;, offering a unified and more powerful way to manage button interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Mini Build: Open Dialogs and Popovers
&lt;/h2&gt;

&lt;p&gt;Let's build a simple HTML file that demonstrates how to open both a popover and a modal dialog using only these new attributes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step-by-Step Implementation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create your HTML structure:&lt;/strong&gt; We'll need buttons, a &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element, and a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; element with the &lt;code&gt;popover&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Link buttons to targets:&lt;/strong&gt; Use &lt;code&gt;commandfor&lt;/code&gt; to point to the &lt;code&gt;id&lt;/code&gt; of your target element.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Specify actions:&lt;/strong&gt; Use &lt;code&gt;command&lt;/code&gt; with the appropriate built-in values for popovers and dialogs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the complete working code for a single &lt;code&gt;index.html&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Invoker Commands: Dialogs &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; Popovers&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex-start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nt"&gt;dialog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ccc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nt"&gt;dialog&lt;/span&gt;&lt;span class="nd"&gt;::backdrop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;popover&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f9f9f9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Invoker Commands: Declarative UI&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Popover Example&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"myPopover"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"toggle-popover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Toggle Popover
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"myPopover"&lt;/span&gt; &lt;span class="na"&gt;popover&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This is a popover controlled declaratively!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"myPopover"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"hide-popover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Close Popover
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Modal Dialog Example&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"myDialog"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"show-modal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Show Modal Dialog
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"myDialog"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;This is a Modal Dialog&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;It blocks interaction with the rest of the page until dismissed.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Press ESC or click the button to close it.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"myDialog"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"close"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Close Dialog
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Custom Command Example&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"my-img"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"--rotate-left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rotate left&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"my-img"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"--rotate-right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rotate right&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"my-img"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"A sample image"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"transition: rotate 0.3s ease;"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
        &lt;span class="c1"&gt;// Custom Command JavaScript&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myImg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-img&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;myImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;command&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--rotate-left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;myImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-90deg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Custom command: --rotate-left received!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--rotate-right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;myImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;90deg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Custom command: --rotate-right received!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Support check: the JavaScript properties are `command` and `commandForElement`.&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;command&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Minimal plan B: call the classic methods on click.&lt;/span&gt;
            &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[commandfor]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commandfor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;command&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-modal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showModal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggle-popover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;togglePopover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hide-popover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hidePopover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Explanation:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Popover&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with &lt;code&gt;id="myPopover"&lt;/code&gt; also has the &lt;code&gt;popover&lt;/code&gt; global attribute. This attribute designates it as a popover element, handling its visibility and top-layer behavior.&lt;/li&gt;
&lt;li&gt;  The "Toggle Popover" button uses &lt;code&gt;commandfor="myPopover"&lt;/code&gt; and &lt;code&gt;command="toggle-popover"&lt;/code&gt;. This tells the browser to toggle the visibility of the element with &lt;code&gt;id="myPopover"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Inside the popover, a "Close Popover" button uses &lt;code&gt;commandfor="myPopover"&lt;/code&gt; and &lt;code&gt;command="hide-popover"&lt;/code&gt; to explicitly close it.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;popover&lt;/code&gt; attribute on its own (without a value) defaults to &lt;code&gt;popover="auto"&lt;/code&gt;, which means it can be "light dismissed" (closed by clicking outside or pressing Escape), and usually only one auto popover is shown at a time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modal Dialog&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element with &lt;code&gt;id="myDialog"&lt;/code&gt; is inherently designed for modal behavior.&lt;/li&gt;
&lt;li&gt;  The "Show Modal Dialog" button uses &lt;code&gt;commandfor="myDialog"&lt;/code&gt; and &lt;code&gt;command="show-modal"&lt;/code&gt;. This opens the dialog as a modal, preventing interaction with the rest of the page until it's closed.&lt;/li&gt;
&lt;li&gt;  Inside the dialog, a "Close Dialog" button uses &lt;code&gt;commandfor="myDialog"&lt;/code&gt; and &lt;code&gt;command="close"&lt;/code&gt; to dismiss it. The browser also handles closing the dialog when the Escape key is pressed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Custom Commands&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The &lt;code&gt;command&lt;/code&gt; attribute isn't limited to built-in actions. You can define custom commands by prefixing their names with &lt;code&gt;--&lt;/code&gt;, similar to CSS custom properties.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;my-img&lt;/code&gt; element listens for a &lt;code&gt;command&lt;/code&gt; event. This event fires on the element referenced by &lt;code&gt;commandfor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  The JavaScript code checks &lt;code&gt;event.command&lt;/code&gt; to determine which custom action (&lt;code&gt;--rotate-left&lt;/code&gt; or &lt;code&gt;--rotate-right&lt;/code&gt;) was triggered and applies the corresponding style. This allows you to create declarative APIs for your custom components, reducing the need for explicit event listeners on the buttons themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotchas and Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Browser Support
&lt;/h3&gt;

&lt;p&gt;The Invoker Commands API, along with the &lt;code&gt;commandfor&lt;/code&gt; and &lt;code&gt;command&lt;/code&gt; attributes, achieved "Baseline" status in December 2025. This means it is supported across all major browsers, including Chrome (135+), Edge (135+), Firefox (144+), and Safari (26.2+).&lt;/p&gt;

&lt;p&gt;However, if your audience includes users on older browser versions, you will need a fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback for Older Browsers
&lt;/h3&gt;

&lt;p&gt;For browsers that don't support the Invoker Commands API, you'd typically fall back to traditional JavaScript event listeners. The example checks support with &lt;code&gt;'command' in HTMLButtonElement.prototype&lt;/code&gt; (the JavaScript properties are &lt;code&gt;command&lt;/code&gt; and &lt;code&gt;commandForElement&lt;/code&gt;) and, when it's missing, wires the same buttons to &lt;code&gt;showModal()&lt;/code&gt;, &lt;code&gt;close()&lt;/code&gt;, &lt;code&gt;togglePopover()&lt;/code&gt; and &lt;code&gt;hidePopover()&lt;/code&gt;. A more robust solution would involve a polyfill if you need consistent behavior across all older browsers, or a more comprehensive progressive enhancement strategy. Be aware that polyfills for this API might not fully replicate native behavior, especially concerning ARIA states and focus management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions and HTTPS
&lt;/h3&gt;

&lt;p&gt;The Invoker Commands API itself doesn't introduce new permission prompts or strict HTTPS requirements beyond what's standard for web platform features. Dialogs and popovers are generally considered safe UI elements. However, for any website in production today, HTTPS is a fundamental security and performance requirement, so you should always be serving your content over HTTPS regardless of whether you're using this API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility (A11y)
&lt;/h3&gt;

&lt;p&gt;One of the significant advantages of the Invoker Commands API is that the browser handles much of the accessibility automatically. When you use &lt;code&gt;commandfor&lt;/code&gt; and &lt;code&gt;command&lt;/code&gt; with built-in actions like &lt;code&gt;show-modal&lt;/code&gt; or &lt;code&gt;toggle-popover&lt;/code&gt;, the browser manages focus trapping, focus restoration on close, Escape key handling, and the necessary ARIA attributes (like &lt;code&gt;aria-expanded&lt;/code&gt;). This vastly simplifies building accessible interactive components compared to writing all that logic manually in JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Commands and JavaScript
&lt;/h3&gt;

&lt;p&gt;While the core benefit is reducing JavaScript, the ability to define custom commands using the &lt;code&gt;command&lt;/code&gt; event handler means you can still leverage this declarative pattern for more complex interactions where pure HTML isn't enough. This provides a clean separation: HTML declares &lt;em&gt;what&lt;/em&gt; command to send, and JavaScript listens for &lt;em&gt;how&lt;/em&gt; to execute it on the target element.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep an eye on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Full ecosystem adoption&lt;/strong&gt;: While major browsers support it, tooling and frameworks might take time to fully integrate and promote this API.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;New built-in commands&lt;/strong&gt;: The specification might expand to include more built-in commands for other interactive elements like &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; or media controls.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Polyfills&lt;/strong&gt;: If you need to support a very broad range of older browsers, keep an eye on the maturity and completeness of polyfills for this API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API" rel="noopener noreferrer"&gt;Invoker Commands API - Web APIs | MDN&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.chrome.com/blog/command-and-commandfor/" rel="noopener noreferrer"&gt;Introducing command and commandfor | Blog - Chrome for Developers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover" rel="noopener noreferrer"&gt;popover HTML global attribute - MDN Web Docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://richi.codes/article/2026-10-17-invoker-commands-api-declarative-ui-control" rel="noopener noreferrer"&gt;richi.codes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>html</category>
      <category>webdev</category>
      <category>invokercommands</category>
      <category>popover</category>
    </item>
  </channel>
</rss>
