<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-AU" xml:base="https://chrismorgan.info/">
	<id>https://chrismorgan.info/web.feed</id>
	<author>
		<name>Chris Morgan</name>
		<uri>https://chrismorgan.info</uri>
		<email>me@chrismorgan.info</email>
	</author>
	<updated>2026-05-20T11:05:00+05:30</updated>
	<link href="/web.feed" rel="self" type="application/atom+xml"/>
	<link href="/web"/>
	<title type="html">Chris Morgan’s pages tagged Web</title>
	<entry>
		<title type="html">A few ways of specifying &lt;br class=w&gt;&lt;b&gt;per-theme colours&lt;&#x2f;b&gt;&lt;br class=w&gt; in only CSS</title>
		<published>2026-05-16T16:35:00+05:30</published>
		<updated>2026-05-20T11:05:00+05:30</updated>
		<link href="css-themed-colours" type="text/html"/>
		<id>https://chrismorgan.info/css-themed-colours</id>
		<content type="html"><![CDATA[
<p><i>You’re reading from my feeds; you will accordingly probably miss out on a few styles and niceties.
<br>In this article’s case: ✔/✓/±/?¿/✗/✘ list markers, and pretty colours on the summary table.
<br><a href="/css-themed-colours">View it on my website</a> for the Authentic Intended Experience. Or just take it as it is.</i>
<hr>

<aside>
	<p style=margin-bottom:0><strong>Table of contents:</strong>
	<br><a href=#assumed-markup>The assumed basic HTML and CSS</a>
	<ol style=margin-block:0;padding-left:2em>
		<li><a href=#old-school>Write it all out the hard way</a> (doesn’t scale very well)
		<li><a href=#palette-variables>Lots of colour palette variables</a> (what normal people do)
		<li><a href=#space-toggle-hack>The space toggle hack</a> (poor man’s <code>if()</code>)
		<li><a href=#color-mix><code>color-mix()</code> with one variable per theme</a> (I settled on this for this site, but I’m weird)
		<li><a href=#light-dark><code>light-dark()</code></a> (consider using this these days, instead of or combined with palette variables)
		<li><a href=#if><code>if()</code></a> (maybe some time soon)
		<li><a href=#keyframes>Paused <code><i>@keyframes</i></code> animation</a> (probably a bad idea)
	</ol>
	<p style=margin-top:0><a href=#summary>Summary</a> (a comparison table)
	<br><a href=#parametric-colours>Bonus 1. Parametric colours</a>
	<br><a href=#function-and-mixin>Bonus 2. <code><i>@function</i></code> and <code><i>@mixin</i></code></a>
</aside>

<p>I was thinking about this as part of putting this website together.
<br class=w>Actually it was because I forgot about <code>light-dark()</code>;
<br class=w>if I’d remembered that earlier I probably wouldn’t have ended up with all this!

<p style=margin-bottom:0><strong>My requirements:</strong> (which may not match your requirements)
<ul style=margin-top:0>
	<li>Must support auto (based on <code>prefers-color-scheme</code>),<br class=w> light, and dark, chosen by radio buttons.
	<li>Must work without any JavaScript (persistence is out of scope).
</ul>

<section id=assumed-markup>
	<h2>The assumed basic HTML and CSS</h2>

	<p>Up to you exactly how to include and structure it, but all the examples that follow assume something like this:

	<pre><code><b>&lt;fieldset&gt;
	&lt;legend></b>Theme<b>&lt;/legend>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-auto</span> <b>checked></b> Follow system<b>&lt;/label>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-light</span><b>></b> Light<b>&lt;/label>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-dark</span><b>></b> Dark<b>&lt;/label>
&lt;/fieldset&gt;</b></code></pre>

	<p>I’m going to assume the availability of a few CSS features:

	<ol>
	<li><p><code><i>@media (prefers-color-scheme: dark)</i></code> for automatic selection.
		<br class=w>It shipped in 2019–2020, which is generally enough.

	<li><p style=margin-bottom:0><code><i>:has()</i></code> for manual selection without needing additional JavaScript.
		<br class=w>It’s newer, <a href=https://caniuse.com/css-has>supported back to <time>2023-12</time></a>;
		<br class=w><em>I</em> consider that enough to rely on in general,
		<br class=w>but if you’re not happy with it, you have two options:

		<style>
		@counter-style x-parenthesised-lower-alpha {
			system: extends lower-alpha;
			suffix: ") ";
		}
		</style>
		<ol type=a style=margin-top:0;list-style-type:x-parenthesised-lower-alpha>
			<li>Only support one behaviour (probably auto) sans-JS.
				<br class=w>This is a very reasonable choice.
				<br class=w>You might add a <code>light</code> or <code>dark</code> class to the root element.
			<li>Shift the radio buttons to be direct children of the body,
				hiding them visually,
				<br class=w>and then target <code><i>#theme-foo:checked ~ * …</i></code>
				<br class=w>instead of <code><i>:root:has(#theme-foo:checked) …</i></code>.
				<br class=w>Messy and with some consequences and inconveniences, but generally possible.
		</ol>
	<li><p>I will also use nested selectors in some places in this article (<time>2023-12</time>);
		<br class=w>but they’re easily flattened if you wish.
	</ol>
</section>

<p>Now to the seven techniques.

<style>
	/* Numbered headings: if they wrap, begin the next line after the number. */
	/* … also used for footnote sorts of things. */
	.x-numbered {
		display: flex;
		& > :first-child {
			white-space-collapse: preserve;
		}
		& > :last-child {
			flex: 1;
		}
	}

	.\✔ { list-style-type: '✔ '; &::marker { color: color-mix(in oklab, #0c0, #0f0 var(--d3)) } }
	.\✓ { list-style-type: '✓ '; &::marker { color: color-mix(in oklab, #090, #0a0 var(--d3)) } }
	.\± { list-style-type: '± '; &::marker { color: color-mix(in oklab, #c84, #d95 var(--d3)); font-weight: bold } }
	.\?\¿ { list-style-type: '?¿ '; &::marker { color: color-mix(in oklab, #c84, #d95 var(--d3)); font-weight: bold } }
	.\✗ { list-style-type: '✗ '; &::marker { color: color-mix(in oklab, #900, #a00 var(--d3)) } }
	.\✘ { list-style-type: '✘ '; &::marker { color: color-mix(in oklab, #d00, #f00 var(--d3)) } }
</style>

<section id=old-school>
	<h2 class=x-numbered><span>1. </span><span>Write it all out the hard way</span></h2>

<p>Old-school and verbose.

<ul>
	<li class=✔>The most compatible.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✘>Syntax is verbose.
	<li class=✘>Dark theme value has to be repeated.
</ul>

<pre><code><i>some-element</i> {
	<b>color:</b> darkred;

	<i>:root:has(#theme-dark:checked) &amp;</i> {
		<b>color:</b> pink;
	}

	<i>@media (prefers-color-scheme: dark)</i> {
		<i>:root:has(#theme-auto:checked) &amp;</i> {
			<b>color:</b> pink;
		}
	}
}</code></pre>
<p>It’s often structured differently, with the nesting effectively inverted as in the next example,
<br class=w>but that’s the gist of it.
</section>

<section id=palette-variables>
	<h2 class=x-numbered><span>2. </span><span>Lots of colour palette variables</span></h2>

<p>Probably the most popular approach, traditionally.
<p>Though I was surprised to realise the implementations only landed in 2014–2017.
<br class=w>Sure feels longer ago than that. But it still feels okay to call it “traditional”,
<br class=w>because people often used Sass variables to similar effect before.
<br class=w>(You didn’t often have multiple themes in those days.)

<ul>
	<li class=✔>Works since <time>2017-04</time>.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✔>Use-site syntax is ideal.
	<li class=±>Separating colour definitions from their use location may help or hinder,
		<br class=w>and may or may not fit nicely into your project.
		<br class=w>It <em>can</em> encourage some bad and some good patterns, and may yield a little more overhead.
		<br class=w>(I’m being vague deliberately, and will not elaborate here.)
	<li class=✘>Dark theme value has to be repeated.
</ul>

<p>The declaration:

<pre><code><i>:root</i> {
	<b>--color-somepurpose:</b> darkred;
<span class=unimportant>	<b>--color-another:</b> …;</span>
<span class=very-unimportant>	⋮</span>
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--color-somepurpose:</b> pink;
<span class=unimportant>	<b>--color-another:</b> …;</span>
<span class=very-unimportant>	⋮</span>
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--color-somepurpose:</b> pink;
<span class=unimportant>		<b>--color-another:</b> …;</span>
<span class=very-unimportant>		⋮</span>
	}
}</code></pre>

<p>Adding more themes is trivial.

<p>And use site:

<pre><code><i>some-element</i> {
	<b>color:</b> var(--color-somepurpose);
}</code></pre>

<p>You can also obviously use variables for most of the remaining approaches.
</section>

<section id=space-toggle-hack>
	<h2 class=x-numbered><span>3. </span><span>The space toggle hack</span></h2>
	<ul>
		<li class=✔>Works since <time>2017-04</time>.
		<li class=✓>It’s easy to see how to add more themes than just light and dark.<!-- Only ✓ instead of ✔ because you *have* to spell out every theme every time, you can’t really have any sort of inheritance where you only change a small number of things from a parent theme. -->
		<li class=±>Use-site syntax is not <em>bad</em>, but it is strange.
		<li class=✗>Every use must enumerate every theme (no inheritance or else branch).
		<li class=✗>The feature is somewhat confusing, and will feel back to front.
		<li class=✗>Tooling and formatters may be incompatible with the trick
			<br class=w>(though <em>most</em> tools will be canny to it by now).
	</ul>

	<p>This is poor man’s <code>if()</code> (which actual function we’ll <a href=#if>get to<svg role="presentation" width=".6875em" height=".6875em" viewBox="0 0 11 11"><path fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" d="m4 7 3 3 3-3M4 2q2-1 3 3v5"/></svg></a>).
	<br class=w>You get a single-branch conditional spelled <code>var(<var>--condition</var>, <var>value</var>)</code>.
	<br class=w>Honestly it’s not really <em>worse</em> than <code>if()</code> for cases like this, just different.

	<p>It works because of a frankly ridiculous implementation detail in Custom Properties.
	<br class=w>I’ll defer to <a href=https://lea.verou.me/blog/2020/10/the-var-space-hack-to-toggle-multiple-values-with-one-custom-property/>Lea Verou for further explanation</a>.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--light:</b> initial;
	<b>--dark:</b> ;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--light:</b> ;
	<b>--dark:</b> initial;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--light:</b> ;
		<b>--dark:</b> initial;
	}
}</code></pre>

<p>And use site:

<pre><code><i>some-element</i> {
	<b>color:</b> var(--light, darkred) var(--dark, pink);
}</code></pre>

<p>And <i lang=fr>voilà</i>: if light, <code>darkred</code>; if dark, pink.

<p>It’s not a <em>popular</em> technique, and I forgot about it when compiling this list.
<br class=w>But <a href=https://lobste.rs/c/rl6apj>someone reminded me after publication</a>, so I added it:
<br class=w>it’s worth considering, especially if you want longer compatibility,
<br class=w>because it and variants really do work rather well.
</section>

<section id=color-mix>
	<h2 class=x-numbered><span>4. </span><span><code>color-mix()</code> with one variable per theme</span></h2>

<p>I don’t recall encountering this technique before
<br class=w>(actually I’ve been surprised at how little attention <code>color-mix()</code> has been paid),
<br class=w>but it works.

<p>Basically, define a colour as a mixture of all the colours across all themes,
<br class=w>but use a variable to set one of the themes to 100% and the rest to 0%.
<br class=w>(In practice, you’re normally just using two themes: light and dark.)

<ul>
	<li class=✓>Works since <time>2023-05</time>.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✓>Syntax is reasonably compact.
	<li class=±>Syntax is likely to be unfamiliar, but is fairly straightforward.
		<br class=w>Pity about the interpolation method part that’s currently necessary.
	<li class=?¿>Opens up interesting ideas for actually <em>mixing</em> colours/themes.
		<br class=w>What does <code><b>--dark:</b> <span class=n>50%</span>;</code> mean?
		<br class=w>You can play around with the consequences <button popovertarget=_prefs><span aria-hidden=true style=font-family:sans-serif;opacity:.4>🎨 A<i style=font-family:serif>a</i></span> on this site</button>!
	<li class=✗>If you do try mixing colours and themes, controlling interpolation is a <em>pain</em>;
		<br class=w>you have to figure out how to express your desired function <em>mathematically</em>.
</ul>

<p>This is the technique I settled on for my own site,
<br class=w>because once I realised the potential,
<br class=w>I wanted to play around with mixing themes.
<br class=w><a href=interpolating-themes>I’ve started writing more about it.</a>
<br class=w>It’s more interesting/involved than you might imagine.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--dark:</b> <span class=n>0%</span>;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--dark:</b> <span class=n>100%</span>;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--dark:</b> <span class=n>100%</span>;
	}
}</code></pre>

<p>Then with each colour we can use <code>color-mix()</code> like so:

<pre><code><i>some-element</i> {
	<b>color:</b> color-mix(in oklab, darkred, pink var(--dark));
}</code></pre>

<p>And it will be <code>darkred</code> in light mode and <code>pink</code> in dark mode.

<p>Mostly you only care about changing <em>colours</em>,
<br class=w>but if you wanted to change other things based on the theme you could,
<br class=w>by making the variable a number instead of a percentage, and using <code>calc()</code>.
<br class=w>(Without having thought through the implications—<br class=w>I wish percentages and numbers were unified, with <code class=n>100%</code> equivalent to <code class=n>1</code>.)

<section>
	<h3>Syntax compatibility hazards</h3>
	<p>Beware of following what <a href=https://drafts.csswg.org/css-color-5/#color-mix>the spec</a> allows and what <a href=https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix>MDN talks of</a>:
	<ol>
		<li><p>The interpolation method is optional, defaulting to <code>oklab</code>,
			but this only happened in <a href=https://github.com/w3c/csswg-drafts/issues/10484#issuecomment-3201053810>late 2025</a>.
			<br class=w>All major browsers have now shipped it,
			<br class=w>but by my definition it’s not going to be safe to rely on for another year and a half or so.
			<br class=w>So for quite some time yet, you’ll still need to write <code>in oklab,</code>.
			<br class=w><a href=https://github.com/mdn/browser-compat-data/issues/28693>MDN browser-compat-data lacks it.</a>
			<br class=w><a href=https://github.com/parcel-bundler/lightningcss/issues/1224>Lightning CSS lacks it.</a>
		<li><p>The spec permits <em>one or more</em> colour specification,
			<br class=w>which is handy for mixing more than two themes,
			<br class=w>but most implementations still only support exactly two.
			<br class=w><a href=https://caniuse.com/mdn-css_types_color_color-mix_variadic_color_arguments>This one <em>is</em> tracked in MDN browser-compat-data.</a>
			<br class=w>Lightning CSS lacks it.
			<br class=w>At the time of writing, only Firefox is shipping this (150, 2026-04-21).
	</ol>
</section>

<section>
	<h3>More than two themes</h3>
<p>You can extend this to more than two themes by setting more variables and mixing more colours.
<br class=w>Defining a couple of new themes named Grass and Ocean:

<pre><code><i>:root</i> {
	<b>--grass:</b> <span class=n>0%</span>;
	<b>--ocean:</b> <span class=n>0%</span>;
}

<i>:root:has(#theme-grass:checked)</i> {
	<b>--grass:</b> <span class=n>100%</span>;
}

<i>:root:has(#theme-ocean:checked)</i> {
	<b>--ocean:</b> <span class=n>100%</span>;
}</code></pre>

And using them:

<pre><code><i>some-element</i> {
	<b>color:</b> <ins>color-mix(in oklab, color-mix(in oklab, </ins>color-mix(in oklab,
		darkred,
		pink var(--dark)<ins>),
		green var(--grass)),
		blue var(--ocean)</ins>
	);
}</code></pre>

<p>Hopefully in 2028 (or once Lightning CSS or similar supports it),
<br class=w>you’ll be able to shorten this to the following
<br class=w>(which will also work better with values between 0% and 100%):

<pre><code>	<b>color:</b> color-mix(<del>in oklab,</del>
		darkred,
		pink var(--dark)<ins>,
		green var(--grass),
		blue var(--ocean)</ins>
	);</code></pre>
</section>

</section>

<section id=light-dark>
	<h2 class=x-numbered><span>5. </span><span><code>light-dark()</code></span></h2>

<ul>
	<li class=±>Works since <time>2024-05</time> (Safari 17.5).
	<br class=w>This just <em>barely</em> satisfies my preferred compatibility requirement,<br class=w>“two years of Safari and a year and a half of the rest”.
	<li class=✗>Can’t use it for more or other themes than just light and dark.
	<li class=✔>Syntax is compact and clear.
</ul>

<p>To begin with, you <em>must</em> set <code><b>color-scheme</b></code> properly.
<br class=w>You <em>should</em> do this with all of the techniques,
<br class=w>for a few reasons such as scroll bar and form element colouring,
<br class=w>but with <code>light-dark()</code> it’s necessary for it to work <em>at all</em>.

<pre><code><i>:root</i> {
	<b>color-scheme:</b> light dark;
}

<i>:root:has(#theme-light:checked)</i> {
	<b>color-scheme:</b> light;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>color-scheme:</b> dark;
}</code></pre>

<p>Then:

<pre><code><i>some-element</i> {
	<b>color:</b> light-dark(darkred, pink);
}</code></pre>

</section>

<section id=if>
	<h2 class=x-numbered><span>6. </span><span><code>if()</code></span></h2>

<ul>
	<li class=✘>Only implemented by Chromium family (<time>2025-05</time>); not yet supported on Firefox or Safari.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✓>Syntax largely matches programmer expectations,
	<li class=±>… but is fairly verbose.
</ul>

<p>See also the <a href=#space-toggle-hack>space toggle hack<svg role="presentation" width=".6875em" height=".6875em" viewBox="0 0 11 11"><path fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" d="m4 4l3-3 3 3M4 9q2 1 3-3v-5"/></svg></a>, which is slightly less general than this,
<br class=w>but frankly similarly expressive for colour purposes, and less verbose.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--theme:</b> light;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--theme:</b> dark;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--theme:</b> dark;
	}
}</code></pre>

<p>Then with each colour use:

<pre><code><i>some-element</i> {
	<b>color:</b> if(
		style(--theme: light): darkred;
		style(--theme: dark): pink;
	);
}</code></pre>

<p>This is differently flexible than <code>color-mix()</code>:
<br class=w><code>color-mix()</code> is limited to colours only, and allows continuous interpolation;
<br class=w><code>if()</code> works on all types, but is limited to discrete values.

<p>Adding more themes is trivial based on what has already been shown.
</section>

<section id=keyframes>
	<h2 class=x-numbered><span>7. </span><span>Paused <code><i>@keyframes</i></code> animation</span></h2>

<p>This is a tricky technique I’ve seen applied occasionally,
<br class=w>but I don’t remember ever seeing it used for <em>colours</em>.

<p>Instead of probably defining colour palette <em>variables</em>,
<br class=w>define colour palette <em>keyframes rules</em> (one per property you use it with!),
<br class=w>where “from” means light mode and “to” means dark mode.
<br class=w>Then at use sites, add a paused animation,
<br class=w>and in dark mode apply a negative delay to put the animation at the “to” (dark) value.

<ul>
	<li class=✘>People will think you crazy. There’s an above-average chance they may be right.<!-- Don’t blame me when <em>you</em> get committed, instead of your code. -->
	<li class=✔>Works since <time>2020-01</time> (when Edge adopted Chromium; other than that it’d be <time>2013-07</time>).
	<li class=?¿>As with <code>color-mix()</code>, the technique <em>begs</em> you to allow mixing colours/themes!
	<li class=✓>If you really want to blend themes, you won’t get better than this for controlling interpolation.
		<br class=w>(It’s almost like it was designed for it!)
		<br class=w>(Though… keyframes are not <em>always</em> better than mathematical functions.)
	<li class=✗>Unless you have no other animations, you’ll be stuck with a lot of repetition,
		<br class=w>and some really painful composition of <code><b>animation</b></code> declarations.
	<li class=✗>I bet you’ll run into specificity-style troubles somewhere along the way,
		<br class=w>because animations are even more powerful than <code><i><nobr>!important</nobr></i></code>.
</ul>

<pre><code><i>@keyframes</i> color-somepurpose {
	<i>from</i> { <b>color:</b> darkred; }
	<i>to</i> { <b>color:</b> pink; }
}

<i>some-element</i> {
	<b>animation:</b> <span class=n>1s</span> paused color-somepurpose forwards;

	<i>:root:has(#theme-dark:checked) &amp;</i> {
		<b>animation-delay:</b> <span class=n>-1s</span>;
	}

	<i>@media (prefers-color-scheme: dark)</i> {
		<i>:root:has(#theme-auto:checked) &amp;</i> {
			<b>animation-delay:</b> <span class=n>-1s</span>;
		}
	}
}</code></pre>

<p>I’m not going to explain this one further;
<br class=w>if you don’t understand it, it’s probably for the best—<br class=w>or else take it as a challenge.
<!-- One of my fears in publishing this is that I will inspire someone. -->

</section>

<style>
	.x-table {
		overflow-x: auto;
		table {
			border-collapse: collapse;
			font-variant-numeric: tabular-nums;
		}
		th, td { padding: .25em .5em; border: 1px solid var(--bg); white-space: nowrap; }
		/* Fun detail: *this* blending is in srgb rather than oklab like normal, because we need its nonlinearity-based higher contrast in dark mode. */
		th { background: color-mix(in srgb, var(--bg), var(--color) 16%); }
		/* Alas, this doesn’t work because of .x-table: thead th { position: sticky; top: 0; }/**/
		tbody th { background: color-mix(in srgb, var(--bg), var(--color) 8%) }
		tr > :first-child { position: sticky; left: 0; }
		td { /*border-bottom: 1px solid color-mix(in srgb, var(--bg), var(--color) 8%);*/ text-align: center; }
		td:not([class]) { border-bottom: 1px solid color-mix(in srgb, var(--bg), var(--color) 16%); }
		td:last-child { border-right: 1px solid color-mix(in srgb, var(--bg), var(--color) 16%); }
		tr > :last-child { text-align: left; }

		@media (width < 48rem) {
			margin-inline: clamp(-3rem,-5vw,-1rem);
			padding-inline: clamp(1rem,5vw,3rem);
			tr > :first-child { position: sticky; left: clamp(-3rem,-5vw,-1rem); }
		}
	}

	.x-ideal    { background-color: color-mix(in oklab, oklch(0.8  0.25 140), oklch(0.4  0.1  140) var(--d1)); color: var(--bold-color) }
	.x-good     { background-color: color-mix(in oklab, oklch(0.8  0.20 130), oklch(0.3  0.08 130) var(--d1)); color: var(--bold-color) }
	.x-okay     { background-color: color-mix(in oklab, oklch(0.8  0.18 100), oklch(0.3  0.07 100) var(--d1)); color: var(--bold-color) }
	.x-poor     { background-color: color-mix(in oklab, oklch(0.8  0.16  60), oklch(0.3  0.08  60) var(--d1)); color: var(--bold-color) }
	.x-bad      { background-color: color-mix(in oklab, oklch(0.8  0.14  30), oklch(0.3  0.1   30) var(--d1)); color: var(--bold-color) }
	.x-horrible { background-color: color-mix(in oklab, oklch(0.73 0.2   25), oklch(0.35 0.16  25) var(--d1)); color: var(--bold-color) }
</style>

<p id=summary>Finally as summary, here’s a comparison table.
<div class=x-table>
	<table>
		<thead>
			<tr>
				<th>Technique
				<th>Supported
				<th>Selector<br>repetition
				<th>Pretty/<br>maintainable?
				<th>Interpolation
				<th>Themes
				<th>What using it suggests
		<tbody>
            <tr><th>Manual<td class=x-ideal>Forever<td class=x-bad>Everywhere<td class=x-bad>Unwieldy<td class=x-poor>No<td class=x-good>Any<td>You only <em>have</em> one theme.<!-- Incidentally, at that stage preprocessor variables are better. CSS Custom Properties only becomes useful with things like @media (prefers-color-scheme: dark). -->
            <tr><th>Variables <span class=unimportant>†</span><td class=x-good>2017-04<td class=x-bad>At definition<br>site only<td class=x-good>Barring the definition<br>repetition, great to use<td class=x-poor>No<td class=x-good>Any<td>You’re pretty normal,<br>but not <em>fashionable</em>.
            <tr><th>Space toggle<br>hack<td class=x-good>2017-04<td class=x-good>No<td class=x-good>Slightly odd,<br>but very effective<td class=x-poor>No<td class=x-good>Any<td>You’re not afraid of<br>esoteric solutions.
            <tr><th><code>color-mix()</code><td class=x-okay>2023-05<td class=x-good>No<td class=x-okay>Unfamiliar, temporary<br>slight verbosity<td class=x-good>Yes, but fiddly<br>if nonlinear<td class=x-good>Any<td>You’re probably having<br>too much fun.<!-- I am, I assure you. -->
            <tr><th><code>light-dark()</code><td class=x-poor>2024-05<td class=x-good>No<td class=x-ideal>Paragon<td class=x-poor>No<td class=x-bad>Only light/dark<td>You’re fashionable.<!-- You’re doing the latest thing, not entirely caring if it hurts others distant from you… yeah, sounds about right. -->
            <tr><th><code>if()</code><td class=x-bad>Only<br>Chromium<td class=x-good>No<td class=x-okay>Clear but verbose<td class=x-poor>No<td class=x-good>Any<td>You don’t care about the<br>health of the web <em>at all</em>.
            <tr><th><code><i>@keyframes</i></code><td class=x-good>2020-01<td class=x-horrible>It’s so much<br>worse than<br>just that<td class=x-horrible>This cell is too small<br>to adequately describe<br>the nightmare<td class=x-ideal>Near-ultimate<br>flexibility<td class=x-poor>In theory any<br>(but please<br>don’t try)<td>Disturbed genius?<br>That’s about my limit for<br>benevolent interpretation.
			<!-- What are the odds I’ll decide to use @keyframes at least once like this? Definitely nonzero. -->
	</table>
</div>
<p class=x-numbered><span class=unimportant>† </span><span>This is variables applied to manual, but you can definitely use it with <br class=w><code>color-mix()</code>, <code>light-dark()</code> and <code>if()</code>, and will probably do so.
<br class=w><code><i>@keyframes</i></code> kinda already includes its own alternative, and it’s probably worse than it sounds.</span>

<aside id=parametric-colours>
	<h2 class=x-numbered><b>Bonus 1. </b><span>Parametric colours</span></h2>

	<p>I don’t put this one in the main list, because it’s not a general solution.
	<br class=w>In my opinion, it’s only useful in isolated circumstances.
	<br class=w>It’s fairly unsound to build colour palettes using it,
	<br class=w>because <em>colour just doesn’t work that way</em>.
	<br class=w>Especially yellow. And relative lightness. Both are headaches.

	<p>(I actually forgot about this until someone reminded me, after publishing.
	<br class=w>In fact, I forgot about it until someone reminded me, after publishing.
	<br class=w>I kinda wrote the technique off back in probably 2021–2022,
	<br class=w>when it was in specs but not yet implemented.)

	<ul>
		<li class=✗>Only works since <time>2024-07</time> (with minor caveats until around <time>2024-11</time>).
		<li class=✘>Very limiting in what you can do.
		<li class=✗>Attempts to apply tidy mathematics to perception, and doesn’t do well.
		<li class=✓>If you can apply it, it can make certain broad changes easy.
	</ul>

	<p>I’m not going to give a full example, but here are a couple of very simple demonstrations,
	<br class=w>and I leave further application to you:
	<pre><code>oklch(from <var>&lt;color></var> l c calc(h + 20) / alpha)
oklch(<var>lightness</var> <var>chroma</var> <var>hue</var>)</code></pre>

</aside>

<aside id=function-and-mixin>
	<h2 class=x-numbered><b>Bonus 2. </b><span><code><i>@function</i></code> and <code><i>@mixin</i></code></span></h2>
	<p>Where syntax is less pleasant, in the future <a href=https://drafts.csswg.org/css-mixins/><code><i>@function</i></code> and <code><i>@mixin</i></code></a> may improve it.
	<br class=w>Even the paused animation approach can be made <em>somewhat</em> less horrible.
	<br class=w>I won’t provide any samples, because you can’t use it yet;
	<br class=w>this stuff is immature, more experimental than <code>if()</code>.
	<br class=w>(Despite this, Chromium has, in its usual trouble-making fashion,
	<br class=w><a href=https://caniuse.com/mdn-css_at-rules_function>already shipped</a> <a href="https://wpt.fyi/results/css/css-mixins?label=experimental&label=master&aligned">most of it</a>. This will probably cause problems.)
</aside>]]></content>
		<category term="css" label="CSS"/>
		<category term="meta=also" label="Meta also"/>
	</entry>
	<entry>
		<title type="html">I’ve banned query strings</title>
		<published>2026-05-08T20:00:01+05:30</published>
		<updated>2026-05-08T20:00:01+05:30</updated>
		<link href="no-query-strings" type="text/html"/>
		<id>https://chrismorgan.info/no-query-strings</id>
		<content type="html"><![CDATA[
<p>I don’t like people adding tracking stuff to URLs.
<br class=w>Still less do I like people adding tracking stuff to <em>my</em> URLs.

<p><span class=path>https://chrismorgan.info<wbr>/no-query-strings<wbr><mark>?ref=example.com</mark></span>? Did I ask?
<br class=w>If I wanted to know I’d look at the <code>Referer</code> header;
and if it isn’t there, it’s probably for a good reason.
<br class=w>You abuse your users by adding that to the link.

<p><span class=path>https://chrismorgan.info<wbr>/no-query-strings<wbr><mark>?utm_source=example&amp;utm_<i class=unimportant>&c.</i></mark></span>?
<br class=w>Hey! That one’s even worse, <a href=https://en.wikipedia.org/wiki/UTM_parameters>UTM parameters</a> are for <em>me</em> to use, not <em>you</em>.
<br class=w>Leave my URLs alone.

<p>So I’ve decided to try a blanket ban for this site:
<strong>no unauthorised query strings</strong>.

<p>At present I don’t use any query strings.
<br class=w>If I ever start using any query strings, I’ll allow only known parameters.
<br class=w>(In past times I used <span class=path>?t=…</span> and <span class=path>?h=…</span> cache-busting URLs for stylesheet URLs;
<br class=w>and I decided I’m okay breaking such requests; there shouldn’t be any legitimate ones.)

<p>Want to see what happens if you add a query string<a href=?>?</a>
Go ahead, try it.
<!-- One fun idea for an *empty* query: serve the original file, but with all text . and ! changed to ? -->

<p>It’s my website: I can do what I want with it.
<p>And you can do what you want with yours!

<p><em>This is currently implemented <a href=Caddyfile#?>in my Caddyfile</a>.</em>

<!--
<section id=changed>
	<h2>Changed</h2>
	<p>If you happen to change what you do because of this, I’d <a href=mailto:me@chrismorgan.info>like to hear</a>.
	<ul>
		<li><a href=https://susam.net/no-query-strings.html>Susam Pal was inspired to stop adding <span class=path>?via=…</span> in Wander</a>
	</ul>
</section>
-->

<aside>
	<h2>Aside: this page’s URL</h2>
	<p>It was <em>very</em> tempting to publish this at <span class=path>https://chrismorgan.info/?</span>
	(path «<span class=path></span>», query <span class=path></span><!-- Despite what many tools may assume, empty string ≠ null -->).

	<p>This would have broken a lot of common but incorrect assumptions, and made some tools cry:
	<br class=w><code>curl</code>, for example, seems to illegitimately strip a trailing question mark
	<br class=w>(could be only for the command line, didn’t test library usage).

	<p>In the end, I decided to respect the notion of path and have mercy on people.
	<br class=w>Myself most of all;
	I’m already pushing Caddy in enough directions it’s not happy with.

	<p>My next plan was to publish it at <span class=path>/%3F</span>
	(path «<span class=path>?</span>», query null),
	<br class=w>but I guess no one has ever tried doing such stupid things with Caddy before;
	<br class=w><a href=https://github.com/caddyserver/caddy/issues/7678 title="Caddy issue #7678: try_files mangles paths containing characters like ? and % (whereas file_server without the rewrite works properly)">if <code><b>try_files</b></code> rewriting is involved it can’t cope</a>.

	<p>So <span class=path>/no-query-strings</span> will do.
	<br class=w>I’ll probably use <span class=path>/?</span> or <span class=path>/%3F</span> later for something <em>else</em> about query strings.
</aside>]]></content>
		<category term="url" label="URL"/>
		<category term="web" label="Web"/>
		<category term="opinions" label="Opinions"/>
		<category term="meta=only" label="Meta only"/>
	</entry>
</feed>
