<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<title>Florens Verschelde</title>
	<id>urn:uuid:c6e9028c-f392-5f5f-9a97-3267a03f1956</id>
	<link href="https://fvsch.com/feed.xml" rel="self" type="application/atom+xml"/>
	<link href="https://fvsch.com" rel="alternate" type="text/html"/>
	<updated>2026-04-28T19:37:01+02:00</updated>
	<icon>https://fvsch.com/assets/images/icon.png?v=s4fph2</icon>
	<author>
		<name>Florens Verschelde</name>
	</author>
	<entry xml:lang="fr">
		<id>urn:uuid:977be89b-b7d7-5231-9d6a-77a4b8538b51</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/gabarits-html"/>
		<title>Gabarits HTML et CSS simples (édition 2024)</title>
		<author><name>Florens Verschelde</name></author>
		<published>2024-12-10T00:00:00+01:00</published>
		<updated>2024-12-10T00:00:00+01:00</updated>
		<summary type="text">Mise à jour d’une série de gabarits de mise en page web, créés initialement en 2008, avec les techniques CSS de 2024.</summary>
		<content type="html">&lt;p&gt;Il y a maintenant 16 ans, j’ai créé &lt;a href=&quot;/media/gabarits-html/2008/index.html&quot;&gt;ces 11 gabarits de mise en page&lt;/a&gt; pour la formation Elephorm &lt;cite&gt;Apprendre XHTML et CSS&lt;/cite&gt;. Ils correspondent aux types de mise en page de site web courants à l’époque, compatibles avec Internet Explorer 6. Ils utilisent principalement &lt;code&gt;float&lt;/code&gt; et &lt;code&gt;position: absolute&lt;/code&gt;, et ne sont pas &lt;i lang=en&gt;responsive&lt;/i&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Pourquoi une mise à jour&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Depuis, j’ai reçu une moyenne de un (&lt;i lang=&quot;en&quot;&gt;one&lt;/i&gt;, &lt;i lang=&quot;es&quot;&gt;uno&lt;/i&gt;) courriel par an demandant si une mise à jour utilisant des styles CSS plus modernes était prévue.&lt;/p&gt;
&lt;p&gt;L’intérêt d’une mise à jour ne me semblait pas évident, car le design de ces gabarits correspond aux tendances du milieu des années 2000, assez éloignées des mises en page courantes dans les années 2010 et au-delà. Je répondais donc cordialement: cherchez ailleurs.&lt;/p&gt;
&lt;p&gt;Cependant, un énième courriel annuel piqua ma curiosité. À quoi pourrait ressembler une mise à jour technique, avec les outils de 2024 et ces designs de 2008? Je me suis alors embarquée dans une réécriture complète.&lt;/p&gt;
&lt;p&gt;Le résultat est désormais visible à côté de la version de 2008, conservée comme archive.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;À explorer en ligne: &lt;a href=&quot;/media/gabarits-html/2024/index.html&quot;&gt;Gabarits HTML et CSS simples (version 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Et sur GitHub, avec l’historique des modifications: &lt;a href=&quot;https://github.com/fvsch/gabarits-html/&quot;&gt;fvsch/gabarits-html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Comme pour la version de 2008, les feuilles de styles sont abondamment commentées pour expliquer les techniques de mise en page utilisées.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Différences avec la version de 2008&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Les couleurs de texte et de fond sont définies dans des variables CSS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Les styles sont &lt;i lang=&quot;en&quot;&gt;responsive&lt;/i&gt; et écrits avec une logique &lt;i lang=&quot;en&quot;&gt;mobile first&lt;/i&gt;: on commence par définir la mise en page la plus simple avec les blocs les uns sous les autres (en général adaptée à l’affichage sur petit écran), et on utilise des &lt;i lang=&quot;en&quot;&gt;Media Queries&lt;/i&gt; pour définir des surcharges de styles pour les écrans plus larges.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dans quelques cas, j’ai aussi utilisé des &lt;i lang=&quot;en&quot;&gt;Container Queries&lt;/i&gt;, afin d’éviter d’avoir à dupliquer des valeurs précises entre différentes &lt;i lang=&quot;en&quot;&gt;Media Queries&lt;/i&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;La plupart des mises en page sont réalisées avec les grilles CSS (&lt;code&gt;display: grid&lt;/code&gt;). Ici, comme les principaux conteneurs sont connus à l’avance, j’ai aussi largement utilisé &lt;code&gt;grid-template-areas&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Au lieu de doubler le &lt;code&gt;body&lt;/code&gt; avec un conteneur &lt;code&gt;&amp;lt;div id=&quot;global&quot;&amp;gt;…&amp;lt;/div&amp;gt;&lt;/code&gt;, l’élément &lt;code&gt;body&lt;/code&gt; est utilisé directement pour la mise en page de ses enfants.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Le cas de figure “conteneur sur toute la hauteur du viewport” peut être géré simplement avec &lt;code&gt;min-height: 100dvh&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Le cas de figure “colonnes de même hauteur” était problématique en 2008. On utilisait des effets de trompe-l’oeil avec une image de fond sur un conteneur, répétée en hauteur, pour dessiner les couleurs de fond des colonnes. Ce n’est plus un problème avec les grilles CSS, avec Flexbox, ou même avec &lt;code&gt;display: table&lt;/code&gt; (que j’ai souvent utilisé à l’époque d’Internet Explorer 8).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:8741e79a-5788-5468-9e4b-e09685301059</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/stale-bots"/>
		<title>Don’t use stale bots</title>
		<author><name>Florens Verschelde</name></author>
		<published>2023-02-25T06:00:00+01:00</published>
		<updated>2023-02-25T06:00:00+01:00</updated>
		<summary type="text">It’s okay for a software project to have a lot of open issues. It’s okay if you’re not able to triage issues, let alone close them. It’s not okay to use stale bots.</summary>
		<content type="html">&lt;p&gt;It’s okay for a software project to have a lot of open issues. It’s okay if you’re not able to triage issues, let alone close them. It’s not okay to use stale bots.&lt;/p&gt;
&lt;article-nav level=&quot;2&quot;&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;What’s a stale bot?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In a &lt;em&gt;bug tracker&lt;/em&gt;&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, a &lt;em&gt;stale bot&lt;/em&gt; is an automated process that marks an issue as “closed” after a period of inactivity.&lt;/p&gt;
&lt;p&gt;If you’re using a piece of software and want to report an issue on a bug tracker, this is how you may end up meeting a stale bot:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You find the bug tracker for the software you’re using, and file a new issue.&lt;/li&gt;
&lt;li&gt;Nobody seems to read your report or post any answer. That’s frustrating, but people are busy, and they’re usually not paid to handle your issue.&lt;/li&gt;
&lt;li&gt;After a few weeks or a few months, a “bot” account adds a comment on your issue to say that it’s “stale”, and closes the issue or announces that it will be closed soon.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a user, meeting a stale bot is often a frustrating experience.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Mismatched mindsets&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;For a user creating an issue in a software project, that issue is strongly related to the software and their experience of the software.&lt;/p&gt;
&lt;p&gt;They ran into a bug, or didn’t understand an API. They may be blocked in their work, or will need extra time to find a workaround. And as long as their experience doesn’t improve, the issue remains relevant; it cannot be “stale”.&lt;/p&gt;
&lt;p&gt;For a project maintainer, issues are artifacts that are only loosely related to the software.&lt;/p&gt;
&lt;p&gt;Issues are often unclear, lack context and steps to reproduce. Just figuring out how the issue relates to the software is a task in and of itself. As time goes by, it becomes very unlikely than whoever opened the issue will come back to reply to requests for information.&lt;/p&gt;
&lt;p&gt;For the maintainer, the &lt;em&gt;issue-as-artifact&lt;/em&gt; may grow “stale”, hardly actionable in the first place and becoming less so as time goes by. Surely closing that issue automatically makes sense?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Well.&lt;/em&gt; Now you have a communication problem.&lt;/p&gt;
&lt;p&gt;From your vantage as a maintainer, you are discarding an artifact. From the reporter of the issue, you are deciding against the possibility of fixing or improving the software.&lt;/p&gt;
&lt;p&gt;And if bridging this perception gap is hard, automating it — using an impersonal form of one-way communication — will only make it worse.&lt;/p&gt;
&lt;p&gt;It might be fine, and indeed useful, to close a vague report where the reporter never replied to requests for information. But when you’re auto-closing issues, you’re also closing issues with good steps to reproduce, screenshots, comments, investigations from users and reduced test cases.&lt;/p&gt;
&lt;p&gt;Auto-closing information-rich issues tells users that their efforts — because it does take time and focus to make a good report — are not valued.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Stale bots break your bug tracker&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By sweeping dirt under the rug, you have created new UX issues for users of your bug tracker.&lt;/p&gt;
&lt;p&gt;If you needed another reason besides &lt;a href=&quot;https://github.com/TomasHubelbauer/fuck-you-stale-bot&quot;&gt;users thinking it’s annoying and insulting&lt;/a&gt;, take a good look at the bug trackers of projects that auto-close issues after 3, 6 or 12 months.&lt;/p&gt;
&lt;p&gt;The list of issues for their GitHub repository may look nice — a few hundred open issues, a few thousand closed issues, good job! — but the actual experience of using that bug tracker is now significantly worse.&lt;/p&gt;
&lt;p&gt;First, users can end up on one of your issues from any external source: a link from another repository, a URL in source code, a link in StackOverflow or in a search result page. They may want to know things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I am holding the software wrong?&lt;/li&gt;
&lt;li&gt;Is it a known limitation or bug?&lt;/li&gt;
&lt;li&gt;Was there a recent fix that I could upgrade to?&lt;/li&gt;
&lt;li&gt;Any workarounds I could use?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main signal answering those questions is the bug status. Is it green for “Open” or purple for “Closed”?&lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;On a repository that closes issues as “stale”, you have to scroll to the bottom of the issue to figure out if “Closed” means “fixed”, “will not fix” or “we’re arbitrarily hiding this issue from the main list”.&lt;/p&gt;
&lt;p&gt;Which leads us to our second UX issue: auto-closed issues don’t appear in search results.&lt;/p&gt;
&lt;p&gt;Now, when a user tries to report an issue and searches the existing issues, &lt;em&gt;as your contributing guidelines say they should&lt;/em&gt;, they will not find that identical issue which was auto-closed&lt;sup id=&quot;fnref1:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote-ref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This leads to more duplicates in your repo. Now everyone is going to waste time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Maintainers&lt;/strong&gt; may redo a lot of the triaging and investigation work that was done 6 months or one year earlier on the auto-closed issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reporters&lt;/strong&gt; are wasting the time spent reporting an issue, following the many steps in the issue template, making screenshots and reduced test cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve seen repos where common problems were reported at least 3 times, with the latest report linking to a “stale” closed issue, itself linking to a previous “stale” issue. What a mess.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Why projects use stale bots&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One reason is brand management.&lt;/p&gt;
&lt;p&gt;Companies with public bug trackers want to look like they’re on top of bug fixes and feature requests. Open-source projects striving for adoption or looking for sponsorship are aware that some in the software community see a large issue count on GitHub dot com as a marker of a “dead” project.&lt;/p&gt;
&lt;p&gt;Stale bots will sweep that dirt under the rug. But while that improves the very first look at a repository,&lt;/p&gt;
&lt;p&gt;Meanwhile, large open-source projects like Chromium or Firefox have hundreds of thousands of open bugs in their bugs trackers, and few people bat an eye. Maybe it helps that those projects are not using GitHub&lt;sup id=&quot;fnref1:4&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote-ref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Here the solution is simple, but hard to make peace with: don’t artificially reduce issue count. If you need to signal that your project is active, there are other ways.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Alternatives to stale bots&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Okay, so you agree that stale bots have big downsides. Still, your project got a bunch of users and you are overwhelmed by way too many issues. What else can you do?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nothing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Accept that large projects will have thousands of open issues. It’s fine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Triage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set up a process to qualify issues. The goal is to curate a subset of issues that can actually be worked on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Decide what to work on. Have a roadmap, prioritize high impact, do paper-cuts and/or stability projects.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every step builds on the previous one. You will need people and work for all steps after the first one.&lt;/p&gt;
&lt;p&gt;If you have declared &lt;em&gt;triage and project management bankruptcy&lt;/em&gt;&lt;sup id=&quot;fnref1:5&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote-ref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;, auto-closing issues won’t fix that.&lt;/p&gt;
&lt;p&gt;If you can’t improve your process, then I suggest you stop at step one. Just don’t use a stale bot.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;Bug trackers or issue trackers are websites where users of a piece of software can report issues they’re running into, and often voice suggestions for new features.&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Other bug trackers have richer statuses, but we’ll focus on GitHub given that’s where most stale bots are lurking.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:3&quot;&gt;
&lt;p&gt;Some will suggest that maintainers and users of the software can just remove the &lt;code&gt;is:open&lt;/code&gt; search filter that GitHub sets by default. But only a fraction of users will do that. It’s more efficient to not create a problem in the first place.&amp;#160;&lt;a href=&quot;#fnref1:3&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:4&quot;&gt;
&lt;p&gt;GitHub might be responsible for half of the idea that a project with many open issues is “dead” or somehow “bad”, thanks to their decision to put counts of open issues and pull requests on every single page of their bug tracker.&amp;#160;&lt;a href=&quot;#fnref1:4&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:5&quot;&gt;
&lt;p&gt;Maybe because you’re running an open-source project with not enough funding compared to its user base.&amp;#160;&lt;a href=&quot;#fnref1:5&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:8c388cec-c88b-52bf-9ccb-a2409326a85d</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/transparent-iframes"/>
		<title>Transparent iframes and dark mode</title>
		<author><name>Florens Verschelde</name></author>
		<published>2022-05-11T12:00:00+02:00</published>
		<updated>2022-05-11T12:00:00+02:00</updated>
		<summary type="text">Today we’re talking about how transparent iframes, dark mode, unreadable text and varying levels of browser support are doing my head in. But don’t worry, there’s light at the end of the tunnel.</summary>
		<content type="html">&lt;p&gt;Today we’re talking about how transparent iframes, dark mode, unreadable text and varying levels of browser support are doing my head in. But don’t worry, there’s &lt;del&gt;light mode&lt;/del&gt; light at the end of the tunnel.&lt;/p&gt;
&lt;p&gt;And just so you know what you’re missing, I workshoped other titles like “Transparent iframes considered darkful” (always wanted to do one of those), “A plague of dark mode iframes” (&lt;a href=&quot;https://songwhip.com/van-der-graaf-generator/a-plague-of-lighthouse-keepers-remastered-2021&quot;&gt;reference 1&lt;/a&gt;) and “Darkmode on the frame of town” (&lt;a href=&quot;https://songwhip.com/bruce-springsteen/darknessontheedgeoftown&quot;&gt;reference 2&lt;/a&gt;).&lt;/p&gt;
&lt;article-nav&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;Iframes, transparent by default&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I work at &lt;a href=&quot;https://stackblitz.com/&quot;&gt;StackBlitz, an online IDE for Web stuff&lt;/a&gt;, and like many online code editors&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; we typically render two HTML documents:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A top-level document on the stackblitz.com origin, showing a code editor and other tools.&lt;/li&gt;
&lt;li&gt;A “preview” document in an iframe, hosted on a different origin (for your own safety), that can be virtually anything that authors of StackBlitz projects generate&lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;: arbitrary HTML and CSS, &lt;code&gt;text/plain&lt;/code&gt; HTTP responses, SVG documents, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is where stuff gets tricky.&lt;/p&gt;
&lt;p&gt;By default, iframes are transparent. If you don’t specify a &lt;code&gt;background-color&lt;/code&gt; in the embedded document (say, on the &lt;code&gt;html&lt;/code&gt; element or on &lt;code&gt;body&lt;/code&gt;), the background color from the parent page shows through.&lt;/p&gt;
&lt;p&gt;So if the background in the parent document is dark, and the document in the iframe uses browser defaults like black text and a &lt;em&gt;transparent&lt;/em&gt; background, you get black text on a dark background and can’t read anything. Whoops.&lt;/p&gt;
&lt;p&gt;Because our users were used to web pages with default black text and white backgrounds, the fix was simple: we styled our preview iframes to have a white background.&lt;/p&gt;
&lt;p&gt;Most quick demo pages would have black or dark text, resulting in black text on a white background. And more stylish demos would set their own text and background colors, managing color contrast and readability themselves. Problem fixed.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Introducing: color-scheme&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;A year ago, I tweaked the HTML for all stackblitz.com pages to include a couple meta tags:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark&quot;&amp;gt;
&amp;lt;meta name=&quot;theme-color&quot; content=&quot;#2e3138&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These values match the default dark theme (nicknamed “DarkBlitz”) that we use for our editor. When users select the light theme instead in our UI, we then update those meta tags at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const theme = getTheme(localStorage.currentTheme);
setMetaTag(&apos;theme-color&apos;, theme.themeColor);
setMetaTag(&apos;color-scheme&apos;, theme.colorScheme);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Though it looks like setting the &lt;code&gt;color-scheme&lt;/code&gt; property in CSS might be compatible with more browsers, because not all browsers seem to pick up on the meta tag change.)&lt;/p&gt;
&lt;p&gt;Why tell browsers about the color scheme used by the page, you ask? Well, quoting MDN:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The color-scheme CSS property allows an element to indicate which color schemes it can comfortably be rendered in. (…) When a user selects one of these color schemes, the operating system makes adjustments to the user interface. This includes form controls, scrollbars, and the used values of CSS system colors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Our rough goal was to hint to browsers that if they render a scrollbar or a checkbox or a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; dropdown, we’d appreciate 😘 if they can do that using a theme that is either light or dark (as declared).&lt;/p&gt;
&lt;p&gt;How does that relate to iframes? Coming up right now.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Attack of the dark mode iframes&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now, authors using our IDE could also add &lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark&quot;&amp;gt;&lt;/code&gt; or the equivalent CSS to their pages. Browsers would react by making the page’s text white, and its background dark.&lt;/p&gt;
&lt;p&gt;Except in iframes, where the text would be white, but the background would remain transparent. Resulting in, you guessed it, white text on our white background.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/4772&quot;&gt;That issue was raised with the CSS Working Group&lt;/a&gt;, and the resolution was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If the color scheme of an iframe differs from embedding document, iframe gets an opaque canvas background appropriate to its color scheme.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the dark mode embedded document, with its &lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark&quot;&amp;gt;&lt;/code&gt; meta tag, would get a browser style similar to &lt;code&gt;:root { background-color: canvas }&lt;/code&gt;, where &lt;code&gt;canvas&lt;/code&gt; would be a dark color. White text on dark background, problem solved!&lt;/p&gt;
&lt;p&gt;Alas!&lt;/p&gt;
&lt;p&gt;Remember that we also have &lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark&quot;&amp;gt;&lt;/code&gt; in the editor page? So we now have a &lt;code&gt;dark&lt;/code&gt; color-scheme in both the &lt;em&gt;embedding&lt;/em&gt; document and the &lt;em&gt;embedded&lt;/em&gt; document, so the iframe stays transparent. White text on white background again.&lt;/p&gt;
&lt;p&gt;Okay, so maybe it was silly to use a white background all along? But remember that all this &lt;code&gt;color-scheme&lt;/code&gt; stuff was specified in recent years and implemented in browsers as lately as 2021. For most users and most browsers, we &lt;em&gt;needed&lt;/em&gt; that white background, as did CodePen (yup, also setting a white background on their preview iframes) and probably others.&lt;/p&gt;
&lt;p&gt;So, what’s the way forward? Do we need to predict what the text color will be and if the iframe will be transparent or not? In our case, we can’t know about what gets rendered in the iframe, so tough luck.&lt;/p&gt;
&lt;p&gt;But maybe we don’t need to control or predict that content. We can just say “hey, here’s what we support”, and let the browser handle it? Again, quoting that resolution:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the color scheme of an iframe differs from embedding document&lt;/strong&gt;, iframe gets an opaque canvas background appropriate to its color scheme.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So we tell the browser that the embedding document only supports a dark color scheme, and we use a dark background behind the iframe. It could be as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root.theme-dark { color-scheme: dark }
:root.theme-light { color-scheme: light }

.preview-iframe {
  /* canvas color depends on the inherited color-scheme */
  background-color: canvas;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if we want to keep a white background for browsers that don’t support &lt;code&gt;color-scheme&lt;/code&gt;, it might be safer to do something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Keep the default white background for backwards compat */
.preview-iframe {
  background-color: white;
}

@supports (color-scheme: dark) {
  :root.theme-dark {
    color-scheme: dark;
  }
  :root.theme-dark .preview-iframe {
    background-color: black;
  }
}

@supports (color-scheme: light) {
  :root.theme-light {
    color-scheme: light;
  }
  :root.theme-light .preview-iframe {
    background-color: white;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Would that approach work? Theoretically, yes.&lt;/p&gt;
&lt;p&gt;In quick tests in Chrome, it seems to be working well. We might even try it on prod (and roll back if it creates more issues than it solves).&lt;/p&gt;
&lt;p&gt;But currently, there are a few browser bugs that might make this solution unreliable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firefox doesn’t seem to support adding an opaque background to iframes with mismatched &lt;code&gt;color-scheme&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;There might be some smaller issues in Chrome as well, according to &lt;a href=&quot;https://codepen.io/AmeliaBR/pen/bGLewQm&quot;&gt;the description in this test case&lt;/a&gt; (but I couldn’t reproduce on macOS with dark and light OS modes).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That Firefox issue means that iframes would always be transparent in Firefox. So using a dark background behind the iframe would break any embedded page that specifies neither a &lt;code&gt;background-color&lt;/code&gt; nor a &lt;code&gt;color-scheme&lt;/code&gt; in Firefox, and that’s &lt;em&gt;most pages&lt;/em&gt;. No good.&lt;/p&gt;
&lt;p&gt;We’d have to apply that solution to browser that are known to implement that opaque background logic, and since &lt;code&gt;@supports (color-scheme: light)&lt;/code&gt; doesn’t test for &lt;em&gt;that&lt;/em&gt;, that means doing some UA sniffing. And I really don’t want to do that.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Quick fix: the least bad solution?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;This week, we announced &lt;a href=&quot;https://blog.cloudflare.com/cloudflare-stackblitz-partnership/&quot;&gt;a partnership with Cloudflare&lt;/a&gt;, who are using StackBlitz to power demos and tutorials of their Wrangler tool for Cloudflare Workers. Many of the demos were returning &lt;code&gt;text/plain&lt;/code&gt; responses, which were shown in our preview iframe.&lt;/p&gt;
&lt;p&gt;What’s the &lt;code&gt;color-scheme&lt;/code&gt; for a document generated for a &lt;code&gt;text/plain&lt;/code&gt; response? Well, that’s entirely up to the browser. Chrome is apparently creating a HTML document like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta name=&quot;color-scheme&quot; content=&quot;light dark&quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;pre style=&quot;word-wrap: break-word; white-space: pre-wrap;&quot;&amp;gt;Hello World&amp;lt;/pre&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It declares that this document supports both &lt;code&gt;light&lt;/code&gt; and &lt;code&gt;dark&lt;/code&gt; schemes, and the browser resolves that to either &lt;code&gt;light&lt;/code&gt; or &lt;code&gt;dark&lt;/code&gt; depending on the OS color scheme, not the parent page’s &lt;code&gt;color-scheme&lt;/code&gt; (at least in my tests with Chrome 101 on macOS).&lt;/p&gt;
&lt;p&gt;So if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you use a dark mode OS (or browser theme maybe?),&lt;/li&gt;
&lt;li&gt;and the default dark theme for the StackBlitz editor,&lt;/li&gt;
&lt;li&gt;and the parent page has &lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark&quot;&amp;gt;&lt;/code&gt;,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;… then you get a transparent iframe in Chrome, with white text, on our white background. The white background that we can’t change to a dark one, because that would break a bunch of content in Firefox.&lt;/p&gt;
&lt;p&gt;So we went for a quick and dirty fix: we removed the &lt;code&gt;color-scheme&lt;/code&gt; meta tag on the parent page.&lt;/p&gt;
&lt;p&gt;Now Chrome resolves the color schemes to &lt;code&gt;color-scheme: normal&lt;/code&gt; for the parent page and &lt;code&gt;color-scheme: dark&lt;/code&gt; for the iframe. And since those don’t match, Chrome makes the iframe opaque (white text, dark &lt;code&gt;canvas&lt;/code&gt; background).&lt;/p&gt;
&lt;p&gt;Meanwhile, Firefox generates a different kind of document for &lt;code&gt;text/plain&lt;/code&gt; responses, and that uses a dark theme (when the OS theme is dark) when opened as the top-level document, but not when rendered as a child document in an iframe. So the text is black, the iframe is transparent, and our background is white, and that works, kinda.&lt;/p&gt;
&lt;p&gt;I think that the last remaining bug we have is: if the iframed document sets &lt;code&gt;color-scheme: dark&lt;/code&gt; and no &lt;code&gt;background-color&lt;/code&gt;, we’ll have white text on a white background in Firefox. Not great, but here’s hoping that Firefox implements the opaque iframe logic soon.&lt;/p&gt;
&lt;p&gt;Then maybe we can add &lt;code&gt;color-scheme&lt;/code&gt; back to our top-level document, and get its purported benefits.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Better fix: white background, local color-scheme!&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Alternatively, we might be able to restore our usage of &lt;code&gt;color-scheme&lt;/code&gt; for our pages if we declare that the &lt;em&gt;iframe&lt;/em&gt; element uses a &lt;code&gt;light&lt;/code&gt; scheme.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root.theme-dark {
  color-scheme: dark;
}

.preview-iframe {
  /* tell browsers to make the iframe opaque
     for documents with color-scheme:dark */
  color-scheme: light;
  background-color: white;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works in Chrome, i.e. when the embedded document has a &lt;code&gt;color-scheme&lt;/code&gt; other than &lt;code&gt;dark&lt;/code&gt;, then the iframe is opaque. It also seems to match &lt;a href=&quot;https://www.w3.org/TR/css-color-adjust-1/#color-scheme-effect&quot;&gt;the text of the spec&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In order to preserve expected color contrasts, in the case of embedded documents typically rendered over a transparent canvas (such as provided via an HTML &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; element), if the used color scheme of &lt;strong&gt;the element&lt;/strong&gt; and the used color scheme of &lt;strong&gt;the embedded document’s root element&lt;/strong&gt; do not match, then the UA must use an opaque canvas of the Canvas color appropriate to the embedded document’s used color scheme instead of a transparent canvas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(Emphasis mine.) If “the element” in that paragraph means the element used to embed a document, i.e. our preview &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; element here, it looks like we’re following the spec correctly by setting &lt;code&gt;background-color: white; color-scheme: light;&lt;/code&gt; on that &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; element.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Should it be simpler?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So here we are. I think I mostly figured it out, in big part thanks to &lt;a href=&quot;https://twitter.com/AmeliasBrain/status/1524063080216289283&quot;&gt;Amelia Bellamy-Royds’s generous help&lt;/a&gt;. And there’s hope that things will improve in browsers and we’ll be able to use &lt;code&gt;color-scheme&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;But I’m wondering, should we be jumping through those hoops? Can’t we have a way to ask browsers “Hey, could you handle this iframe’s background for me, since you know what’s going on there and I don’t? Thanks, you’re a peach.” Or, conversely, “Look, I want this iframe to be transparent no matter what, and I’m taking on the responsibility here”.&lt;/p&gt;
&lt;p&gt;Right now, the opaque iframe background mechanism is something that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;requires expert knowledge to opt in (it’s far from obvious that the answer to “how do I get the browser to set its default background-color for this page in an iframe, like it does when opening the same page in a new tab?” is “use a mismatched &lt;code&gt;color-scheme&lt;/code&gt; value on purpose”);&lt;/li&gt;
&lt;li&gt;doesn’t have a way to opt out.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Is it good enough as-is, or could that improve? And if we want to improve at least some of these problems, how could we do it?&lt;/p&gt;
&lt;p&gt;We should probably document things more, let’s say by updating MDN to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe&quot;&gt;add a new section on the iframe page&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme&quot;&gt;another one on the color-scheme page&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;On top of that, should there be an explicit way to manage iframe transparency, let’s say with a HTML attribute for the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; element or with a CSS property (&lt;code&gt;iframe-transparency: auto | transparent | opaque&lt;/code&gt; or something)?&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;I also like &lt;a href=&quot;https://codepen.io/&quot;&gt;CodePen&lt;/a&gt; for quick CSS tests, and I hear that &lt;a href=&quot;https://codesandbox.io/&quot;&gt;CodeSandbox&lt;/a&gt; and &lt;a href=&quot;https://www.gitpod.io/&quot;&gt;GitPod&lt;/a&gt; are pretty good too!&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Especially since we &lt;a href=&quot;https://blog.stackblitz.com/posts/introducing-webcontainers/&quot;&gt;launched WebContainers&lt;/a&gt;, which let you run Node.js code in the browser — so you could have an &lt;code&gt;express&lt;/code&gt; server returning any type of HTTP response for that preview iframe.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:e7b723fd-d263-5342-92dd-0b1a5c7f4c56</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/calculators"/>
		<title>Designing calculator apps</title>
		<author><name>Florens Verschelde</name></author>
		<published>2021-03-16T13:00:00+01:00</published>
		<updated>2021-03-16T13:00:00+01:00</updated>
		<summary type="text">Looking at different calculator apps, we reflect on the imitative nature of all software and the dreaded concept of skeuomorphism.</summary>
		<content type="html">&lt;p&gt;Let’s say you want to calculate &lt;code&gt;35 × 1.2&lt;/code&gt;, and you have a pocket calculator. You whip it out, type on the keys, and the result looks like this:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/calculators/calculator-result.png&quot; width=&quot;280&quot; height=&quot;360&quot; alt=&quot;Screenshot of the macOS calculator app showing a numeric pad, buttons for basic operations, and this output: 31.666666666666667&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Hm, maybe? That looks a bit strange. How do you know for sure?&lt;/p&gt;
&lt;p&gt;With luck, you know that multiplying &lt;code&gt;35&lt;/code&gt; by a number higher than &lt;code&gt;1&lt;/code&gt; should give you a result higher than &lt;code&gt;35&lt;/code&gt;, not &lt;em&gt;lower&lt;/em&gt;. Also that fractional part smells fishy. That’s definitely wrong.&lt;/p&gt;
&lt;p&gt;You could try again, but your confidence is shaken. Is the calculator broken? Probably not. Will you mess up again?&lt;/p&gt;
&lt;p&gt;What went wrong? In this case, you made a couple mistakes while inputing the calculation. We can use &lt;em&gt;magic&lt;/em&gt; to show you the past:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/calculators/calculator-steps.png&quot; width=&quot;775&quot; height=&quot;360&quot; alt=&quot;A series of three screnshots of the macOS calculator app, showing the numbers 38, 1.2, and the previously mentioned result.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Here’s what happened: you hit the &lt;code&gt;8&lt;/code&gt; key instead of the &lt;code&gt;5&lt;/code&gt; key; and then the &lt;code&gt;÷&lt;/code&gt; key instead of the &lt;code&gt;×&lt;/code&gt; key. You got a result for &lt;code&gt;38 ÷ 1.2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To some of us, this is a familiar experience. We’ve lost many points on Math tests because of a momentary lapse of attention, because of finicky calculator keys.&lt;/p&gt;
&lt;p&gt;I had a Math teacher who would ban pocket calculators that didn’t show both your input and its result at the same time. If a calculator couldn’t show this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;38 ÷ 1.2
=    31.666666666667&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You couldn’t use it. Not in class, not during tests. It’s a recipe for useless mistakes. You need to be able to check your work.&lt;/p&gt;
&lt;p&gt;If you give important information to an &lt;em&gt;information system&lt;/em&gt; and it disappears as soon as you enter it, you can’t check your work. You can’t catch mistakes.&lt;/p&gt;
&lt;p&gt;Some humans have excellent attention and working memory. But we don’t design tools just for the lucky few, do we?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Apple’s Calculator app on macOS imitates an electronic calculator. It uses metaphors from the physical world, such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A display showing a single value at a time.&lt;/li&gt;
&lt;li&gt;Buttons that represent physical keys.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Optionally, if you know the keyboard shortcut or go looking through the menus, you can show a second window called a “Paper Tape”. It shows a log of your calculations and looks like this:&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;570&quot; height=&quot;600&quot; src=&quot;https://fvsch.com/articles/calculators/sharp-paper-tape.jpg&quot; alt=&quot;A physical calculator manufactured by Sharp, with a roll of thin paper attached to the back. One end of the paper roll goes into the top of the machine and comes out with a series of numbers and signs printed on it.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Another well-loved calculator app on macOS is &lt;a href=&quot;https://www.pcalc.com/mac/&quot;&gt;PCalc&lt;/a&gt;. It has a single-value display, “keys”, a separate “paper tape” window, themes if you want it to look like a physical calculator, and 3 memory registers like the best HP and Casio machines out there. Its users sing its praises in Mac App Store reviews.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In 2005, an Australian high school student named Nik Youdale built a small calculator app over the summer. It mimicked the look and features of a pocket calculator. His friend, Zac Cohan, was unimpressed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I couldn’t understand why it only displayed a single number at a time, and why it wouldn’t evaluate an entire mathematical expression like you’d see written out on paper.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Zac wanted more out of a calculator. A few months later, he got another idea. Instead of copying a traditional calculator, they could copy the &lt;em&gt;page of paper&lt;/em&gt; where one writes down calculations, results and notes. Zac asked, &lt;q&gt;What if the calculator was actually part of ‘the page’?&lt;/q&gt;&lt;/p&gt;
&lt;p&gt;They built an application, Soulver, based on that idea. It’s a paper pad that magically runs your calculations. The current version, Soulver 3, works very much like their first prototype:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/calculators/soulver.jpg&quot; width=&quot;775&quot; height=&quot;360&quot; alt=&quot;A screenshot of the Soulver app showing 4 lines of text. On the left, phrases like “$3k earnings / 5 people”; on the right, a column with results such as “$600.00”.&quot;&gt;
  &lt;figcaption&gt;Input and output are always visible, and input lines (on the left) can be edited at any time. Soulver also lets you store specific results in variables and reuse them on later lines.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Zac also wanted Soulver to be able to understand natural language, or at least to identify numbers, units, and specific commands within text. He has been steadily improving this feature for 15 years, and in my experience it works quite well.&lt;/p&gt;
&lt;p&gt;Soulver is my &lt;a href=&quot;https://soulver.app/&quot;&gt;calculator app of choice on macOS&lt;/a&gt;. If you want a similar app for Windows, Android or on the Web, the Soulver FAQ &lt;a href=&quot;https://documentation.soulver.app/faq#is-soulver-available-for-ios-windows-or-android&quot;&gt;lists a few options&lt;/a&gt;. Quotes in this section are from Zac Cohan’s article &lt;a href=&quot;https://medium.com/soulver/the-creation-of-soulver-454dee1e2fd1&quot;&gt;“The creation of soulver”&lt;/a&gt;, which is a great read.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;“Skeuomorphism”, the persistence of essential features from old objects as ornamental cues, was a hot topic in the design community in the early 2010s. The design of Apple’s iOS 7 was seen as an about-turn, from apps that mimicked paper and leather to a “flat” minimalist look. Skeuomorphism is dead!&lt;/p&gt;
&lt;p&gt;But as we’ve seen with Apple’s Calculator, an app’s visual style can be “flat” while faithfully mimicking a physical counterpart.&lt;/p&gt;
&lt;p&gt;At the risk of beating a dead horse, I’ll argue that Soulver’s design is “skeumorphic” as well. It reproduces the experience of writing calculations on paper with a pencil, writing down a result, referencing that result later on, etc. Soulver’s first icon showed a couple sheets of paper, a pencil, and a pocket calculator.&lt;/p&gt;
&lt;p&gt;Soulver’s design is superior not because it is less of an imitation, but because Zac’s choice of &lt;em&gt;what to imitate&lt;/em&gt; was better. He wanted to focus on the paper and pen experience — saying what you want and writing it down to supplement your working memory. The experience of painstakingly hitting keys on a calculator? That could be automated and abstracted away. In an icon update, the pocket calculator was replaced by a digital display, showing results only.&lt;/p&gt;
&lt;p&gt;This design may not work for everyone. The happy users of PCalc love how closely it mimicks the electronic calculators of their youth; the more complex the better! Maybe they have above average working memory. Maybe they loathed having to write down every single step of a calculation in school tests. Maybe they just want to use something they already know.&lt;/p&gt;
&lt;p&gt;Finally, the popularity of Soulver, PCalc, and even Apple’s Calculator.app is dwarfed by the number one calculator app out there: &lt;em&gt;Excel&lt;/em&gt;. Spreadsheets are the main computer calculators that so many workers reach out to.&lt;/p&gt;
&lt;p&gt;In a ghastly whisper, the dead horse’s soul asks: &lt;q&gt;are spreadsheets skeuomorphic too?&lt;/q&gt; Probably. They’re designed after accounting ledgers and paper spread-sheets, after all.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:4baef108-e14e-59e6-935a-52224a141d9e</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/raw-power-mac"/>
		<title>RAW Power 3: an affordable digital photo development app for macOS</title>
		<author><name>Florens Verschelde</name></author>
		<published>2021-01-11T13:00:00+01:00</published>
		<updated>2021-01-11T13:00:00+01:00</updated>
		<summary type="text">My personal review of RAW Power, a photo editing app for macOS and iOS that is shaping up to be an alternative to Lightroom or Aperture.</summary>
		<content type="html">&lt;p&gt;I used to dabble in digital photography, even attended a series of workshops for a year. I’m still amateur level, I think, and haven’t shot much in the past 7 years or so. Still, I have a few thousand pictures, some edited and some not at all, that sit in my archives, and I’d like to edit and publish at least some of those.&lt;/p&gt;
&lt;p&gt;Only problem: I didn’t have any photo management and development software I’m comfortable with.&lt;/p&gt;
&lt;p&gt;I used to own Lightroom 3 and 4. I still do, but they probably don’t install on current Macs, and Adobe made it hard if not impossible to get a license for the current Lightroom Classic app without paying 20 euros per month. So I’ve been looking for alternatives.&lt;/p&gt;
&lt;article-nav&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;Rounding up photo editors&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here’s a list of what I found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Capture One, the priciest option for pros (around €500).&lt;/li&gt;
&lt;li&gt;DxO PhotoLab, more affordable (€130); I’ve heard some good things.&lt;/li&gt;
&lt;li&gt;ON1 Photo Raw (€102), not a good option for my M1 Mac&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Exposure X6 (€110), which seems interesting.&lt;/li&gt;
&lt;li&gt;Skylum Luminar (€90 standalone, €150 with their new “AI” tools), which was slow when I tried it a few years back but might have improved since.&lt;/li&gt;
&lt;li&gt;Darkroom (€88, or a €22/year subscription), seems to integrate with the Apple Photos library and provide a bunch of tools in a nicely designed package &lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Photoscape X (€44), seems marketed for the Instagram crowd but could be powerful anyway.&lt;/li&gt;
&lt;li&gt;Picktorial ($5/month subscription).&lt;/li&gt;
&lt;li&gt;Apple’s own Photos app (included with macOS), which I tried briefly, but I don’t want to import my photos from my archive folders on external drives and into their Photos Library; I hear the development tools are a bit limited too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also got Affinity Photo and Pixelmator Pro, each for around €50. While they can do some RAW development, and have some great features, they’re closer to Photoshop in spirit. So not my cup of tea. They also don’t include a library browser or manager.&lt;/p&gt;
&lt;p&gt;There are a few open-source options too, such as Darktable, RawTherapee and LightZone. I didn’t consider them because they tend to have limited macOS support&lt;sup id=&quot;fnref1:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote-ref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, a steep learning curve, and questionable usability&lt;sup id=&quot;fnref1:4&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote-ref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Finally, after reading &lt;a href=&quot;https://austinmann.com/trek/iphone-proraw&quot;&gt;this review of the Apple ProRAW format&lt;/a&gt;, I discovered a small macOS and iOS app called &lt;a href=&quot;https://www.gentlemencoders.com/raw-power-for-macos/&quot;&gt;RAW Power&lt;/a&gt;. It piqued my interest since it’s cheap (around €30), runs natively on Apple’s new CPUs, and looks similar in spirit to early versions of Lightroom and Aperture.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;The RAW Power UI&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;RAW Power is developed by Nik Bhatt, who worked for years at Apple on the Aperture team. A bunch of user comments call RAW Power a spiritual successor to Aperture, though at this time its feature set seems more limited (with a few exceptions where it’s more up-to-date).&lt;/p&gt;
&lt;p&gt;It was originally a development extension for Apple Photos, before gaining a photo library module in version 2 (improved further in version 3), making it usable as a standalone app as well. You can get a trial version on the app’s website, which is fully functional and adds a watermark to exported photos.&lt;/p&gt;
&lt;p&gt;Before trying it out, I took an hour to watch &lt;a href=&quot;https://www.youtube.com/watch?v=ME229JqbM48&amp;amp;list=PLgF1Bfd-tVLa8sizMIG2g42x6CdPUmHsb&quot;&gt;this video tutorial series recorded by Nik&lt;/a&gt;. I especially liked this one, &lt;a href=&quot;https://www.youtube.com/watch?v=ONNR8CDgQEc&quot;&gt;RAW Power 3 for Mac: Rating and Filtering Workflow&lt;/a&gt;. That workflow is not super specific to RAW Power, and could be reproduced in several apps, but sharing this method was a nice touch.&lt;/p&gt;
&lt;p&gt;The RAW Power UI looks like this:&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-ui.png&quot; width=&quot;650&quot; height=&quot;408&quot; alt=&quot;Screenshot of the RAW Power app.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;The UI can be customized a bit, and includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Top: a toolbar that can be customized and rearranged&lt;/li&gt;
&lt;li&gt;Left: a library explorer showing folders or Apple Photos albums&lt;/li&gt;
&lt;li&gt;Center: the main photo view&lt;/li&gt;
&lt;li&gt;Bottom a tab strip&lt;/li&gt;
&lt;li&gt;Right: a sidebar which shows either photo metadata or RAW development tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All those UI components can be hidden or toggled. And if you hide the main photo viewer, the thumbnail strip becomes a bigger thumbnail grid.&lt;/p&gt;
&lt;p&gt;This lets you customize your workspace however you want, but sometimes I find that I want to go from one UI configuration to another, and clicking 3 buttons to change things one way and then 3 buttons to change it back is a bit painful.&lt;sup id=&quot;fnref1:5&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote-ref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;On the customizing front, you can also reorder and show or hide each of the RAW “adjustments” modules that appear in the Edit sidebar. The interface for that would benefit from some improvements though, since you need to use different menus to add a module, hide a module, and reorder modules; it would be possible to unify all that in a single, more intuitive config view.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-adjustment-order.png&quot; width=&quot;505&quot; height=&quot;390&quot; alt=&quot;The Reorder Adjustments dialogue&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Personally, I’m missing a way to quickly show the photo’s metadata when I’m in Edit mode&lt;sup id=&quot;fnref1:6&quot;&gt;&lt;a href=&quot;#fn:6&quot; class=&quot;footnote-ref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;. I have to switch the sidebar from Edit to Info, which takes a good second for some reason, on top of making me lose my place. I’d also love to keep a histogram in Info mode.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;The adjustment tools&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The adjustment sidebar has a bunch of good tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A good histogram with black/white and color clipping highlighting.&lt;/li&gt;
&lt;li&gt;White Balance (with a decent Auto option and manual control).&lt;/li&gt;
&lt;li&gt;A good Crop tool (I like being able to control the precise pixel size and coordinates, it came in handy working on a photo series where I’m trying to align 20 pictures taken from the same spot!), and a Perspective tool that is maybe less intuitive.&lt;/li&gt;
&lt;li&gt;Basics/Tone/Enhance has the usual suspects: exposure, brightness, highlights and shadows, clarity…&lt;/li&gt;
&lt;li&gt;Black &amp;amp; White, Channel Mixer, HSL Color.&lt;/li&gt;
&lt;li&gt;Curves, Levels, Vignette.&lt;/li&gt;
&lt;li&gt;A LUT tool that lets you apply predefined styles, including some film simulation or black and white ones. I wasn’t familiar with the “LUT” term, but apparently LUTs are a kind of color map that pairs each input color to its destination color, and there are a bunch of LUTs available online from different providers, with both free and paid options.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-basics.png&quot; width=&quot;300&quot; height=&quot;390&quot; alt=&quot;The Basics, Tone and Enhance controls&quot;&gt;
  &lt;figcaption&gt;Not sure why Brightness is a “Basics” control but “Exposure” is a “Tone” control. Or why Deepen and Lighten are in “Enhance” and not “Tone”, or why “Sharpen” is in a different category (with a single slider) and not in “Enhance”. So I’ve reordered those modules to put them together.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Things I like about the adjustment sliders in RAW Power:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s easy to see which slider you tweaked (used the system highlight color, which is pink in my case) and which uses the default value.&lt;/li&gt;
&lt;li&gt;Double-click resets to the default value.&lt;/li&gt;
&lt;li&gt;The numbers are text inputs, and can be edited manually for precision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some things I don’t like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some text inputs require two clicks to get focus. (Nik reached out by email and confirmed that it’s a know issue with a UI component he’s using in the app.)&lt;/li&gt;
&lt;li&gt;Some text inputs support the Up and Down keys to increment or decrement the value, and others don’t, depending on the module.&lt;/li&gt;
&lt;li&gt;Based on system settings, it decided I want a comma as the decimal separator, and wouldn’t accept input like &lt;code&gt;1.5&lt;/code&gt; (resetting to the default value instead). I worked around it by changing my system settings, but being more forgiving and handing both dots and commas as decimal separators would be better.&lt;/li&gt;
&lt;li&gt;The way out of bounds values are handled (if I input &lt;code&gt;5&lt;/code&gt; and the maximum is &lt;code&gt;2.0&lt;/code&gt;, please use &lt;code&gt;2.0&lt;/code&gt; instead of discarding my input).&lt;/li&gt;
&lt;li&gt;Slider values are single digit numbers plus two decimals, e.g. &lt;code&gt;1.00&lt;/code&gt; or &lt;code&gt;-2.54&lt;/code&gt;. Most of them go from &lt;code&gt;0.00&lt;/code&gt; to &lt;code&gt;1.00&lt;/code&gt;, and would be easier to work with if they were shown as integers between &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;100&lt;/code&gt; (same precision, but you don’t have to type a decimal separator all the time).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Arguably, I’m finicky. That’s the problem when you use and review software as a UI developer &amp;amp; designer. 😛&lt;/p&gt;
&lt;p&gt;Overall I like the tools, they’re rather powerful. There are a few useful presets you can apply, and a couple “Auto Enhance” features which look decent but not quite magical.&lt;/p&gt;
&lt;p&gt;One tool which is both powerful and a bit confusing is the RAW Processing adjustment. It shows 9 sliders and a couple options:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-raw-processing.png&quot; width=&quot;300&quot; height=&quot;315&quot; alt=&quot;The RAW Processing controls, with sliders such as Boost, Black Point, Luma Nose, Color Noise, Moiré, and RAW Sharpen&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;The noise controls are part of this first RAW processing pass. I haven’t worked with very noisy images yet, so I’m not sure if they’re good. The Color Noise correction seems to work well.&lt;/p&gt;
&lt;p&gt;The “Boost” feature is interesting. It’s set at its maximum by default. If you move it back to 0%, you get a much more washed-out, lifeless picture, like you get in some RAW processing software out of the box (especially in some of the open-source ones, when they’re not set up to apply some default enhancements). Comparing the results at 0%, 50% and 100% Boost, I tend to like what it does.&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/boost-demo.jpg&quot; width=&quot;680&quot; height=&quot;450&quot; alt=&quot;A side-by-side comparison of the same image with two different development settings. Left one looks a bit gray and lacks contrast. Right one is more colorful and contrasted.&quot;&gt;
  &lt;figcaption&gt;Boost at 0% and 100%&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In the video tutorials, Nik shows how sometimes Boost goes a bit too far, and it’s better to lower it a bit or completely before applying other adjustments (such as highlight recovery). I’ve sometimes moved it down to 90% or 80%, but rarely all the way down.&lt;/p&gt;
&lt;p&gt;Another slider I’ve used at times is Black Point, which er makes more or fewer parts of the image black or dark?&lt;/p&gt;
&lt;p&gt;Sometimes it looks like there are a few ways to do the same thing, especially when it comes to contrast with: Boost, RAW Contrast, Contrast, Curves and Levels, to name a few. Or micro-contrast with RAW Sharpen, Clarity and Sharpen. It probably takes some time and expertise to learn what works best for you and your images.&lt;/p&gt;
&lt;p&gt;I just discovered the &lt;a href=&quot;https://gentlemencoders.com/wp-content/uploads/2020/04/RAW-Power-3.0-for-Mac-Help.pdf&quot;&gt;RAW Power user manual (50 page PDF)&lt;/a&gt;, which has more details on how adjustments work. Maybe that’ll clarify what the best approach is for some use cases.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-curves.png&quot; width=&quot;300&quot; height=&quot;390&quot; alt=&quot;The Curves adjustment tool&quot;&gt;
  &lt;figcaption&gt;The Curves tool is powerful and lets you set as many points as you want, but it’s hard to move a point just right on the vertical axis, and a little nudge can have an overblown effect. I was a bit more at ease with Lightroom’s four number inputs, which make it easy to create a symmetrical S-curve and control how strong the effect is.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Finally, there are no local adjustment tool: no brushes, no gradients, no repair, etc. While things like machine-learning based local repairs can be done by other software working on an exported TIFF file, it would be great to at least have support for gradient filters, e.g. to handle sky-vs-land exposure.&lt;/p&gt;
&lt;p&gt;RAW Power 3.2 added support for Apple’s ProRAW (part of the DNG 1.6 format), which includes precomputed local adjustments from the iPhone’s software as a kind of mask or extra layer (I’m not sure)  that you can turn on or off or apply partially. Maybe some of the work done on ProRAW support lays the groundwork for local adjustments? One can dream. 😄&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Data portability&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In an ideal world, adjustments made to a RAW photo in one app would be understood by another app. Sadly we don’t leave in this world, because RAW development and image correction algorithms are specific to each app, and there’s no standard even for common things such as exposure correction, black and white or color processing, curves, vignette, embedded LUTs, etc.&lt;/p&gt;
&lt;p&gt;So development settings done in Lightroom, RAW Power or DxO PhotoLab won’t be interoperable at all. Okay. But can we at least make sure that those development settings are saved reliably?&lt;/p&gt;
&lt;p&gt;Lightroom lets you write development settings as “sidecar files”, which use the &lt;code&gt;.xmp&lt;/code&gt; extension, or embedded inside DNG files. This comes in handy when syncing files via Dropbox, saving photos on an external drive, or changing computers or operating system.&lt;/p&gt;
&lt;p&gt;RAW Power saves development data in &lt;code&gt;Documents/RAW Power&lt;/code&gt;, with no option to use sidecar files. This folder may look like this:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/raw-power-mac/rawpower-metadata.png&quot; width=&quot;555&quot; height=&quot;370&quot; alt=&quot;The RAW Power folder, which stores JPEG previews and development data with a .rawpower extension&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;If you move or rename a photo, you risk losing the link with this development data, as stated in the help pages:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RAW Power stores its editing and rating / flag information in files stored within its “sandbox”, which is a directory inside your home folder on your disk. RAW Power stores information in the sandbox that lets it connect its data with your original images. Part of the identifier is the file name of your image, and part is the identifier generated by macOS. As a result of this system, RAW Power can connect editing and rating information to your original even if you move the file elsewhere on your disk. However, if you rename it, or move it to another volume, the link is broken.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So if you store photos on a drive, that drive dies and you later get a backup of your photos: sorry, your development data will be lost or not linked to the photos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;, Nik Bhatt reached out by email to give more information about this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While the link can be broken if you move a file to another volume (or restore from a backup), there are two features in the app for reconnecting the edits to the originals. One is called Reconnect and the other is Restore Edits.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I haven’t tried those features yet, but they look like they could handle reconnecting edits to files restored from a backup.&lt;/p&gt;
&lt;p&gt;I’d still like to have a way to rename files without losing metadata or having to reconnect metadata to the renamed files. Whether that’s by having sidecar files witch matching filenames alongside the source files, or by having more options for renaming files within Raw Power — or ideally, both.&lt;/p&gt;
&lt;p&gt;Nik says he wants to improve file management in future releases, so I’ll keep my eyes on that.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;My recommendation&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Despite a few pain points and concerns, I find that RAW Power is a capable tool and I’ll probably use it more in the future, while keeping an eye on the pricier competition.&lt;/p&gt;
&lt;p&gt;If you liked Lightroom Classic or Aperture, RAW Power might be a good fit for you, provided you can forgive a few issues and missing features.&lt;/p&gt;
&lt;p&gt;RAW Power is, as far as I can tell, indie software with a team of one and a low price. It should give you a lot of value for your money.&lt;/p&gt;
&lt;p&gt;I heartily recommend giving it a try.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;I bought a ON1 Photo Raw 2020 license in a sale last year. It fails to install on my computer. The 2021 version — a somewhat pricy upgrade — is an Intel app that runs too slowly to be comfortable. I might reconsider later if the price and performance are right.&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Subjectively and just based on screenshots, Darkroom has the most attractive design.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:3&quot;&gt;
&lt;p&gt;Darktable tells you “Good Luck :)” if you want to install a macOS version.&amp;#160;&lt;a href=&quot;#fnref1:3&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:4&quot;&gt;
&lt;p&gt;That’s not a condemnation of those projects. I understand that open-source side projects made by developers might focus on Linux support and/or powerful features that scratch one’s itch over the years-long design process needed for best-in-class usability.&amp;#160;&lt;a href=&quot;#fnref1:4&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:5&quot;&gt;
&lt;p&gt;For browsing and rating I might show the left sidebar, a thumbnail grid with medium thumbnails, and the metadata sidebar. For editing a particular photo, I’d hide the left sidebar, show the main photo view, and either hide the thumbnails or keep them at the smallest size. If I count the clicks needed to go from the first configuration to the other one: that’s 4 clicks.&amp;#160;&lt;a href=&quot;#fnref1:5&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:6&quot;&gt;
&lt;p&gt;For instance, I’ve wanted to check data such as the shooting date and hour when tweaking white balance, to make sure the white balance is consistent with the hour and season when I’m going for a realistic look.&amp;#160;&lt;a href=&quot;#fnref1:6&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:2380b5bb-eb96-5bed-9f3c-8ad36e8deb61</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/learning-xstate"/>
		<title>Learning XState by refactoring an old project</title>
		<author><name>Florens Verschelde</name></author>
		<published>2021-01-06T17:00:00+01:00</published>
		<updated>2021-01-06T17:00:00+01:00</updated>
		<summary type="text">I’ve been wanting to learn XState, a JavaScript state machine library. I had one exercise in mind: porting the hand-rolled state code in the small click precision game I built last year, if possible.</summary>
		<content type="html">&lt;p&gt;For a while I’ve wanted to learn the &lt;a href=&quot;https://xstate.js.org&quot;&gt;XState library&lt;/a&gt;, which is presented as a solution to gnarly UI state issues where “what should we show and how should it act?” becomes hard to determine.&lt;/p&gt;
&lt;p&gt;Since the best way to learn is to actually build something, I’m thinking I can try to port an old project of mine over to XState, and see how it turns out. Continue reading if you want to follow along!&lt;/p&gt;
&lt;article-nav&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;Why state machines?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’ve been building a bunch of app or app-like user interfaces with React and other frameworks, and you quickly run into situations where a screen or component’s UI state is represented by a series of variables.&lt;/p&gt;
&lt;p&gt;For instance, if a component loads data from a server, you can end up with variables like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt; (&lt;code&gt;undefined&lt;/code&gt;, empty Array, or Array with data)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt; (&lt;code&gt;undefined&lt;/code&gt; or an error object or message)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loading&lt;/code&gt; (boolean)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You end up writing conditions like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const hasContent =
  !error &amp;amp;&amp;amp;
  !loading &amp;amp;&amp;amp;
  Array.isArray(data) &amp;amp;&amp;amp;
  data.length &amp;gt; 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a couple more UX or business rules to this UI component, and you’re in trouble. Your code will be littered with complex conditions, and it’s easy to get one of those wrong and end up with subtle bugs where different parts of the UI disagree about what state the application or component is in. So, what’s a poor UI developer to do?&lt;/p&gt;
&lt;p&gt;Enter finite-state machines.&lt;/p&gt;
&lt;p&gt;As a literature major with no formal computer science training, I’m not quite sure what I’m talking about, but apparently a &lt;a href=&quot;https://en.wikipedia.org/wiki/Finite-state_machine&quot;&gt;“finite-state machine”&lt;/a&gt; is a programming concept where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the program has a list of possible states;&lt;/li&gt;
&lt;li&gt;it can be in only one state at a time;&lt;/li&gt;
&lt;li&gt;and it defines how each state can or cannot transition to other states.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This can be used to describe physical devices like elevators, traffic lights, vending machines and more, and often those devices do run programs that implement a finite-state machine.&lt;/p&gt;
&lt;p&gt;Last year I built a tiny &lt;a href=&quot;https://click-precision-game.netlify.app/&quot;&gt;click precision game&lt;/a&gt;, with zero experience in game development. I used &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt;, and designed the game’s state somewhat naively. Often thinking “I should use an established formalism here” but being pressed for time, I improvised some ad hoc code.&lt;/p&gt;
&lt;p&gt;Shortly after that, I heard of XState, a JavaScript library that helps with implementing state machines. And I wondered how would things have worked out if I had used XState. Let’s find out!&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;How the game currently works&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Since this is a refactoring operation, we should review key parts of the &lt;a href=&quot;https://github.com/fvsch/click-precision-game/tree/v1.0.0&quot;&gt;current code for click-precision-game&lt;/a&gt;. How does it handle the main gameplay state?&lt;/p&gt;
&lt;p&gt;Let’s look at the main gameplay in the &lt;a href=&quot;https://github.com/fvsch/click-precision-game/blob/v1.0.0/src/components/Playground.svelte&quot;&gt;&lt;code&gt;Playground.svelte&lt;/code&gt; component&lt;/a&gt;. There’s a lot of logic there. The game follows a linear timeline where each instance of the game is divided in two parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A countdown, divided in a few phases (showing “click the green square”, “3”, “2”, “1”).&lt;/li&gt;
&lt;li&gt;Then a series of 20 “turns”, each turn divided in 2 phases: the “turn” itself (time when the game’s target is visible and must be clicked), and a “cooldown” (short pause before the next turn).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That information is encoded as a collection of objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const PLAY_PHASES = {
  START: {
    next: &quot;COUNTDOWN_3&quot;,
    durationRatio: 1,
    minDuration: 1000,
    countdown: 4,
    showTarget: false,
  },
  COUNTDOWN_3: {
    next: &quot;COUNTDOWN_2&quot;,
    durationRatio: 0.5,
    minDuration: 400,
    countdown: 3,
    showTarget: false,
  },
  COUNTDOWN_2: {
    next: &quot;COUNTDOWN_1&quot;,
    /* … */
  },
  COUNTDOWN_1: {
    next: &quot;TURN&quot;,
    /* … */
  },
  TURN: {
    next: &quot;COOLDOWN&quot;,
    /* … */
  },
  COOLDOWN: {
    next: &quot;TURN&quot;,
    /* … */
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the &lt;code&gt;Playground&lt;/code&gt; component is mounted, we’re triggering the first phase:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;onMount(() =&amp;gt; {
  startPhase(&quot;START&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the &lt;code&gt;startPhase&lt;/code&gt; function does the heavy lifting. Here it is, partly abridged and annotated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function startPhase(key) {
  // If we’re just starting a turn
  if (key === &quot;TURN&quot;) {
    // increment the turn count
    turns += 1;
    // reset the success/failure state to a neutral value
    turnState = TURN_STATE.INITIAL;
    // place the target at a random position
    targetPosition = getTargetPosition();
  }

  // If ending a turn, we check the component state
  // to see if we have registered a successful click
  // on the target
  if (key === &quot;COOLDOWN&quot;) {
    if (turnState === TURN_STATE.SUCCESS) {
      successCount += 1;
    }
  }

  // Get config for this phase
  const phase = PLAY_PHASES[key];
  // update UI
  countdown = phase.countdown;
  showTarget = phase.showTarget;
  // schedule next phase
  timeout = setTimeout(
    () =&amp;gt; startPhase(phase.next),
    Math.max(phase.minDuration, phase.durationRatio * $gameSpeed)
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;countdown&lt;/code&gt; and &lt;code&gt;showTarget&lt;/code&gt; variables are reactive, and tracked by Svelte. When we update their value, Svelte does its &lt;a href=&quot;https://svelte.dev/blog/svelte-3-rethinking-reactivity&quot;&gt;reactivity magic&lt;/a&gt; and updates the view. If we were in React land, we could probably use &lt;code&gt;useState&lt;/code&gt; and update these state variables with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const [countdown, setCountdown] = useState();
const [showTarget, setShowTarget] = useState();

const startPhase = (key) =&amp;gt; {
  /* … */
  setCountdown(phase.countdown);
  setShowTarget(phase.showTarget);
  /* … */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anyway, that’s how it works right now. Doing a bit of reading on &lt;a href=&quot;https://statecharts.github.io/&quot;&gt;statecharts&lt;/a&gt; and &lt;a href=&quot;https://css-tricks.com/robust-react-user-interfaces-with-finite-state-machines/&quot;&gt;this introduction to state machines by David Khourshid on CSS-Tricks&lt;/a&gt;, it looks like I was close enough to a hand-rolled finite state machine. Yay me!&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Getting comfortable with XState&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Originally I wanted to open the XState docs in one tab, my current code on another screen, and just start converting stuff and figuring things out along the way.&lt;/p&gt;
&lt;p&gt;Turns out that XState is a bit more complex than I thought. It’s not very hard, but there are a few concepts to learn: machines, services, actions, actors maybe? Is that more than I bargained for?&lt;/p&gt;
&lt;p&gt;So, change of plan: before tacking the gameplay state, I want to make sure I’m comfortable with basic usage of XState and how to integrate it in Svelte.&lt;/p&gt;
&lt;p&gt;Let’s start with a simpler bit of state then; I picked the main navigation state.&lt;/p&gt;
&lt;p&gt;The game has 3 different screens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;setup&quot;&lt;/code&gt; (configures a game’s parameters before starting)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;playing&quot;&lt;/code&gt; (plays the game’s main loop)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;results&quot;&lt;/code&gt; (shows your points)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We start at the Setup screen:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/learning-xstate/game-setup-screen.png&quot; width=&quot;504&quot; height=&quot;504&quot; alt=&quot;A start screen with form fields for choosing a target size, playground size and game speed, and a “Start” button.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;The information for “which screen should we show?” is stored as a string in a Svelte store (a reactive variable that can be shared between components). It’s defined like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { writable } from &quot;svelte/store&quot;;

export const screen = writable(&quot;setup&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in our root component we have a kind of switch statement where we pick which component to render:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  import { screen } from &quot;../store.js&quot;;
  import Setup from &quot;./Setup.svelte&quot;;
  /* … */
&amp;lt;/script&amp;gt;

{#if $screen === &quot;setup&quot;}
  &amp;lt;Setup /&amp;gt;
{:else if $screen === &quot;playing&quot;}
  &amp;lt;Playground /&amp;gt;
{:else if $screen === &quot;results&quot;}
  &amp;lt;Results /&amp;gt;
{/if}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To navigate between screens, we change the store’s value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function startPlaying() {
  $screen = &quot;playing&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: we’re using the dollar syntax, &lt;code&gt;$screen&lt;/code&gt;, to access the value of the &lt;code&gt;screen&lt;/code&gt; store in a reactive way (&lt;a href=&quot;https://svelte.dev/docs#4_Prefix_stores_with_$_to_access_their_values&quot;&gt;read more in the Svelte docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Converting this to XState is probably overkill, but using XState we can enforce a few rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Restrict the state to known values (e.g. make it impossible to set the current screen to &lt;code&gt;&quot;whoops&quot;&lt;/code&gt; if that’s not a known state).&lt;/li&gt;
&lt;li&gt;Specify relationships between states, e.g. only the &lt;code&gt;&quot;playing&quot;&lt;/code&gt; state can go to the &lt;code&gt;&quot;results&quot;&lt;/code&gt; state.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s create a basic XState machine to keep track of the current screen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/state/screen.js
import { Machine } from &quot;xstate&quot;;

const screenMachine = Machine({
  id: &quot;screen&quot;,
  initial: &quot;setup&quot;,
  states: {
    setup: {},
    playing: {},
    results: {}
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Right now our 3 states have no rules or features attached, but we’ll flesh them out later.&lt;/p&gt;
&lt;p&gt;Now if I’m understanding correctly, a XState “machine” is a static set of rules, but it is itself stateless: it doesn’t have a current state or a state history. To hold a “current value”, we need a XState “service”:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/state/screen.js
import { interpret, Machine } from &quot;xstate&quot;;

const screenMachine = Machine({
  id: &quot;screen&quot;,
  initial: &quot;setup&quot;,
  states: {
    setup: {},
    playing: {},
    results: {}
  }
});

const screenService = interpret(screenMachine);
export default screenService;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This feels a bit like a class-vs-instance thing. We have a kind of blueprint (a class, or a machine in this case) which is used to create a structure that holds data or state (instance/service). Not sure if that makes sense, this is just how it looks to me. 😅&lt;/p&gt;
&lt;p&gt;Let’s update our top component to use the &lt;code&gt;screenService&lt;/code&gt; instead of the &lt;code&gt;screen&lt;/code&gt; Svelte store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  import screenService from &quot;../state/screen.js&quot;;
  import Setup from &quot;./Setup.svelte&quot;;
  /* … */
&amp;lt;/script&amp;gt;

{#if screenService.state.matches(&quot;setup&quot;)}
  &amp;lt;Setup /&amp;gt;
{:else if screenService.state.matches(&quot;playing&quot;)}
  &amp;lt;Playground /&amp;gt;
{:else if screenService.state.matches(&quot;results&quot;)}
  &amp;lt;Results /&amp;gt;
{/if}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks good to me, but alas! it fails. We get a blank screen. What’s going on in the Console?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: Attempted to read state from uninitialized service &apos;screen&apos;. Make sure the service is started first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Indeed, when we run &lt;code&gt;console.log(screenService.state)&lt;/code&gt;, that’s &lt;code&gt;undefined&lt;/code&gt;! Looks like we have to start the service to make it go to its initial state (&lt;code&gt;&quot;setup&quot;&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const screenService = interpret(screenMachine);
screenService.start();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works! I’m seeing the Setup screen, and if I change the &lt;code&gt;initial&lt;/code&gt; value in &lt;code&gt;stateMachine&lt;/code&gt; and reload the page I can see the Playground or Results components — though Results are broken, because the result data is missing.&lt;/p&gt;
&lt;p&gt;So we have a navigation state. Now we need some navigation actions, i.e. a way to go from one state to the other.&lt;/p&gt;
&lt;p&gt;XState handles that with events, which are defined on the machine. It looks like, by convention, state names are lowercase and event names are uppercase, so we’ll follow that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const screenMachine = Machine({
  id: &quot;screen&quot;,
  initial: &quot;setup&quot;,
  states: {
    setup: {
      on: {
        START_PLAYING: &quot;playing&quot;
      }
    },
    playing: {
      on: {
        SHOW_RESULTS: &quot;results&quot;,
        SHOW_SETUP: &quot;setup&quot;
      }
    },
    results: {
      on: {
        SHOW_SETUP: &quot;setup&quot;
      }
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;to start playing, we must be on the &lt;code&gt;&quot;setup&quot;&lt;/code&gt; state and send a &lt;code&gt;&quot;START_PLAYING&quot;&lt;/code&gt; event;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;&quot;playing&quot;&lt;/code&gt; state can go to the &lt;code&gt;&quot;results&quot;&lt;/code&gt; state or to the &lt;code&gt;&quot;setup&quot;&lt;/code&gt; state (e.g. if the user wants to interrupt a game);&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;&quot;results&quot;&lt;/code&gt; state can only go back to the &lt;code&gt;&quot;setup&quot;&lt;/code&gt; state, to start a new game.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And we could add events to handle more features. For instance, if we want to add a “Try Again” button on the results screen that starts a new game with the same parameters, we can add: &lt;code&gt;PLAY_AGAIN: &quot;playing&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We send events with the service’s &lt;code&gt;send&lt;/code&gt; method. Let’s start with the &lt;code&gt;Setup&lt;/code&gt; page. It listens to the form’s &lt;code&gt;&quot;submit&quot;&lt;/code&gt; event and navigates, so we’ll update that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;!-- src/components/Setup.svelte --&amp;gt;
&amp;lt;script&amp;gt;
  import screenService from &quot;../state/screen.js&quot;;

  function onSubmit(event) {
    event.preventDefault();
    screenService.send(&quot;START_PLAYING&quot;);
  }
&amp;lt;/script&amp;gt;

&amp;lt;form on:submit={onSubmit}&amp;gt;
  &amp;lt;!-- … --&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And… it doesn’t work.&lt;/p&gt;
&lt;p&gt;Nothing in the Console. Hmm, how else can we debug what’s happening? We have two options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add a bunch of &lt;code&gt;console.log&lt;/code&gt;s;&lt;/li&gt;
&lt;li&gt;use the Redux DevTools browser extension and &lt;a href=&quot;https://xstate.js.org/docs/guides/interpretation.html#options&quot;&gt;tell XState to send it state information&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking closer, once we call &lt;code&gt;send(&quot;START_PLAYING&quot;)&lt;/code&gt;, here is what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The event is dispatched correctly on the &lt;code&gt;screenService&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;screenService&lt;/code&gt;’s state changes to &lt;code&gt;&quot;playing&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;But nothing in &lt;code&gt;Game.svelte&lt;/code&gt; reacts to this change.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We need to make Svelte aware of changes in the &lt;code&gt;screenService&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Thankfully, XState’s docs have &lt;a href=&quot;https://xstate.js.org/docs/recipes/svelte.html&quot;&gt;a recipe for using XState with Svelte&lt;/a&gt;, which shows two solutions. My favorite is to treat the XState service as a Svelte store, which is possible because a XState service has a &lt;code&gt;subscribe&lt;/code&gt; method that &lt;a href=&quot;https://svelte.dev/docs#Store_contract&quot;&gt;fulfills Svelte’s “store contract”&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In short, that means we can use &lt;code&gt;$screenService&lt;/code&gt; as a reactive version of &lt;code&gt;screenService.state&lt;/code&gt;. Our code becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  import screenService from &quot;../state/screen.js&quot;;
  /* … */
&amp;lt;/script&amp;gt;

{#if $screenService.matches(&quot;setup&quot;)}
  &amp;lt;Setup /&amp;gt;
{:else if $screenService.matches(&quot;playing&quot;)}
  &amp;lt;Playground /&amp;gt;
{:else if $screenService.matches(&quot;results&quot;)}
  &amp;lt;Results /&amp;gt;
{/if}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, navigating between screens finally works!&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Tackling the main game loop&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Okay, that wasn’t super simple and there was a bit of a learning curve, but we finally know how to make a basic state machine with XState. Should we create one for the &lt;code&gt;PLAY_PHASES&lt;/code&gt; structure we described earlier? As a reminder, it looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const PLAY_PHASES = {
  START: {
    next: &quot;COUNTDOWN_3&quot;
  },
  COUNTDOWN_3: {
    next: &quot;COUNTDOWN_2&quot;
  },
  COUNTDOWN_2: {
    next: &quot;COUNTDOWN_1&quot;
  },
  COUNTDOWN_1: {
    next: &quot;TURN&quot;
  },
  TURN: {
    next: &quot;COOLDOWN&quot;
  },
  COOLDOWN: {
    next: &quot;TURN&quot;
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first-level keys are our states, and we have just one event, &lt;code&gt;&quot;next&quot;&lt;/code&gt;, telling us what the next state should be.&lt;/p&gt;
&lt;p&gt;As a XState machine, this could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const playMachine = Machine({
  id: &quot;play&quot;,
  initial: &quot;start&quot;,
  states: {
    start: {
      on: { NEXT: &quot;countdown_3&quot; }
    },
    countdown_3: {
      on: { NEXT: &quot;countdown_2&quot; }
    },
    countdown_2: {
      on: { NEXT: &quot;countdown_1&quot; }
    },
    countdown_1: {
      on: { NEXT: &quot;turn&quot; }
    },
    turn: {
      on: { NEXT: &quot;cooldown&quot; }
    },
    cooldown: {
      on: { NEXT: &quot;turn&quot; }
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While working on the XState refactor for this article, I realized that the 4 first states (from &lt;code&gt;&quot;start&quot;&lt;/code&gt; to &lt;code&gt;&quot;countdown_1&lt;/code&gt;) represent a single countdown animation with 4 “stages”. To simplify the state logic a bit, I decided to turn those states into a single &lt;code&gt;“countdown”&lt;/code&gt; state, and moved the animation logic to &lt;a href=&quot;https://github.com/fvsch/click-precision-game/blob/v2.0.0/src/components/Countdown.svelte&quot;&gt;a CSS animation in the Countdown component&lt;/a&gt;. This simplifies our machine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const playMachine = Machine({
  id: &quot;play&quot;,
  initial: &quot;countdown&quot;,
  states: {
    countdown: {
      on: { NEXT: &quot;turn&quot; }
    },
    turn: {
      on: { NEXT: &quot;cooldown&quot; }
    },
    cooldown: {
      on: { NEXT: &quot;turn&quot; }
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another problem to solve: how do we start this &lt;code&gt;playMachine&lt;/code&gt; at the right time in our app’s life?&lt;/p&gt;
&lt;p&gt;One solution would be to create and start a new &lt;code&gt;playService&lt;/code&gt; every time that the &lt;code&gt;Playground&lt;/code&gt; component is mounted.&lt;/p&gt;
&lt;p&gt;Another option is to merge our &lt;code&gt;screenMachine&lt;/code&gt; and our &lt;code&gt;playMachine&lt;/code&gt; in one &lt;a href=&quot;https://xstate.js.org/docs/guides/hierarchical.html&quot;&gt;hierarchical state machine&lt;/a&gt;. The states from our &lt;code&gt;playMachine&lt;/code&gt; can be children of the &lt;code&gt;playing&lt;/code&gt; state.&lt;/p&gt;
&lt;p&gt;(You don’t need to have a single machine for your whole app’s state. It’s perfectly okay to have several machines for several features or screens. But since the game loop only exists in the &lt;code&gt;&quot;playing&quot;&lt;/code&gt; screen, a single hierarchical machine makes sense here.)&lt;/p&gt;
&lt;p&gt;Our merged machine will handle most of the game’s state, so let’s call it &lt;code&gt;gameMachine&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/state/game.js
import { interpret, Machine } from &quot;xstate&quot;;

const gameMachine = Machine({
  id: &quot;game&quot;,
  initial: &quot;setup&quot;,
  states: {
    setup: {
      on: {
        START_PLAYING: &quot;playing&quot;,
      }
    },
    playing: {
      on: {
        SHOW_RESULTS: &quot;results&quot;,
        SHOW_SETUP: &quot;setup&quot;
      },
      initial: &quot;countdown&quot;,
      states: {
        countdown: {
          on: { NEXT: &quot;turn&quot; }
        },
        turn: {
          on: { NEXT: &quot;cooldown&quot; }
        },
        cooldown: {
          on: { NEXT: &quot;turn&quot; }
        }
      }
    },
    results: {
      on: {
        SHOW_SETUP: &quot;setup&quot;
      }
    }
  }
});

const gameService = interpret(gameMachine);
gameService.start();

export default gameService;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This machine can be &lt;a href=&quot;https://xstate.js.org/viz/?gist=897990dba921cb329062644d3b551c65&quot;&gt;visualized using XState’s interactive visualizer&lt;/a&gt;:&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe width=&quot;100%&quot; height=&quot;400&quot; src=&quot;https://xstate.js.org/viz/?gist=897990dba921cb329062644d3b551c65&amp;amp;embed=1&quot; title=&quot;OK&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;p&gt;If you try clicking on the event names in the visualization, you’ll see that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We start at the &lt;code&gt;setup&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;We can go to the &lt;code&gt;playing&lt;/code&gt; state, which starts at its &lt;code&gt;playing.countdown&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;playing&lt;/code&gt; state, we can go to &lt;code&gt;playing.turn&lt;/code&gt;, then to &lt;code&gt;playing.cooldown&lt;/code&gt;, then to &lt;code&gt;playing.turn&lt;/code&gt; again, and we stay there in a loop.&lt;/li&gt;
&lt;li&gt;We can go from the &lt;code&gt;playing&lt;/code&gt; state forward to the &lt;code&gt;results&lt;/code&gt; state, or back to &lt;code&gt;setup&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pretty good. Now, in the &lt;code&gt;Playground&lt;/code&gt; component we need to react to changes in the game state to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;show or hide the countdown;&lt;/li&gt;
&lt;li&gt;show the square target with a new position, and increment the turn count;&lt;/li&gt;
&lt;li&gt;hide the square target.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of that logic happens in the &lt;code&gt;startPhase&lt;/code&gt; function we described earlier. Our previous logic relied on calling &lt;code&gt;startPhase&lt;/code&gt; recursively, with a delay. Schematically, it looked like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { onMount } from &quot;svelte&quot;;
import { PLAY_PHASES } from &quot;./constants.js&quot;;

onMount(() =&amp;gt; {
  startPhase(&quot;countdown&quot;);
});

function startPhase(key) {
  // 1. Perform side effects (update the UI):
  // …
  // 2. Set a timer for the next phase:
  const nextKey = PLAY_PHASES[key].next;
  const duration = PLAY_PHASES[key].duration;
  setTimeout(() =&amp;gt; {
    startPhase(nextKey);
  }, duration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With our state machine, we’re going to split this in two:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We’re going to call &lt;code&gt;startPhase&lt;/code&gt; every time the &lt;code&gt;gameService&lt;/code&gt;’s state changes. For that, we need to subscribe to updates.&lt;/li&gt;
&lt;li&gt;At the end of a game phase (in our &lt;code&gt;setTimeout&lt;/code&gt; function), we’re going to send a &lt;code&gt;&quot;NEXT&quot;&lt;/code&gt; event instead of calling &lt;code&gt;startPhase&lt;/code&gt; directly.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { onMount, onDestroy } from &quot;svelte&quot;;
import gameService from &quot;../state/game.js&quot;;
import { gamePhaseDurations } from &quot;../state/setup.js&quot;;

// Listen to state transitions
onMount(() =&amp;gt; {
  gameService.onTransition(startPhase);
});
onDestroy(() =&amp;gt; {
  gameService.off(startPhase);
});

function startPhase(state) {
  // state.value looks like: {playing: &quot;countdown&quot;}
  const key = state.value.playing;
  // 1. Perform side effects (update the UI):
  // …
  // 2. Set a timer for the next phase:
  const duration = $gamePhaseDurations[key];
  setTimeout(() =&amp;gt; {
    gameService.send(&quot;NEXT&quot;);
  }, duration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we’re done!&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/fvsch/click-precision-game/blob/v2.0.0/src/components/Playground.svelte&quot;&gt;actual logic&lt;/a&gt; is a bit more complex — it handles things like counting turns and points, sending a &lt;code&gt;send(&quot;SHOW_RESULTS&quot;)&lt;/code&gt; event after 20 turns, etc. — but this is a good representation of how the state machine powers the game loop.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;My impressions of XState&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I found XState to be more complex than I had anticipated. Granted I knew next to nothing about state machines and &lt;a href=&quot;https://statecharts.github.io&quot;&gt;statecharts&lt;/a&gt;. But the learning curve was a bit steep, I think, for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There’s a bit of jargon to wrap your head around.&lt;/li&gt;
&lt;li&gt;XState has a lot of features, and it’s easy to get lost asking yourself if one feature is essential for your use case or not, or that other feature, or maybe this one…&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At first I tried diving in for a couple hours, writing some code, expecting to know my way around the concepts and library in that time-frame. That didn’t work at all.&lt;/p&gt;
&lt;p&gt;So I took a step back, actually read some of the docs, watched a couple videos and read some articles, before diving back in with a smaller scope: porting the navigation state.&lt;/p&gt;
&lt;p&gt;After that, porting the main game loop to XState was still a lot of work, but mostly because my own UI logic had a lot of parts to account for, and changing some of its state management meant I had a lot of loose ends to reconnect.&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;620&quot; height=&quot;402&quot; src=&quot;https://fvsch.com/articles/learning-xstate/telephone-switchboard-operator.jpg&quot; alt=&quot;An operator plugging cables in an very big switchboard&quot;&gt;
  &lt;figcaption&gt;Me trying to make &lt;code&gt;Playground.svelte&lt;/code&gt; work again.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;While I initially wanted to spend 1–2 days learning XState, I ended up spending close to a week learning and refactoring, and then 2–3 days writing this post (I’m a slow writer).&lt;/p&gt;
&lt;p&gt;There were a few more things I wanted to try, but had to drop for lack of time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Statecharts can have “extended state”, &lt;a href=&quot;https://xstate.js.org/docs/guides/context.html&quot;&gt;called “Context” in XState&lt;/a&gt;. It looks like it would make sense to store the points and turn count in the &lt;code&gt;gameService&lt;/code&gt;’s context.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;With the turn count stored in the &lt;code&gt;gameService&lt;/code&gt;, the branching logic for going to the next turn &lt;em&gt;or&lt;/em&gt; ending the game could be part of the state machine as well (instead of the &lt;code&gt;Playground&lt;/code&gt; component’s state and the &lt;code&gt;startPhase&lt;/code&gt; function). Probably using &lt;a href=&quot;https://xstate.js.org/docs/guides/guards.html&quot;&gt;Guarded Transitions&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I tried putting the playing phase duration config on the state machine, using &lt;a href=&quot;https://xstate.js.org/docs/guides/states.html#state-meta-data&quot;&gt;state meta data&lt;/a&gt;, but it was awkward and created other issues for that use case.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Would I use XState on a new project? Definitely. Being able to reason about an app, screen or feature’s state by reading a statechart definition is invaluable, and the visualizations are a nice touch. And while I haven’t used most of the advanced features, reading up on them makes me confident that I could handle complex use cases.&lt;/p&gt;
&lt;p&gt;A word of caution though: when bringing XState to a team project, I’d factor in the learning curve for everyone involved. It’s probably worth doing some basic training, at least.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:391da616-7151-55cd-9e51-317b569a07b2</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/laptop-fundraiser"/>
		<title>Laptop fundraiser: complete!</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-11-12T09:00:00+01:00</published>
		<updated>2020-11-12T09:00:00+01:00</updated>
		<summary type="text">I asked if people wanted to gift me money to buy a laptop. Here’s what happened then.</summary>
		<content type="html">&lt;p&gt;A couple months ago &lt;a href=&quot;/devtools-retrospective&quot;&gt;I asked if people wanted to gift me money&lt;/a&gt; to buy a laptop, and besides a tweet or two I haven’t written an update on what happened. It went super well!&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/laptop-fundraiser/ko-fi-goal.png&quot; width=&quot;400&quot; height=&quot;166&quot; alt=&quot;Screenshot of the ko-fi goal “Gift me a new laptop my sweeties”, with a completely filled progress bar&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;In a couple days we were around 85% of the €1350 goal, and a few days later we were at 100%.&lt;/p&gt;
&lt;p&gt;I’m super thankful to everyone who contributed financially, and to everyone who had nice words about my work on Firefox DevTools.
Special thanks to &lt;a href=&quot;https://twitter.com/digitarald&quot;&gt;Harald Kirschner&lt;/a&gt;, your compliments &lt;a href=&quot;https://www.youtube.com/watch?v=zi8ShAosqzI&quot;&gt;make me blush&lt;/a&gt; and I’m definitely putting you up as a reference on my CV!&lt;/p&gt;
&lt;p&gt;Another special thanks goes out to &lt;a href=&quot;https://www.softwareishard.com/&quot;&gt;Jan Odvarko&lt;/a&gt;, who asked Mozilla if they had a used laptop lying around.
Turns out they did!
So in September I got a package from Mozilla Paris with a 15″ 2017 Macbook Pro.
Thanks a bunch folks!&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/laptop-fundraiser/mbp-2017.jpg&quot; width=&quot;600&quot; height=&quot;450&quot; alt=&quot;A Macbook Pro cardboard box with a big black cat inside.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;We also got a second cat, pictured above.
Quad-paw 4.1 Megafloof cuddler, very little memory.
(Not a replacement for first cat.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So what am I doing with the money?&lt;/strong&gt;
I’m still buying a laptop, as intended.
Because I’m finishing my current contract and returning the work computers we’ve been using at home since this summer, we’ll need a second computer.&lt;/p&gt;
&lt;p&gt;The 2017 MBP is great, but its keyboard has Sticky Key Syndrome, so for now it can only be used for real work as a desktop (with an external keyboard); there’s a repair program for the butterfly keyboards, but so far it’s been impossible to get an appointment where I live due to the pandemic.&lt;/p&gt;
&lt;p&gt;I also wanted something more portable, and hesitated a bit: cheaper Windows or Linux ultrabook, Chromebook, Surface, iPad, Macbook Air maybe? In the end, I followed the news about Apple’s upcoming ARM laptops, and decided to get a new Macbook Air with the M1 chip.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/laptop-fundraiser/mba-2020.jpg&quot; width=&quot;500&quot; height=&quot;320&quot; alt=&quot;&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;I’m not much of an Apple fan&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, but I’m kinda excited about this one. I’m going to try to use it as a work computer and see how it goes, while we try to use the larger MBP for family needs&lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. It’s probably not the most sensible configuration, but I felt like trying that!&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;I tend to think that all operating systems are terrible and computers were a mistake, but macOS or decent Linux/Gnome configs are the least frustrating options for me.&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Watching horror movies in bed.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:2a243c67-536b-5547-be25-8a0b008f829c</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/small-analytics"/>
		<title>A quick look at privacy-focused analytics for small sites</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-09-25T00:00:00+02:00</published>
		<updated>2020-09-25T00:00:00+02:00</updated>
		<summary type="text">Looking for tools that are privacy-conscious, tell me which pages are the most visited, and are cheap enough.</summary>
		<content type="html">&lt;article-nav level=&quot;2&quot;&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;What’s the point of analytics?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;This blog has been analytics-free since 2013. Before that I used Google Analytics, then removed it as Google became too pervasive for my taste. I wasn’t looking at stats much, anyway.&lt;/p&gt;
&lt;p&gt;Was I getting sweet sweet views? Once in a while I’d get an email with comments or questions, and some of my articles would get shared a few times on Twitter; At times I’ve used Twitter’s search to check if an article was getting traction.&lt;/p&gt;
&lt;p&gt;Perhaps that is vain. But if I’m going to spend whole days writing and editing, I need something to feed into &lt;a href=&quot;https://en.wikipedia.org/wiki/Reward_system&quot;&gt;my brain’s reward system&lt;/a&gt;. It’s a bit out of whack, my brain. Needs a gentle push on the regular.&lt;/p&gt;
&lt;p&gt;So I ended up looking at analytics tools that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Are privacy-conscious, and don’t feed information to Google, Amazon, Microsoft or Palantir.&lt;/li&gt;
&lt;li&gt;Tell me which pages are the most visited, because it can feel nice, and it tells me which old articles may benefit from updates.&lt;/li&gt;
&lt;li&gt;Are cheap enough.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A lot of paid analytics services have an entry price for monthly pageviews below a 10k limit. I’ll be looking at the price above that as well, because historically my site has been slightly above that limit (though it’s probably lower than that now).&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;GDPR and Do Not Track&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Cookie banners are an ugly and tacky distraction, and I’d rather have zero analytics data than put one up.&lt;/p&gt;
&lt;p&gt;I’m hoping that if a service that doesn’t use user ids — not storing IP addresses, no fingerprinting, no cookies or &lt;code&gt;localStorage&lt;/code&gt;, etc. — then it doesn’t count as tracking or as requiring user consent.&lt;/p&gt;
&lt;p&gt;Sounds reasonable to me, but don’t take it as legal advice that it’s okay to do that; especially on sites with user accounts or user data entry or payments.&lt;/p&gt;
&lt;p&gt;One thing I’m not sure about: the &lt;a href=&quot;https://en.wikipedia.org/wiki/Do_Not_Track&quot;&gt;DNT (Do Not Track) HTTP header&lt;/a&gt;. Should I disable analytics for every visitor with &lt;code&gt;DNT: 1&lt;/code&gt;? If the analytics is private and minimal enough, does it count as “tracking”? I’m tempted to ignore it, as long as I’m using a private enough analytics service. But maybe that’s dropping the ball on a commitment to privacy and respecting user intent?&lt;/p&gt;
&lt;p&gt;I was wondering if some browsers where sending a &lt;code&gt;DNT: 1&lt;/code&gt; header by default. Looks like virtually no browser is doing that. You’re most likely to get this header from Firefox users, if they set their Tracking Protection level to “Strict” (not the default level). Brave has a &lt;a href=&quot;https://support.brave.com/hc/en-us/articles/360017905612-How-do-I-turn-Do-Not-Track-on-or-off-&quot;&gt;fairly hidden preference&lt;/a&gt;. Safari doesn’t support DNT at all; I think they removed support at some point, citing inefficiency and a fingerprinting risk.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;A random collection of tools&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Not a review. I haven’t tried most of these.&lt;/p&gt;
&lt;h3&gt;Server logs&lt;/h3&gt;
&lt;p&gt;The received wisdom is that server logs are not reliable since, unlike client-side analytics, they record a lot of bot requests. (There’s nothing special about client-side analytics that filters out bots, but the majority of bots don’t run full browsers to make HTTP requests, because it costs a lot more to do that.)&lt;/p&gt;
&lt;p&gt;Still, it was interesting to see Netlify take a stab at building analytics out of server logs, and maybe they got it right enough to be useful. If you’re on Netlify and can pay $9/month, &lt;a href=&quot;https://www.netlify.com/products/analytics/&quot;&gt;do check it out&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Matomo (formerly known as Piwik)&lt;/h3&gt;
&lt;p&gt;The big, old, &lt;a href=&quot;https://matomo.org/&quot;&gt;PHP-and-MySQL open source alternative to Google Analytics&lt;/a&gt;. Available as a paid hosted service too, but at a price point too steep for hobbyists (€19/month). Does a lot out of the box, and has plugins for more.&lt;/p&gt;
&lt;p&gt;Looks like an option, and my web host has an instance already set up. But maybe not simple or minimal enough? It looks like they have &lt;a href=&quot;https://matomo.org/docs/privacy/#step-4-respect-donottrack-preference&quot;&gt;a lot of settings or steps to follow&lt;/a&gt; to increase user privacy or avoid having to show a cookie consent banner.&lt;/p&gt;
&lt;h3&gt;Fathom&lt;/h3&gt;
&lt;p&gt;Best known of the handful of small analytics tools that cropped up recently, &lt;a href=&quot;https://usefathom.com&quot;&gt;Fathom&lt;/a&gt; starts at $14/month. Looks nice. I’m a bit concerned that &lt;a href=&quot;https://usefathom.com/features&quot;&gt;the first item on their Features page&lt;/a&gt; is “Bypass ad-blockers”.&lt;/p&gt;
&lt;p&gt;Looks like &lt;a href=&quot;https://github.com/usefathom/fathom&quot;&gt;the open-source version&lt;/a&gt;, was set aside when the Fathom team focused on building a commercial offer.&lt;/p&gt;
&lt;h3&gt;Simple Analytics&lt;/h3&gt;
&lt;p&gt;Privacy focused, built by a 2 person team, starts at $19/month. They do have &lt;a href=&quot;https://docs.simpleanalytics.com/bypass-ad-blockers&quot;&gt;instructions for bypassing ad-blockers&lt;/a&gt;, looks like a common request. They’re European based and do seem to focus on GDPR compliance though; their &lt;a href=&quot;https://docs.simpleanalytics.com/what-we-collect&quot;&gt;What We Collect&lt;/a&gt; page is a good read.&lt;/p&gt;
&lt;p&gt;They seem to have nice features, plus &lt;a href=&quot;https://simpleanalytics.com/roadmap&quot;&gt;a public roadmap&lt;/a&gt;. Overall Simple Analytics tries to balance a strong privacy focus with catering to businesses and marketeers’s needs.&lt;/p&gt;
&lt;h3&gt;Plausible&lt;/h3&gt;
&lt;p&gt;Initially built by a developer who &lt;a href=&quot;https://plausible.io/blog/the-analytics-tool-i-want&quot;&gt;wasn’t convinced by Fathom or Simple Analytics&lt;/a&gt; — though while they built &lt;a href=&quot;https://plausible.io/&quot;&gt;Plausible&lt;/a&gt; it’s likely that those tools improved too. Now a two-person team.&lt;/p&gt;
&lt;p&gt;I like that it focuses on bloggers and freelancers as a target market, and its pricing might be the most accessible yet: $12/month for 100k pageviews, and yearly billing gets you a nice 33% discount. Roughly half the price of the competition.&lt;/p&gt;
&lt;p&gt;Plausible is also fully open-source (unlike Fathom), but its &lt;a href=&quot;https://github.com/plausible/analytics/blob/master/HOSTING.md&quot;&gt;hosting requirements&lt;/a&gt; are a bit complex; this will probably rule out self-hosting for single users, but could be interesting for companies.&lt;/p&gt;
&lt;h3&gt;Friendly Analytics&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://friendly.is/en/analytics&quot;&gt;Stylized as “🙂 Friendly”&lt;/a&gt;. Launched in March 2020, hosted in Switzerland, GDPR-compliant, etc. Looks like they’re based on Matomo, but with customs settings and maybe a custom UI? Not cheap: starts at €9/month, but for very few page views (5k max), so you’ll probably need the €19/month plan instead.&lt;/p&gt;
&lt;p&gt;They’re an “Open Startup” and &lt;a href=&quot;https://friendly.is/en/blog&quot;&gt;publish detailed revenue numbers on their blog&lt;/a&gt;. Could make for some interesting reading.&lt;/p&gt;
&lt;h3&gt;Metrical&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://metrical.xyz/about&quot;&gt;Another 2-person team&lt;/a&gt;, this time from Italy. Does not track visits &lt;a href=&quot;https://docs.metrical.xyz/privacy/what-we-track&quot;&gt;for users with a Do Not Track header&lt;/a&gt;. Currently free while in beta, and pricing is announced at $7/month or $50/year (for a 50k monthly pageviews limit).&lt;/p&gt;
&lt;p&gt;I read about Metrical in &lt;a href=&quot;https://manuel.friger.io/blog/privacy-analytics-experiment&quot;&gt;this comparative review of Fathom, Simple Analytics and Metrical&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Goatcounter&lt;/h3&gt;
&lt;p&gt;An open-source, self-hostable analytics package written in Go (requires a SQLite or PostgreSQL database).
Also &lt;a href=&quot;https://www.goatcounter.com/&quot;&gt;available as a hosted service&lt;/a&gt;, with a generous free tier that should cover most small sites;
when using the free tier, the developer recommends &lt;a href=&quot;https://www.goatcounter.com/contribute&quot;&gt;donating money&lt;/a&gt; to help the service stay up.&lt;/p&gt;
&lt;p&gt;The app’s design is maybe a bit less appealing than others, but it has a nice “early Basecamp” vibe if that’s your thing.
Goatcounter &lt;a href=&quot;https://www.arp242.net/dnt.html&quot;&gt;chooses to ignore DNT headers&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Umami&lt;/h3&gt;
&lt;p&gt;An open-source project &lt;a href=&quot;https://medium.com/@caozilla/how-i-wrote-umami-in-30-days-a290372b80e4&quot;&gt;built in 30 days by Mike Cao&lt;/a&gt;, &lt;a href=&quot;https://github.com/mikecao/umami&quot;&gt;Umami&lt;/a&gt; is a simple, self-hosted analytics app. The project was announced less than a month ago, but it has seen steady releases since then.&lt;/p&gt;
&lt;p&gt;If you’re comfortable installing and running a Node.js app with a MySQL or PostgreSQL database, it could be a fun option.&lt;/p&gt;
&lt;h3&gt;Ackee&lt;/h3&gt;
&lt;p&gt;Another &lt;a href=&quot;https://ackee.electerious.com/&quot;&gt;open-source, self-hostable project&lt;/a&gt;. Version 2.0 was published last month and requires the latest Node.js (Node 14) and a MongoDB database.&lt;/p&gt;
&lt;p&gt;I tried the demo quickly, and it looks really good but I didn’t find it as usable as Umami (for example).&lt;/p&gt;
&lt;h3&gt;Shynet&lt;/h3&gt;
&lt;p&gt;A &lt;a href=&quot;https://github.com/milesmcc/shynet&quot;&gt;Python-based open-source project&lt;/a&gt; (self-hosted, requires a PostgreSQL database). Looks pretty good. Their recommandation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shynet isn’t for everyone. It’s great for personal projects and small to medium size websites, but hasn’t been tested with ultra-high traffic sites. It also requires a fair amount of technical know-how to deploy and maintain, so if you need a one-click solution, you’re best served with other tools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Though that is probably true of all the new-ish, self-hosted solutions listed here.&lt;/p&gt;
&lt;h3&gt;Offen&lt;/h3&gt;
&lt;p&gt;An open-source web app that you need to deploy yourself (binary executable plus a database, SQLite by default).
&lt;a href=&quot;https://www.offen.dev&quot;&gt;Offen has a different take on user privacy&lt;/a&gt; than all other services and software listed here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your site’s visitors must opt in to analytics.&lt;/li&gt;
&lt;li&gt;Visitors can access the data that Offen collected about them, and can delete it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;End users get a view similar to what you get as a site administrator, but with only their own visits and pageviews in the data set.
&lt;a href=&quot;https://www.offen.dev&quot;&gt;Try it out by visiting offen.dev&lt;/a&gt;, accepting tracking, then looking for the “Try as user” link.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;What I’m picking&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’ve only had broad impressions of the different services and software packages so far.
For a small business, I might go with Plausible or Simple Analytics.
For this small blog, plans around $15–20/month are excessive, but Metrical and maybe Plausible might work.&lt;/p&gt;
&lt;p&gt;I’m also considering giving Umami a try.
It should run on &lt;a href=&quot;https://www.alwaysdata.com/&quot;&gt;my current web host&lt;/a&gt;, at no extra cost.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update:&lt;/em&gt; I’ve been using Umami for three years now, and I like it.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:e3de44ba-880f-53a6-acc6-40ef6e786d67</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/devtools-retrospective"/>
		<title>A DevTools Retrospective: Or, Fund Me Maybe</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-08-20T00:00:00+02:00</published>
		<updated>2020-08-20T00:00:00+02:00</updated>
		<summary type="text">In which I look back at my design and code work on Firefox DevTools and, well, ask kind readers to help me buy a laptop.</summary>
		<content type="html">&lt;figure&gt;
  &lt;img width=&quot;280&quot; src=&quot;https://fvsch.com/articles/devtools-retrospective/old-laptop.jpg&quot; alt=&quot;Laptop with Firefox and DevTools stickers.&quot;&gt;
  &lt;img width=&quot;280&quot; src=&quot;https://fvsch.com/articles/devtools-retrospective/idiot-son.jpg&quot; alt=&quot;Cat chewing a house plant.&quot;&gt;
  &lt;figcaption&gt;
    My 5 year old laptop won’t start up anymore. Plus my stupid son ate through the charger cable.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So my laptop died a few months ago and I’ve made do with my work computer instead, but recently I’ve considered buying a new one to not depend on work equipment.&lt;/p&gt;
&lt;p&gt;In the past 2 years, I’ve also spent a lot of my time — including time off from my day job — to work on &lt;a href=&quot;https://firefox-dev.tools/&quot;&gt;Firefox DevTools&lt;/a&gt; as a volunteer contributor. As my involvement in this project is coming to a close, I’ve been looking back and thinking: what if I got a small something out of it, beyond a bit experience, as a thank you?&lt;/p&gt;
&lt;p&gt;So I’ve decided: I am asking you, kind reader, to consider chipping in to help me pay for a new laptop. I’m targeting something like a Macbook Air (€1200 with VAT) or the cheapest Macbook Pro (€1500), or a similar laptop with Windows or Linux. &lt;a href=&quot;https://ko-fi.com/fvsch&quot;&gt;I’ve set up a Ko-fi page with a €1350 goal&lt;/a&gt;, and you can give anything you want, if you appreciate the work I did.&lt;/p&gt;
&lt;p&gt;To be clear, I’m not in any financial hardship. I could buy a laptop without help, but I’ll appreciate it if I can cover most or some of the cost this way. Read on if you want more context on why I’m asking for contributions.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;What did I do as a contributor?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I started contributing to Firefox DevTools in Spring 2018, initially to work on small UI details and land CSS fixes for the Inspector, Console or Network Monitor. Over two years I have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Helped dozens of volunteers, Outreachy candidates and Google Summer of Code candidates get started with the DevTools codebase (usually over Bugzilla and Slack). I’ve also led or participated in code reviews.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/buglist.cgi?bug_status=RESOLVED&amp;amp;bug_status=VERIFIED&amp;amp;resolution=FIXED&amp;amp;emailtype1=exact&amp;amp;query_format=advanced&amp;amp;emailassigned_to1=1&amp;amp;list_id=15382408&amp;amp;email1=florens%40fvsch.com&quot;&gt;Fixed 106 bugs&lt;/a&gt; of various sizes (from a few hours to week-long efforts).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/firefox-devtools/ux/issues&quot;&gt;Participated in many UX discussions&lt;/a&gt; to provide feedback and ideas.&lt;/li&gt;
&lt;li&gt;Led a few design projects, including an icon refresh (shipped) and a redesign of the Settings page (accepted but not implemented).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One example: if you’re using Firefox DevTools on Linux, your experience may have improved overnight &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1458224&quot;&gt;thanks to this simple CSS patch&lt;/a&gt; that fixed a lot of issues with font sizes and icon alignments; it took just a few lines of code, but days of investigation and tests!&lt;/p&gt;
&lt;p&gt;I’ve worked remotely with &lt;a href=&quot;https://violasong.com&quot;&gt;Victoria Wang&lt;/a&gt;, &lt;a href=&quot;https://patrickbrosset.com&quot;&gt;Patrick Brosset&lt;/a&gt;, &lt;a href=&quot;https://nicolaschevobbe.com&quot;&gt;Nicolas Chevobbe&lt;/a&gt;, &lt;a href=&quot;https://digitarald.de&quot;&gt;Harald Kirschner&lt;/a&gt;, &lt;a href=&quot;https://mtigley.dev&quot;&gt;Micah Tigley&lt;/a&gt;, &lt;a href=&quot;https://jasonlaster.github.io&quot;&gt;Jason Laster&lt;/a&gt;, &lt;a href=&quot;https://davidwalsh.name&quot;&gt;David Walsh&lt;/a&gt;, &lt;a href=&quot;https://yzen.github.io&quot;&gt;Yura Zenevich&lt;/a&gt; and many others.&lt;/p&gt;
&lt;p&gt;I also participated in three Mozilla All Hands weeks, which were fun! (But they were also work, somewhat tiring, and it did require me to use up my job’s paid time off.&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;
&lt;p&gt;On the visible side, here are some of the design-y work I’ve done. Click through to to see full-size images.&lt;/p&gt;
&lt;figure&gt;
  &lt;a href=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-figma-docs@2x.png&quot;&gt;&lt;img class=&quot;border&quot; width=&quot;300&quot; src=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-figma-docs@300w.png&quot; alt=&quot;Screenshot of my DevTools Figma project&quot;&gt;&lt;/a&gt;
  &lt;figcaption&gt;An overview of design documents I’ve worked on in my Figma “DevTools” project. It’s mostly icon work and small mock-ups to explain ideas.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
  &lt;a href=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-toolbox-icons@2x.png&quot;&gt;&lt;img class=&quot;border&quot; width=&quot;340&quot; src=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-toolbox-icons@1x.png&quot; alt=&quot;Exploration and final set of icons for the DevTools tabs.&quot;&gt;&lt;/a&gt;
  &lt;figcaption&gt;A redesign of the DevTools tab icons, made to follow the 2017 &lt;a href=&quot;&quot; https:&gt;Photon Design System&lt;/a&gt; guidelines. This was my most visible user-facing work.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
  &lt;a href=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-settings-redesign@2x.png&quot;&gt;&lt;img class=&quot;border&quot; width=&quot;600&quot; src=&quot;https://fvsch.com/articles/devtools-retrospective/devtools-settings-redesign@2x.png&quot; alt=&quot;Screenshots of a redesign of the DevTools Settings page.&quot;&gt;&lt;/a&gt;
  &lt;figcaption&gt;I’ve worked on a redesign of the DevTools settings page, to break it up in more readable chunks. You can &lt;a href=&quot;https://fx-devtools-settings.netlify.app/&quot;&gt;try out the HTML prototype&lt;/a&gt; (but do try it in Firefox, since it uses CSS Logical Properties not supported by Chrome and Safari at this time), and &lt;a href=&quot;https://github.com/fvsch/devtools-settings&quot;&gt;check out the repository&lt;/a&gt;.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Why am I asking for money?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Because I felt like it! 😁&lt;/p&gt;
&lt;p&gt;Just to make it clear, I don’t think I’m entitled to money here. Nobody has to give me anything! (As a side note, if I was trying to crowd-fund payment for the time worked on DevTools, at 1-2 days a week for something like 18 months straight, that goal would be much higher than $1.5k.)&lt;/p&gt;
&lt;p&gt;Asking for money now might sound strange, especially since I didn’t start contributing to DevTools with money in mind. I wanted to scratch some itches, and I was excited about having patches I wrote improving a UI used by hundreds of thousands. And I also wanted to help out so that we don’t end up in a Chrome monoculture for good.&lt;/p&gt;
&lt;p&gt;But after 6-12 months of doing good work, I ended up thinking: I like the work and it seems to be useful, let’s get paid for it and do it full time!&lt;/p&gt;
&lt;p&gt;Sadly I’m pretty bad at advocating for myself, and I never actually applied for a role. I did reach out to a few team members, but what I heard was that there was no budget for more people, beyond maybe short-term contracts (not an option for me at the time).&lt;/p&gt;
&lt;p&gt;Then in the last year it became clear that Mozilla management didn’t see DevTools as a vital investment, and in 2020 reorgs and layoffs have strongly impacted DevTools.&lt;/p&gt;
&lt;p&gt;So that was it. Combined with some frustrations about not being able to do or land some work on my side because I could not secure reviews or other help&lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, I mentally called it quits.&lt;/p&gt;
&lt;p&gt;What prompted me to ask random people for money was this tweet:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&apos;re worried about the future of #Firefox, #Gecko, #MDN and other projects from #Mozilla there&apos;s one way you can help: contribute.&lt;/p&gt;
&lt;p&gt;I always hang out in our #Introduction channel, so if you feel like coding and want to help just ping me.&lt;/p&gt;
&lt;p&gt;—&lt;a href=&quot;https://twitter.com/gabrielesvelto/status/1293633109032935426&quot;&gt;@gabrielesvelto&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I have some &lt;em&gt;feelings&lt;/em&gt; about that.&lt;/p&gt;
&lt;p&gt;We’re talking about doing free work here. If you want those projects to succeed, are worried they might not, and want to make an impact, that could mean &lt;em&gt;a lot&lt;/em&gt; of free work. Is that work going to lead to significant financial loss? Maybe to burnout or an increased risk of burnout, if doing that work on top of a day job? Maybe a mix of both?&lt;/p&gt;
&lt;p&gt;Meanwhile, this work is adding value to projects and products (especially Gecko and Firefox) that Mozilla Corporation derives money from. That money funds big compensations for the C-Suite, and salaries for engineers that surpass anything I ever earned in my own career&lt;sup id=&quot;fnref1:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote-ref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. That’s true for many other contributors too, of course, and is part of a wider criticism of open-source.&lt;/p&gt;
&lt;p&gt;(And maybe talking about this just after the layoffs is ill-timed, but the layoffs are the reason why people are calling for a surge of contributions.)&lt;/p&gt;
&lt;p&gt;Finally, &lt;a href=&quot;https://twitter.com/fvsch/status/1293641513679884288&quot;&gt;as I replied on Twitter&lt;/a&gt;, if you want people to work for free you should provide some incentives, including psychological ones. For large projects like Gecko or Firefox, the technical complexity, specific processes and bureaucracy&lt;sup id=&quot;fnref1:4&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote-ref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; mean it’s hard to just “scratch an itch” and see results quickly. Instead, one incentive can be to ship fixes and features to tens of thousands or millions of users — an appeal that smaller projects can’t provide.&lt;/p&gt;
&lt;p&gt;But to be able to do that and not waste your time, you will need support from the core team. So you need that team to not be fired, cut in half or otherwise scaled down. And you will need some confidence that once your efforts pane out and your work is ready to ship to users, the product will not have been discontinued (ahem, Firefox OS). And if we cannot guarantee that, because strategic decisions lie in the hands of unaccountable corporate management, should we really call for people to provide free work?&lt;/p&gt;
&lt;p&gt;So I was thinking about all that, all those mixed feelings about open-source, and I ended up at: you know what, fuck it, I want some money too. My friends and family always tell me I’m super bad at valuing myself and asking for money, promotions or raises — so maybe I should break that cycle.&lt;/p&gt;
&lt;p&gt;I’m asking for money. A symbolic amount, really, compared to the work I did, but still an amount that I can use for something practical.&lt;/p&gt;
&lt;p&gt;If it works out, great, and if it doesn’t… eh. 🤷‍♀️&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Update&lt;/em&gt;: the response to this has been super positive, and we made the goal in 4 days! I wrote &lt;a href=&quot;/laptop-fundraiser&quot;&gt;this follow-up post&lt;/a&gt;, it has a cat picture too.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;Mozilla shoulders all other costs of inviting volunteer to the Mozilla All Hands, including plane tickets, hotel rooms, catering, and expense stipends. I don’t have exact numbers, but the total cost for inviting a single volunteer three times is probably in the $10–15k range?&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;For example, for the Settings page implementation, I was motivated to do a lot of the work but would have required mentorship or timely help. One developer also had ongoing work on that panel that he could share with me, but he was moved from the DevTools team to the Firefox for Android project.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:3&quot;&gt;
&lt;p&gt;To be clear, I don’t have any issue with current or former Mozilla employees, especially the great people I’ve been lucky to work with. My point is that when taking a step back and looking at Mozilla or company-sponsored open-source projects in general, some people are getting compensated and others not, and maybe it should give us pause.&amp;#160;&lt;a href=&quot;#fnref1:3&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:4&quot;&gt;
&lt;p&gt;It seems largely similar in Chromium or WebKit; it comes from the scope and reach of the project, not from Mozilla or Google being particularly bad at it.&amp;#160;&lt;a href=&quot;#fnref1:4&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:20b1f605-d09c-5bfe-a5d4-15d419ebaf68</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/kirby-typing"/>
		<title>PHP code typing with Kirby CMS</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-08-12T00:00:00+02:00</published>
		<updated>2020-08-12T00:00:00+02:00</updated>
		<summary type="text">Kirby 3 is built with PHP 7 and uses type annotations extensively, but this often won’t be reflected in an IDE. This post explores why, and what workarounds you can use.</summary>
		<content type="html">&lt;p&gt;As I was updating this site and writing &lt;a href=&quot;/kirby-logic&quot;&gt;my previous post on Kirby CMS&lt;/a&gt;, I sometimes struggled to get good code autocompletion.&lt;/p&gt;
&lt;p&gt;Kirby 3 is built with PHP 7 and uses type annotations extensively, but this often won’t be reflected in an IDE.
This post explores why, and what workarounds you can use.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Why you might want completions&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Unlike CMSes like WordPress and Drupal which come with default themes that generate HTML code for you out of the box, Kirby uses a very hands-off approach to building web pages.
You can start from scratch and write your own PHP controllers and templates to render what you want, using &lt;a href=&quot;https://getkirby.com/docs/guide/templates/php-api&quot;&gt;Kirby’s PHP API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That’s great if you want to build something bespoke.
But it also means that you’re going to spend a lot of time looking at the &lt;a href=&quot;https://getkirby.com/docs/reference/objects/page&quot;&gt;reference documentation&lt;/a&gt;.
Before you know it, you’re juggling between your code editor and 15 open documentation tabs.&lt;/p&gt;
&lt;figure&gt;
  &lt;img class=&quot;border&quot; src=&quot;https://fvsch.com/articles/kirby-typing/kirby-page-reference.png&quot; alt=&quot;Screenshot of the Kirby Reference page for the $page object&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;That’s where code completion — sometimes called “intellisense” — helps.
It lets you be a bit lazy and access a lot of that reference information directly in your code editor, as you type.&lt;/p&gt;
&lt;p&gt;Ideally, when your editor sees a PHP variable like &lt;code&gt;$page&lt;/code&gt; in a Kirby template, it should known that &lt;code&gt;$page&lt;/code&gt; is an instance of the &lt;code&gt;Kirby\Cms\Page&lt;/code&gt; class.
And it should offer code completions and suggestions from that class.
No need to check the docs!&lt;/p&gt;
&lt;p&gt;Sadly, this doesn’t work out of the box with Kirby, in a few common situations.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Runtime magic&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Kirby tries to be approchable for people who are not pro developers or PHP programmers.
Its creator, &lt;a href=&quot;https://bastianallgeier.com&quot;&gt;Bastian Allgeier&lt;/a&gt;, is a designer and developer, and wants to cater to a wider audience of designers, webmasters, tinkerers, etc.
So Kirby tries to do some magic things like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load templates (and controllers, if any) by file name. Follow the convention, and your code will run[^2].&lt;/li&gt;
&lt;li&gt;Automatically inject variables in your templates and in your controllers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means that the template for a page of type &lt;code&gt;&apos;article&apos;&lt;/code&gt; can be named &lt;code&gt;site/templates/article.php&lt;/code&gt; and be as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;h1&amp;gt;&amp;lt;?= $page-&amp;gt;title() ?&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;?= $page-&amp;gt;text() ?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The downside of this approach is that your code editor has no information about this &lt;code&gt;$page&lt;/code&gt; variable.
It cannot know if it’s a string, a number, or an object (and what kind of object).
Your editor might even warn you that this variable was never defined, and highlight it as an error!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://getkirby.com/docs/guide/templates/controllers&quot;&gt;Kirby controllers&lt;/a&gt; use a different kind of magic.
Controllers are defined as functions returned by a script, such as &lt;code&gt;site/controllers/article.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
return function ($page, $site) {
  // Inject two new variables, $content and $articles,
  // in the &apos;article&apos; template
  return [
    &apos;content&apos; =&amp;gt; $page-&amp;gt;text()-&amp;gt;markdown(),
    &apos;articles&apos; =&amp;gt; $site-&amp;gt;find(&apos;blog&apos;)-&amp;gt;children()
  ];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the &lt;code&gt;$page&lt;/code&gt; and &lt;code&gt;$site&lt;/code&gt; parameters in the function?
Kirby will look at the function’s parameter names and automatically provide an instance of the current page for a parameter named &lt;code&gt;$page&lt;/code&gt;, of the Site object for a parameter named &lt;code&gt;$site&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;This lets us ask for a few built-in objects in any order we want, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;return function ($site, $kirby, $page) { … }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But again, the downside is that this is magic happening deep inside Kirby’s own code, and your IDE will have no idea what those variables are.
No code completion for you!&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Adding type hints and annotations&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’ve found a couple solution to this problem.&lt;/p&gt;
&lt;p&gt;For controllers, since we’re using function parameters to “request” specific objects, we can add &lt;a href=&quot;https://www.php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration&quot;&gt;type declarations using PHP’s syntax&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/controllers/article.php
use Kirby\Cms\App;
use Kirby\Cms\Page;
use Kirby\Cms\Site;

return function (Site $site, App $kirby, Page $page) {
  …
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that for backward compatibility with Kirby 2, Kirby 3 provides shorter &lt;a href=&quot;https://getkirby.com/docs/reference/@/aliases&quot;&gt;class aliases&lt;/a&gt;, including &lt;code&gt;Kirby&lt;/code&gt; (for &lt;code&gt;Kirby\Cms\App&lt;/code&gt;), &lt;code&gt;Page&lt;/code&gt; and &lt;code&gt;Site&lt;/code&gt;.
Personally, I like using the longer, more explicit class names.&lt;/p&gt;
&lt;p&gt;Now, let’s talk about templates.
Templates will receive variables defined by Kirby (like the &lt;code&gt;$page&lt;/code&gt; and &lt;code&gt;$kirby&lt;/code&gt; objects), and any other variable you return in a controller.&lt;/p&gt;
&lt;p&gt;Sadly there is no way in PHP to declare “hey I know that this &lt;code&gt;$page&lt;/code&gt; variable exists in this context, and I’m going to tell you what it is”.
Unlike in TypeScript where you could write something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;declare var page: Page;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The good news is, there is a widely supported way to declare variable types in PHP, &lt;a href=&quot;https://github.com/php-fig/fig-standards/blob/2668020622d9d9eaf11d403bc1d26664dfc3ef8e/proposed/phpdoc-tags.md#517-var&quot;&gt;using the PHPDoc &lt;code&gt;@var&lt;/code&gt; tag&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/templates/article.php

/** @var Kirby\Cms\Page $page */

&amp;lt;h1&amp;gt;&amp;lt;?= $page-&amp;gt;title() ?&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;?= $page-&amp;gt;text() ?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PHPDoc comments are not a native PHP feature, so declaring a type this way won’t affect how your code runs at all.
But most code editors and IDEs with good PHP support will understand this special comment syntax, and will read it as “there is a variable $page in the current scope, and it’s an instance of the &lt;code&gt;Kirby\Cms\Page&lt;/code&gt; class”.&lt;/p&gt;
&lt;p&gt;And if you have created a &lt;a href=&quot;https://getkirby.com/docs/guide/templates/page-models&quot;&gt;Page Model&lt;/a&gt; for this template, you can declare the type using the page model’s class name, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/models/article.php

class ArticlePage extends Kirby\Cms\Page {
  public function getArticleBody(): string {
    if ($this-&amp;gt;content()-&amp;gt;body()-&amp;gt;isNotEmpty()) {
      return $this-&amp;gt;content()-&amp;gt;body()-&amp;gt;markdown();
    }
    return &apos;&apos;;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/templates/article.php

/** @var ArticlePage $page */

&amp;lt;h1&amp;gt;&amp;lt;?= $page-&amp;gt;title() ?&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;?= $page-&amp;gt;getArticleBody() ?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;span&gt;Making sure your editor has good PHP support&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Finally, if you want to see code completions in your editor or IDE of choice, type annotations may not be enough.
You also need an editor or IDE with good PHP support!&lt;/p&gt;
&lt;p&gt;Personally, I use &lt;a href=&quot;https://www.jetbrains.com/phpstorm/&quot;&gt;PhpStorm&lt;/a&gt; when writing more than a couple lines of PHP, because it just works.
The downside is that it’s paid software, and not cheap.&lt;/p&gt;
&lt;p&gt;I’ve also tried using &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt; with the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client&quot;&gt;PHP Intelephense&lt;/a&gt; extension. It works well, but do follow the “Quick Start” steps in the extension’s page.&lt;/p&gt;
&lt;p&gt;There are a few other options, such as &lt;a href=&quot;https://netbeans.apache.org/&quot;&gt;NetBeans&lt;/a&gt;, but I haven’t tried them out.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:f859a063-0612-5eac-ab80-b43c5cd4baea</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/kirby-logic"/>
		<title>Where should you put logic code in Kirby CMS?</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-06-01T12:00:00+02:00</published>
		<updated>2020-06-01T12:00:00+02:00</updated>
		<summary type="text">Kirby CMS has a few options for where to put your PHP logic: in templates, controllers, page models or page methods. My favorite is page models, and we can work around their main limitation easily.</summary>
		<content type="html">&lt;p&gt;This blog has been running on &lt;a href=&quot;https://getkirby.com&quot;&gt;Kirby CMS&lt;/a&gt; for 6 years.
And I’ve been migrating it from Kirby 2 to Kirby 3, which is taking a bit of work.&lt;/p&gt;
&lt;p&gt;I like to do a bunch of custom things with my site’s structure and content,
so instead of relying on “the Kirby way” I’m implementing a bunch of stuff myself.&lt;/p&gt;
&lt;p&gt;That means writing PHP code that queries and manipulates data
— often called “logic” code by programmers —
and I’ve struggled a bit with understanding where I can put this code in the project.&lt;/p&gt;
&lt;article-nav&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;Logic in templates? Sure.&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One thing I like about Kirby is that it’s powerful and doesn’t require you to jump through hooks.
So if you want to put all your logic in a &lt;a href=&quot;https://getkirby.com/docs/guide/templates/basics&quot;&gt;template file&lt;/a&gt;, you can!&lt;/p&gt;
&lt;p&gt;In the following example, we’re querying all the child pages of the current page, keeping the “Published” ones and sorting them by date, then listing the latest 20 pages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/templates/blog.php
$posts = $page-&amp;gt;children()
  -&amp;gt;filterBy(&apos;isPublished&apos;, true)
  -&amp;gt;orderBy(&apos;date&apos;, &apos;desc&apos;)
  -&amp;gt;limit(20);
?&amp;gt;

&amp;lt;h1&amp;gt;&amp;lt;?= $page-&amp;gt;title() ?&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;?php foreach($posts as $item): ?&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;?= $item-&amp;gt;title() ?&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;?php endforeach; ?&amp;gt;
&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(This template would be used for any page which has the &lt;code&gt;&apos;blog&apos;&lt;/code&gt; content type, meaning its content file is something like &lt;code&gt;content/my-page/blog.txt&lt;/code&gt;. See &lt;a href=&quot;https://getkirby.com/docs/guide/content/creating-pages#creating-published-pages-manually&quot;&gt;“Creating pages” in the Kirby documentation&lt;/a&gt; for more information.)&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Or in controllers, maybe?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Putting logic in template is great for people just getting started with Kirby, or people who may not be professional programmers.
But if you want to separate your logic from the template, you can move it to &lt;a href=&quot;https://getkirby.com/docs/guide/templates/controllers&quot;&gt;a controller function&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/controllers/blog.php

return function ($page) {
  $posts = $page-&amp;gt;children()
    -&amp;gt;filterBy(&apos;isPublished&apos;, true)
    -&amp;gt;orderBy(&apos;date&apos;, &apos;desc&apos;)
    -&amp;gt;limit(20);
  return [&apos;posts&apos; =&amp;gt; $posts];
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But what if you want to share some logic between different templates, controllers or page types?
It would be awesome if you could call something like &lt;code&gt;$page-&amp;gt;getPublishedChildren()&lt;/code&gt;, from anywhere.&lt;/p&gt;
&lt;p&gt;Kirby offers two ways to do that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://getkirby.com/docs/reference/plugins/extensions/page-methods&quot;&gt;Page Methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getkirby.com/docs/guide/templates/page-models&quot;&gt;Page Models&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;span&gt;Creating a page method&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Page methods apply to all &lt;code&gt;$page&lt;/code&gt; objects, regardless of page type.
They’re a slightly older technique in Kirby, and in Kirby 3 you need to declare a plugin to add page methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/plugins/my-plugin/index.php

Kirby::plugin(&apos;me/my-plugin&apos;, [
  &apos;pageMethods&apos; =&amp;gt; [
    &apos;getPublishedChildren&apos; =&amp;gt; function() {
      return $this-&amp;gt;children()
        -&amp;gt;filterBy(&apos;isPublished&apos;, true)
        -&amp;gt;orderBy(&apos;date&apos;, &apos;desc&apos;)
        -&amp;gt;limit(20);
    }
  ]
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few downsides to page methods:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Having to register a plugin is a bit awkward.&lt;/li&gt;
&lt;li&gt;If you’re using PHP types and intellisense in your editor, your editor has no way to know that &lt;code&gt;$this&lt;/code&gt; in your page method is an instance of Kirby’s &lt;code&gt;Page&lt;/code&gt; class. That means you don’t get code completions and validation in your editor.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;span&gt;Creating a page model&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The newer option is to use page models, which are defined as PHP classes extending Kirby’s &lt;code&gt;Page&lt;/code&gt; class.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/models/blog.php

class BlogPage extends Page {
  public function getPublishedChildren() {
    return $this-&amp;gt;children()
      -&amp;gt;filterBy(&apos;isPublished&apos;, true)
      -&amp;gt;orderBy(&apos;date&apos;, &apos;desc&apos;)
      -&amp;gt;limit(20);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks a little bit cleaner to me, mainly because we avoid creating a plugin.
And since we’re extending the &lt;code&gt;Page&lt;/code&gt; class, now our code editors will know what methods the &lt;code&gt;$this&lt;/code&gt; object has&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;And reading the Kirby documentation, and especially the &lt;a href=&quot;https://getkirby.com/docs/guide&quot;&gt;Guide&lt;/a&gt;, it looks like Page Models are the preferred mechanism for adding functionality to page objects.&lt;/p&gt;
&lt;p&gt;But they have one big downside: they only apply to a given page type, the &lt;code&gt;&apos;blog&apos;&lt;/code&gt; page type in this example.
If you try to use &lt;code&gt;$page-&amp;gt;getPublishedChildren()&lt;/code&gt; on a page with a different type, you get an error.&lt;/p&gt;
&lt;p&gt;Are we forced to use Page Methods in a plugin to share functionality between pages? Well, maybe not.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Creating a default page model&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Our page models already inherit from Kirby’s &lt;code&gt;Page&lt;/code&gt; class.
What if we added one more class to that hierarchy?&lt;/p&gt;
&lt;p&gt;Let’s start by moving our method to a new class, which we’ll call &lt;code&gt;DefaultPage&lt;/code&gt;.
Ideally we should pick a name that doesn’t match a page type (meaning we don’t have pages with a content file named &lt;code&gt;default.txt&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/models/default.php

class DefaultPage extends Page {
  public function getPublishedChildren() {
    /* ... */
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can create a class for our &lt;code&gt;&apos;blog&apos;&lt;/code&gt; page type, which extends &lt;code&gt;DefaultPage&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/models/blog.php
require_once &apos;default.php&apos;;

class BlogPage extends DefaultPage {
  // Add methods specific to the &apos;blog&apos; type
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that we have to require &lt;code&gt;default.php&lt;/code&gt; explicitly to load the &lt;code&gt;DefaultPage&lt;/code&gt; class,
because Kirby won’t load it automatically since it doesn’t match a real page type.&lt;/p&gt;
&lt;p&gt;If we add a new page type, we can create an empty page model that inherits from &lt;code&gt;DefaultPage&lt;/code&gt;,
and it will inherit the same shared methods.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php // site/models/article.php
require_once &apos;default.php&apos;;

class ArticlePage extends DefaultPage {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fair warning: this means that to fully emulate a global “page method”,
we have to create a page model for every single page type we have!&lt;/p&gt;
&lt;p&gt;If that’s too verbose, your options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Going back to using &lt;a href=&quot;https://getkirby.com/docs/reference/plugins/extensions/page-methods&quot;&gt;the page method technique&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getkirby/ideas/issues/556&quot;&gt;Upvoting this feature request&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;At least if we’re using a code editor with good support for PHP, like PhpStorm; in my experience &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt; was too limited out of the box, and I couldn’t figure out which plugins to try to improve that.&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:b5a4426e-1289-521f-8fa9-bba009912d93</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/body-copy-sizes"/>
		<title>A short history of body copy sizes on the Web</title>
		<author><name>Florens Verschelde</name></author>
		<published>2020-01-01T00:00:00+01:00</published>
		<updated>2020-01-01T00:00:00+01:00</updated>
		<summary type="text">It’s hard to pick a font size that is just right, especially as you try to adapt to different screens and scenarios. Looking at the recent history of how we got here can give us some perspective.</summary>
		<content type="html">&lt;p&gt;It’s hard to pick a font size that is &lt;em&gt;just right&lt;/em&gt;, especially as you try to adapt to different screens and scenarios. Looking at the recent history of how we got here can give us some perspective.&lt;/p&gt;
&lt;p&gt;When I started working on Web stuff around 2005, there were two extremely popular font styles for body copy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10px Verdana;&lt;/li&gt;
&lt;li&gt;11px Arial.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those two styles appeared on maybe 90 percent of professionally built sites, to be seen by users on IE5, IE5.5 and IE6 on Windows XP and earlier versions. They also looked similar, thanks to heavy font hinting, lack of font smoothing or sub-pixel rendering, and the fact that Verdana has a bigger x-height so 10px Verdana was roughly equal to 11px Arial, only with slightly wider letters.&lt;/p&gt;
&lt;p&gt;Ten and 11 pixels may seem puny today, but in the early 2000s that was deemed readable for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the 800×600 and 1024×768 screens of the late 1990s and early 2000s had biggish pixels, so the result was on the small side but not as small as it might look today;&lt;/li&gt;
&lt;li&gt;designers and their clients were accustomed to 9, 10 and 11 point sizes for body copy in print (books, magazines, leaflets…), and the prospect of using bigger values felt like shouting at readers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With relatively little experience of the Web as an autonomous medium, graphic designers and marketing departments relied on previous knowledge — from e.g. QuarkXPress and Microsoft Word. “How do I translate this point size, which works in my leaflet or magazine ad, to a HTML size?”&lt;/p&gt;
&lt;p&gt;Of course, there is no way to reliably translate typographic points to pixels, because pixels &lt;em&gt;don’t have a universal physical size&lt;/em&gt;. Screens have different pixel-per-inch ratios. The original Macintosh had a 72ppi screen (or perhaps 68ppi?&lt;sup id=&quot;fnref1:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote-ref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;). Twenty years later, in 2004, screens in the 80–90ppi range were common. A few years laters, pixels had gotten a bit smaller, and screens were often in the 90–120ppi range, while most iPhones had a 160ppi resolution&lt;sup id=&quot;fnref1:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote-ref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. Despite the popular misconception, and even before the Retina transition started, the Web’s resolution &lt;em&gt;was not 72ppi&lt;/em&gt;; it never was a single thing.&lt;/p&gt;
&lt;p&gt;However, we don’t need to work out the exact points-per-inch resolution of every device to make sensible design choices. &lt;em&gt;Trying things out&lt;/em&gt; should always trump dpi, ppi, Retina, or even pixel counts.&lt;/p&gt;
&lt;p&gt;In November 2006, iA’s Oliver Reichenstein ran a simple experiment: he compared a magazine’s body copy at arms’ length and a typical site’s body copy at a common, eye-to-desktop-screen distance. The website’s text looked much smaller. Oliver &lt;a href=&quot;https://ia.net/know-how/100e2r&quot;&gt;argued for setting the body copy to the browser’s default&lt;/a&gt;, or &lt;code&gt;100%&lt;/code&gt;, which by convention is &lt;code&gt;16px&lt;/code&gt; in common browsers. In 2006, and even a few years later, it was a revolutionary proposition. Web designers and clients thought it was extreme. Five years later, we still had to &lt;a href=&quot;https://romy.tetue.net/stop-arial-11px&quot;&gt;fight for the death of 11px body copy (example, in French)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Text that is too small takes more time to read. Users may have to lean towards the screen, hold mobile devices closer, squint, or just concentrate more. As designers and developers, we strive to not ask for such extra effort from people who use or read our work.&lt;/p&gt;
&lt;p&gt;On average, online text got bigger — at least in nominal pixel sizes — in the late 2000s and early 2010s. The exact reasons why are anyone’s guess, so here’s mine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the medium’s matured, thanks to arguments such as the one spelled out by Oliver Reichenstein;&lt;/li&gt;
&lt;li&gt;text in the 10–12px range looked tiny on the iPhone and other early smartphones (resolutions in the 150–200ppi range).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I remember web designers and developers leading the way with blogs, professional news sites and the occasional client project set in “bigger” sizes in the 14–18px range. This evolution seeped into generalist news sites, one high-profile redesign after the other; nowadays, theguardian.com has &lt;code&gt;1.0625rem&lt;/code&gt; (17px) text with &lt;code&gt;1.5&lt;/code&gt; line-height, nytimes.com used &lt;code&gt;17px&lt;/code&gt; font-size and &lt;code&gt;26px&lt;/code&gt; line-height circa 2017, and in 2019 it went up to &lt;code&gt;20px&lt;/code&gt; font-size and &lt;code&gt;30px&lt;/code&gt; line-height on wide screens; body copy in the 18–21 pixel range is common (Medium, bostonglobe.com, newyorker.com, liberation.fr…).&lt;/p&gt;
&lt;p&gt;Those numbers only mark a point in time. Back in 2003, 12px Arial might have been a generously big, very readable option for that majority of users on 800×600 screens with font smoothing turned off. The feeling that the browser’s default font-size was too big, which was very present when Oliver’s article was published in 2006, was partly a cultural thing, but it had some technical reasons as well. Likewise, we’ve started to feel that 16px is rather small for all but the smaller screens, again for a combination of reasons.&lt;/p&gt;
&lt;p&gt;Then there is the very big body copy trend. In April 2012, influential web designer Jeffrey Zeldman &lt;a href=&quot;https://www.zeldman.com/2012/04/18/redesigning-in-public-again/&quot;&gt;redesigned his site with 24px Georgia body copy&lt;/a&gt; (and 32px for the opening paragraph of each post). Many fellow designers were puzzled, and some complained that the design was hard to read or reported having to zoom out on laptops or desktop computers to be able to read comfortably. Still, that design lasts today. Other designers have used similar sizes, e.g. Trent Walton &lt;a href=&quot;https://trentwalton.com/2012/06/19/fluid-type/&quot;&gt;stated his preference for 20–24px text&lt;/a&gt; (or 125–150%).&lt;/p&gt;
&lt;p&gt;The most recent example of that trend is Jeremy Keith’s &lt;a href=&quot;https://resilientwebdesign.com/&quot;&gt;Resilient Web Design&lt;/a&gt; online book. Jeremy uses a CSS lock&lt;sup id=&quot;fnref1:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote-ref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; to vary the font size depending on viewport width, between two boundaries: &lt;code&gt;100%&lt;/code&gt; and &lt;code&gt;250%&lt;/code&gt;. At 320px, you get (with default browser settings) a 16px font size. At 1600px, you get 40px text. Obviously this is a design choice, and I recon that — similar to what Trent Walton described — the intended effect for laptop and desktop use is that readers lean back and read with their faces away from the screen rather than leaning towards it. It’s a design that invites taking your time instead of skimming through the text.&lt;/p&gt;
&lt;p&gt;While that design makes for a good reading experience on smaller screens (especially smartphones and tablets, in my tests), I find it difficult on larger screens. The main issues I have with it are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Seeing very few lines of text at a time. For instance, 10 lines of text on a 13 inch laptop. I have some level of attention deficiency when reading, and this setup removes a lot of visual context when I try to scroll and read; I generally try to fight the attention deficiency by selecting every other paragraph I’m reading, but when the design only shows one or two paragraphs at a time, that doesn’t help.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Each line of text is &lt;em&gt;physically wide&lt;/em&gt;, requiring the reader’s eyes to span a wider angle than usual. This could have two undesired effects: readers might end up doing more fixations to read the same line of text (e.g. 3–5 instead of 2–4); and in more extreme examples, the wider eye movements could cause eye strain or fatigue.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s say someone is seating in a sofa with a laptop on their lap, and currently reading an article. And let’s suppose that for this person in this setting with their own preferences and current level of fatigue etc., there exists an ideal font size for reading whatever text they’re reading. For instance, it could be 22px.&lt;/p&gt;
&lt;p&gt;The reading process involves &lt;a href=&quot;https://en.wikipedia.org/wiki/Eye_movement_in_reading#Saccades&quot;&gt;saccades and fixations&lt;/a&gt;. On each fixation (which can span a quarter of a second), they are only seeing a small portion of text in focus:&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img class=&quot;border&quot; width=&quot;640&quot; src=&quot;https://fvsch.com/articles/body-copy-sizes/fixation-simulation-1.png&quot; alt=&quot;Visual simulation of an eye fixation on medium sized text&quot;&gt;
  &lt;figcaption&gt;Look, I’m no scientist, so I’m hoping that’s actually representative of the real thing but take it with a spoonful of salt okay?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now if the same text was quite bigger, but the other parameters (like the eye–screen distance) didn’t change, I’m guessing the result would look like this:&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img class=&quot;border&quot; width=&quot;640&quot; src=&quot;https://fvsch.com/articles/body-copy-sizes/fixation-simulation-2.png&quot; alt=&quot;Visual simulation of an eye fixation on big sized text&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;With the focus area staying the same size, and bigger text, I suspect that the eye can discern fewer letters correctly on each fixation. This is why my hypothesis is that for really big text (like Resilient Web Design’s &lt;code&gt;250%&lt;/code&gt; body copy on wider screens), readers will need to use more eye fixations to read the same text, and might lose in reading speed and experience fatigue sooner. I don’t have the skills needed to test this hypothesis, but I’d be wary of the &lt;em&gt;very big text&lt;/em&gt; trend.&lt;/p&gt;
&lt;p&gt;Personally, I favor more limited tweaks in font size. I like starting with a &lt;code&gt;100%&lt;/code&gt; basis for small screens, bump it for large phones or tablets (say, &lt;code&gt;110%&lt;/code&gt; or &lt;code&gt;115%&lt;/code&gt;), and maybe go up to &lt;code&gt;125%&lt;/code&gt; on laptops and larger screens. Then I tweak those values depending on the font I’m using, the look I’m going for, and what I’ve seen in testing on a variety of devices.&lt;/p&gt;
&lt;p&gt;I’m also sad that we’re somehow chasing after device makers, operating system and browser developers, and trying to tweak font sizes every other year to adapt to what is out there in the market. The very concept of raising font size a bit depending on the screen width should raise eyebrows. Isn’t it the device’s job to make sure that &lt;code&gt;font-size: 100%&lt;/code&gt; is readable?&lt;/p&gt;
&lt;p&gt;In theory, CSS pixels should &lt;a href=&quot;https://www.w3.org/TR/css-values/#absolute-lengths&quot;&gt;match a “reference pixel” defined as an angle of vision&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;strong&gt;reference pixel&lt;/strong&gt; is the visual angle of one pixel on a device with a pixel density of 96dpi and a distance from the reader of an arm’s length. For a nominal arm’s length of 28 inches, the visual angle is therefore about 0.0213 degrees. For reading at arm’s length, 1px thus corresponds to about 0.26 mm (1/96 inch).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But this rule can only be followed if hardware makers, operating system and browser developers all collaborate towards that goal, which is rare enough. Especially since hardware vendors are more interested in selling screens optimized for video resolutions (“1080p”, “4K”), even when it makes the whole UI awkwardly small.&lt;/p&gt;
&lt;p&gt;In theory again, browser makers should be able to change the &lt;code&gt;16px&lt;/code&gt; default font size to adapt to modern devices. But too much existing content relies on this default size not ever changing.&lt;/p&gt;
&lt;p&gt;And so, we guess; we test; we tweak.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot;&gt;
&lt;p&gt;Whether technically correct or an approximation (my own calculations show a resolution of 68dpi), the “72dpi” resolution allowed designers to easily transpose point sizes to pixel sizes. Since there are 72 typographic points in an inch, at 72dpi each pixel is exactly one point.&amp;#160;&lt;a href=&quot;#fnref1:1&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot;&gt;
&lt;p&gt;Retina displays didn’t change the “system point per inch” resolution, instead each system point was mapped to a 2×2 square of &lt;em&gt;physical pixels&lt;/em&gt;. Since the CSS &lt;code&gt;px&lt;/code&gt; unit works similarly as system points on those devices, and doubling the physical pixel resolution did not impact the size of HTML text, I skipped talking about resolutions measured in physical pixels (e.g. 320ppi) altogether.&amp;#160;&lt;a href=&quot;#fnref1:2&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:3&quot;&gt;
&lt;p&gt;A Responsive Web Design technique that lets you transition smoothly between two property values when the screen gets smaller or bigger technique. First &lt;a href=&quot;https://madebymike.com.au/writing/precise-control-responsive-typography/&quot;&gt;implemented in CSS by Mike Riethmuller&lt;/a&gt;, and &lt;a href=&quot;https://blog.typekit.com/2016/08/17/flexible-typography-with-css-locks/&quot;&gt;developed further by Tim Brown&lt;/a&gt;. See &lt;a href=&quot;https://fvsch.com/css-locks&quot;&gt;The math of CSS locks&lt;/a&gt; for details.&amp;#160;&lt;a href=&quot;#fnref1:3&quot; rev=&quot;footnote&quot; class=&quot;footnote-backref&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
	</entry>
	<entry xml:lang="fr">
		<id>urn:uuid:1538d319-7e34-5613-a949-11fdbbab9f01</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/et-tu-cliques-a-cote-du-bouton"/>
		<title>Et tu cliques cliques cliques, à côté du bouton ♫</title>
		<author><name>Florens Verschelde</name></author>
		<published>2019-12-24T00:00:00+01:00</published>
		<updated>2019-12-24T00:00:00+01:00</updated>
		<summary type="text">Que ce soit à la souris ou sur un écran tactile, un pourcentage de nos clics tombera à côté de la cible, voire sur le bouton voisin. C’est un peu frustrant, mais n’est-ce pas une fatalité ? Regardons ça de plus près.</summary>
		<content type="html">&lt;p&gt;Que ce soit à la souris ou sur un écran tactile, un pourcentage de nos clics tombera à côté de la cible, voire sur le bouton voisin. C’est un peu frustrant, mais n’est-ce pas une fatalité ? Regardons ça de plus près.&lt;/p&gt;
&lt;p&gt;Par exemple, vous avez forcément croisé la fameuse pop-up d’inscription à une indispensable newsletter :&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-1.jpg&quot; alt=&quot;Fig. 1 Inscris-toi !!!&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Bien planquée dans le coin supérieur droit, une petite croix : le bouton de fermeture. Alors on se dépêche, vite on clique, et rien. Quoi ? Zoomons un peu pour voir ce qu’il se passe.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-2.jpg&quot; alt=&quot;Fig. 2 Seule l’icone est cliquable.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Dans sa grande bonté, le/la designer a dessiné une icone de 8 pixels de côté. Puis, lors du développement, l’intégrateur ou la développeuse ne s’est pas posé de question et a utilisé une image de 8 pixels de côté également, sans réserver un espace cliquable plus grand.&lt;/p&gt;
&lt;p&gt;Et maintenant, troisième étape de notre histoire, l’utilisateur·trice clique à côté. Alors on réessaye, parce qu’on n’a toujours pas l’intention de s’inscrire à cette newsletter. En se méfiant, après ce premier échec et un début de frustration.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;À la souris, il faut ralentir considérablement et amener le pointeur bien au dessus de l’icone, et surveiller si la flèche se transforme en main ou si un autre changement visuel signale que là, c’est bon, on peut y aller. Ensuite, on clique, mais attention : le mouvement ou la vibration du clic peuvent déplacer le pointeur de quelques pixels.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sur un écran tactile, on prendra son doigt le plus fin et approchera l’écran presque orthogonalement pour essayer de « cliquer » le plus précisément possible. Il est rare qu’un effet visuel vienne indiquer clairement quelle partie de la pulpe du doigt a réellement touché l’écran, et s’il y en avait un il serait de toute façon masqué par notre doigt. Alors il faudra peut-être trois, cinq ou 10 essais pour fermer cette satané pop-up.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Et ça, c’est dans dans des conditions favorables : sans handicap, sans tendinite, et sans tenter d’utiliser un site important en marchant dans la rue avec un smartphone à la main.&lt;/p&gt;
&lt;p&gt;Alors que tout ça aurait pu être beaucoup plus simple avec un bouton adapté :&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-3.jpg&quot; alt=&quot;Fig. 3 Bouton de fermeture de 40px de côté, avec une icone un peu plus visible de 12px&quot;&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Pourquoi c’est pas facile de cliquer un petit bouton ?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Et par opposition, pourquoi agrandir la zone cliquable d’un bouton faciliterait la vie des utilisateurs·trices ?&lt;/p&gt;
&lt;p&gt;C’est le moment « pour marcher il faut mettre un pied d’vant l’autre » de cet article, mais restez avec moi, ça va bien se passer.&lt;/p&gt;
&lt;p&gt;Première raison : pour utiliser un bouton, un lien ou tout élément interactif d’une interface, il faut déjà le trouver. Intuitivement, on comprend qu’un élément trop petit sera difficile à identifier dans une interface car peu visible.&lt;/p&gt;
&lt;p&gt;Vous connaissez l’expression « chercher une aiguille dans une botte de foin » ? À votre avis, si une botte de foin contient une aiguille, un parapluie et un lampadaire IKEA de 2 mètres, lequel de ces objets sera le plus simple à trouver en premier ? Et en deuxième ?&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-4.jpg&quot; alt=&quot;Fig. 4 Meule de foin dont dépassent une lampe et un parapluie.&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;Bien sûr la taille n’est pas le seul critère, d’autres rentrent en ligne de compte :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;La forme. Une forme conventionnelle sera identifiée plus vite et plus souvent. Par exemple : le manche recourbé d’un parapluie cane, le texte souligné d’un lien, l’aspect surélevé d’un bouton-poussoir.&lt;/li&gt;
&lt;li&gt;L’emplacement. La position dans la page (à un endroit attendu ou moins conventionnel), et la position par rapport à d’autres éléments (voir la loi de proximité dans la &lt;a href=&quot;https://fr.wikipedia.org/wiki/Psychologie_de_la_forme&quot;&gt;Psychologie de la forme&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Le contraste des éléments graphiques entre eux et avec leur environnement (&lt;a href=&quot;https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html&quot;&gt;&lt;cite lang=&quot;en&quot;&gt;Understanding Success Criterion 1.4.11: Non-text Contrast&lt;/cite&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ces critères participent à ce que le psychologue James J. Gibson a nommé &lt;a href=&quot;https://fr.wikipedia.org/wiki/Affordance&quot;&gt;l’affordance d’un objet&lt;/a&gt;, sujet très large et passionnant, mais que je vous propose de laisser de côté pour revenir à nos boutons.&lt;/p&gt;
&lt;p&gt;Mettons que vous ayez aperçu et identifié le bouton de fermeture de cette satanée pop-up. Deuxième étape : il faut maintenant l’activer. Pour ça, il faut l’atteindre, et ça c’est un vrai petit voyage :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Avec une souris ou un pavé tactile, il faut déplacer le pointeur entre sa position actuelle et la position de la cible.&lt;/li&gt;
&lt;li&gt;Sur un écran tactile, de smartphone par exemple, il faut déplacer un doigt vers la cible sans toucher l’écran, par exemple déplacer un pouce de sa position de repos vers le bouton.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Dans les deux cas, c’est un mouvement qui comprend une phase d’accélération et une phase de décélération. Et parfois, on s’arrête trop vite ou trop tard, un peu comme en voiture ou à vélo.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-5.jpg&quot; alt=&quot;&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;En ergonomie des interfaces humain-machine (IHM), on retrouve cette problématique dans la &lt;a href=&quot;https://fr.wikipedia.org/wiki/Loi_de_Fitts&quot;&gt;loi de Fitts&lt;/a&gt;. Dans les années 1950, en étudiant entre autres les cockpits d’avion de l’armée américaine, Paul Fitts estime que le temps moyen nécessaire pour atteindre physiquement une cible (par exemple un bouton ou un levier) dépend de deux paramètres : 1. la distance de la cible et 2. la taille de la cible.&lt;/p&gt;
&lt;p&gt;(Pour plus de détail, on pourra lire &lt;a href=&quot;https://ux.stackexchange.com/questions/22738/has-fitts-law-been-adapted-to-touch-screens/23258#23258&quot;&gt;cette explication très intéressante en anglais&lt;/a&gt; de la formule proposée par Fitts et son application aux interfaces informatiques.)&lt;/p&gt;
&lt;p&gt;En gros, les cibles éloignées et les petites cibles demandent plus de temps. Les petites cibles éloignées étant les plus exigeantes.&lt;/p&gt;
&lt;p&gt;Cela s’explique en partie par un phénomène connu dans l’expérience humaine et en sciences cognitives : le compromis entre la vitesse d’exécution et la précision (&lt;a href=&quot;https://www.frontiersin.org/articles/10.3389/fnins.2014.00150/full&quot;&gt;&lt;cite lang=&quot;en&quot;&gt;The speed-accuracy tradeoff: history, physiology, methodology, and behavior&lt;/cite&gt;&lt;/a&gt;). Par exemple, si je veux appuyer rapidement sur un bouton sur un écran tactile, je ne vais pas prendre le temps de regarder le mouvement de mon doigt pour obtenir une information qui me permet de corriger ou optimiser mon mouvement : je risque donc de perdre en précision.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/zoneclic-intro-6.jpg&quot; alt=&quot;&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;En tant que designers et développeurs·euses d’interfaces, nous pouvons faire deux suppositions :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Nos utilisateurs et utilisatrices seront souvent pressées ou distraites, et ne manipuleront pas nos interfaces délicatement et patiemment comme s’il s’agissait de fragiles nouveaux-nés.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En cas d’« erreur » de manipulation, par exemple un clic enregistré à côté d’un bouton voire sur le bouton voisin, nos utilisateurs et utilisatrices ne comprendront pas les raisons techniques et ergonomiques précises de l’erreur, et pourront percevoir l’interface comme buguée (ça m’a emmené ailleurs que prévu, j’ai dû cliquer plusieurs fois pour que ça marche, etc.), lente (j’ai dû cliquer plusieurs fois avant que ça ne finisse par réagir, ça « lague »), et plus généralement comme frustrante voire hostile.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En conséquence, notre boulot est de réduire autant que possible le nombre d’erreurs, et pour ça la loi de Fitts nous apprend que nous pouvons jouer sur la distance et sur la taille des éléments interactifs.&lt;/p&gt;
&lt;p&gt;Petit problème : pour la distance, c’est compliqué. Elle est rarement fixe et prévisible, nous ne connaissons pas l’état du défilement de la page, la position de départ du curseur, etc. Sur smartphone, il existe des usages courants comme l’utilisation du pouce et une position de repos vers le bas de l’écran, mais même certaines distances courtes peuvent être difficiles d’accès (car il faut tordre la main ou le poignet). Donc pragmatiquement, on peut considérer qu’on a toujours une chance importante de se retrouver dans un cas difficile.&lt;/p&gt;
&lt;p&gt;Il nous reste donc une variable majeure pour faciliter la vie des utilisatrices et utilisateurs : &lt;strong&gt;agrandissons-donc ces fichus boutons !&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Okay mais tout le monde sait ça hein, c’est la base !&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Malheureusement cette question n’est pas si connue que ça, car nous tombons tous et toutes quotidiennement sur des interfaces qui ne respectent pas ce principe. Et pas seulement sur des sites perso ou amateurs. Voici quelques exemples.&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;360&quot; alt=&quot;Capture d’écran du site Inc.com avec &quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/exemple-inc.png&quot;&gt;
  &lt;figcaption&gt;
    Sur Inc. (publication de Mansueto Ventures qui publie également le magazine Fast Company), les boutons de l’en-tête ont une taille cliquable minuscule (deuxième ligne, en vert). Ma proposition de correction (troisième ligne, en rose) utilise une taille minimum de 36 × 36 pixels. Idéalement il faudrait prévoir plus grand encore, et pour cela mieux répartir les boutons entre la gauche et la droite.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Mais peut-être que du côté de la presse en ligne on ne sait pas coder, alors que chez les vrais développeurs si, on sait ? Faut voir, parce que chez GitHub ce n’est pas ça non plus :&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;360&quot; alt=&quot;Capture d’écran du menu mobile de github.com&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/exemple-github.png&quot;&gt;
  &lt;figcaption&gt;
    Les boutons de l’en-tête se limitent à la surface de l’icone, et cliquer à côté est excessivement aisé. La hauteur du champ de recherche pourrait être augmentée, et principaux liens du menu sont plutôt corrects sauf le lien “Marketplace” au milieu de la liste. Enfin, le texte des liens ou l’icone de profil touche le bord gauche du bouton, ce qui reste risqué : un peu de padding ne ferait pas de mal.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Peut-être que ça se passe mieux dans les entreprises spécialisées en design ? Hmm, pas toujours. On ne va pas prendre d’exemple sur le site d’Adobe, ce serait cruel. Mais si on regarde du côté d’Invision, il y a du bon et du moins bon :&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;1106&quot; alt=&quot;Capture d’écran de la navigation principale sur le site d’Invision&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/exemple-invision.png&quot;&gt;
  &lt;figcaption&gt;
    Chez Invision, lorsqu’un bouton est visuellement grand, sa zone cliquable est grande également (logique…). Par contre dans l’en-tête, la plupart des liens se limitent à la surface du texte. Mention spéciale pour le petit triangle à côté de “Design Community” : il suffit de cliquer un pixel plus à droite pour cliquer dans le vide.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Un autre problème courant, qu’on peut voir dans les colonnes du pied de page, c’est l’utilisation de &lt;code&gt;line-height&lt;/code&gt; pour espacer les éléments d’un menu. Un &lt;code&gt;line-height&lt;/code&gt; important donne une bonne hauteur cliquable, mais dès que le texte passe sur deux ligne c’est la catastrophe. À votre avis, dans les cinq lignes de texte suivantes, combien y a-t-il de liens ?&lt;/p&gt;
&lt;blockquote&gt;
  Invision For&lt;br&gt;&lt;br&gt;
  Startups&lt;br&gt;&lt;br&gt;
  Pricing&lt;br&gt;&lt;br&gt;
  Students &amp;amp;&lt;br&gt;&lt;br&gt;
  Teachers
&lt;/blockquote&gt;
&lt;p&gt;Mon conseil : combinez un &lt;code&gt;line-height&lt;/code&gt; raisonnable (par exemple &lt;code&gt;line-height: 1.5&lt;/code&gt;) et du padding pour obtenir une hauteur de bouton correcte, et testez avec du texte sur plusieurs lignes. Et si ça « casse » le design, parlez-en aux designers !&lt;/p&gt;
&lt;p&gt;Autre écueil : attention à ne pas suggérer visuellement une surface cliquable, avec des couleurs de fond, des bordures, des décorations visuelles ou des effets au survol… tout en implémentant une zone cliquable plus petite. Vous invitez vos utilisateurs·trices à cliquer dans le vide !&lt;/p&gt;
&lt;figure&gt;
  &lt;img width=&quot;620&quot; alt=&quot;Capture d’écran de la navigation principale sur le site de Google Design&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/exemple-google-design.png&quot;&gt;
  &lt;figcaption&gt;
    Sur la page d’accueil de Google Design, les catégories principales montrent une bordure au survol, qui suggère une zone cliquable très large. Malheureusement, ce n’est pas le cas, et les liens ont une surface beaucoup plus réduite.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Les tailles de bouton recommandées pour les interfaces tactiles&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Une taille de bouton suffisante, c’est quoi au juste ? Ça va dépendre en partie du public (certaines interfaces « expertes » ont une forte densité d’information et des contrôles plus petits que la moyenne), mais aussi du support.&lt;/p&gt;
&lt;p&gt;Sur mobile, on peut suivre les recommandations d’Apple pour iOS et de Google pour Android, qui sont assez proches.&lt;/p&gt;
&lt;figure&gt;
  &lt;img alt=&quot;Schéma représentant une barre d’outils d’application mobile avec trois boutons larges et espacés, et un contre-exemple avec 9 boutons plus petits et serrés&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/guidelines-ios-button-size.png&quot;&gt;
  &lt;figcaption&gt;
    Les &lt;cite lang=&quot;en&quot;&gt;Human Interface Guidelines&lt;/cite&gt; d’iOS recommandent une zone réactive de 44 pixels de côté minimum, et déconseillent les boutons trop rapprochés. Source : &lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/#general-layout-considerations&quot;&gt;General Layout Considerations&lt;/a&gt;, © Apple.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
  &lt;img alt=&quot;Schéma représentant deux icones-bouton et un bouton textuel. Dans les trois cas la zone cliquable dessinée en surimpression est plus large que l’icone ou que la bordure du bouton.&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/guidelines-material-touch-button-size.png&quot;&gt;
  &lt;figcaption&gt;
    Les recommandations &lt;cite lang=&quot;en&quot;&gt;Material Foundation&lt;/cite&gt; de Google spécifient des zones réactives de 48 pixels de côté minimum, avec au moins 8 pixels entre les boutons. Source : &lt;a href=&quot;https://material.io/design/layout/spacing-methods.html#touch-targets&quot;&gt;Material Design: Spacing methods&lt;/a&gt;, © Google.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Un exemple intéressant dans cette dernière illustration : en (3), le bouton utilise un style standard de Material Design avec une hauteur de 36 pixels. Mais la zone tactile doit toujours faire au moins 48 pixels de haut, alors que faire ?&lt;/p&gt;
&lt;p&gt;Sur certaines plateformes, on peut rendre la zone tactile d’un élément plus grande que l’élément lui-même. Par exemple, avec React Native on pourra utiliser la propriété &lt;code&gt;hitSlop&lt;/code&gt; de certains composants.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;lt;Button hitSlop={{ top: 6, bottom: 6, left: 0, right: 0 }}&amp;gt;&amp;lt;/Button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sur le Web, c’est plus compliqué. Deux solutions :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Utiliser un élément &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; ou &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; transparent avec du padding, et placer un &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; stylé comme un bouton à l’intérieur.&lt;/li&gt;
&lt;li&gt;Utiliser un pseudo-élément positionné en absolu pour élargir la zone cliquable.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.my-button {
  position: relative;
}

.my-button::after {
  content: &quot;&quot;;
  position: absolute;
  top: -6px;
  bottom: -6px;
  left: 0;
  right: 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apparté sur les unités de mesure : iOS utilise des « points système », notés &lt;code&gt;pt&lt;/code&gt; ; Android des “device points” notés &lt;code&gt;dp&lt;/code&gt; ; et sur le Web on travaille en « pixels CSS », notés &lt;code&gt;px&lt;/code&gt;. Ces trois unités sont équivalentes dans la plupart des cas, et peuvent correspondre à des quantités variables de pixels physiques (suivant la densité de l’écran et la configuration du système d’exploitation).&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Les tailles de bouton pour les interfaces à la souris&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sur des interfaces qui ont été conçues principalement pour la souris, les boutons sont généralement plus petits. Par exemple sur macOS, les boutons des barres d’outils font 22 pixels de haut, et les &lt;cite lang=&quot;en&quot;&gt;Human Interface Guidelines&lt;/a&gt; pour macOS &lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/macos/buttons/push-buttons/&quot;&gt;ne recommandent pas de taille minimum&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
  &lt;img alt=&quot;Schéma représentant deux designs comportant des icones d’environ 20 pixels de côté, avec dessiné en surimpression une zone cliquable de 24 pixels de côté.&quot; src=&quot;https://fvsch.com/articles/et-tu-cliques-a-cote-du-bouton/guidelines-material-click-button-size.png&quot;&gt;
  &lt;figcaption&gt;
    Material Design &lt;del&gt;recommande&lt;/del&gt; &lt;ins&gt;recommandait encore récemment&lt;/ins&gt; une taille minimum de 24 pixels pour les boutons dans les interfaces à la souris. On notera que dans ces deux exemples on pourrait facilement agrandir les zones cliquables sans modifier le design visuel. Source : &lt;a href=&quot;https://material.io/design/layout/spacing-methods.html#touch-targets&quot;&gt;Material Design: Spacing methods&lt;/a&gt;, © Google.
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Personnellement, je trouve la recommandation de Material Design un peu faible, et préfère utiliser des zones cliquables de 30, 40 pixels ou plus. De plus, il devient compliqué de savoir si une interface sera utilisée à la souris, sur un écran tactile (en particulier avec les grandes tablettes et les ordinateurs portables tactiles), pour ne parler que des cas les plus courants ! Donc la pertinence de recommandations pour les interfaces à la souris est, à mon sens, de plus en plus contestable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mise à jour :&lt;/strong&gt; les designers de Google semblent d’accord, et viennent de mettre à jour la documentation de Material Design pile au moment où nous publions cet article ! La nouvelle recommandation, à la page « Accessibilité », suggère &lt;a href=&quot;https://material.io/design/usability/accessibility.html#layout-typography&quot;&gt;des zones cliquables de 44 pixels de côté minimum&lt;/a&gt; pour les interfaces à la souris ou au stylet.&lt;/p&gt;
&lt;p&gt;Même à la souris et sans handicap ou difficulté motrice, une cible de 24 pixels peut être difficile à atteindre, par exemple si on est un peu pressé.&lt;/p&gt;
&lt;p&gt;Pour l’illustrer, voici un petit jeu de précision. Le but de ce jeu est de cliquer une vingtaine de cibles carrées de 24 pixels, toutes affichées exactement une seconde. Sachant que le temps de réaction humain moyen est de 250 millisecondes, cela laisse 750 millisecondes pour déplacer le pointeur et cliquer.&lt;/p&gt;
&lt;p&gt;Je vous propose une première partie en gardant les paramètres par défaut, et on se retrouve juste après. Prêt·e ?&lt;/p&gt;
&lt;div id=&quot;click-precision-game&quot;&gt;&lt;/div&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://click-precision-game.netlify.com/build/bundle.css&quot;&gt;
&lt;script defer src=&quot;https://click-precision-game.netlify.com/build/bundle.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;Alors, comment ça s’est passé ? Si vous avez obtenu un score de moins de 90, comment pouvez-vous l’améliorer (donc diminuer le taux d’erreurs) ? Vous avez trois leviers :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Augmenter la taille de la cible. Ne serait-ce qu’en montant à 32 pixels, la différence devrait être sensible.&lt;/li&gt;
&lt;li&gt;Diminuer la taille du conteneur, et donc les distances à parcourir.&lt;/li&gt;
&lt;li&gt;Augmenter le temps de jeu, pour être moins pressé·e.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;N’hésitez pas à faire quelques essais. Et si jamais vous en tiriez quelques enseignements pour les interfaces que vous designez ou développez, ce serait bien sûr parfaitement fortuit. Bonnes parties, et joyeux Noël !&lt;/p&gt;
&lt;p&gt;P.S.&amp;nbsp;: le code du jeu &lt;a href=&quot;https://github.com/fvsch/click-precision-game&quot;&gt;est open-source&lt;/a&gt; et on peut le retrouver &lt;a href=&quot;https://click-precision-game.netlify.com/&quot;&gt;sur cette page&lt;/a&gt;.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="fr">
		<id>urn:uuid:0f96bcf5-5174-5dd8-9ab8-eed88a3bd7d3</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/interview-alsacreations"/>
		<title>Interview sur Alsacréations</title>
		<author><name>Florens Verschelde</name></author>
		<published>2019-12-03T00:00:00+01:00</published>
		<updated>2019-12-03T00:00:00+01:00</updated>
		<summary type="text">J’ai donné une petite interview pour le site alsacreations.com, un site communautaire autour des technologies web.</summary>
		<content type="html">&lt;p&gt;J’ai donné une petite interview pour le site &lt;a href=&quot;https://www.alsacreations.com&quot;&gt;alsacreations.com&lt;/a&gt;, un site communautaire autour des technologies web créé par &lt;a href=&quot;https://goetter.fr/&quot;&gt;Raphaël Goetter&lt;/a&gt; et &lt;a href=&quot;https://www.blup.fr/&quot;&gt;Rodolphe Rimelé&lt;/a&gt; et auquel j’ai beaucoup participé il y a une dizaine d’années.&lt;/p&gt;
&lt;p&gt;Il y a deux questions qui m’ont particulièrement intéressé, et je me permets de recopier mes réponses ici pour mémoire.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Alsacréations: Comment envisages-tu le futur de CSS ou de l’intégration en général ? (on pense notamment à CSS-in-JS)&lt;/h3&gt;
&lt;p&gt;Pour les CSS dans le JavaScript, c’est plus un symptôme de ce qu’il se passe depuis quelques années. Je n’ai rien contre ces outils, moi ça ne me gêne pas d’utiliser &lt;a href=&quot;https://www.styled-components.com/&quot;&gt;styled-components&lt;/a&gt; ou &lt;a href=&quot;https://css-blocks.com/&quot;&gt;CSS Blocks&lt;/a&gt;, pas plus que Sass ou d’autres. Mais ça fait partie d’une tendance générale à empiler des couches techniques les unes par dessus les autres, qui correspond mieux aux schémas mentaux des développeurs·euses «classiques» que des intégrateurs·trices.&lt;/p&gt;
&lt;p&gt;On se retrouve avec des projets où pour participer sur les styles ou l’accessibilité il faut pouvoir utiliser Git mais aussi Docker, maitriser JavaScript et React, configurer webpack, et écrire les styles dans une syntaxe JavaScript ou en pseudo-CSS. C’est quand même beaucoup d’obstacles pour des gens qui connaissent peu ces outils mais ont une expertise sur le HTML et l’accessibilité, le CSS, les navigateurs web, etc. Je parle régulièrement avec des gens qui faisaient de l’intégration web principalement ou en plus d’autres missions, et qui se sont rabattus sur du design ou de la gestion de projet car l’environnement technique devenait trop «pour et par les devs».&lt;/p&gt;
&lt;p&gt;Je ne sais pas comment ça va évoluer, mais en tout cas je m’intéresse à tout ce qui tire un peu dans le sens inverse : permettre à des designers et/ou intégrateurs·trices de produire des choses sans être bloqué par plein de couches techniques. Peut-être une simplification des frameworks JS pour que le JS s’efface autant que possible, comme dans &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt; ? Est-ce qu’il faut forcément coder, ou bien est-ce que la manipulation directe comme dans &lt;a href=&quot;https://webflow.com/&quot;&gt;Webflow&lt;/a&gt; ne fonctionnerait pas mieux au final ? Est-ce qu’on peut mélanger code et manipulation directe, pour bosser ensemble avec plusieurs profils ?&lt;/p&gt;
&lt;h3&gt;Alsacréations: Si tu avais à former un·e débutant·e en HTML et CSS, sur quels thèmes insisterais-tu particulièrement ?&lt;/h3&gt;
&lt;p&gt;Pour moi le plus important serait de rattacher HTML et CSS à un objectif plus large. Quand j’ai fait de la formation il y a 10 ans, j’avais une approche très linéaire, HTML et sémantique d’abord puis CSS ensuite, etc. Avec le recul je trouve ça un peu formel et pas très motivant.&lt;/p&gt;
&lt;p&gt;Avec des débutant·e·s en développement web voire en développement tout court, j’insisterais sur l’idée de créer quelque chose. Dans ce cadre, HTML et CSS ne sont que deux outils sur cinq ou 6. On peut bosser sur plein de sujets : de l’animation, des ébauches d’application multimédia (par exemple une application pour générer des sons ou de la musique), une galerie de photos, mettre en page un poème ou une tablature de guitare, etc. L’important serait de publier le résultat (par exemple sur GitHub Pages, Netlify, ou en codant directement sur Glitch) et le partager à quelques personnes : voilà ce que j’ai fait.&lt;/p&gt;
&lt;p&gt;Avec un public de développeurs·euses qui a déjà une formation initiale en développement “backend” mais très peu d’expérience en UI, le risque principal c’est le découragement car CSS est un domaine à part. Les compétences déjà acquises en Java, PHP, Python ou SQL ne se transfèrent pas vraiment à l’apprentissage de CSS (&lt;a href=&quot;https://www.youtube.com/watch?v=aHUtMbJw8iA&quot;&gt;Miriam Suzanne, Why Is CSS So Weird?&lt;/a&gt;). Et si on n’a pas anticipé ce problème, on peut se retrouver en pleine dissonance cognitive (&lt;a href=&quot;https://vimeo.com/channels/beyondtellerrand/373397621&quot;&gt;Natalya Shelburne, CSS at the Intersection&lt;/a&gt;) et abandonner.&lt;/p&gt;
&lt;p&gt;Du coup, s’il faut se familiariser avec un nouveau domaine et une culture de design et d’ergonomie, pourquoi ne pas enseigner directement ces concepts de design, en utilisant CSS comme un exercice d’application ? Et enseigner directement des bases d’accessibilité, en utilisant HTML pour la mise en application ? En somme, viser un résultat plutôt qu’enseigner un langage.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:3c3acfaf-912e-5382-af1c-17719a902650</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/static-site-generators"/>
		<title>Static site generators</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-10-07T00:00:00+02:00</published>
		<updated>2018-10-07T00:00:00+02:00</updated>
		<summary type="text">I’ve been looking for a decent static site generator to build a simple, 10-page-or-so documentation site, and I’m failing. Here are some notes on my journey, to serve as a warning sign to future travellers, and thoughts on what static site generators could do better.</summary>
		<content type="html">&lt;p&gt;I’ve been looking for a decent static site generator to build a simple, 10-page-or-so documentation site, and I’m failing. Here are some notes on my journey, to serve as a warning sign to future travellers, and thoughts on what static site generators could do better.&lt;/p&gt;
&lt;article-nav&gt;&lt;/article-nav&gt;
&lt;h2&gt;&lt;span&gt;What is a static site generator, really?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’d like to try and describe static site generators as if we hadn’t encountered this class of software yet. Please do not skip this section, even if you already (think you) know the answer.&lt;/p&gt;
&lt;p&gt;Static site generators (SSG) are a class of software programs that help with creating websites. They’re often written by developers — professionals or hobbyists — to power their own small website or blog, and as such they tend to have a limited feature set.&lt;/p&gt;
&lt;p&gt;SSG are close parents to &lt;em&gt;scripts&lt;/em&gt;: short programs (a few hundred lines of code), which can be used from a command prompt to achieve specific tasks. They’re often built as a hobby by one developer, sometimes getting help from a few others months or years later. They almost never offer a graphical user interface.&lt;/p&gt;
&lt;p&gt;One core principle of those scripts is that they work with a source collection of plain text files (aka “content”), process those files to transform them to the HTML format, and wrap them in more HTML. This is often called a “build” step. Before running the build command, you would have a file tree that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;My Cool Website
└── content
    ├── 2018-09-07-cool-blog-post.txt
    └── 2018-10-01-other-blog-post.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And after typing some text to run a command in a “terminal” or “command prompt” application, you would get something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;My Cool Website
├── content
│   ├── 2018-09-07-cool-blog-post.txt
│   └── 2018-10-01-other-blog-post.txt
└── output
    ├── cool-blog-post.html
    ├── index.html
    ├── other-blog-post.html
    └── rss.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that setup, you get a simple blog with one HTML page for each blog post, an index (or home) page that lists published posts, and a RSS feed. You can write your own styles if you know CSS, or — with some SSG, but not all — use a “theme” or an example project as a starting point.&lt;/p&gt;
&lt;p&gt;Users of static site generators do this work on their own computer, and after the “output” folder has been generated they are expected to know how to put those files online to publish them. This can require software such as a FTP client, or more complex setups involving one or two additional command line tools.&lt;/p&gt;
&lt;p&gt;There are hundreds of static site generators, with many &lt;a href=&quot;https://www.staticgen.com/&quot;&gt;listed on StaticGen.com&lt;/a&gt;. Some of the most popular ones are &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt;, &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt; and &lt;a href=&quot;https://hexo.io/&quot;&gt;Hexo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When it comes to editing features, there are not many to speak of. Users are expected to know what formats like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics&quot;&gt;HTML&lt;/a&gt; and &lt;a href=&quot;https://commonmark.org/help/&quot;&gt;Markdown&lt;/a&gt; are, how to prepare images themselves (in Photoshop, Gimp or similar software), how to insert images inside a page’s content by using code that may look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;![My cat at 4 months](./sniffles-4months.jpg)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or even something more complex:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;![My cat at 4 months]({{&amp;lt; imgproc sniffles-4months.jpg Resize &quot;600x&quot; /&amp;gt;}})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point we must recognize that static site generators are tools made by developers &lt;em&gt;for developers&lt;/em&gt;. They require a ton of previous knowledge: HTML, Markdown, command line interfaces, FTP or some other way to publish HTML files online, and of course you would need some kind of web hosting to put those files. Users will also need to dive into technical documentation for their tool of choice when they want to customize their site’s pages, work in “templates”, learn a templating language, and probably write some CSS.&lt;/p&gt;
&lt;p&gt;The most popular static site generator, Jekyll, may have the best documentation pages of the lot. And yet, it still puts a bunch of technical language front-and-center, on the project’s home page:&lt;/p&gt;
&lt;figure&gt;
  &lt;img class=&quot;border&quot; src=&quot;https://fvsch.com/articles/static-site-generators/jekyll-quick-start.png&quot; width=&quot;560&quot; alt=&quot;A series of 4 terminal commands, starting with: gem install bundler jekyll…&quot;&gt;
  &lt;figcaption&gt;Jekyll’s core message, “Get up and running in seconds”&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is developer-talk, &lt;em&gt;because static site generators are tools made by developers for themselves and their peers&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;On the other hand, “content management” software such as WordPress, Drupal or &lt;a href=&quot;https://getkirby.com/&quot;&gt;my personal favorite, Kirby&lt;/a&gt;, target a dual audience: content editors (who write and manage content), and developers (who set up the CMS and implement the site’s design and features).&lt;/p&gt;
&lt;p&gt;I’m making this point because, if we own up to who our software’s audience is, then we can consider what we can do for that audience and hopefully design less terrible software.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;A developer’s idea of elegance&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I sometimes talk with developers who praise static site generators as &lt;em&gt;simple&lt;/em&gt;, which is hogwash. I’ve seen a few projects where content editors were handed off a site based on a SSG (especially in startups where devs may dominate the conversation), and could not manage any change without a lot of training and assistance. Nothing simple about that.&lt;/p&gt;
&lt;p&gt;What those developers probably mean is that they find static site generators &lt;em&gt;elegant&lt;/em&gt;. A programmer thinks that a solution is elegant when it uses their current knowledge to achieve something with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;few steps,&lt;/li&gt;
&lt;li&gt;no repetition, and&lt;/li&gt;
&lt;li&gt;not too much indirection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Static site generators are an elegant solution for developers because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It rests on our previous knowledge of: file and code formats, the command line, the plain-files-and-scripts philosophy, &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;ssh&lt;/code&gt; or &lt;code&gt;scp&lt;/code&gt; and other deployment strategies, etc.&lt;/li&gt;
&lt;li&gt;Using this base knowledge, it offers solutions to boring, repetitive tasks. For example, Markdown syntax lets us avoid repeating &lt;code&gt;&amp;lt;p&amp;gt;…&amp;lt;/p&amp;gt;&lt;/code&gt; tags in our content; templates let us avoid tasks like manually repeating HTML structure and updating menus and content lists.&lt;/li&gt;
&lt;li&gt;Finally, using the filesystem to store content lets us work in a single place (avoiding database software and languages, which would be overkill for limited datasets).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m all for elegant solutions that fit my mindset (aka just “elegant solutions”), and I’ve used static site generators in the past, including Pelican, Hyde, Jekyll and Metalsmith.&lt;/p&gt;
&lt;p&gt;Here’s the thing, though: it’s been a terrible experience. It turns out that &lt;strong&gt;static site generators are terrible at handling content&lt;/strong&gt;. Which is too bad, because that’s one of their very few features to begin with.&lt;/p&gt;
&lt;p&gt;Static site generators are elegant on principle, but are not designed to deal with content more complex than a handful of pages and a list of blog posts. And I’m not talking about the speed of building hundreds of pages or more (Jekyll is notoriously slow, Hugo is fast), but the sheer ability to &lt;em&gt;use content however you want&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Static site generators are bad at content&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let’s back up a bit. Most static site generators were built — or at least are advertised — as alternatives to database-driven CMS software. We’ve already shown that they may only be a valid alternative for developers and technical users with a lot of prior knowledge. But they are missing another core capability of CMS software.&lt;/p&gt;
&lt;p&gt;In your average PHP or Python CMS, as in many Web frameworks, querying content from a relational database is always possible (and more or less straightforward). Linked objects and/or hierarchical content structures are built in features. Building home pages, landing pages, lists, modular pages, product pages with many sections, etc., out of content hierarchies or relations is standard practice; CMS software that was too limited to do that either evolved or was replaced by their competition.&lt;/p&gt;
&lt;p&gt;When I’ve used static site generators in the past ten years, there were a few pain points like lacking documentation and strange and incompatible conventions. But the nail in the coffin was always that it’s either impossible or way too hard to &lt;strong&gt;build a single page from several pieces of content&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Recently, I’ve looked at static site generators again, because I wanted to convert Kaliop’s &lt;a href=&quot;https://kaliop.github.io/frontend-style-guide/2.0/&quot;&gt;Frontend Style Guide&lt;/a&gt;, originally built with Kirby CMS, to something that could be built to static HTML in fewer steps &lt;em&gt;by developers&lt;/em&gt; (e.g. &lt;code&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This site is made of a handful of long “chapter” pages, with a top-level table of contents which lists all chapters and their sections:&lt;/p&gt;
&lt;figure&gt;
  &lt;img class=&quot;border&quot; alt=&quot;Screenshot showing a full table of contents with 3 levels of titles&quot; src=&quot;https://fvsch.com/articles/static-site-generators/style-guide.png&quot; srcset=&quot;https://fvsch.com/articles/static-site-generators/style-guide.png 1x,https://fvsch.com/articles/static-site-generators/style-guide@2x.png 2x&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;For easier &lt;em&gt;content management&lt;/em&gt; and reordering of sections, I had broken each chapter into different section files, so that the index page is generated from three levels of content. Looking at the “General Rules” branch only, the content hierarchy looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;2.0
└─ general
   ├─ 1-readme
   ├─ 2-editorconfig
   ├─ 3-dry
   └─ 4-clean&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When generating the full table of contents, I expect to be able to write (in pseudo-code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;for chapter in page.children:
    print chapter.title
    for section in chapter.children:
        print section.title&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, when generating a chapter page, I expect that I can easily print the full chapter content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;for section in page.children:
    print section.title
    print section.content&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actual templates would be a bit more complex, since we need to wrap this content in HTML tags, and maybe pass it to template partials or components. But retrieving content from a hierarchy of local files should be straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I haven’t found a single static site generator that handles this use case gracefully.&lt;/strong&gt; Instead, after reviewing the documentation of three dozens tools (listed on &lt;a href=&quot;https://www.staticgen.com/&quot;&gt;StaticGen&lt;/a&gt;), and actively trying to build my use case with a handful of them, I’ve seen a range of limitations and awkward workarounds:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sometimes it’s just not possible to work with content hierarchies and partial content. (Most JavaScript static site generators fall into that category.)&lt;/li&gt;
&lt;li&gt;Or you have to write some configuration code that populates “Collections” ahead of build time. (Too indirect, and breaks if you change your information architecture a bit. We need something a bit closer to direct manipulation!)&lt;/li&gt;
&lt;li&gt;Finally, a short list of tools allow listing a page’s children or “resources”, but have bugs when dealing with more than one level (&lt;a href=&quot;https://www.getgutenberg.io/&quot;&gt;Gutenberg&lt;/a&gt;) or impose arbitrary restrictions on what page “types” can list different types of content (&lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you &lt;em&gt;can&lt;/em&gt; list content from children, grandchildren or other locations, it’s often not possible to &lt;em&gt;process&lt;/em&gt; this content as Markdown or access its metadata.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;A few words of warning about Hugo&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt; static site generator, praised by many for its speed, is more capable than most. There are surely many good things to say about Hugo, but in the spirit of warning folks about the terribleness of software: I wouldn’t recommend it for anyone who wants a smooth learning experience.&lt;/p&gt;
&lt;p&gt;For one thing, it requires learning about way too many specific concepts: Sections, Lists, Taxonomies, Page Bundles, Leaf Bundles, Template lookup order, and the perfectly unintuitive differences between a &lt;code&gt;index.md&lt;/code&gt; page and a &lt;code&gt;_index.md&lt;/code&gt; page. (Or are those Sections, or Bundles? Is one a Section and the other a Page? Are Sections and Bundles “pages” too? I have no idea.)&lt;/p&gt;
&lt;p&gt;I particularly dislike the way Hugo &lt;a href=&quot;https://gohugo.io/templates/lookup-order/&quot;&gt;matches a page to a dozen possible template files&lt;/a&gt; or more — taking a page from Drupal’s book of idiosyncratic awfulness. Now, I understand that this is meant to make it possible to use different “themes”, and at worst your pages will still render with the theme’s &lt;code&gt;layouts/_default/list.html&lt;/code&gt; or &lt;code&gt;layouts/_default/single.html&lt;/code&gt; templates. But if you want to write your own HTML and CSS, the template lookup stuff is a nuisance (much like it is in the Drupal world when you want any kind of control over the HTML output).&lt;/p&gt;
&lt;p&gt;On top of the complexity of Hugo’s design, its documentation is often subpar. For instance, the &lt;a href=&quot;https://gohugo.io/content-management/page-resources/&quot;&gt;“Content Management &amp;gt; Page Resources” page&lt;/a&gt; does not explain much about, well, content, or content management. I expected a description showing content examples (some markdown or a file structure maybe), then information about how to use it in templates and other contexts. Instead, it’s a kind of API documentation listing “Properties” and “Methods”.&lt;/p&gt;
&lt;p&gt;The second issue is that while Hugo builds pages &lt;em&gt;fast&lt;/em&gt;, it often &lt;em&gt;builds the wrong thing fast&lt;/em&gt;. For instance, if I rename a &lt;code&gt;index.md&lt;/code&gt; file to &lt;code&gt;_index.md&lt;/code&gt;, I need to run the &lt;code&gt;hugo&lt;/code&gt; command two or three times to get it to render with the correct template. Using &lt;code&gt;hugo server&lt;/code&gt; seems even less reliable.&lt;/p&gt;
&lt;p&gt;Hugo is also unhelpful when a source Markdown file doesn’t render anything (there is no log or warning) or outputs a cryptic &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;/pre&amp;gt;&lt;/code&gt;. This doesn’t help the learning curve.&lt;/p&gt;
&lt;p&gt;That being said, I heard that Hugo is an interesting choice if you’re building hundreds or thousands of pages (because it builds these pages really quickly), in which case it might be worth spending a couple weeks learning to use it.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;What could we do differently?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There are thousands of static site generators out there (counting a bunch of scripts with 5 stars on GitHub) because thousands of developers looked at existing solutions and said “eh, I’ll just build my own”.&lt;/p&gt;
&lt;p&gt;As a thought experiment, here are the core topics I would look at if I wanted to build my own tool from scratch.&lt;/p&gt;
&lt;h3&gt;1. Content naming conventions&lt;/h3&gt;
&lt;p&gt;Conventions for naming content files are necessary to allow users to create content files quickly and get predictable results. Those conventions must be short, clearly defined, clearly explained (you need great docs here), and intuitive (when possible, follow existing conventions rather than roll your own).&lt;/p&gt;
&lt;p&gt;Major issues to solve regarding source content:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What will translate to a web page?&lt;/li&gt;
&lt;li&gt;What will be ignored?&lt;/li&gt;
&lt;li&gt;How may users control the transformations that are applied to content (processing of Markdown or other formats, templates, image transformations)? Should this be defined in the content or elsewhere?&lt;/li&gt;
&lt;li&gt;How do you generate alternative views for the same page, e.g. a HTML page and a RSS or Atom feed?&lt;/li&gt;
&lt;li&gt;How can you generate category and tag pages? More generally, how do you generate any limited set of pages based on querying information from other content, without having to manually create a new (probably empty) content file?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of those concerns can be addressed in templates or in configuration or other code, but there’s value in using elegant conventions to let users directly manipulate or tweak content to get a desired result.&lt;/p&gt;
&lt;h3&gt;2. Give feedback on content-to-output mapping&lt;/h3&gt;
&lt;p&gt;The generator should provide, by default, feedback on which files are picked up, which are ignored, and why. When a file is picked up and processed in some way, it should say so too.&lt;/p&gt;
&lt;p&gt;To surface this information efficiently, the generator may need to come with a GUI (a web UI could work). I would be looking at tools like &lt;a href=&quot;https://fractal.build/&quot;&gt;Fractal&lt;/a&gt; for inspiration.&lt;/p&gt;
&lt;h3&gt;3. More code should happen in templates&lt;/h3&gt;
&lt;p&gt;Most generators use restrictive template engines, and feed a restricted context (aka a set of data and functions) to those templates. This seems to come from a misguided need to keep templates “simple”, “logic-less”, and other kinds of baloney. Look, in a big MVC application built by people with different roles and technical knowledge, it might be useful to have your very responsible backend developers write controllers and let designers or frontend developers write HTML in a sandbox. Well, that’s a questionable approach too.&lt;/p&gt;
&lt;p&gt;Anyway, as we said we’re making a tool for technically-minded people, and should give them power to actually do stuff. And since we’re talking about websites built ahead-of-time, the risks of logic in templates don’t apply as much.&lt;/p&gt;
&lt;p&gt;Another consequence of restricted templates is that SSG documentation will then tell you to work in a third place: not in content, not in templates, but in “config code”. This is a problem because A) it’s probably one place too many and B) it’s often unclear at what time this config code is running or how often: once for every page generation or template run, or just once at the start of a build?&lt;/p&gt;
&lt;p&gt;So, do more work in templates. We’re making somewhat simple websites here, it’s going to be okay. It’s cool if you can refactor your code to avoid duplication or separate concerns or whatever, but this shouldn’t be a requirement; users will only jump through hoops if they’re working on a big enough project to justify it.&lt;/p&gt;
&lt;h3&gt;4. A full content API&lt;/h3&gt;
&lt;p&gt;There should be a full content traversal API, which could be modelled after &lt;a href=&quot;https://getkirby.com/docs/templates/api&quot;&gt;Kirby’s API&lt;/a&gt; (itself inspired by jQuery). Other sources of inspiration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Globbing, e.g. &lt;code&gt;find(&apos;posts/*.md&apos;)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;DOM traversal (there’s no reason why your content tree cannot be a tree of nodes).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One concern with offering a full content API in templates is that if you run a template for a thousand pages, and do the same work (including querying the filesystem) every time, you’re going to have performance issues. I have a few possible optimizations in mind (fragmenting content queries and memoization), but I’ll admit that as a UI developer this is not my forte.&lt;/p&gt;
&lt;p&gt;On top of traversing and retrieving content, templates should be able to transform formats (Markdown to HTML, parsing JSON and maybe YAML) and process images.&lt;/p&gt;
&lt;h3&gt;5. Markdown plus front-matter is too limiting&lt;/h3&gt;
&lt;p&gt;Originating in simple blog engines, static site generators treat pages as a single content chunk (often in Markdown) with some metadata sprinkled on top. If you need several long chunks of content (say, a product short description, long description, technical specs, and a list of vendors), you’re out of luck.&lt;/p&gt;
&lt;p&gt;Theoretically, you can put any kind of text content in YAML “front-matter”, but editing that content without breaking the YAML syntax is a pain.&lt;/p&gt;
&lt;p&gt;One workaround is using separate files in the same folder or in subfolder, than use the content API to retrieve them. Each such “resource” can have a body and metadata.&lt;/p&gt;
&lt;p&gt;Kirby does things a bit differently (though it’s always possible to use a page’s files or child pages too), using a custom format with arbitrary fields:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Title: My Title
----
Date: 2018-12-25
----
Desc: Short description for list views and SEO.
----
Body:
## A field contains arbitrary text
So we can use a field value and parse it as Markdown or whatever.
…
----
Notes: …
----
References: …&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Other sources of inspiration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue single-file components, and, well, XML&lt;/li&gt;
&lt;li&gt;Inside the page’s body: shortcodes (WordPress, Hugo), Web Components, Vue components (Vuepress)&lt;/li&gt;
&lt;li&gt;Separators using HTML comment syntax, like &lt;code&gt;&amp;lt;!--more--&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m not sure what would be the “best” solution, but it should be possible to unlock more power without completely breaking with convention.&lt;/p&gt;
&lt;h3&gt;6. No theming system&lt;/h3&gt;
&lt;p&gt;A theming infrastructure imposes a lot of technical restrictions and indirection, such as template lookup orders (see: Hugo), having to specify a handful of default template names (Hugo, again), and having to specify strange content conventions for mapping content to those default template names (still Hugo). Frankly, if a user really wants a template inheritance and theme inheritance mechanism, they’ll probably bite the bullet and work with WordPress or Drupal.&lt;/p&gt;
&lt;p&gt;In the lightweight CMS world, having a theming system is the main reason why &lt;a href=&quot;https://getgrav.org/&quot;&gt;Grav&lt;/a&gt; is less straightforward than &lt;a href=&quot;https://getkirby.com/&quot;&gt;Kirby&lt;/a&gt; (which inspired it): themes and plugins rely on metadata in pages, so you have to put detailed config-as-metadata in every page to enable and configure theme and plugin features, which often feels like randomly pressing buttons and hoping to get a result.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:63e3255e-a704-591d-8822-1049013b3ea9</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/styling-buttons"/>
		<title>Styling buttons, the right way</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-05-09T00:00:00+02:00</published>
		<updated>2018-05-09T00:00:00+02:00</updated>
		<summary type="text">Learn how to create an accessible button style which can be used with the correct semantic element: &lt;a&gt; or &lt;button&gt;.</summary>
		<content type="html">&lt;p&gt;If you’re building a website or a web app, you probably have buttons. And maybe links that look like buttons? Anyway, it’s important to get them right.&lt;/p&gt;
&lt;p&gt;In this tutorial we’ll create basic styles for &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; elements, and a custom &lt;code&gt;.btn&lt;/code&gt; CSS component. You will find a demo page for each step of the process.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Demo&lt;/th&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://fvsch.com/articles/styling-buttons/step1-reset.html&quot;&gt;Step&amp;nbsp;1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;#step-1&quot;&gt;Resetting &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; styles&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://fvsch.com/articles/styling-buttons/step2-basic.html&quot;&gt;Step&amp;nbsp;2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;#step-2&quot;&gt;Writing a “button” CSS component&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://fvsch.com/articles/styling-buttons/step3-states.html&quot;&gt;Step&amp;nbsp;3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;#step-3&quot;&gt;Styling hover and active states&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://fvsch.com/articles/styling-buttons/step4-final.html&quot;&gt;Step&amp;nbsp;4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;#step-4&quot;&gt;Managing focus styles&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;step-1&quot;&gt;&lt;span&gt;Resetting &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; styles&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As a rule, 99.9% of the clickable things in your website or app should be either &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; elements. If you’re not sure what element to use in a given situation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If it goes to a different URL or changes most of the page’s content, use a link (&lt;code&gt;&amp;lt;a href=&quot;some_url&quot;&amp;gt;…&amp;lt;/a&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Otherwise, use a generic button (&lt;code&gt;&amp;lt;button type=&quot;button&quot;&amp;gt;…&amp;lt;/button&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Using the correct element has a few advantages: it’s SEO-friendly (especially for links!), it works well with keyboard navigation, and it improves accessibility for all users.&lt;/p&gt;
&lt;p&gt;In spite of this, developers rarely use the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element. All over the Web, we can see a lot of buttons that trigger JavaScript actions, and on closer inspection it turns out they’re coded with &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why is the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element so underused?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Knowledge:&lt;/strong&gt; many developers don’t know about it (learning &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element&quot;&gt;HTML’s 100+ elements&lt;/a&gt; takes a little while).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Styling:&lt;/strong&gt; &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; comes with complex default styles, which can make it hard to achieve a custom look.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thankfully, the styling part can be fixed!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/**
 * Reset button styles
 * It takes a little bit of work to achieve a “blank slate” look.
 */
button {
  padding: 0;
  border: none;
  font: inherit;
  color: inherit;
  background-color: transparent;
  /*
    Show a hand cursor on mouse-over instead of the default arrow cursor.
    (Some argue that we should keep the default arrow cursor for buttons, to be consistent with how desktop operating systems treat buttons.)
  */
  cursor: pointer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We end up with buttons that look like regular text.&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe title=&quot;Demo of step 1: resetting button styles&quot; class=&quot;autoheight&quot; src=&quot;https://fvsch.com/articles/styling-buttons/step1-reset.html&quot; width=&quot;100%&quot; height=&quot;100&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;p&gt;The downside of this approach is that now we have to style &lt;em&gt;all&lt;/em&gt; our buttons, or &lt;a href=&quot;https://en.wikipedia.org/wiki/Affordance&quot;&gt;users won’t recognize them&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If we want to avoid that, another option is to use this style as a mixin (with Sass or another preprocessor) and apply it selectively:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-scss&quot;&gt;@mixin button-reset {
  padding: 0;
  border: none;
  font: inherit;
  color: inherit;
  background-color: transparent;
  cursor: pointer;
}

.my-custom-button {
  @include button-reset;
  padding: 10px;
  background-color: skyblue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;button type=&quot;button&quot;&amp;gt;
  I use default browser styles
&amp;lt;/button&amp;gt;

&amp;lt;button type=&quot;button&quot; class=&quot;my-custom-button&quot;&amp;gt;
  And I’m using custom styles instead
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-2&quot;&gt;&lt;span&gt;Writing a “button” CSS component&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now that we’ve removed default styles, let’s define our own button styles. This is what we’d like to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a “button” style that can be applied to both links or buttons;&lt;/li&gt;
&lt;li&gt;we want to apply it selectively, because we’ll have other link and button styles in our pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This calls for a CSS component. A CSS component is a style or collection of styles which we can apply using classes, often on top of a few different types of HTML elements. You may be familiar with this concept from CSS frameworks like Bootstrap or Foundation.&lt;/p&gt;
&lt;p&gt;We’ll call this component &lt;code&gt;.btn&lt;/code&gt; — like Bootstrap does, but we’ll restrict ourselves to a single color and size, to keep things simple.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.btn {
  /* default for &amp;lt;button&amp;gt;, but useful for &amp;lt;a&amp;gt; */
  display: inline-block;
  text-align: center;
  text-decoration: none;

  /* create a small space when buttons wrap on 2 lines */
  margin: 2px 0;

  /* invisible border (will be colored on mouse-over) */
  border: solid 2px transparent;
  border-radius: 0.4em;

  /* size comes from text &amp;amp; padding (no width/height) */
  padding: 0.5em 1em;

  /* make sure colors have enough contrast! */
  color: #ffffff;
  background-color: #9555af;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is what our button component looks like:&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe class=&quot;autoheight&quot; src=&quot;https://fvsch.com/articles/styling-buttons/step2-basic.html&quot; width=&quot;100%&quot; height=&quot;260&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;p&gt;You may be wondering why contrast is such a big deal. That second line of buttons looks nice; who doesn’t like a pastel look?&lt;/p&gt;
&lt;p&gt;Simply put: &lt;em&gt;with good contrast, you can reach more users&lt;/em&gt;. Some of your users have imperfect vision. Or bad screens that make washed out colors hard to differentiate. Others may be looking at your site on their phone, in broad daylight. You can &lt;a href=&quot;https://webaim.org/resources/contrastchecker/&quot;&gt;check your contrast ratios here&lt;/a&gt;, and &lt;a href=&quot;https://www.nngroup.com/articles/low-contrast/&quot;&gt;read more about the UX of text contrast here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;step-3&quot;&gt;&lt;span&gt;Styling hover and active states&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;It’s cool that your button looks nice, but… users are going to interact with it, and they will need visual feedback when the button’s state changes.&lt;/p&gt;
&lt;p&gt;Browsers come with default styles for states like “hover” (mouse pointer is hovering the element) and “active” (the element is being pressed); and by resetting button styles we’ve removed some of those. We would also like to have styles for mouse-over,
and overall we want styles that are visible &lt;em&gt;and&lt;/em&gt; match our design.&lt;/p&gt;
&lt;p&gt;Let’s start with a style for the &lt;code&gt;:active&lt;/code&gt; state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Old-school &quot;push button&quot; effect on clic + color tweak */
.btn:active {
  transform: translateY(1px);
  filter: saturate(150%);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could change the button’s colors when clicked, but I want to reserve this effect for our mouse-over styles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Swap colors on mouse-over */
.btn:hover {
  color: #9555af;
  border-color: currentColor;
  background-color: white;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s our result — try out the active and hover styles. In the next section, we’ll deal with focus styles.&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe class=&quot;autoheight&quot; src=&quot;https://fvsch.com/articles/styling-buttons/step3-states.html&quot; width=&quot;100%&quot; height=&quot;100&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;step-4&quot;&gt;&lt;span&gt;Managing focus styles&lt;/span&gt;&lt;/h2&gt;
&lt;h3&gt;What are focus styles?&lt;/h3&gt;
&lt;p&gt;Users of your websites or web apps can use a keyboard or some other input software or device (gamepads, speech input software, head pointers, motion tracking or eye tracking, single switch entry devices, etc.) to navigate the page. Those methods will move the current “focus” from one element to the next, so that the user may activate an interactive element or type in a focused text field.&lt;/p&gt;
&lt;p&gt;Even if you primarily use a mouse or trackpad, you might still be using the Tab key when using a web form, to jump from one form field to the next. Even your mouse users are keyboard users some of the time.&lt;/p&gt;
&lt;p&gt;To serve all users, we need the currently focused element to be clearly visible. Thankfully, browsers do it by default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Historically, browsers like Internet Explorer and older versions of Firefox had a very subtle focus style: a thin dotted outline. This led to the recommandation to &lt;a href=&quot;https://adrianroselli.com/2017/02/avoid-default-browser-focus-styles.html&quot;&gt;add your own custom and more accessible focus style&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In recent years, Chrome, Edge, Firefox and other browsers ship with a default focus style that uses a double outline with two colors, which is more often accessible on varied backgrounds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re not sure what to do, you can keep the browser’s default style. But if you do want to customize it, read on.&lt;/p&gt;
&lt;h3&gt;Custom focus styles&lt;/h3&gt;
&lt;p&gt;Let’s define a custom focus style with the aptly-named &lt;code&gt;:focus&lt;/code&gt; pseudo-class. We’d like this style to follow accessibility guidelines, including:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/WCAG21/#focus-visible&quot;&gt;WCAG 2.1 (Recommendation) Success Criterion 2.4.7 Focus Visible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance-minimum.html&quot;&gt;WCAG 2.2 (Draft): Understanding Success Criterion 2.4.11: Focus Appearance (Minimum)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;When a user interface component has keyboard focus, the focus indicator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encloses the visual presentation of the user interface component;&lt;/li&gt;
&lt;li&gt;Has a contrast ratio of at least 3:1 between its pixels in the focused and unfocused states;&lt;/li&gt;
&lt;li&gt;Has a contrast ratio of at least 3:1 against adjacent colors.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;So we basically need a kind of outline or border, and it should be contrasted enough against part of the element itself and its environment, which can be a bit hard to always achieve. For instance, if you have a button with a gray border on a white page, and you just want to change the border’s color when that button is focused, picking a color can be tricky:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.btn {
  border: solid 1px gray;
}

.btn:focus {
  /* hide the browser&apos;s default outline, because we&apos;re showing our own focus styles */
  outline-color: transparent;

  /* ❌ blue is contrasted enough with the white background (8.51:1), but not contrasted enough with the border&apos;s initial gray color (2.42:1) */
  border-color: blue;

  /* ✅ black is different enough from both white (21:1) and gray (5.92:1) */
  border-color: black;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when you’re not sure what the surrounding background color is going to be, it can be even harder to ensure your focus styles have good contrast every time.&lt;/p&gt;
&lt;p&gt;To make our focus styles always perceptible, we can try creating a double-outline effect like the default focus style of Chrome and Firefox. Because the &lt;code&gt;outline&lt;/code&gt; property only accepts a single outline, we’ll have to use a combination of &lt;code&gt;outline&lt;/code&gt; and &lt;code&gt;box-shadow&lt;/code&gt; to achieve the same effect.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.btn:focus {
  /* paint a 2px white pseudo-outline using box-shadow */
  box-shadow: 0 0 0 2px #fff;
  /* then paint a 2px dark pink outline and offset it to reveal one or two pixels of the pseudo-outline */
  outline: solid 2px #d59;
  outline-offset: 1px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works pretty well. And since we are setting a custom &lt;code&gt;outline&lt;/code&gt; value, we don’t have to do anything like &lt;code&gt;outline-color: transparent&lt;/code&gt; or &lt;code&gt;outline: none&lt;/code&gt; to override the default outline style.&lt;/p&gt;
&lt;h3&gt;Sticky focus styles&lt;/h3&gt;
&lt;p&gt;There is one tricky issue left. When you click a link or button, two states will apply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:active&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:focus&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;:active&lt;/code&gt; state stops as soon as you stop pressing the mouse button or trackpad. But in most browsers, clicking an interactive element gives it focus, so the &lt;code&gt;:focus&lt;/code&gt; style applies until the user clicks somewhere else on the page.&lt;/p&gt;
&lt;p&gt;This has been a headache for many web designers and developers for almost two decades. I cannot count the number of clients who have told me &lt;q&gt;“when I click here, there is a strange border around the button, please remove it!”&lt;/q&gt;&lt;/p&gt;
&lt;p&gt;Even if you know that this strange border helps make their site accessible, convincing a client to leave it be can be an uphill battle! (And if you don’t know, sadly you’ll find plenty of blogs, forum posts and StackOverflow answers telling you to “just use &lt;code&gt;outline: none&lt;/code&gt;” and call it a day.)&lt;/p&gt;
&lt;p&gt;Thanksfully there is a better way, implemented in all browsers in recent years: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible&quot;&gt;the &lt;code&gt;:focus-visible&lt;/code&gt; pseudo-class&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How it works in a nutshell: for buttons and links, browsers will set the &lt;code&gt;:focus-visible&lt;/code&gt; state on an element after a keyboard or keyboard-like interaction, but not after a click.&lt;/p&gt;
&lt;p&gt;We can replace &lt;code&gt;:focus&lt;/code&gt; with &lt;code&gt;:focus-visible&lt;/code&gt; to fix our issue with focus styles sticking around after a click:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.btn:focus-visible {
  /* paint a 2px white pseudo-outline using box-shadow */
  box-shadow: 0 0 0 2px #fff;
  /* then paint a 2px dark pink outline and offset it to reveal one or two pixels of the pseudo-outline */
  outline: solid 2px #d59;
  outline-offset: 1px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that Web browsers have recently moved to use &lt;code&gt;:focus-visible&lt;/code&gt; for their own default focus styles. In that case, we don’t need to do anything about default focus styles, because our custom &lt;code&gt;:focus-visible&lt;/code&gt; style will override them.&lt;/p&gt;
&lt;p&gt;And if some browsers are showing focus styles on &lt;code&gt;:focus&lt;/code&gt; instead of &lt;code&gt;:focus-visible&lt;/code&gt;, we can use this style to make sure that only our custom focus styles would show up, and only on &lt;code&gt;:focus-visible&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* disable the default outline on a focused element which doesn’t have the :focus-visible state (e.g. a button after a mouse click) */
.btn:focus:not(:focus-visible) {
  outline-color: transparent;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, here is our our final result:&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe class=&quot;autoheight&quot; src=&quot;https://fvsch.com/articles/styling-buttons/step4-final.html&quot; width=&quot;100%&quot; height=&quot;180&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;p&gt;Go ahead and &lt;a href=&quot;https://fvsch.com/articles/styling-buttons/step4-final.html&quot;&gt;look at the final code&lt;/a&gt; to review everything we’ve seen in this tutorial.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:36fa7811-2674-5f64-a90f-62be9a0d8444</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/command-line-tips"/>
		<title>Command line tips</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-05-03T00:00:00+02:00</published>
		<updated>2018-05-03T00:00:00+02:00</updated>
		<summary type="text">Semi-random tips I’ve learned that help working with the command line.</summary>
		<content type="html">&lt;p&gt;I’d like to share a few command line things I’ve learned in the past years.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#what&quot;&gt;The command what?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tldr&quot;&gt;You need help (and tldr is great)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#bash-keyboard&quot;&gt;Bash keyboard shortcuts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#homebrew&quot;&gt;MacOS: install Homebrew and GNU coreutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-big&quot;&gt;How big is this directory?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rmrf&quot;&gt;How to &lt;em&gt;not&lt;/em&gt; delete everything&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;what&quot;&gt;&lt;span&gt;The command what?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’m talking about the command prompt found in macOS when using the built-in “Terminal” application, or on Windows when you install something like &lt;a href=&quot;https://gitforwindows.org/&quot;&gt;Git Bash&lt;/a&gt;.
It’s a text-based interface where you type commands to navigate in folders, create or delete files, and much more.
If you’re curious and want to get started, &lt;a href=&quot;https://tutorial.djangogirls.org/en/intro_to_command_line/&quot;&gt;this tutorial is pretty good for learning from scratch&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the next sections, examples will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# This line is a comment
$ command parameter --option&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dollar character (&lt;code&gt;$&lt;/code&gt;) indicates a command that you can type.
You should not type this character, only what comes after it.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;&lt;span&gt;You need help (and &lt;code&gt;tldr&lt;/code&gt; is great)&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes you remember the name of a command, but how should you use it?
What arguments and options does it take?&lt;/p&gt;
&lt;p&gt;Most operating systems will come with documentation files called “man pages” (man is for manual).
For example if I want to be reminded how the &lt;code&gt;cd&lt;/code&gt; (change directory) command works, I could type &lt;code&gt;man cd&lt;/code&gt; and press the Enter key, and I would get this:&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img class=&quot;border&quot; src=&quot;https://fvsch.com/articles/command-line-tips/terminal-man-cd.png&quot; alt=&quot;A very big wall of text&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;That’s great, but those doc pages are long often hard to read.
Can I get a summary?&lt;/p&gt;
&lt;p&gt;One option is to use the command’s built-in help, if it exists.
For example you can call &lt;code&gt;mycommand --help&lt;/code&gt;.
But some commands don’t have a help option, some use &lt;code&gt;-help&lt;/code&gt; (single hyphen), some use &lt;code&gt;-h&lt;/code&gt;, some use &lt;code&gt;mycommand help&lt;/code&gt; (no hyphen), it’s a mess!&lt;/p&gt;
&lt;p&gt;My solution is to use the &lt;code&gt;tldr&lt;/code&gt; command.
It’s an &lt;a href=&quot;https://tldr.sh/&quot;&gt;open-source documentation project&lt;/a&gt; for common commands, which shows very short usage examples.
It looks like this:&lt;/p&gt;
&lt;figure class=&quot;full&quot;&gt;
  &lt;img class=&quot;border&quot; src=&quot;https://fvsch.com/articles/command-line-tips/terminal-tldr-cd.png&quot; alt=&quot;Short usage examples for cd, using tldr cd&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;You can &lt;a href=&quot;https://tldr.ostera.io/cd&quot;&gt;use it online&lt;/a&gt;, but it’s quicker to use it straight from the command line.
You will need to have Node.js and &lt;code&gt;npm&lt;/code&gt; installed to use it, though, so it’s only useful if you’re using those tools already.&lt;/p&gt;
&lt;p&gt;Installation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Install with npm
$ npm install --global tldr
...

# Time to use your new powers!
$ tldr rm&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;bash-keyboard&quot;&gt;&lt;span&gt;Bash keyboard shortcuts&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;When you’re using a Terminal or command prompt on Linux or macOS, you’re probably using the Bash program.
(On Windows, you can install Git Bash to use that instead of cmd.exe.)&lt;/p&gt;
&lt;p&gt;Bash is, among other things, the program that allows you to write commands an see their output.&lt;/p&gt;
&lt;p&gt;One thing really frustrating with Bash and terminals in general is that you can’t just click around to move the input cursor, like you would in a code editor.
If you’re like me, you probably spent a lot of time using the left and right arrows to move the cursor in a line of text.&lt;/p&gt;
&lt;p&gt;So here are a few keyboard shortcuts that can help:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Up&lt;/code&gt;: fill the command prompt with the last command; useful if you need to correct it.
Use “Up” several times to go back in history.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + Left&lt;/code&gt; and &lt;code&gt;Ctrl + Right&lt;/code&gt; (or &lt;code&gt;Alt + Left|Right&lt;/code&gt; on macOS): move the cursor by a full word.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + A&lt;/code&gt;: move the cursor to the start of the line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + E&lt;/code&gt;: move the cursor to the end of the line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + K&lt;/code&gt;: delete all characters to the right.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + U&lt;/code&gt;: delete all characters to the left.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;homebrew&quot;&gt;&lt;span&gt;MacOS: install Homebrew and GNU coreutils&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If you’re going to work on the command line often on macOS, it’s probably a good idea to install &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt;, which will help you install more command-line tools.&lt;/p&gt;
&lt;p&gt;Once you’ve installed Homebrew, I recommend using GNU Core Utilities, which contains the GNU version of tools like &lt;code&gt;ls&lt;/code&gt; or &lt;code&gt;rm&lt;/code&gt;.
Compared to the similar tools that come with macOS, they’re often a bit more usable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Install coreutils, then follow the instructions
# to use them instead of macOS’s built-in ones
$ brew install coreutils&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;how-big&quot;&gt;&lt;span&gt;How big is this directory?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I’ve had trouble remembering how to check how big a directory is.
For the past 15 years, I would have to switch to a graphical file explorer to do that.&lt;/p&gt;
&lt;p&gt;Sometimes I would find this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Show the size of everything in current directory
$ du -sh *&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But I would never manage to remember it.
Then it hit me that it sounds a bit like “douche”, so if you need something to help you memorize it, you’re welcome.&lt;/p&gt;
&lt;p&gt;Here’s another trick for listing folders sorted by size.
This only works on Linux, or on macOS &lt;a href=&quot;#homebrew&quot;&gt;if you’re using GNU Core Utilities&lt;/a&gt;:&lt;/p&gt;
&lt;h2 id=&quot;rmrf&quot;&gt;&lt;span&gt;How to &lt;em&gt;not&lt;/em&gt; delete everything&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;When you want to delete a directory which contains a lot of files, you’re probably going to use &lt;code&gt;rm -rf&lt;/code&gt;.
It might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ rm -rf some/path&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if you mess up the path part, you can end up deleting the wrong folder.
For instance, maybe you wanted to delete a bunch of old log files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo rm -rf /var/log/some-program/old-logs/*&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But when you types, you pressed Enter by mistake, and the resulting command is now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo rm -rf /&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shit.&lt;/p&gt;
&lt;p&gt;There’s one way to make this kind of human error less likely: write the &lt;code&gt;-rf&lt;/code&gt; part last:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo rm /var/log/some-program/old-logs/* -rf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you hit Enter by mistake, the command will be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ sudo rm /
rm: cannot remove &apos;/&apos;: Is a directory&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phew.&lt;/p&gt;
&lt;p&gt;This is a good habit to get into:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write &lt;code&gt;rm&lt;/code&gt; then the directory’s path.&lt;/li&gt;
&lt;li&gt;Check the path, does it look okay?&lt;/li&gt;
&lt;li&gt;If it’s okay, write &lt;code&gt;-rf&lt;/code&gt; at the end.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The main issue here is that on Linux this will work perfectly, because the GNU version of &lt;code&gt;rm&lt;/code&gt; can take options at the beginning or at the end.
But on macOS, the BSD version of &lt;code&gt;rm&lt;/code&gt; will tell you that &lt;code&gt;-rf&lt;/code&gt; is not a file.
My workaround for this is &lt;a href=&quot;#homebrew&quot;&gt;installing GNU coreutils with Homebrew&lt;/a&gt;.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:6cecca07-37be-548a-b625-3538e8eb9679</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/svg-gradient-fill"/>
		<title>Gradient fills for SVG icons</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-03-26T00:00:00+02:00</published>
		<updated>2018-03-26T00:00:00+02:00</updated>
		<summary type="text">It’s definitely possible! But it’s not as easy as using linear-gradient in CSS. We explore a few techniques for using gradient fills on SVG icons in a web page.</summary>
		<content type="html">&lt;p&gt;In my 2016 article &lt;a href=&quot;/svg-icons&quot;&gt;How to work with SVG icons&lt;/a&gt;, I concluded the “Advanced” section with this warning: sorry, gradient fills won’t work.&lt;/p&gt;
&lt;p&gt;I was mostly referring to code like &lt;code&gt;fill: linear-gradient(red, blue)&lt;/code&gt; which does not work because &lt;code&gt;fill&lt;/code&gt; is from SVG which has its own gradient system, and &lt;code&gt;linear-gradient&lt;/code&gt; is from CSS and made mostly for backgrounds. The two don’t mix well.&lt;/p&gt;
&lt;p&gt;I knew we could probably use SVG’s &lt;code&gt;&amp;lt;linearGradient/&amp;gt;&lt;/code&gt; element but hadn’t really tried it. Then a few months later I did try it, and it worked!, but I forgot to blog about it. So, how can we do this thing?&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Putting gradients in your HTML&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The most reliable technique I found was to add a SVG element in your HTML page (at the start or end of the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, for instance). It should define a &lt;code&gt;&amp;lt;linearGradient/&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;svg style=&quot;width:0;height:0;position:absolute;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&amp;gt;
  &amp;lt;linearGradient id=&quot;my-cool-gradient&quot; x2=&quot;1&quot; y2=&quot;1&quot;&amp;gt;
    &amp;lt;stop offset=&quot;0%&quot; stop-color=&quot;#447799&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;50%&quot; stop-color=&quot;#224488&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;100%&quot; stop-color=&quot;#112266&quot; /&amp;gt;
  &amp;lt;/linearGradient&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This element should &lt;em&gt;not&lt;/em&gt; be hidden with &lt;code&gt;display:none&lt;/code&gt;, because some browsers will just ignore the gradient if we do that. Making it zero pixels high seems to work.&lt;/p&gt;
&lt;p&gt;You can then use this gradient on your SVG icon:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;svg class=&quot;icon&quot; fill=&quot;url(#my-cool-gradient) #447799;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&amp;gt;
  &amp;lt;use xlink:href=&quot;#symbol-id&quot;&amp;gt;&amp;lt;/use&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or in your CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.icon {
  /* gradient and fallback color */
  fill: url(#my-cool-gradient) #447799;
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;frame padding&quot; style=&quot;text-align:center&quot;&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill: url(#my-cool-gradient) blue;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;#icon-leaf&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill: url(#my-cool-gradient) blue;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;#icon-carpet&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;figcaption&gt;
    These two icons should be using a turquoise-to-dark-blue gradient.&lt;br&gt;
    Credits: &lt;a href=&quot;https://thenounproject.com/search/?q=leaf&amp;amp;i=75382&quot;&gt;Leaf by Gabriele Malaspina&lt;/a&gt; and &lt;a href=&quot;https://thenounproject.com/search/?q=carpet&amp;amp;i=1023407&quot;&gt;Carpet by Ben Davis&lt;/a&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Note that you cannot customize the gradient on a per-icon basis. If you want different gradients, you will need to create different SVG gradient definitions in you HTML.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Changing gradient colors with CSS variables&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If we want to set our gradient colors from CSS, we could do it using CSS variables. We’re going to write our gradient definitions using CSS custom properties (&lt;code&gt;var(--my-custom-property)&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; style=&quot;width:0;height:0;position:absolute;&quot;&amp;gt;
  &amp;lt;linearGradient id=&quot;gradient-horizontal&quot;&amp;gt;
    &amp;lt;stop offset=&quot;0%&quot; stop-color=&quot;var(--color-stop-1)&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;50%&quot; stop-color=&quot;var(--color-stop-2)&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;100%&quot; stop-color=&quot;var(--color-stop-3)&quot; /&amp;gt;
  &amp;lt;/linearGradient&amp;gt;
  &amp;lt;linearGradient id=&quot;gradient-vertical&quot; x2=&quot;0&quot; y2=&quot;1&quot;&amp;gt;
    &amp;lt;stop offset=&quot;0%&quot; stop-color=&quot;var(--color-stop-1)&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;50%&quot; stop-color=&quot;var(--color-stop-2)&quot; /&amp;gt;
    &amp;lt;stop offset=&quot;100%&quot; stop-color=&quot;var(--color-stop-3)&quot; /&amp;gt;
  &amp;lt;/linearGradient&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can set — and, if need be, change — those colors in our CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#gradient-horizontal {
  --color-stop-1: #a770ef;
  --color-stop-2: #cf8bf3;
  --color-stop-3: #fdb99b;
}
#gradient-vertical {
  --color-stop-1: #00c3ff;
  --color-stop-2: #77e190;
  --color-stop-3: #ffff1c;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, finally, use them as icon fills:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.icon-hgradient {
  fill: url(#gradient-horizontal) gray;
  /* We could use it as a stroke fill too:
  stroke: url(#gradient-horizontal) gray; */
}
.icon-vgradient {
  fill: url(#gradient-vertical) gray;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the result (in &lt;a href=&quot;https://caniuse.com/#feat=css-variables&quot;&gt;browser supporting CSS Custom Properties&lt;/a&gt;):&lt;/p&gt;
&lt;figure class=&quot;frame padding&quot; style=&quot;text-align:center&quot;&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill: url(#gradient-vertical) gray;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;#icon-leaf&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill: url(#gradient-horizontal) gray;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;#icon-carpet&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;access-label&quot;&gt;Testing icons with SVG gradient fills and CSS variables&lt;/span&gt;
  &lt;figcaption&gt;The gradient fill is painted for each path of the icon. To avoid color mismatches (like the junction between the leaf and the stem), it might be useful to merge all or most paths in the icon’s source SVG.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Using gradients in external files&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;This technique works in Firefox, and used to work in Edge (confirmed in Edge 14 and 15, regressed in Edge 16 and Insider Preview). In the following test:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Both icons are fetched from an external sprite: &lt;a href=&quot;https://fvsch.com/articles/svg-gradient-fill/sprite.svg&quot;&gt;sprite.svg&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The first icon uses a gradient from this HTML page, and the second one from &lt;code&gt;sprite.svg&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.icon-sprite-gradient {
  fill: url(sprite.svg#my-warm-gradient) red;
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;frame padding&quot; style=&quot;text-align:center&quot;&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill:url(&apos;#my-warm-gradient&apos;) red;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;https://fvsch.com/articles/svg-gradient-fill/sprite.svg#icon-leaf&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;svg class=&quot;icon&quot; style=&quot;fill:url(&apos;https://fvsch.com/articles/svg-gradient-fill/sprite.svg#my-warm-gradient&apos;) red;&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;https://fvsch.com/articles/svg-gradient-fill/sprite.svg#icon-carpet&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;figcaption&gt;Non-supporting browsers (Chrome, Safari, latest Edge…) should show a red fallback fill for the second icon.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Using gradients in CSS as data URLs&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;What if I told you that you could define your gradients in SVG, but put the SVG in your CSS as data URLs? Okay now I’m just being silly. But it works! At least it works in Firefox :P&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Notice the `id=&quot;grad&quot;` in the SVG URL and how we’re using it at the end. Note that the `#` in hexadecimal colors must be escaped as `%23`. */
.icon-gradient-url {
  fill: url(&quot;data:image/svg+xml,&amp;lt;svg xmlns=&apos;http://www.w3.org/2000/svg&apos;&amp;gt;&amp;lt;linearGradient id=&apos;grad&apos;&amp;gt;&amp;lt;stop offset=&apos;0%&apos; stop-color=&apos;%23ff00cc&apos;/&amp;gt;&amp;lt;stop offset=&apos;100%&apos; stop-color=&apos;%23333399&apos;/&amp;gt;&amp;lt;/linearGradient&amp;gt;&amp;lt;/svg&amp;gt;#grad&quot;) purple;
}

/* Same SVG, in base64 */
.icon-gradient-base64 {
  fill: url(&quot;data:image/svg+xml;base64,PHN2...zdmc+#grad&quot;) purple;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See how we reference the gradient’s &lt;code&gt;id&lt;/code&gt; with &lt;code&gt;#grad&lt;/code&gt; at the end of the URL? Only Firefox seems to understand this syntax. Eh, too bad.&lt;/p&gt;
&lt;figure class=&quot;frame padding&quot; style=&quot;text-align:center&quot;&gt;
  &lt;svg class=&quot;icon icon-gradient-url&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;https://fvsch.com/articles/svg-gradient-fill/sprite.svg#icon-leaf&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;svg class=&quot;icon icon-gradient-base64&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
    &lt;use xlink:href=&quot;https://fvsch.com/articles/svg-gradient-fill/sprite.svg#icon-carpet&quot;&gt;&lt;/use&gt;
  &lt;/svg&gt;
  &lt;figcaption&gt;Trying to show a magenta to purple gradient. Non-supporting browsers (Chrome, Safari, Edge…) should show a purple fallback fill.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;&lt;span&gt;Let’s recap&lt;/span&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Yes you can have gradient fills!&lt;/li&gt;
&lt;li&gt;But you need to include SVG gradients in your HTML&lt;/li&gt;
&lt;li&gt;Colors can be hardcoded in the SVG gradient (in HTML), or set in CSS (using CSS variables)&lt;/li&gt;
&lt;li&gt;All icons using a given gradient will use the same colors, you can’t override on a specific color like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.icon-gradient-override {
  /* This works, as shown before */
  fill: url(#gradient-horizontal) gray;
  /* But this will do nothing… */
  --color-stop-1: white;
  --color-stop-2: gray;
  --color-stop-3: black;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need a lot of different gradient fills, this technique might not work for you. Creating 10 or 20 different SVG gradients in your HTML is not practical. But for simpler use cases, this should work in all current browsers (including IE11 if we avoid CSS variables).&lt;/p&gt;&lt;!-- SVG gradient for gradient fill --&gt;
&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; style=&quot;width:0;height:0;position:absolute;&quot;&gt;
  &lt;linearGradient id=&quot;my-cool-gradient&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
    &lt;stop offset=&quot;0%&quot; stop-color=&quot;#447799&quot; /&gt;
    &lt;stop offset=&quot;50%&quot; stop-color=&quot;#224488&quot; /&gt;
    &lt;stop offset=&quot;100%&quot; stop-color=&quot;#112266&quot; /&gt;
  &lt;/linearGradient&gt;
  &lt;linearGradient id=&quot;my-warm-gradient&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
    &lt;stop offset=&quot;0%&quot; stop-color=&quot;#CC7744&quot; /&gt;
    &lt;stop offset=&quot;50%&quot; stop-color=&quot;#992244&quot; /&gt;
    &lt;stop offset=&quot;100%&quot; stop-color=&quot;#661133&quot; /&gt;
  &lt;/linearGradient&gt;
  &lt;linearGradient id=&quot;gradient-horizontal&quot;&gt;
    &lt;stop offset=&quot;0%&quot; stop-color=&quot;var(--color-stop-1)&quot; /&gt;
    &lt;stop offset=&quot;50%&quot; stop-color=&quot;var(--color-stop-2)&quot; /&gt;
    &lt;stop offset=&quot;100%&quot; stop-color=&quot;var(--color-stop-3)&quot; /&gt;
  &lt;/linearGradient&gt;
  &lt;linearGradient id=&quot;gradient-vertical&quot; x2=&quot;0&quot; y2=&quot;1&quot;&gt;
    &lt;stop offset=&quot;0%&quot; stop-color=&quot;var(--color-stop-1)&quot; /&gt;
    &lt;stop offset=&quot;50%&quot; stop-color=&quot;var(--color-stop-2)&quot; /&gt;
    &lt;stop offset=&quot;100%&quot; stop-color=&quot;var(--color-stop-3)&quot; /&gt;
  &lt;/linearGradient&gt;
  &lt;symbol id=&quot;icon-carpet&quot; viewBox=&quot;0 0 38 60&quot;&gt;&lt;path d=&quot;M34 18v33.9c0 .3-.1.6-.3.8-.4.3-.8.3-1.2.3H5a1 1 0 0 1-1-1V8c0-.6.4-1 1-1h28c.6 0 1 .4 1 1v10zm3-18a1 1 0 0 0-1 1v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2h-2V1a1 1 0 0 0-2 0v2H6V1a1 1 0 0 0-2 0v2H2V1a1 1 0 0 0-2 0v58a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0v-2h2v2a1 1 0 0 0 2 0V1c0-.6-.4-1-1-1z&quot;/&gt;&lt;path d=&quot;M8 16c0-.6.4-1 1-1h3v-3c0-.6.4-1 1-1h3V9H6v10h2v-3zM11 28.5l21 21V39h-3a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-3h-3a1 1 0 0 1-1-1v-4c0-.6.4-1 1-1h3v-3c0-.6.4-1 1-1h3v-3c0-.6.4-1 1-1h3V9h-1.6L11 28.5z&quot;/&gt;&lt;path d=&quot;M30 22c0 .6-.4 1-1 1h-3v3c0 .6-.4 1-1 1h-3v2h3c.6 0 1 .4 1 1v3h3c.6 0 1 .4 1 1v3h2V19h-2v3zM27.6 9H18v3c0 .6-.4 1-1 1h-3v3c0 .6-.4 1-1 1h-3v3c0 .6-.4 1-1 1H6v9.6l2.8-2.8L27.6 9zM16 45h-3a1 1 0 0 1-1-1v-3H9a1 1 0 0 1-1-1v-3H6v14h6v-3c0-.6.4-1 1-1h3v-2z&quot;/&gt;&lt;path d=&quot;M6 33.4V35h3c.6 0 1 .4 1 1v3h3c.6 0 1 .4 1 1v3h3c.6 0 1 .4 1 1v4c0 .6-.4 1-1 1h-3v2h16.6L9.5 30 6 33.3z&quot;/&gt;&lt;/symbol&gt;
  &lt;symbol id=&quot;icon-leaf&quot; viewBox=&quot;0 0 100 100&quot;&gt;&lt;path d=&quot;M50.6 30.6l-3.6 3-3.5 3.1-3.4 3.5-1.6 1.7-1.6 1.8c-1 1.3-2.2 2.5-3.1 3.8l-3 3.8c-1.7 2.6-3.5 5.2-5 7.9-1.5 2.6-2.8 5.2-4 7.7l-1.6 3.7-1.4 3.6c-.8 2.2-1.6 4.3-2.1 6.2l-1.4 5-1 4.3-.1.2-.2 1.2a3.4 3.4 0 0 0 6.6 1c3-19.9 14.6-41.1 31.8-58.3l.7-.7a3.4 3.4 0 0 0 .2-4.7l-2.7 2.2z&quot;/&gt;&lt;path d=&quot;M85 6a51.3 51.3 0 0 0-61.4 52.2l.2-.2c1.5-2.7 3.3-5.4 5.2-8l3-4 3.2-3.7 1.6-1.9 1.8-1.8 3.4-3.4 3.6-3.2 3.7-3c2.3-2 4.8-3.7 7.1-5.3 2.3-1.7 4.6-3 6.7-4.4C65.2 18 67.2 17 69 16c1.7-1 3.3-1.7 4.6-2.4l4-2a.9.9 0 0 1 .8 1.6l-4 2c-1.3.7-2.9 1.4-4.6 2.4-1.7 1-3.6 2-5.7 3.4-2 1.3-4.3 2.7-6.5 4.3-2.3 1.6-4.7 3.3-7 5.3l-3.6 3-3.5 3.1-3.4 3.5-1.6 1.7-1.6 1.8c-1 1.3-2.2 2.5-3.1 3.8l-3 3.8c-1.7 2.6-3.5 5.2-5 7.9L24 62.6l.6 3.9A51.3 51.3 0 0 0 85 6z&quot;/&gt;&lt;/symbol&gt;
&lt;/svg&gt;

&lt;!-- Demo styles --&gt;
&lt;style&gt;
.access-label {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  white-space: nowrap !important;
}
.icon {
  width: 180px;
  height: 180px;
  vertical-align: top;
}
#gradient-horizontal {
  --color-stop-1: #a770ef;
  --color-stop-2: #cf8bf3;
  --color-stop-3: #fdb99b;
}
#gradient-vertical {
  --color-stop-1: #00c3ff;
  --color-stop-2: #77e190;
  --color-stop-3: #ffff1c;
}
.icon-gradient-url {
  fill: url(&quot;data:image/svg+xml,&lt;svg xmlns=&apos;http://www.w3.org/2000/svg&apos;&gt;&lt;linearGradient id=&apos;grad&apos;&gt;&lt;stop offset=&apos;0%&apos; stop-color=&apos;%23ff00cc&apos;/&gt;&lt;stop offset=&apos;100%&apos; stop-color=&apos;%23333399&apos;/&gt;&lt;/linearGradient&gt;&lt;/svg&gt;#grad&quot;) purple;
}
.icon-gradient-base64 {
  fill: url(&quot;data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZCc+PHN0b3Agb2Zmc2V0PScwJScgc3RvcC1jb2xvcj0nI2ZmMDBjYycvPjxzdG9wIG9mZnNldD0nMTAwJScgc3RvcC1jb2xvcj0nIzMzMzM5OScvPjwvbGluZWFyR3JhZGllbnQ+PC9zdmc+#grad&quot;) purple;
}
&lt;/style&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:14584e59-faeb-50fa-b4fb-50c511332598</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/firefox-privacy-settings"/>
		<title>A few wishes for privacy settings in Firefox</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-02-07T00:00:00+01:00</published>
		<updated>2018-02-07T00:00:00+01:00</updated>
		<summary type="text">Firefox could do more to limit online tracking. Recently with the Quantum effort, Mozilla rebranded Firefox along three attributes: speed, customization and privacy. Yet the browser’s defaults allow most tracking techniques by default, for the sake of compatibility with existing websites.</summary>
		<content type="html">&lt;p&gt;&lt;em&gt;Update: as I was writing this post, Mozilla was already working on privacy features and settings. Firefox’s Tracking Protection was released in 2018 and enabled by default for all users in 2019.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Firefox could do more to limit online tracking. Recently with the Quantum effort, Mozilla rebranded Firefox along three attributes: speed, customization and privacy. Yet the browser’s defaults allow most tracking techniques by default, for the sake of compatibility with existing websites.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;About tracking&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Most web browsers leak a lot of user data by default. For instance:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;They allow third-party services (which a website almost always uses, e.g. Google Analytics, Facebook Connect, and ad providers) to track users’ activity across many websites, using third-party and first-party cookies.&lt;/li&gt;
&lt;li&gt;They allow sites to know where a user is coming from (with the &lt;code&gt;Referer&lt;/code&gt; HTTP header).&lt;/li&gt;
&lt;li&gt;Through JavaScript, they give access to a lot of information on the browser itself, the operating system I’m using, my time zone, how many processor cores I have, and some of my preferences (e.g. whether I allow cookies, which languages I prefer reading). This and other technical information can be used to identify you uniquely across the web, a practice dubbed “fingerprinting”.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m probably forgetting a few things. Try &lt;a href=&quot;https://panopticlick.eff.org/&quot;&gt;the EFF’s Panopticlick tool&lt;/a&gt; to get an idea of how resistant your browser is to tracking and fingerprinting.&lt;/p&gt;
&lt;p&gt;Most users don’t realize how much random companies they never deal with (such as ad networks) and other companies they do deal with (such as Google and Facebook) manage to know about them.&lt;/p&gt;
&lt;p&gt;I’ve had a few friends and relatives asking how to remove a virus from their browser… where by “a virus” they meant “many sites I visit show me the same ads that seem to know too much about me, so a virus must be spying everything I do and inject ads in my computer”. After explaining a bit, I cleaned their cookies and installed &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/&quot;&gt;uBlock Origin&lt;/a&gt; for them.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Why browsers don’t do more&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Of course all the technical features I mentioned are not provided &lt;em&gt;for&lt;/em&gt; tracking users, they are simply abused that way. But why won’t browsers stop this abuse? Why even load scripts — executable code with access to a bunch of information and features! — from other domains, why enable cookies for third-parties, etc.&lt;/p&gt;
&lt;p&gt;In short: since it was possible before, most sites rely on these loose permissions now, and would break if they were tightened.&lt;/p&gt;
&lt;p&gt;There are still opportunities for tightening these features and preventing at least some of the massive tracking going on, but different browser vendors have different priorities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Chrome has a good focus on security but is all-in on tracking, because spying on users is Google’s business model.&lt;/li&gt;
&lt;li&gt;Firefox is in a strange position because their brand is about independence and privacy but ultimately the bulk of their money comes from ad-and-tracking companies: Google, Yahoo and perhaps one of their competitors in the future. Also if a website stops working in Firefox then users are likely to blame Firefox for the breakage (and not the website for their invasive tracking techniques), and jump ship to Chrome.&lt;/li&gt;
&lt;li&gt;Upstarts like Brave target a niche of tech-savvy privacy-conscious users, which want an on-by-default ad blocker, so they have some breathing room here. Also they’re based on Chromium so that limits other kinds of breakage (e.g. all the “Chrome only” web apps launching these days, from Google and others), which buys them some goodwill.&lt;/li&gt;
&lt;li&gt;Safari enjoys a monopolistic position on iOS (since other browsers on iOS must use WebKit for rendering pages), and has good usage numbers on macOS, so they can get away with off-by-default third-party cookies and more recent things such as &lt;a href=&quot;https://webkit.org/blog/7675/intelligent-tracking-prevention/&quot;&gt;Intelligent Tracking Protection&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;span&gt;What Mozilla &lt;em&gt;is&lt;/em&gt; doing&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Note: I’m not an employee and not a big contributor to Mozilla projects. The information in this section comes from reading updates on Mozilla blogs and Twitter accounts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Back to Firefox. While I wish Mozilla would have their Tracking Protection feature on by default, I understand that it can be a hard sell: many Firefox users would not be aware of that feature or what its advantages are, and they would resent the occasional site breakage.&lt;/p&gt;
&lt;p&gt;Which is why Mozilla is experimenting with such features in Private Browsing Mode only: in this mode, users can expect things to maybe break, and are warned about it.&lt;/p&gt;
&lt;figure&gt;
  &lt;img alt=&quot;Screenshot of Firefox’s Private Browsing new tab page&quot; src=&quot;https://fvsch.com/articles/firefox-privacy-settings/private-browsing-page.png&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;In Private Browsing windows, &lt;a href=&quot;https://support.mozilla.org/en-US/kb/tracking-protection&quot;&gt;Tracking Protection&lt;/a&gt; is on by default, and Mozilla is working on &lt;a href=&quot;https://blog.mozilla.org/security/2018/01/31/preventing-data-leaks-by-stripping-path-information-in-http-referrers/&quot;&gt;shortening the Referer HTTP header for third-party domains&lt;/a&gt; so that sites can only know which site you’re coming from and not which specific page (or worse, what private information the URL contained, in some bad cases).&lt;/p&gt;
&lt;p&gt;Mozilla is also researching &lt;a href=&quot;https://blog.mozilla.org/data/2018/01/26/improving-privacy-without-breaking-the-web/&quot;&gt;how often privacy-centric restrictions break websites&lt;/a&gt;; basically they’re trying to figure out what they can do without losing a bunch of users.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mozilla.org/en-US/firefox/mobile/&quot;&gt;Firefox Focus&lt;/a&gt; is another attempt at privacy-by-default, launched as a separate product (and sub-brand).&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Privacy settings should be simpler&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Currently, a more private setup in Firefox requires:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Activating Tracking Protection for all sites in the “Privacy” page of the browser preferences. This setting is also way down in the page.&lt;/li&gt;
&lt;li&gt;Tweaking some settings in the “Privacy” page, some being a bit painful to get right (blocking third-party cookies or restricting cookies to the current section only).&lt;/li&gt;
&lt;li&gt;If you’re feeling even more adventurous, there are a bunch of settings to tweak in &lt;code&gt;about:config&lt;/code&gt;, for HTTP Referer and more.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This should be simpler, and more palatable to most users.&lt;/p&gt;
&lt;p&gt;My wish would be for Mozilla to design a combo of privacy features which users could activate in one click. And because nothing is ever new, &lt;em&gt;in spirit&lt;/em&gt; it would be somewhat close to what Microsoft did with Internet Explorer’s security levels:&lt;/p&gt;
&lt;figure&gt;
  &lt;img alt=&quot;The security pane of IE7’s Internet Options&quot; src=&quot;ie7-internet-zone-high&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;I don’t have fond memories of this setting because, as a developer, it meant that a client’s IE6 or IE7 would sometimes block all scripting. But in retrospect I do find that it had some merits as a preference, if not always in the specific technical choices that were made.&lt;/p&gt;
&lt;p&gt;Two, maybe three levels of protection could be offered:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Default (current behavior, which should not stop Mozilla from upgrading its privacy by blocking third-party cookies and maybe offering something similar to Safari’s Intelligent Tracking Protection…)&lt;/li&gt;
&lt;li&gt;More Private (enables Tracking Protection, disables third-party cookies altogether or limits them to the current session, and maybe adds some other technical restrictions such as truncating Referer and fingerprinting resistance.)&lt;/li&gt;
&lt;li&gt;Private Browsing Only (same as #2, with some added technical restrictions and all browsing is in Private Browsing Mode)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s a very rough idea and maybe these specific profiles don’t make sense. But I think it’s an idea worth pursuing, to offer better privacy defaults and options to all users.&lt;/p&gt;
&lt;p&gt;What’s more, this setting should not be hidden in the “Privacy” settings page, but should be near the top of the “General” settings page or at the top of the “Privacy” page at worst. It should also come with an onboarding campaign to explain the trade-offs of each mode and offer a chance to switch modes right there. Finally, there might be ways to reflect this setting — and maybe offer a chance to switch — on each tab, in the navigation bar or on the New Tab page itself.&lt;/p&gt;
&lt;p&gt;I have no idea if the rambling post can be of any help achieving better privacy by default in Firefox or any browser, but here it is anyway. Thanks for reading.&lt;/p&gt;</content>
	</entry>
	<entry xml:lang="en">
		<id>urn:uuid:c8076f02-e7e3-5e2f-a519-8b86426c00b7</id>
		<link rel="alternate" type="text/html" href="https://fvsch.com/css-print-background"/>
		<title>Printing background colors with CSS and SVG</title>
		<author><name>Florens Verschelde</name></author>
		<published>2018-01-24T00:00:00+01:00</published>
		<updated>2018-01-24T00:00:00+01:00</updated>
		<summary type="text">Web browsers do not print background colors by default. Here’s a trick for printing a flat color, e.g. if you have a white logo or graphic.</summary>
		<content type="html">&lt;p&gt;A client’s site had a white SVG logo on top of a color background.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;header style=&quot;background:black&quot;&amp;gt;
  &amp;lt;img src=&quot;https://fvsch.com/articles/css-print-background/white-logo.svg&quot;
       alt=&quot;The site’s name&quot;&amp;gt;
&amp;lt;/header&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When working on the print styles, we had a problem: browsers would disable background colors and print the logo white on white. And in this case we could not change the logo image or use a &lt;code&gt;filter: invert(100%)&lt;/code&gt; on it.&lt;/p&gt;
&lt;p&gt;We could have used the non-standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust&quot;&gt;&lt;code&gt;-webkit-print-color-adjust&lt;/code&gt;&lt;/a&gt; CSS property, but what about other browsers?&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Faking a background with SVG&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By default, browsers will print content images. We can inject one in our page and display it as a kind of background, in 3 steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a pseudo-element (&lt;code&gt;::before&lt;/code&gt; or &lt;code&gt;::after&lt;/code&gt;) and an image URL in the &lt;code&gt;content&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;For a solid color, we can make a simple SVG image, which works well as a data-URL.&lt;/li&gt;
&lt;li&gt;Finally, we use absolute positioning to place this pseudo-element in the background.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;header {
  position: relative;
}
header &amp;gt; * {
  position: relative;
  z-index: 2;
}
header::before {
  position: absolute;
  z-index: 1;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  overflow: hidden;
  /*
    In the URL, change the background to your own color.
    Warning: the `#` character must be escaped as %23,
    for example: `background:%23FFFF00` (yellow).
  */
  content: url(&quot;data:image/svg+xml,&amp;lt;svg xmlns=&apos;http://www.w3.org/2000/svg&apos; viewBox=&apos;0 0 10 10&apos; style=&apos;background:YOUR_COLOR_HERE&apos; /&amp;gt;&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is a fully working example in an iframe. You can test it by using your browser’s Print Preview on this page, or &lt;a href=&quot;https://fvsch.com/articles/css-print-background/example.html&quot;&gt;on the example page directly&lt;/a&gt;.&lt;/p&gt;
&lt;figure class=&quot;frame&quot;&gt;
  &lt;iframe class=&quot;autoheight&quot; src=&quot;https://fvsch.com/articles/css-print-background/example.html&quot; width=&quot;100%&quot; height=&quot;500&quot;&gt;&lt;/iframe&gt;
&lt;/figure&gt;
&lt;p&gt;As noted, it’s a nice trick but don’t overuse it: you could cost your users a lot of money in printer ink.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;Browser support&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Works on all modern desktop browsers (tested in Edge 14–16, Firefox 58–60, Chrome 61–63, Safari 11)&lt;/li&gt;
&lt;li&gt;Not tested on mobile browsers&lt;/li&gt;
&lt;li&gt;Does &lt;em&gt;not&lt;/em&gt; work in IE11&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;span&gt;Three things to watch out for&lt;/span&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The pseudo-element and the image are two separate entities. While the pseudo-element will cover its parent, the image will take the full width, and its height will depend on its aspect ratio. This means that the image will overflow its container, and we have to hide it with &lt;code&gt;overflow:hidden&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In this example, we use a square image (&lt;code&gt;viewBox=&apos;0 0 10 10&apos;&lt;/code&gt;); if you need a taller image, use a rectangle one (for example &lt;code&gt;viewBox=&apos;0 0 10 50&apos;&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Making sure that your &lt;code&gt;url(…)&lt;/code&gt; is a valid data-URL can be tricky. In practice there is not a lot to escape, but if you are using double quotes to wrap your URL you will need single quotes for the XML attributes, and vice-versa. The &lt;code&gt;#&lt;/code&gt; character identifies a fragment in URLs, so you will need to escape it using percent-encoding (&lt;code&gt;%23&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content>
	</entry>
</feed>
