<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Design Notes on Liftoff</title><link>https://hugo-liftoff.kevinhorst.de/en/categories/design-notes/</link><description>Recent content in Design Notes on Liftoff</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://hugo-liftoff.kevinhorst.de/en/categories/design-notes/index.xml" rel="self" type="application/rss+xml"/><item><title>Icons without a build step</title><link>https://hugo-liftoff.kevinhorst.de/en/articles/icons-without-a-build-step/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://hugo-liftoff.kevinhorst.de/en/articles/icons-without-a-build-step/</guid><description>&lt;p&gt;Icon fonts ship every glyph to every visitor. Sprite sheets need a
build step and a naming convention nobody remembers. Liftoff takes a
third route: it fetches each icon from the Iconify API while Hugo
builds, and inlines the SVG.&lt;/p&gt;
&lt;div class="codeblock"&gt;
 &lt;div class="codeblock__head"&gt;
 &lt;span class="codeblock__lang"&gt;go-html-template&lt;/span&gt;
 &lt;button type="button" class="codeblock__copy" data-action="copy-code" aria-label="Copy"&gt;Copy&lt;/button&gt;
 &lt;/div&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;icon.html&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;simple-icons:codeberg&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Any set on Iconify works, addressed as &lt;code&gt;set:name&lt;/code&gt;. A bare name falls
back to Material Design Icons, so &lt;code&gt;check&lt;/code&gt; and &lt;code&gt;mdi:check&lt;/code&gt; are the same
icon.&lt;/p&gt;</description></item><item><title>A performance budget you can actually keep</title><link>https://hugo-liftoff.kevinhorst.de/en/articles/a-performance-budget/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://hugo-liftoff.kevinhorst.de/en/articles/a-performance-budget/</guid><description>&lt;p&gt;A performance budget only works if it is short enough to remember.
Three numbers are enough for a content site.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Budget&lt;/th&gt;
					&lt;th&gt;Target&lt;/th&gt;
					&lt;th&gt;What breaks it&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Requests per page&lt;/td&gt;
					&lt;td&gt;Under 10&lt;/td&gt;
					&lt;td&gt;Web fonts, analytics&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;CSS&lt;/td&gt;
					&lt;td&gt;Under 30 kB&lt;/td&gt;
					&lt;td&gt;Utility frameworks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JavaScript&lt;/td&gt;
					&lt;td&gt;Under 10 kB&lt;/td&gt;
					&lt;td&gt;Anything with a build&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Liftoff spends its JavaScript on three things: the theme toggle, the
copy buttons on code blocks, and the blueprint filter. All three
degrade to something usable when scripting is off, which is the only
reason they are allowed to exist at all.&lt;/p&gt;</description></item><item><title>Dark mode that respects the system</title><link>https://hugo-liftoff.kevinhorst.de/en/articles/dark-mode-defaults/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://hugo-liftoff.kevinhorst.de/en/articles/dark-mode-defaults/</guid><description>&lt;p&gt;A theme toggle that defaults to dark for everyone is a preference
disguised as a feature. Liftoff reads &lt;code&gt;prefers-color-scheme&lt;/code&gt; first, and
only writes to &lt;code&gt;localStorage&lt;/code&gt; once someone actually picks a scheme.&lt;/p&gt;
&lt;div class="codeblock"&gt;
 &lt;div class="codeblock__head"&gt;
 &lt;span class="codeblock__lang"&gt;js&lt;/span&gt;
 &lt;button type="button" class="codeblock__copy" data-action="copy-code" aria-label="Copy"&gt;Copy&lt;/button&gt;
 &lt;/div&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;liftoff.theme&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;(prefers-color-scheme: light)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That ordering matters. A visitor whose system is light gets a light
page on first load, without a flash of dark. A visitor who then picks
dark keeps dark, even if their system changes later, because an
explicit choice outranks an inferred one.&lt;/p&gt;</description></item></channel></rss>