A place to cache linked articles (think custom and personal wayback machine)
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

index.html 41KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Color Theme Switcher (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f0f0ea">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f0f0ea">
  26. <!-- Documented, feel free to shoot an email. -->
  27. <link rel="stylesheet" href="/static/david/css/style_2020-06-19.css">
  28. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  29. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <script type="text/javascript">
  36. function toggleTheme(themeName) {
  37. document.documentElement.classList.toggle(
  38. 'forced-dark',
  39. themeName === 'dark'
  40. )
  41. document.documentElement.classList.toggle(
  42. 'forced-light',
  43. themeName === 'light'
  44. )
  45. }
  46. const selectedTheme = localStorage.getItem('theme')
  47. if (selectedTheme !== 'undefined') {
  48. toggleTheme(selectedTheme)
  49. }
  50. </script>
  51. <meta name="robots" content="noindex, nofollow">
  52. <meta content="origin-when-cross-origin" name="referrer">
  53. <!-- Canonical URL for SEO purposes -->
  54. <link rel="canonical" href="https://mxb.dev/blog/color-theme-switcher/">
  55. <body class="remarkdown h1-underline h2-underline h3-underline hr-center ul-star pre-tick">
  56. <article>
  57. <header>
  58. <h1>Color Theme Switcher</h1>
  59. </header>
  60. <nav>
  61. <p class="center">
  62. <a href="/david/" title="Aller à l’accueil">🏠</a> •
  63. <a href="https://mxb.dev/blog/color-theme-switcher/" title="Lien vers le contenu original">Source originale</a>
  64. </p>
  65. </nav>
  66. <hr>
  67. <main>
  68. <p class="lead">Last year, the design gods decided that dark modes were the new hotness. "Light colors are for suckers", they laughed, drinking matcha tea on their fixie bikes or whatever.</p>
  69. <p>And so every operating system, app and even some websites (mine included) suddenly had to come up with a dark mode. Fortunately though, this coincided nicely with widespread support for CSS <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/--*">custom properties</a> and the introduction of a new <code>prefers-color-scheme</code> media query.</p>
  70. <p>There’s lots of tutorials on <a href="https://css-tricks.com/dark-modes-with-css/">how to build dark modes</a> already, but why limit yourself to light and dark? Only a Sith deals in absolutes.</p>
  71. <p>That’s why I decided to build a new feature on my site:<br/><strong>dynamic color themes!</strong> Yes, instead of two color schemes, I now have ten! That’s eight better than the previous website!</p>
  72. <p>Go ahead and try it, hit that <strong>paintroller-button</strong> in the header.<br/>I’ll wait.</p>
  73. <p><em>If you’re reading this somewhere else, the effect would look something like this:</em></p>
  74. <p class="extend"><video poster="/assets/media/theme-switcher/theme-switcher-still.png" preload="metadata" muted="" controls=""><source src="/assets/media/theme-switcher/theme-switcher.webm" type="video/webm"><source src="/assets/media/theme-switcher/theme-switcher.mp4" type="video/mp4"/></source></video></p>
  75. <p>Nice, right? Let’s look at how to do that!</p>
  76. <h2 id="h-define-color-schemes"><a class="heading-anchor" href="#h-define-color-schemes">#</a> Define Color Schemes</h2>
  77. <p>First up, we need some data. We need to define our themes in a central location, so they’re easy to access and edit. My site uses <a href="https://www.11ty.dev/">Eleventy</a>, which lets me create a simple JSON file for that purpose:</p>
  78. <pre class="language-json"><code class="language-json"><br/><span class="token punctuation">[</span><br/> <span class="token punctuation">{</span><br/> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"bowser"</span><span class="token punctuation">,</span><br/> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Bowser's Castle"</span><span class="token punctuation">,</span><br/> <span class="token property">"colors"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br/> <span class="token property">"primary"</span><span class="token operator">:</span> <span class="token string">"#7f5af0"</span><span class="token punctuation">,</span><br/> <span class="token property">"secondary"</span><span class="token operator">:</span> <span class="token string">"#2cb67d"</span><span class="token punctuation">,</span><br/> <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"#fffffe"</span><span class="token punctuation">,</span><br/> <span class="token property">"border"</span><span class="token operator">:</span> <span class="token string">"#383a61"</span><span class="token punctuation">,</span><br/> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#16161a"</span><span class="token punctuation">,</span><br/> <span class="token property">"primaryOffset"</span><span class="token operator">:</span> <span class="token string">"#e068fd"</span><span class="token punctuation">,</span><br/> <span class="token property">"textOffset"</span><span class="token operator">:</span> <span class="token string">"#94a1b2"</span><span class="token punctuation">,</span><br/> <span class="token property">"backgroundOffset"</span><span class="token operator">:</span> <span class="token string">"#29293e"</span><br/> <span class="token punctuation">}</span><br/> <span class="token punctuation">}</span><span class="token punctuation">,</span><br/> <span class="token punctuation">{</span>...<span class="token punctuation">}</span><br/><span class="token punctuation">]</span></code></pre>
  79. <p>Our color schemes are objects in an array, which is now available during build. Each theme gets a <code>name</code>, <code>id</code> and a couple of color definitions. The parts of a color scheme depend on your specific design; In my case, I assigned each theme eight properties.</p>
  80. <div class="callout callout--tip"><span class="callout__icon"><svg class="icon icon--lightbulb" role="img" aria-hidden="true"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons/icons.sprite.svg#icon-lightbulb"/></svg></span><div class="callout__content"><p>It's a good idea to give these properties logical names instead of visual ones like "light" or "muted", as colors vary from theme to theme. I've also found it helpful to define a couple of "offset" colors - these are used to adjust another color on interactions like hover and such.</p></div></div>
  81. <p>In addition to the “default” and “dark” themes I already had before, I created eight more themes this way. I used a couple of different sources for inspiration; the ones I liked best are <a href="https://color.adobe.com/explore">Adobe Color</a> and <a href="https://www.happyhues.co/">happyhues</a>.</p>
  82. <p>All my themes are named after Mario Kart 64 race tracks by the way, because why not.</p>
  83. <h2 id="h-transform-to-custom-css-properties"><a class="heading-anchor" href="#h-transform-to-custom-css-properties">#</a> Transform to Custom CSS Properties</h2>
  84. <p>To actually use our colors in CSS, we need them in a different format. Let’s create a stylesheet and make custom properties out of them. Using Eleventy’s template rendering, we can do that by generating a <code>theme.css</code> file from the data, looping over all themes. We’ll use a macro to output the color definitions for each.</p>
  85. <p>I wrote this in Nunjucks, the templating engine of my choice - but you can do it in any other language as well.</p>
  86. <pre class="language-css"><code class="language-css"><br/>---<br/><span class="token property">permalink</span><span class="token punctuation">:</span> <span class="token string">'/assets/css/theme.css'</span><br/><span class="token property">excludeFromSitemap</span><span class="token punctuation">:</span> true<br/>---<br/><br/><span class="token punctuation">{</span>% macro <span class="token function">colorscheme</span><span class="token punctuation">(</span>colors<span class="token punctuation">)</span> %<span class="token punctuation">}</span><br/> <span class="token selector">--color-bg:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.background <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-bg-offset:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.backgroundOffset <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-text:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.text <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-text-offset:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.textOffset <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-border:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.border <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-primary:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.primary <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-primary-offset:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.primaryOffset <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;<br/> --color-secondary:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> colors.secondary <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token selector">;</span><br/><span class="token punctuation">{</span>% endmacro %<span class="token punctuation">}</span><br/><br/><br/><span class="token punctuation">{</span>%- set default = themes|<span class="token function">getTheme</span><span class="token punctuation">(</span><span class="token string">'default'</span><span class="token punctuation">)</span> -%<span class="token punctuation">}</span><br/><span class="token punctuation">{</span>%- set dark = themes|<span class="token function">getTheme</span><span class="token punctuation">(</span><span class="token string">'dark'</span><span class="token punctuation">)</span> -%<span class="token punctuation">}</span><br/><br/><br/><span class="token selector">:root</span> <span class="token punctuation">{</span><br/> <span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token function">colorscheme</span><span class="token punctuation">(</span>default.colors<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><br/><span class="token punctuation">}</span><br/><br/><span class="token atrule"><span class="token rule">@media</span><span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br/> <span class="token selector">:root</span> <span class="token punctuation">{</span><br/> <span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token function">colorscheme</span><span class="token punctuation">(</span>dark.colors<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><br/> <span class="token punctuation">}</span><br/><span class="token punctuation">}</span><br/><br/><br/><span class="token punctuation">{</span>% for theme in themes %<span class="token punctuation">}</span><br/><span class="token selector">html[data-theme='{{ theme.id }}']</span> <span class="token punctuation">{</span><br/> <span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token function">colorscheme</span><span class="token punctuation">(</span>theme.colors<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><br/><span class="token punctuation">}</span><br/><span class="token punctuation">{</span>% endfor %<span class="token punctuation">}</span><br/></code></pre>
  87. <h2 id="h-using-colors-on-the-website"><a class="heading-anchor" href="#h-using-colors-on-the-website">#</a> Using colors on the website</h2>
  88. <p>Now for the tedious part - we need to go through all of the site’s styles and replace every color definition with the corresponding custom property. This is different for every site - but your code might look like this if it’s written in SCSS:</p>
  89. <pre class="language-scss"><code class="language-scss"><span class="token selector">body </span><span class="token punctuation">{</span><br/> <span class="token property">font-family</span><span class="token punctuation">:</span> sans-serif<span class="token punctuation">;</span><br/> <span class="token property">line-height</span><span class="token punctuation">:</span> <span class="token variable">$line-height</span><span class="token punctuation">;</span><br/> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token variable">$gray-dark</span><span class="token punctuation">;</span><br/><span class="token punctuation">}</span></code></pre>
  90. <p>Replace the static SCSS variable with the theme’s custom property:</p>
  91. <pre class="language-scss"><code class="language-scss"><span class="token selector">body </span><span class="token punctuation">{</span><br/> <span class="token property">font-family</span><span class="token punctuation">:</span> sans-serif<span class="token punctuation">;</span><br/> <span class="token property">line-height</span><span class="token punctuation">:</span> <span class="token variable">$line-height</span><span class="token punctuation">;</span><br/> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-text<span class="token punctuation">)</span><span class="token punctuation">;</span><br/><span class="token punctuation">}</span></code></pre>
  92. <div class="callout callout--warning"><span class="callout__icon"><svg class="icon icon--warning" role="img" aria-hidden="true"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons/icons.sprite.svg#icon-warning"/></svg></span><div class="callout__content"><p><strong>Attention:</strong> Custom Properties are supported in <a href="https://caniuse.com/#search=custom%20properties">all modern browsers</a>, but if you need to support IE11 or Opera Mini, be sure to provide a fallback.</p></div></div>
  93. <p>It’s fine to mix static preprocessor variables and custom properties by the way - they do different things. Our line height is not going to change dynamically.</p>
  94. <p>Now do this for every instance of <code>color</code>, <code>background</code>, <code>border</code>, <code>fill</code> … you get the idea. Told you it was gonna be tedious.</p>
  95. <h2 id="h-building-the-theme-switcher"><a class="heading-anchor" href="#h-building-the-theme-switcher">#</a> Building the Theme Switcher</h2>
  96. <p>If you made it this far, congratulations! Your website is now themeable (in theory). We still need a way for people to switch themes without manually editing the markup though, that’s not very user-friendly. We need some sort of UI component for this - a theme switcher.</p>
  97. <h3 id="h-generating-the-markup"><a class="heading-anchor" href="#h-generating-the-markup">#</a> Generating the Markup</h3>
  98. <p>The switcher structure is pretty straightforward: it’s essentially a list of buttons, one for each theme. When a button is pressed, we’ll switch colors. Let’s give the user an idea what to expect by showing the theme colors as little swatches on the button:</p>
  99. <figure class="extend"><img src="/assets/media/theme-switcher/theme-buttons.jpg" loading="lazy" alt="a row of buttons, showing the theme name and color swatches"/><figcaption>Fact: All good design is derivative of Mario Kart</figcaption></figure>
  100. <p>Here’s the template to generate that markup. We’ll use inline style attributes here to display the background, text and accent colors. The button also holds its <code>id</code> in a <code>data-theme-id</code> attribute, we will pick that up with Javascript later.</p>
  101. <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><br/>{% for theme in themes %}<br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__item<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__btn<span class="token punctuation">"</span></span> <span class="token attr-name">data-theme-id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ theme.id }}<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.background <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__name<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.text <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.name }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__palette<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__hue<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.primary <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.colors.primary }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__hue<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.secondary <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.colors.secondary }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__hue<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.border <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.colors.border }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__hue<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.text <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.colors.text }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>themeswitcher__hue<span class="token punctuation">"</span></span><span class="token style-attr language-css"><span class="token attr-name"> <span class="token attr-name">style</span></span><span class="token punctuation">="</span><span class="token attr-value"><span class="token selector">background-color:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> theme.colors.textOffset <span class="token punctuation">}</span><span class="token punctuation">}</span></span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>{{ theme.colors.textOffset }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><br/>{% endfor %}<br/><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><br/></code></pre>
  102. <p>There’s some styling involved as well, but I’ll leave that out for brevity here. If you’re interested in the extended version, you can find all the code in <a href="https://github.com/maxboeck/mxb">my site’s github repo</a>.</p>
  103. <h3 id="h-setting-the-theme"><a class="heading-anchor" href="#h-setting-the-theme">#</a> Setting the Theme</h3>
  104. <p>The last missing piece is some Javascript to handle the switcher functionality.</p>
  105. <pre class="language-js"><code class="language-js"><br/><span class="token keyword">class</span> <span class="token class-name">ThemeSwitcher</span> <span class="token punctuation">{</span><br/> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br/> <br/> <span class="token keyword">this</span><span class="token punctuation">.</span>activeTheme <span class="token operator">=</span> <span class="token string">'default'</span><br/> <span class="token keyword">this</span><span class="token punctuation">.</span>hasLocalStorage <span class="token operator">=</span> <span class="token keyword">typeof</span> Storage <span class="token operator">!==</span> <span class="token string">'undefined'</span><br/><br/> <br/> <span class="token keyword">this</span><span class="token punctuation">.</span>themeSelectBtns <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'button[data-theme-id]'</span><span class="token punctuation">)</span><br/> <br/> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>themeSelectBtns<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">btn</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span><br/> <span class="token keyword">const</span> id <span class="token operator">=</span> btn<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>themeId<br/> btn<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setTheme</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">)</span><br/> <span class="token punctuation">}</span><span class="token punctuation">)</span><br/> <span class="token punctuation">}</span><br/><span class="token punctuation">}</span><br/><br/><br/><br/><span class="token keyword">if</span> <span class="token punctuation">(</span>window<span class="token punctuation">.</span><span class="token constant">CSS</span> <span class="token operator">&amp;&amp;</span> <span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">supports</span><span class="token punctuation">(</span><span class="token string">'color'</span><span class="token punctuation">,</span> <span class="token string">'var(--fake-var)'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br/> <span class="token keyword">new</span> <span class="token class-name">ThemeSwitcher</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br/><span class="token punctuation">}</span></code></pre>
  106. <p>When somebody switches themes, we’ll take the theme id and set is as the <code>data-theme</code> attribute on the document. That will trigger the corresponding selector in our <code>theme.css</code> file, and the chosen color scheme will be applied.</p>
  107. <p>Since we want the theme to persist even when the user reloads the page or navigates away, we’ll save the selected id in <code>localStorage</code>.</p>
  108. <pre class="language-js"><code class="language-js"><span class="token function">setTheme</span><span class="token punctuation">(</span><span class="token parameter">id</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br/> <br/> <span class="token keyword">this</span><span class="token punctuation">.</span>activeTheme <span class="token operator">=</span> id<br/> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'data-theme'</span><span class="token punctuation">,</span> id<span class="token punctuation">)</span><br/><br/> <br/> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>hasLocalStorage<span class="token punctuation">)</span> <span class="token punctuation">{</span><br/> localStorage<span class="token punctuation">.</span><span class="token function">setItem</span><span class="token punctuation">(</span><span class="token string">"theme"</span><span class="token punctuation">,</span> id<span class="token punctuation">)</span><br/> <span class="token punctuation">}</span><br/><span class="token punctuation">}</span></code></pre>
  109. <p>On a server-rendered site, we could store that piece of data in a cookie instead and apply the theme id to the html element before serving the page. Since we’re dealing with a static site here though, there is no server-side processing - so we have to do a small workaround.</p>
  110. <p>We’ll retrieve the theme from <code>localStorage</code> in a tiny additional script in the head, right after the stylesheet is loaded. Contrary to the rest of the Javascript, we want this to execute as early as possible to avoid a FODT (“flash of default theme”).</p>
  111. <p>OK that’s not actually a real term. I made that up.</p>
  112. <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/css/main.css<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><br/> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"><br/> <br/> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'theme'</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <br/> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'data-theme'</span><span class="token punctuation">,</span> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'theme'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br/> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span><br/><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">&gt;</span></span></code></pre>
  113. <p>If no stored theme is found, the site uses the default color scheme (either light or dark, depending on the users <a href="https://web.dev/prefers-color-scheme/">system preference</a>).</p>
  114. <h2 id="h-get-creative"><a class="heading-anchor" href="#h-get-creative">#</a> Get creative</h2>
  115. <p>You can create any number of themes this way, and they’re not limited to flat colors either - with some extra effort you can have patterns, gradients or even GIFs in your design. Although just because you can doesn’t always mean you should, as is evidenced by my site’s new <em>Rainbow Road</em> theme.</p>
  116. <p>Please don’t use that one.</p>
  117. </main>
  118. </article>
  119. <hr>
  120. <footer>
  121. <p>
  122. <a href="/david/" title="Aller à l’accueil">🏠</a> •
  123. <a href="/david/log/" title="Accès au flux RSS">🤖</a> •
  124. <a href="http://larlet.com" title="Go to my English profile" data-instant>🇨🇦</a> •
  125. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">📮</a> •
  126. <abbr title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340">🧚</abbr>
  127. </p>
  128. <template id="theme-selector">
  129. <form>
  130. <fieldset>
  131. <legend>Thème</legend>
  132. <label>
  133. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  134. </label>
  135. <label>
  136. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  137. </label>
  138. <label>
  139. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  140. </label>
  141. </fieldset>
  142. </form>
  143. </template>
  144. </footer>
  145. <script type="text/javascript">
  146. function loadThemeForm(templateName) {
  147. const themeSelectorTemplate = document.querySelector(templateName)
  148. const form = themeSelectorTemplate.content.firstElementChild
  149. themeSelectorTemplate.replaceWith(form)
  150. form.addEventListener('change', (e) => {
  151. const chosenColorScheme = e.target.value
  152. localStorage.setItem('theme', chosenColorScheme)
  153. toggleTheme(chosenColorScheme)
  154. })
  155. const selectedTheme = localStorage.getItem('theme')
  156. if (selectedTheme && selectedTheme !== 'undefined') {
  157. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  158. }
  159. }
  160. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  161. window.addEventListener('load', () => {
  162. let hasDarkRules = false
  163. for (const styleSheet of Array.from(document.styleSheets)) {
  164. let mediaRules = []
  165. for (const cssRule of styleSheet.cssRules) {
  166. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  167. continue
  168. }
  169. // WARNING: Safari does not have/supports `conditionText`.
  170. if (cssRule.conditionText) {
  171. if (cssRule.conditionText !== prefersColorSchemeDark) {
  172. continue
  173. }
  174. } else {
  175. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  176. continue
  177. }
  178. }
  179. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  180. }
  181. // WARNING: do not try to insert a Rule to a styleSheet you are
  182. // currently iterating on, otherwise the browser will be stuck
  183. // in a infinite loop…
  184. for (const mediaRule of mediaRules) {
  185. styleSheet.insertRule(mediaRule.cssText)
  186. hasDarkRules = true
  187. }
  188. }
  189. if (hasDarkRules) {
  190. loadThemeForm('#theme-selector')
  191. }
  192. })
  193. </script>
  194. </body>
  195. </html>