piss

entries

  1. Algorithmic hover states with contrast-color()
    daverupert.com 2026-01-08T16:46:00+00:00
  2. Using your design system colors with contrast-color()
    daverupert.com 2026-01-09T03:21:00+00:00
  3. Interpolate contrast-color() to manipulate lightness
    daverupert.com 2026-01-09T15:35:00+00:00
  4. Focus rings with nested contrast-color()?
    daverupert.com 2026-01-11T18:30:00+00:00
  5. The best version of my site so far...
    daverupert.com 2026-01-18T14:29:00+00:00
  6. Waiting for the power to go out
    daverupert.com 2026-01-24T21:44:00+00:00
  7. I'm swearing off APIs entirely
    daverupert.com 2026-01-26T06:27:00+00:00
  8. Write about the future you want
    daverupert.com 2026-02-04T15:45:00+00:00
  9. Magic Words
    daverupert.com 2026-02-09T16:03:00+00:00
  10. Priority of idle hands
    daverupert.com 2026-02-23T03:13:00+00:00
  11. Smaller and dumber
    daverupert.com 2026-02-23T05:33:00+00:00
  12. People are not friction
    daverupert.com 2026-03-20T15:54:00+00:00
  13. Before I go: People like it when other people make things
    daverupert.com 2026-04-04T17:00:00+00:00
  14. Ozempic dreams
    daverupert.com 2026-04-04T19:18:00+00:00
  15. Inverted themes with light-dark()
    daverupert.com 2026-04-07T15:31:00+00:00
  16. When moving fast, talking is the first thing to break
    daverupert.com 2026-04-13T15:10:00+00:00
  17. I don't want a screenshot of your Claude conversation
    daverupert.com 2026-04-15T15:17:00+00:00
  18. 10,000-watt GPU meet 40-watt lump of meat
    daverupert.com 2026-04-21T19:36:00+00:00
  19. The duality of language models in the browser
    daverupert.com 2026-05-04T00:33:00+00:00
  20. Vibe Check №42
    daverupert.com 2026-05-05T13:41:00+00:00

Algorithmic hover states with contrast-color()

daverupert.com

source

<p>Firefox 146 added support for <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/contrast-color"><code>contrast-color()</code></a> joining Safari 26 in the First Implementor’s Club. For those unfamiliar, <code>contrast-color(&lt;color&gt;)</code> is a new CSS function that will take a <code>&lt;color&gt;</code> as input and returns either <code>white</code> or <code>black</code> depending on which has the most contrast.</p> <p>The quintessential example is choosing a foreground text <code>color</code> with the best contrast.</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">button</span> <span class="p">{</span> <span class="py">--button-bg</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span> <span class="nl">background</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">);</span> <span class="nl">color</span><span class="p">:</span> <span class="n">contrast-color</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">));</span> <span class="c">/* @returns black (5.25:1 WCAG AA Pass) not white (3.99:1 WCAG AA Fail) */</span> <span class="p">}</span> </code></pre></div></div> <p>If someone changes <code>--button-bg</code> to purple, the foreground color automatically resolves to the either white or black, whichever has more contrast. It avoids having to set an extra token for color and takes the guess work out of picking an accessible foreground color.</p> <p>I think this is going to be an incredible boon to design systems where I don’t control what <code>--button-bg</code> is, but I do care about providing accessible experiences. And I think that’s the goal of this feature; to have “smart defaults” that lead to more accessible websites, easier algorithmically-driven color systems, and better “theme a whole website from a single color picker” demos.</p> <h2>Sure contrast-color() can do foreground colors, but what about backgrounds?</h2> <p>We’re having conversations at work about algorithmically driven rest/hover/active states for Buttons. In the current <a href="https://web.dev/baseline/">Baseline</a> you can use <code>color-mix()</code> to lighten/darken colors on <code>:hover</code>…</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">button</span> <span class="p">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">);</span> <span class="nl">color</span><span class="p">:</span> <span class="n">contrast-color</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">));</span> <span class="err">&amp;:hover,</span> <span class="err">&amp;:focus</span> <span class="err">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="n">color-mix</span><span class="p">(</span> <span class="n">in</span> <span class="n">srgb</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">)</span> <span class="m">90%</span><span class="p">,</span> <span class="no">black</span> <span class="m">10%</span> <span class="p">)</span> <span class="p">}</span> <span class="err">}</span> </code></pre></div></div> <p>The code above will dim your button on <code>:hover</code> 10% by mixing in black. But what if our Buttons are already dark (black, navy, etc)? In that situation we want to lighten the background-color instead of dimming. We can glue on new classes like <code>button.lighten-on-hover</code> or <code>button.invert-hover</code> and that works… until we get to light and dark theme modes of our Button where you probably want to lighten/darken oppositely depending on the mode…</p> <p>Ugh. In that situation you’d have <code>@media (prefers-color-scheme: dark)</code>, <code>[data-theme=&quot;dark&quot;]</code> styles in your Button styles and people are mad now because the Button styles are too complex. There’s got to be a better way!</p> <p>Well, do I have good news for you…</p> <p class="codepen" style="height: 500px; display: flex; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/davatron5000/pen/bNpzYzX"> contrast-color() powered lighten/darken bg on hover </a> by Dave Rupert (<a href="https://codepen.io/davatron5000">@davatron5000</a>) on <a href="https://codepen.io">CodePen</a>.</span> </p> <p>Per my previous conversations, I wondered if we could use <code>contrast-color()</code> to programmatically lighten/darken an button based on its current <code>background-color</code>. If the Button is black, and the contrast-color is white, let’s mix in white (and vice versa):</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span> <span class="py">color-scheme</span><span class="p">:</span> <span class="n">light</span> <span class="n">dark</span><span class="p">;</span> <span class="py">--button-bg</span><span class="p">:</span> <span class="n">light-dark</span><span class="p">(</span><span class="n">navyblue</span><span class="p">,</span> <span class="n">lightpurple</span><span class="p">);</span> <span class="p">}</span> <span class="nt">button</span> <span class="p">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">);</span> <span class="nl">color</span><span class="p">:</span> <span class="n">color-contrast</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">));</span> <span class="err">&amp;:hover,</span> <span class="err">&amp;:focus</span> <span class="err">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="n">color-mix</span><span class="p">(</span> <span class="n">in</span> <span class="n">srgb</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">)</span> <span class="m">75%</span><span class="p">,</span> <span class="n">contrast-color</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">))</span> <span class="m">10%</span> <span class="p">)</span> <span class="p">}</span> <span class="err">}</span> </code></pre></div></div> <p>Huzzah! Now our Button’s hover states go in the desired direction and our foreground <code>color</code> is intrinsically styled based on its own <code>background-color</code>. Nice. From a design systems perspective I’m pretty excited about the possibility to remove a bunch of state-based tokens from our collection.</p> <h3>Now available in… everywhere?</h3> <p>This approach only works in Safari and Firefox. However, if I use <a href="https://lea.verou.me/blog/2024/contrast-color/">Lea Verou’s method of polyfilling <code>contrast-color()</code></a>, we can pop in a custom <code>@function --contrast-color()</code> that works in Chromium 139+. The final working solution looks like this:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* @function supported in Chromium */</span> <span class="k">@function</span> <span class="n">--contrast-color</span><span class="p">(</span><span class="n">--bg-color</span><span class="p">)</span> <span class="p">{</span> <span class="py">--l</span><span class="p">:</span> <span class="n">clamp</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="p">(</span><span class="n">l</span> <span class="p">/</span> <span class="n">var</span><span class="p">(</span><span class="n">--l-threshold</span><span class="p">,</span> <span class="m">0.623</span><span class="p">)</span> <span class="n">-</span> <span class="m">1</span><span class="p">)</span> <span class="err">*</span> <span class="n">-infinity</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="py">result</span><span class="p">:</span> <span class="n">oklch</span><span class="p">(</span><span class="n">from</span> <span class="n">var</span><span class="p">(</span><span class="n">--bg-color</span><span class="p">)</span> <span class="n">var</span><span class="p">(</span><span class="n">--l</span><span class="p">)</span> <span class="m">0</span> <span class="n">h</span><span class="p">);</span> <span class="p">}</span> <span class="nt">button</span> <span class="p">{</span> <span class="c">/* contrast-color() supported in Safari &amp; Firefox */</span> <span class="py">--button-fg</span><span class="p">:</span> <span class="n">contrast-color</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">));</span> <span class="err">@supports</span> <span class="err">not</span> <span class="err">(</span><span class="nl">color</span><span class="p">:</span> <span class="n">contrast-color</span><span class="p">(</span><span class="no">red</span><span class="p">))</span> <span class="err">{</span> <span class="n">--button-fg</span><span class="p">:</span> <span class="n">--contrast-color</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">));</span> <span class="p">}</span> <span class="nt">background</span><span class="o">:</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--button-bg</span><span class="o">);</span> <span class="nt">color</span><span class="o">:</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--button-fg</span><span class="o">);</span> <span class="o">&amp;</span><span class="nd">:hover</span><span class="o">,</span> <span class="o">&amp;</span><span class="nd">:focus</span><span class="p">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="n">color-mix</span><span class="p">(</span> <span class="n">in</span> <span class="n">srgb</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-bg</span><span class="p">)</span> <span class="m">75%</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--button-fg</span><span class="p">)</span> <span class="m">10%</span> <span class="p">);</span> <span class="p">}</span> <span class="err">}</span> </code></pre></div></div> <p>Depending on your browser matrix, this may work for you. It’s probably a <em>smidge</em> too new for us to roll out to customers, right now but for personal sites heck yeah.</p> <p>This is interesting tech and I’m excited to dig in more. And spoiler alert, this is the first post in a small little series I have already written up for you.</p>