<?xml version="1.0" encoding="UTF-8"?>
  <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/">
  <channel>
    <title>Jean Tinland</title>
    <atom:link href="https://www.jeantinland.com/feed/" rel="self" type="application/rss+xml" />
    <link>https://www.jeantinland.com</link>
    <description>A lot of words that may or may not be all useful! Take what you need :)</description>
    <lastBuildDate>Sat Apr 04 2026 10:14:07 GMT+0200 (Central European Summer Time)</lastBuildDate>
    <language>en-EN</language>
    <sy:updatePeriod>weekly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
  <title>Automatic uptime checks from my phone</title>
  <link>https://www.jeantinland.com/blog/automatic-uptime-checks-from-my-phone/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/automatic-uptime-checks-from-my-phone/</guid>
  <description><![CDATA[A simple method to monitor the uptime of my apps using Vercel's free plan and my iPhone.]]></description>
  <content:encoded><![CDATA[<p>Tired of being blind regarding the availability of my websites and services, I created a really basic setup allowing myself to be alerted in less than an hour if any of my apps fail to respond.</p><p>All my apps are running on a single OVH VPS. Knowing this, I couldn&#39;t setup a monitoring tool on the same infrastructure just in case of global failure.</p><h2 id="finding-another-hosting-platform">Finding another hosting platform <a class="heading-anchor" href="#finding-another-hosting-platform">#</a></h2><p>Having used Next.js a lot at my latest job, I deployed a lot of things on Vercel&#39;s platform. I also knew a free tier was available allowing hosting small apps with low traffic for personal use: a status checker app was the perfect use-case.</p><p>On top of that, a project specific <code>CRONTAB</code> allows for an easy task scheduling.</p><h2 id="the-api">The API <a class="heading-anchor" href="#the-api">#</a></h2><p>Next.js synergy with Vercel made me choose to spawn a minimalist Next.js app exposing a single <code>/api/check</code> endpoint.</p><p>When calling this route protected by a secret, a simple <code>.json</code> configuration file is parsed. From it, a collection of URLs to call is iterated over. On success, everything is fine, continue... On failure, a configurable number of retries is made. Definitive errors are listed inside a simple report sent via email through <code>nodemailer</code> alerting me of any app failure relatively fast. <em>I couldn&#39;t think of a simpler solution.</em></p><p>Below the content of such an email:</p><div class="code-block :brd-grd code-block--no-language code-block--no-line-numbers" data-label="ansi"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">The following endpoint failed health checks:</span></span><span class="line"></span><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">Name: jeantinland.com</span></span><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">URL: https://www.jeantinland.com</span></span><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">Attempts: 3</span></span><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">Timeout: 3000ms</span></span><span class="line"><span style="color:#1e2737;--shiki-dark:#bbbbbb">Checked at: 2026-01-20T17:49:36.211Z</span></span><span class="line"><span style="color:#e78482;--shiki-light-font-weight:bold;--shiki-dark:#e78482;--shiki-dark-font-weight:bold">Error: HTTP 403 Forbidden</span></span></code></pre></div><h2 id="triggering-the-checkup">Triggering the checkup <a class="heading-anchor" href="#triggering-the-checkup">#</a></h2><p>My first approach was simply to set up a repeating <code>CRON</code> task every hour of the day <strong>except the &quot;Hobby&quot; plan wouldn&#39;t allow more than a daily task</strong>. A status checker checking if things are working properly only one time a day isn&#39;t really useful. I&#39;m not running critical services that need permanent observability but triggering a check every hour seemed the right amount to aim for when I imagined the solution.</p><h2 id="the-missing-piece">The missing piece <a class="heading-anchor" href="#the-missing-piece">#</a></h2><p>As the integrated <code>CRONTAB</code> wasn&#39;t an option, I first created a local <code>CRON</code> job on my laptop knowing well enough it was only a band aid as it was going to fail as soon as I close the lid off.</p><p><em>Then, it clicked.</em></p><p>The real need was to be able to send a <code>GET</code> request reliably every hour. And <strong>if my laptop isn&#39;t open all day long, my phone is</strong>.</p><p>Thanks to the &quot;Automation&quot; system available out of the box on iPhone, it is possible to do exactly so.</p><p>I created 15 identical tasks:</p><ul><li><strong>When</strong>: &quot;Every hour from 07:30AM to 09:30PM&quot;</li><li><strong>Do</strong>: &quot;Get contents of <code>.../api/check?secret=xxx</code>&quot;</li></ul><p><strong>That&#39;s it.</strong></p><h2 id="afterthought">Afterthought <a class="heading-anchor" href="#afterthought">#</a></h2><p>Looking at this setup, we can also think of eliminating the intermediate API and only use iOS shortcuts by creating an automation for each app to monitor and analyze the returned content in order to detect failures.</p><p>This approach would multiply the number of automations needed by a factor of the number of checks needed each across the day.</p><p>The API allows centralizing everything in a single place and handling check-up and retry logic.</p>]]></content:encoded>
</item>
<item>
  <title>FYI there is more to the HTML id attribute</title>
  <link>https://www.jeantinland.com/blog/fyi-there-is-more-to-the-html-id-attribute/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/fyi-there-is-more-to-the-html-id-attribute/</guid>
  <description><![CDATA[Understanding the HTML id attribute beyond simple identification]]></description>
  <content:encoded><![CDATA[<p>I always used the <em>HTML</em><code>id</code> attribute without asking myself why it must be unique within a document. I simply assumed it was the role of an id to be unique.</p><p>Mind you all, I stumbled on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/id" target="_blank"  rel='noopener noreferrer'>mdn documentation page</a>, and there is more to this simple attribute.</p><p>When you declare an <code>id</code> attribute, the <em>HTML</em> element on which it is attached is automatically exposed on the global <code>window</code> object.</p><p>This allows for quick access to this element from anywhere in your page.</p><copy-button></copy-button><div class="code-block :brd-grd code-block--no-line-numbers" data-label="javascript"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// This</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> element</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> document</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">getElementById</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#98A8C5">"myElementId"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// Is equivalent to</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> element</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> window</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">myElementId</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span></code></pre></div><p>The only caveat is name collisions: your <code>ìd</code> attribute value must be a valid JavaScript identifier and must not collide with existing properties on the <code>window</code> object.</p>]]></content:encoded>
</item>
<item>
  <title>A naive accessible CSS tooltips implementation</title>
  <link>https://www.jeantinland.com/blog/a-naive-accessible-css-tooltips-implementation/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/a-naive-accessible-css-tooltips-implementation/</guid>
  <description><![CDATA[Implementing a basic accessible tooltip system in pure HTML/CSS.]]></description>
  <content:encoded><![CDATA[<p>Looking for a deep rabbit hole? A tooltip system covering all edge cases is exactly what you are looking for.</p><p>That said, we will work on the opposite here: how to implement the most basic accessible tooltip system in pure <em>HTML/CSS</em>. <em>The <code>title</code> HTML attribute will not be covered, but that would in fact be the absolute winner in a &quot;simplest tooltip system&quot; contest.</em></p><p>If you want to skip the explanations and get directly to the code, head to the <a href="#the-full-css-implementation" target="_self"  >full CSS implementation</a> section.</p><h2 id="what-do-we-need">What do we need? <a class="heading-anchor" href="#what-do-we-need">#</a></h2><p>Our system will leverage both the <code>data-attributes</code><em>HTML</em> specification and the <em>WAI</em> standard. Meaning, a lot of <code>aria-label</code> and <code>data-tooltip</code> are to be seen below.</p><h2 id="first-the-html-markup">First, the <em>HTML</em> markup <a class="heading-anchor" href="#first-the-html-markup">#</a></h2><p>We&#39;ll create a simple <code>aria-label</code> on the desired element: a button containing an <em>SVG</em> icon.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-label</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"RSS Feed"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-hidden</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"true"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/icons/rss.svg#icon"</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><blockquote><p>Do not forget to set an <code>aria-hidden=&quot;true&quot;</code> attribute on decorative icons. Otherwise, they will be announced by screen readers regardless of their uselessness.</p></blockquote><p>Now that we have an accessible button, a <code>data-tooltip</code> attribute needs to be added in order to flag elements on which we want to display a tooltip.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-label</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"RSS Feed"</span><span style="color:#E78482;--shiki-dark:#E78482"></span><span class="highlighted-word"><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">""</span></span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-hidden</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"true"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/icons/rss.svg#icon"</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>Everything is in place; we&#39;ll go on with the <em>CSS</em>.</p><h2 id="then-a-minimalist-stylesheet">Then, a minimalist stylesheet <a class="heading-anchor" href="#then-a-minimalist-stylesheet">#</a></h2><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">] {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> relative</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> --offset</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 8</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> attr</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#39465E;--shiki-dark:#FFF9EE">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> absolute</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> translateX</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">-50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> max-content</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> padding</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 6</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 10</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> black</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background-color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> white</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> font-size</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 15</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> white-space</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> nowrap</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> pointer-events</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transition</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> opacity </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">160</span><span style="color:#39465E;--shiki-dark:#FFF9EE">ms</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 10</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>These rules are laying the foundations of our tooltip system. <code>--offset</code> variable can be adjusted to fit your need. Same for <code>padding</code>, <code>color</code>, <code>background-color</code>, <code>font-size</code> and <code>transition</code> properties.</p><p>A last required set of rules needs to be defined in order to make everything work.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> not</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> hover</span><span style="color:#39465E;--shiki-dark:#FFF9EE">)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:active</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#E78482;--shiki-dark:#E78482"> :focus:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:focus-visible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">))</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> hover</span><span style="color:#39465E;--shiki-dark:#FFF9EE">)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#E78482;--shiki-dark:#E78482"> :focus-visible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>The <code>@media not (hover: hover)</code> media query disables the tooltip system on touch devices.</p><p><code>:is(:active, :focus:not(:focus-visible))</code> also disables the tooltip on an active or focused element but ignore keyboard-focused elements.</p><p>The last rule wrapped inside the <code>@media (hover: hover)</code> simply makes the tooltip associated with the hovered element visible.</p><p>We&#39;re good to go: we now have a really basic system displaying a bottom tooltip on hovered elements having both an <code>aria-label</code> &amp; <code>data-tooltip</code> attributes.</p><h2 id="going-further">Going further <a class="heading-anchor" href="#going-further">#</a></h2><p>As our tooltips are always displayed at the bottom, we may face problems: tooltips can be shown outside the viewport. It can easily be circumvented by adding a direction to our <code>data-tooltip</code> attributes when needed.</p><p>Supposing our button is displayed in the footer of your website, add a <code>top</code> value in the existing <code>data-tooltip</code> attribute.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-label</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"RSS Feed"</span><span style="color:#E78482;--shiki-dark:#E78482"> data-tooltip</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5" class="highlighted-word">"top"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> aria-hidden</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"true"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/icons/rss.svg#icon"</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>The following <em>CSS</em> rules handle displaying tooltips in all directions.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top-left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top-right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"bottom-left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"bottom-right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>It also relies on the <code>--offset</code> custom property providing a uniform positioning across tooltips displayed in every direction.</p><p>This is it, a complete <em>naive</em> tooltip system with ~85 lines of <em>CSS</em>. <strong>It is framework agnostic and won&#39;t require any maintenance</strong>.</p><h2 id="and-of-course-the-caveats">And of course, the caveats <a class="heading-anchor" href="#and-of-course-the-caveats">#</a></h2><p>With these 85 lines, we are far from the <a href="https://www.radix-ui.com/primitives/docs/components/tooltip" target="_blank"  rel='noopener noreferrer'><em>Radix UI</em></a> or <a href="https://base-ui.com/react/components/tooltip" target="_blank"  rel='noopener noreferrer'><em>Base UI</em></a> implementation.</p><p>We have indeed no collision detection, automatic positioning, or accessibility announcements when displayed.</p><p>That said, both collision and positioning should not be vital, as you should keep the length of your tooltip to the shortest possible. <strong>Be concise; nobody wants to read a whole novel when hovering over a button or a link.</strong> Tooltips should be used to give extra context on an element, not to explain everything about it.</p><h2 id="the-full-css-implementation">The full CSS implementation <a class="heading-anchor" href="#the-full-css-implementation">#</a></h2><p>In case you want to copy/paste the full implementation, here it is:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">] {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> relative</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> --offset</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 8</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> attr</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#39465E;--shiki-dark:#FFF9EE">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> absolute</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> translateX</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">-50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> max-content</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> padding</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 6</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 10</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> black</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background-color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> white</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> font-size</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 15</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> white-space</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> nowrap</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> pointer-events</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transition</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> opacity </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">160</span><span style="color:#39465E;--shiki-dark:#FFF9EE">ms</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 10</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> not</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> hover</span><span style="color:#39465E;--shiki-dark:#FFF9EE">)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:active</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#E78482;--shiki-dark:#E78482"> :focus:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:focus-visible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">))</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> hover</span><span style="color:#39465E;--shiki-dark:#FFF9EE">)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:hover</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#E78482;--shiki-dark:#E78482"> :focus-visible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top-left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"top-right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"bottom-left"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">[</span><span style="color:#E78482;--shiki-dark:#E78482">aria-label</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">][</span><span style="color:#E78482;--shiki-dark:#E78482">data-tooltip</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"bottom-right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> +</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> var</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">--offset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">));</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> unset</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div>]]></content:encoded>
</item>
<item>
  <title>Optional web components</title>
  <link>https://www.jeantinland.com/blog/optional-web-components/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/optional-web-components/</guid>
  <description><![CDATA[How to use web components in a progressive enhancement way, making them optional for users without JavaScript.]]></description>
  <content:encoded><![CDATA[<p>Custom elements are a lot! They provide a way of building reusable (or not) pieces of interface that will stay strong over passing time. Its being a web standard brings a certain peace of mind when using it. On top of that, total control can be achieved over the level of isolation it needs from the parent app or website.</p><p>However, given that a good amount of people are walking around with <em>JavaScript</em> fully disabled, your website shouldn&#39;t rely on it for critical UI parts.</p><blockquote><p>My remarks are not aimed at complex web apps or UX parts that could never work without JS enabled: this isn&#39;t what is targeted here.</p></blockquote><h2 id="a-right-fitting-use-case">A right-fitting use case <a class="heading-anchor" href="#a-right-fitting-use-case">#</a></h2><p>This website exposes a <code>&lt;theme-selector /&gt;</code> in the sticky navigation. This selector is a web component displaying a simple select with the 3 following options:</p><ul><li>Auto = system theme</li><li>Light = forced light theme</li><li>Dark = forced dark theme</li></ul><p>The default option being &quot;auto&quot;, this website will automatically sync with the visitor system theme. Once the component is loaded, it detects if the user has already chosen a theme, in which case its choice will be stored in <code>window.localStorage</code>, then applies it. But what happens if the user has disabled JS?</p><p><strong>Simply nothing</strong>.</p><p>In case <em>JavaScript</em> is never executed, the website is only synced with the system theme. Nothing indicating this information is shown to the user, and that&#39;s too bad.</p><h2 id="progressive-enhancement">Progressive enhancement <a class="heading-anchor" href="#progressive-enhancement">#</a></h2><p>I found out the best way to reconcile this web component with a version of this website served without <em>JavaScript</em> was to initialize it this way:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">theme-selector</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">":btn"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> aria-label</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"Theme (needs JS)"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> data-tooltip</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> disabled</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">Auto </span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">theme-selector</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p><code>&lt;theme-selector /&gt;</code> now has a <code>button</code> as a child displaying a default value. It also has a <code>aria-label</code> showing a &quot;Needs JS&quot; message when hovered and is also <code>disabled</code> at the start. The web component is handling styles with its <code>class</code> attribute: that way it will be correctly displayed with or without JS.</p><p>Now, if JS is allowed to run, the web component will replace its <code>innerHTML</code> with a shadow root containing a <code>select</code> with the three possible options. The web component will also update the <code>aria-label</code> and remove the <code>disabled</code> attribute in its <code>connectedCallback</code>:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="javascript | theme-selector.js"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">connectedCallback</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">() {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> this</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">setAttribute</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#98A8C5">"aria-label"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "Change theme"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> this</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">removeAttribute</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#98A8C5">"disabled"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> /* ... */</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>This approach brings two main benefits:</p><ul><li>A disabled theme selector is still displayed, showing a tooltip indicating it needs JS to work correctly.</li><li>A placeholder is displayed in case <code>theme-selector.js</code> is loaded from a really slow network preventing a layout shift.</li></ul><h2 id="another-example">Another example <a class="heading-anchor" href="#another-example">#</a></h2><p>In a more simple use case, <a href="https://www.jeantinland.com/toolbox/simple-bar/" target="_blank"  rel='noopener noreferrer'>like this animated demo</a>, a fallback image is displayed in case <em>JavaScript</em> is not enabled:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">simple-bar-demo</span><span style="color:#E78482;--shiki-dark:#E78482"> playing</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"true"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">noscript</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"introduction__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/toolbox/simple-bar/preview.jpg"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> alt</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"simple-bar demo"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">noscript</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">simple-bar-demo</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>Here, <strong>the <code>noscript</code> tag is important, as without it, the fallback image would start to load immediately</strong> before the <code>&lt;simple-bar-demo /&gt;</code> web component is fully loaded. It is needed in order to prevent unnecessary assets loading.</p><p>There are surely more applications than I can think of!</p>]]></content:encoded>
</item>
<item>
  <title>120kb less</title>
  <link>https://www.jeantinland.com/blog/120kb-less/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/120kb-less/</guid>
  <description><![CDATA[A story about how I reduced my homepage load size by about 65% just by using a different grain effect implementation]]></description>
  <content:encoded><![CDATA[<p>After quite some times working on rebuilding my website from the ground up and trying to spare user from downloading every useless bytes, I just found out that I was forcing a nice download of <strong>120kb</strong> only for displaying a really discrete grain effect over the background.</p><p><strong>If it seems easy to plan then orchestrate a full optimization of a system or a website, it is another set of affairs preventing long term degradation</strong>.</p><p>As I&#39;m not planning on building an observability system for my personal website, I&#39;ll have to be careful with asset additions and changes like this one.</p><h2 id="the-culprit">The culprit <a class="heading-anchor" href="#the-culprit">#</a></h2><p>Once done with every thinkable tweak allowing for the lightest payload possible, I introduced, on second thought, a film-like grain effect applied over the background of my website. It was a simple <code>.png</code> file of <code>250px/250px</code>, I didn&#39;t think much of its size. On top of that, it was converted and served either as a <code>webp</code> or an <code>avif</code> depending on the browser capabilities.</p><p>Taking my homepage as an example, with this new <code>grain.avif</code> image, I was going from these stats:</p><copy-button></copy-button><div class="code-block :brd-grd code-block--no-language code-block--no-line-numbers" data-label="ansi"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">35 requests</span></span><span class="line"><span style="color:#8fc8bb;--shiki-light-font-weight:bold;--shiki-dark:#8fc8bb;--shiki-dark-font-weight:bold">27.4 kB transferred</span></span><span class="line"><span style="color:#8fc8bb;--shiki-light-font-weight:bold;--shiki-dark:#8fc8bb;--shiki-dark-font-weight:bold">66.1 kB resources</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">Finish: 10.95s</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">DOMContentLoaded: 4.71 s</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">Load: 8.91 s</span></span></code></pre></div><p>To these:</p><copy-button></copy-button><div class="code-block :brd-grd code-block--no-language code-block--no-line-numbers" data-label="ansi"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">36 requests</span></span><span class="line"><span style="color:#e78482;--shiki-light-font-weight:bold;--shiki-dark:#e78482;--shiki-dark-font-weight:bold">147 kB transferred</span></span><span class="line"><span style="color:#e78482;--shiki-light-font-weight:bold;--shiki-dark:#e78482;--shiki-dark-font-weight:bold">185 kB resources</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">Finish: 11.33 s</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">DOMContentLoaded: 4.67 s</span></span><span class="line"><span style="color:#1e273780;--shiki-dark:#bbbbbb80">Load: 9.26 s</span></span></code></pre></div><blockquote><p><em>Stats recorded with a 3G throttled connection.</em></p></blockquote><p>The impact on loading time is anecdotal, but multiplying the transferred size by more than 5 feels like chopping my optimization work with an axe.</p><h2 id="a-simple-fix">A simple fix <a class="heading-anchor" href="#a-simple-fix">#</a></h2><p>At first, I was tempted to simply remove this effect, thus immediately resolving the issue. Except I&#39;m quite fond of the texture it adds to my otherwise bland background.</p><p>Then, I searched for an alternate way of making a noisy grain effect that would be lighter and found it in the <code>svg</code> specification.</p><p>The following simple code was the answer:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="xml | grain.svg"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> viewBox</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'0 0 110 110'</span><span style="color:#E78482;--shiki-dark:#E78482"> xmlns</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'http://www.w3.org/2000/svg'</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">filter</span><span style="color:#E78482;--shiki-dark:#E78482"> id</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'grain'</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">feTurbulence</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> type</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'fractalNoise'</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> baseFrequency</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'0.55'</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> numOctaves</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'3'</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> stitchTiles</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'stitch'</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">filter</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">rect</span><span style="color:#E78482;--shiki-dark:#E78482"> width</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'100%'</span><span style="color:#E78482;--shiki-dark:#E78482"> height</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'100%'</span><span style="color:#E78482;--shiki-dark:#E78482"> filter</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">'url(#grain)'</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>This <code>svg</code> file weighs only <strong>292b</strong>: more than <strong>119kb</strong> less than the previous image.</p><h2 id="the-result">The result <a class="heading-anchor" href="#the-result">#</a></h2><p>Here, side by side, are the old and new noisy grain effects:</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/120kb-less/comparison.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/120kb-less/comparison.webp" type="image/webp"/><img src="/_generated/bare/images/blog/120kb-less/comparison.jpg" alt="" loading="eager" width="940" height="522"/></picture></div></div></div></div></p><p>It is clearly not the exact grain when shown bare like that, but when used as an overlay, the effect is pretty much the same using this bit of <code>css</code> and adjusting opacity:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css | grain.css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.grain</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> absolute</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> inset</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> repeat</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> center</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">/</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">110</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> url</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/grain.svg"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border-radius</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line highlighted"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 20</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> pointer-events</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p><code>opacity</code> needs to be adjusted: on a light background you may need to set it higher than on a dark one. <code>mix-blend-mode</code> can also be used in order to improve the grain integration depending on the color of your background.</p><h2 id="bonus-a-live-noise-grain-generator">Bonus: a live noise grain generator <a class="heading-anchor" href="#bonus-a-live-noise-grain-generator">#</a></h2><p>You&#39;ll find below a small web component I built in order to generate your own grain effect <code>svg</code> file with custom parameters. You can adjust the <code>baseFrequency</code> and <code>numOctaves</code> of the grain effect. Opacity can also be adjusted and text toggled for preview purposes.</p><grain-generator><noscript><div class=":alrt"><code>&lt;grain-generator &#x2F;&gt;</code> needs JavaScript to bo enabled in order to work correctly. </div></noscript></grain-generator><p>See <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feTurbulence" target="_blank"  rel='noopener noreferrer'>MDN documentation</a> if you are interested in understanding the different parameters available for the <code>&lt;feTurbulence&gt;</code> SVG element.</p><p>As I keep moving things around on my website, it seems mandatory to take it slow and reflect on every change if I want to keep things clean.</p>]]></content:encoded>
</item>
<item>
  <title>Using only system fonts</title>
  <link>https://www.jeantinland.com/blog/using-only-system-fonts/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/using-only-system-fonts/</guid>
  <description><![CDATA[A simple tutorial demonstrating the use of default system fonts]]></description>
  <content:encoded><![CDATA[<p>Working on my new website and try to spare my visitors from downloading useless bytes of data, I identified my font files being among the heaviest bits retrieved by every visitor.</p><p>I know these files are cached but still, it is at least downloaded once in a while, that&#39;s already too much for something as subjective as fonts.</p><p>The further we go, the more clean and lean the defaults system font are becoming.</p><p>But how to use these fonts in your <em>CSS</em>?</p><h2 id="there-goes-the-sans-serif-fonts">There goes the sans-serif fonts <a class="heading-anchor" href="#there-goes-the-sans-serif-fonts">#</a></h2><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">--sans-serif-font:</span></span><span class="line"><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> system-ui</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> -apple-system</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> blinkmacsystemfont</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Segoe UI"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Roboto"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Oxygen"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Ubuntu"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Cantarell"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Fira Sans"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Droid Sans"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Helvetica Neue"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> arial</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> sans-serif</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span></code></pre></div><p><code>system-ui</code> will be the default font set system-wide by the user. You could instead use <code>ui-sans-serif</code> in order to be sure that the displayed font is really a sans serif font, but this value isn&#39;t supported outside <em>Safari</em> for now.</p><p>The browser will try to load every other values successively until one matches. All fonts used here are matching the default fonts used in every major OS (<em>macOS</em>, <em>Windows</em>, <em>Ubuntu</em>, etc.)</p><h2 id="monospaced-fonts">Monospaced fonts <a class="heading-anchor" href="#monospaced-fonts">#</a></h2><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">--monospace-font:</span></span><span class="line"><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> ui-monospace</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> menlo</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> monaco</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Cascadia Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Segoe UI Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Roboto Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Oxygen Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Ubuntu Monospace"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">Source</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> Code</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Pro"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Fira Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Droid Sans Mono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Courier New"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> monospace;</span></span></code></pre></div><p>The same logic applies here, <code>ui-monospace</code> is currently only supported in <em>Safari</em>, but the next matching font will be used depending on user OS.</p><h2 id="and-last-but-not-least-serif-fonts">And last but not least, serif fonts <a class="heading-anchor" href="#and-last-but-not-least-serif-fonts">#</a></h2><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">--serif-font:</span></span><span class="line"><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> ui-serif</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Georgia</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Garamond</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Cambria</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> "Times New Roman"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Times</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> serif;</span></span></code></pre></div><p>Again, same here, <code>ui-serif</code> is not widely supported. The other fonts should come pre-installed in almost all OS, you can order them as it suits you.</p><h2 id="font-uniformization">Font uniformization <a class="heading-anchor" href="#font-uniformization">#</a></h2><p>As it is not possible to know what font will be used for each user in the end, you can set the <code>font-size-adjust</code><em>CSS</em> property to <code>ex-height 0.5</code> in order to uniformize the space occupied by the loaded font regardless of its family.</p><p>This should prevent UI breaking in most cases.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">body</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> font-size-adjust</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> ex-height </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">0.5</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>A nice explanation by Alex Kladov is available <a href="https://matklad.github.io/2025/07/16/font-size-adjust.html" target="_blank"  rel='noopener noreferrer'>here</a> if you want to know more about this <em>CSS</em> property.</p><p>If you are looking to optimize your pages loading and avoid either the blocking while font load or the font swap when enabled, I recommend you to ditch these customs fonts and a rely on the user system ones. They are almost always good enough.</p><p>In my opinion, you&#39;ll differentiate your design a lot more with delightful colors, design, and animations. Fonts can often be overrated.</p>]]></content:encoded>
</item>
<item>
  <title>CSS only image gallery</title>
  <link>https://www.jeantinland.com/blog/css-only-image-gallery/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/css-only-image-gallery/</guid>
  <description><![CDATA[A simple way of creating an image gallery without JavaScript]]></description>
  <content:encoded><![CDATA[<p>If you need to display a collection of images to your visitors but also want to avoid loading heavy dependencies, you may like to see what pure <em>CSS</em> is capable of. We can make these images zoomable with exactly 0 byte of <em>JavaScript</em>.</p><p>The idea is to leverage the <a href="https://developer.mozilla.org/fr/docs/Web/HTML/Reference/Global_attributes/tabindex" target="_blank"  rel='noopener noreferrer'><code>tabindex</code></a> attribute coupled with the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus" target="_blank"  rel='noopener noreferrer'><code>:focus</code></a> &amp; <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within" target="_blank"  rel='noopener noreferrer'><code>:focus-within</code></a><em>CSS</em> pseudo selectors.</p><blockquote><p>Images used are loaded from <a href="https://picsum.photos" target="_blank"  rel='noopener noreferrer'>picsum.photos</a>, which takes its sources from <a href="https://unsplash.com" target="_blank"  rel='noopener noreferrer'>Unsplash</a>.</p></blockquote><h2 id="html-markup-and-basic-styles">HTML markup and basic styles <a class="heading-anchor" href="#html-markup-and-basic-styles">#</a></h2><p>Assuming you need to display a variable number of images, you&#39;ll have to generate a variable markup looking like this:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery image-gallery--4"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__item"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__backdrop"</span><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"-1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"https://picsum.photos/seed/7cb42b45/1500/750"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__item"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__backdrop"</span><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"-1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"https://picsum.photos/seed/4dd8300c/1500/750"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__item"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__backdrop"</span><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"-1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"https://picsum.photos/seed/b66a4549/1500/750"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__item"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__backdrop"</span><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"-1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"https://picsum.photos/seed/5783fa29/1500/750"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> tabindex</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>Note that the <code>.image-gallery</code> element also has a class containing the number of image displayed inside the gallery: <code>.image-gallery--4</code>. Adding this information helps a lot when styling all layout possibilities, as you&#39;ll see below in the <em>CSS</em> part.</p><p>Each <code>.image-gallery__item</code> contains a backdrop element with <code>tabindex=&quot;-1&quot;</code> making it focusable on click but not accessible while cycling through items with the <code>tab</code> key.</p><p>At last, the <code>img</code> is added inside this backdrop with a <code>tabindex=&quot;0&quot;</code> allowing to focus it on click and with the <code>tab</code> key.</p><p>We&#39;ll need to add some styles to this gallery:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> display</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> grid</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-template-columns</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> repeat</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">2</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">fr</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> gap</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> margin</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">min-width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1001</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-template-columns</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> repeat</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">4</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">fr</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__item</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> height</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> aspect-ratio</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1400</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> / </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">650</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--1</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__item</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-column</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> / </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">-1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">min-width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1001</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-template-rows</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">fr</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--1</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--3</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-template-rows</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 250</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 250</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__item</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> linear-gradient</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> to</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> bottom</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> right</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> #f5f5f5</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> #c5c5c5</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> #fff</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#D1AB66;--shiki-dark:#FFD484">min-width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1001</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:is</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--2</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--3</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__item</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:nth-child</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">))</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--1</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--2</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--3</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__item</span><span style="color:#E78482;--shiki-dark:#E78482">:nth-child</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">4</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--1</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--2</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--3</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--4</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> )</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__item</span><span style="color:#E78482;--shiki-dark:#E78482">:nth-child</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">n + 5</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-column</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> span </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery--1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) </span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__item</span><span style="color:#E78482;--shiki-dark:#E78482">:nth-child</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery--2</span><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__item</span><span style="color:#E78482;--shiki-dark:#E78482">:nth-child</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-column</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> span </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> grid-row</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> span </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__backdrop</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> height</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transition</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> background-color </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">160</span><span style="color:#39465E;--shiki-dark:#FFF9EE">ms</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__image</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> height</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 100</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> object-fit</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> cover</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border-radius</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> inherit</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> cursor</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> zoom-in</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> user-select</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> outline</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>We now have a nice responsive image gallery handling any number of images:</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/css-only-image-gallery/image-gallery.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/css-only-image-gallery/image-gallery.webp" type="image/webp"/><img src="/_generated/bare/images/blog/css-only-image-gallery/image-gallery.jpg" alt="" loading="eager" width="2652" height="996"/></picture></div></div></div></div></p><p>This is of course only a suggestion of design, it can be adjusted to fit perfectly your needs.</p><h2 id="making-it-interactive">Making it interactive <a class="heading-anchor" href="#making-it-interactive">#</a></h2><p>We can now focus on making these images zoomable! And that&#39;s the beauty of <em>CSS</em>, with only two sets of rules, the magic happens.</p><p>This one will display in <code>position: fixed</code> the image backdrop when its child image will be focused:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__backdrop</span><span style="color:#E78482;--shiki-dark:#E78482">:not</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#E78482;--shiki-dark:#E78482">:focus</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span><span style="color:#E78482;--shiki-dark:#E78482">:focus-within</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> fixed</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> top</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> display</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> flex</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> align-items</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> center</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> justify-content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> center</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background-color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> rgba</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">0</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0.1</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> cursor</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> zoom-out</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> user-select</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>And this one will also display in <code>position: fixed</code> the image on top of the backdrop when focused:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__image</span><span style="color:#E78482;--shiki-dark:#E78482">:focus</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> max-width</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 90</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> height</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> auto</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> max-height</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 90</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> object-fit</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> contain</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border-radius</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 8</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> pointer-events</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 3</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> animation</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> image-gallery-zoom-in </span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">160</span><span style="color:#39465E;--shiki-dark:#FFF9EE">ms</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@keyframes</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> image-gallery-zoom-in</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> 0% {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> opacity</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 0</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> scale</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">0.98</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>The <code>pointer-events: none</code> property will allow clicks to go through, which will make the user lose focus over the image and focus the backdrop. In consequence, the gallery will close the opened image.</p><p>As images are now focusable elements thanks to the <code>tabindex=&quot;0&quot;</code> attribute, you cycle between images with <code>tab</code> and cycle back with <code>shift</code>+<code>tab</code>.</p><p>A nice touch would be to inform the user about the controls allowing to cycle through the images. It would of course need to be displayed only if an image is focused and not on a mobile device.</p><p>You can add this piece of <em>HTML</em> at the end of the <code>.image-gallery</code> container. It must be located inside this container in order to have a parent selector with <code>:has</code>.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image-gallery__keyboard-indicator"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> Use </span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">Tab</span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> or </span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">Shift</span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">+</span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">Tab</span><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> to navigate between images</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__keyboard-indicator</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> display</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> none</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> position</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> fixed</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> bottom</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 26</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> left</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> padding</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 8</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 12</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> font-size</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 14</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background-color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> #f5f5f5</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border-radius</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 4</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> transform</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> translateX</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">-50</span><span style="color:#39465E;--shiki-dark:#FFF9EE">%</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> z-index</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 4</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">@media</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> (</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">pointer</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> fine</span><span style="color:#39465E;--shiki-dark:#FFF9EE">)</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery</span><span style="color:#E78482;--shiki-dark:#E78482">:has</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__image</span><span style="color:#E78482;--shiki-dark:#E78482">:focus</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">)</span></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE"> .image-gallery__keyboard-indicator</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> display</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> block</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.image-gallery__keyboard-indicator</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ></span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE"> code</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> padding</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 2</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 4</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> font-size</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#8FC8BB"> calc</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB">1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">rem</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> -</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 2</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> background-color</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> #fff</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 1</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> solid</span><span style="color:#D1AB66;--shiki-dark:#FFD484"> #c2c2c2</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> border-radius</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> 4</span><span style="color:#39465E;--shiki-dark:#FFF9EE">px</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>This indicator will be displayed only when the gallery is focused, thanks to <code>.image-gallery:has(.image-gallery__image:focus)</code>.</p><p>The <code>@media (pointer: fine)</code> allows for targeting only device using a mouse, which generally brings its keyboard.</p><blockquote><p>This bit will only work in browsers supporting <code>:has</code> pseudo selector so we&#39;ll call it progressive enhancement.</p></blockquote><h2 id="result">Result <a class="heading-anchor" href="#result">#</a></h2><p><video src="/public/images/blog/css-only-image-gallery/image-gallery-demo.mp4#t=0.001" controls muted loop></video></p><p>Sure, it is less comfortable than the experience provided by a full-blown Lightbox plugin, but it comes at absolutely no cost.</p><p>If your needs are simple, start with the simplest solution you can come up with.</p>]]></content:encoded>
</item>
<item>
  <title>Leaving Next.js behind</title>
  <link>https://www.jeantinland.com/blog/leaving-nextjs-behind/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/leaving-nextjs-behind/</guid>
  <description><![CDATA[Why I decided to leave Next.js and Vercel for a simpler, more independent solution.]]></description>
  <content:encoded><![CDATA[<p>Working wxith <em>Next.js</em> at my previous workplace and using it almost exclusively for generating static websites, I didn&#39;t think twice before using it for my personal website.</p><p>And it was great: with <em>Vercel</em> on the free plan, I didn&#39;t have to think about anything. A simple <code>git push</code> and my website was up to date a minute later. With the low traffic I&#39;m experiencing on my website, it doesn&#39;t costed me a dime!</p><p>So why part away?</p><p>Working with <em>Next.js</em> on a website as simple as mine always felt like using war machine where a way, way, more basic tool could be used. On top of that, I was never fully at ease when using <em>Vercel</em> as a hosting solution. I was feeling like my website should be hosted at home, with a local hosting provider.</p><p>I&#39;m not going to speak about other controversies like <a href="https://omarabid.com/nextjs-vercel" target="_blank"  rel='noopener noreferrer'>this one</a> on a vendor lock-in background but I can at least say that I&#39;m more comfortable with a fully independent open-source solution.</p><p>I also wanted to make simpler things and rely less on <em>JavaScript</em> for everything.</p><h2 id="an-ode-to-simplicity">An ode to simplicity <a class="heading-anchor" href="#an-ode-to-simplicity">#</a></h2><p>Instead of relying on a big meta framework like <em>Next.js</em> coupled with <em>Vercel</em>&#39; infrastructure, my website is now built with <em>Hono</em> and self hosted on a tiny VPS provided by <em>OVH</em>, a French hosting provider.</p><p>This new website, with a design <strong>heavily</strong> inspired by <a href="https://zed.dev/" target="_blank"  rel='noopener noreferrer'>zed.dev</a>, is statically generated locally on my machine then deployed on my VPS with a basic <code>rsync</code> command.</p><p><em>HTML</em> is generated by <em>JSX</em> components - <em>Hono</em> provides support out of the box - and styled by <em>CSS</em> leveraging the new layer system allowing full control over specificity in a context of a global stylesheet.</p><p><em>Hono</em>&#39;s <em>JSX</em> let me reuse a lot of my <em>Next.js</em> assets as it supports async components.</p><h2 id="escaping-the-bundling-hell">Escaping the bundling hell <a class="heading-anchor" href="#escaping-the-bundling-hell">#</a></h2><p>Sure, I still use a <code>package.json</code> and rely on some external dependencies while I develop and build my website:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="json"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">{</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "devDependencies"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "@types/bun"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "latest"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "@types/uglify-js"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^3.17.5"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "classnames"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^2.5.1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "front-matter"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^4.0.2"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "hono"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^4.8.5"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "lightningcss"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^1.30.1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "marked"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^16.1.1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "serve"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^14.2.4"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "sharp"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^0.34.3"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "shiki"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^3.8.1"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "uglify-js"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^3.19.3"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> "zx"</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "^8.7.1"</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>But in the end, I only have <code>.html</code> and <code>.css</code> files.</p><h2 id="trying-to-work-without-javascript">Trying to work without <em>JavaScript</em><a class="heading-anchor" href="#trying-to-work-without-javascript">#</a></h2><p><em>React.js</em> was really great for building absolutely anything but it was also pushing me to use <em>JavaScript</em> everywhere, even when it wasn&#39;t needed. The goal, with this new website, was to use JavasScript only as a last resort. As I wanted my website to still be fully functional without <em>JavaScript</em> enabled.</p><p>In order to eliminate almost all <em>JavaScript</em> usage, I had to rethink most of the interactive parts of my website and leave some functionality behind.</p><p>I made a lot of things work with pure <em>CSS</em>:</p><h3 id="a-responsive-navigation">A responsive navigation <a class="heading-anchor" href="#a-responsive-navigation">#</a></h3><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/leaving-nextjs-behind/css-responsive-navigation.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/leaving-nextjs-behind/css-responsive-navigation.webp" type="image/webp"/><img src="/_generated/bare/images/blog/leaving-nextjs-behind/css-responsive-navigation.jpg" alt="" loading="eager" width="812" height="697"/></picture></div></div></div></div></p><h3 id="a-basic-image-gallery-with-zoom">A basic image gallery with zoom <a class="heading-anchor" href="#a-basic-image-gallery-with-zoom">#</a></h3><p><video src="/public/images/blog/leaving-nextjs-behind/css-image-gallery.mp4#t=0.001" controls muted loop></video></p><blockquote><p><strong>24/07/2025 edit:</strong> The process of creating this image gallery is now detailed <a href="/blog/css-only-image-gallery/" target="_self"  >in this article</a>.</p></blockquote><h2 id="using-javascript-on-non-critical-parts">Using <em>JavaScript</em> on non-critical parts <a class="heading-anchor" href="#using-javascript-on-non-critical-parts">#</a></h2><p>Even if wanted a website working with <em>HTML</em> and <em>CSS</em> only, there were some things impossible to create without <em>JavaScript</em>.</p><p>The perfect example is <a href="/toolbox/simple-bar/" target="_self"  >this interactive demo</a> for <em>simple-bar</em> which obviously cannot exists without <em>JavaScript</em>.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/leaving-nextjs-behind/simple-bar-demo.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/leaving-nextjs-behind/simple-bar-demo.webp" type="image/webp"/><img src="/_generated/bare/images/blog/leaving-nextjs-behind/simple-bar-demo.jpg" alt="" loading="eager" width="2334" height="796"/></picture></div></div></div></div></p><p>Using web components and in this case a shadow root makes this demo fully autonomous and isolated from the rest of the website.</p><p>A <code>noscript</code> tag allows to render an image instead of the demo if <em>JavaScript</em> is disabled:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">simple-bar-demo</span><span style="color:#E78482;--shiki-dark:#E78482"> playing</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"true"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">noscript</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">image</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> className</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"introduction__image"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/toolbox/simple-bar/preview.jpg"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> alt</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"Simple Bar Demo"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">noscript</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">simple-bar-demo</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>I also re-implemented my <a href="/blog/lazy-load-svg-icons-with-use-react-js/" target="_self"  >lazy loaded icon component</a> using a web component. Its source can be found <a href="https://github.com/Jean-Tinland/lazy-icon-web-component" target="_blank"  rel='noopener noreferrer'>here</a>, on my GitHub.</p><p>Using a <code>noscript</code> fallback made it &quot;<em>JavaScript</em> less&quot; compatible:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="ts"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> default</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> function</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> Icon</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">({ </span><span style="color:#39465E;--shiki-dark:#FFF9EE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> className</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#74829B;--shiki-dark:#98A8C5"> `/public/images/icons/</span><span style="color:#74829B;--shiki-dark:#F5F5F5">${</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">code</span><span style="color:#74829B;--shiki-dark:#F5F5F5">}</span><span style="color:#74829B;--shiki-dark:#98A8C5">.svg#icon`</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">lazy</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">-</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">icon</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> code</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">code</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">} </span><span style="color:#1E2737;--shiki-dark:#FFF9EE">class</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">className</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> &#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">noscript</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">></span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;</span><span style="color:#39465E;--shiki-dark:#FFF9EE">svg</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> xmlns</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/2000/svg"</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> viewBox</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0 0 24 24"</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> class</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">className</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ></span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">use</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">href</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">} </span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">/></span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;/</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">></span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;/</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">noscript</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">></span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;/</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">lazy</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">-</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">icon</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><h2 id="a-handmade-image-component">A handmade Image component <a class="heading-anchor" href="#a-handmade-image-component">#</a></h2><p>The <code>Image</code> component provided by <em>Next.js</em> was really easy to use as a simple drop-in replacement for <code>&lt;img /&gt;</code> tags. As I was moving from an hybrid static website running with a Node.js server to a fully static website, I had to find another way to optimize my images.</p><p>I settled on creating a custom <code>&lt;Image /&gt;</code> component that would handle image optimization and resizing during build time. Generating all my website images at once takes around 2 minutes but I only have to do it once globally, otherwise, images are generated during dev time.</p><p>This <code>&lt;Image /&gt;</code> component is async and uses <code>sharp</code> in order to transform the requested image in 3 output files:</p><ul><li>An optimized version in the same format</li><li>A <code>.webp</code> version</li><li>An <code>.avif</code> version</li></ul><p>These 3 files are used inside a <code>&lt;picture&gt;</code> tag:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">&#x3C;!-- Input --></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">image</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> className</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"about__description-picture"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/public/images/about/picture.png"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> width</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"{60}"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> height</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"{60}"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> loading</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"lazy"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> alt</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"Jean Tinland"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">/></span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">&#x3C;!-- Output --></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">picture</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"about__description-picture"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">source</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> srcset</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/_generated/w60h60/images/about/picture.avif"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> type</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image/avif"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">source</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> srcset</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/_generated/w60h60/images/about/picture.webp"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> type</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"image/webp"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">img</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> src</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/_generated/w60h60/images/about/picture.png"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> alt</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"Jean Tinland"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> loading</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"lazy"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">picture</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>That way, the browser loads the first image format compatible.</p><h2 id="enters-the-speculation-rules">Enters the speculation rules <a class="heading-anchor" href="#enters-the-speculation-rules">#</a></h2><p>Hosting my website in France outside a CDN made it slower than before for almost all my visitors as very few comes from France.</p><p>Reducing all the assets size - mostly with a big refactor and minification - plus enabling <code>HTTP/2</code> already reduced loading time a lot.</p><p>A nice touch was to enable this relatively recent feature pushed by the Chrome team: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API" target="_blank"  rel='noopener noreferrer'>the Speculation Rules API</a>.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="ts"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> rules</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> prerender</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> where</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> { </span><span style="color:#D1AB66;--shiki-dark:#FFD484">and</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [{ </span><span style="color:#D1AB66;--shiki-dark:#FFD484">href_matches</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "/*"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }] }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> eagerness</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "moderate"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> ]</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> prefetch</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> urls</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/portfolio/"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "/toolbox/"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "/blog/"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> referrer_policy</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "no-referrer"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> ]</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">};</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> default</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> function</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> SpeculationRules</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">() {</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x3C;</span><span style="color:#39465E;--shiki-dark:#FFF9EE">script</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> type</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"speculationrules"</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> dangerouslySetInnerHTML</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">{{ </span><span style="color:#39465E;--shiki-dark:#BBBBBB">__html</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> JSON</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">stringify</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">rules</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) }}</span></span><span class="line"><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> /></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>With theses rules every internal link hovered triggers a pre-render of the targeted page allowing for a really fast navigation. Coupled with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API" target="_blank"  rel='noopener noreferrer'>View Transition API</a>, everything feels snappy and smooth!</p><h2 id="closing-thoughts">Closing thoughts <a class="heading-anchor" href="#closing-thoughts">#</a></h2><p>I know this doesn&#39;t look like much, but the feeling of creating something more powerful with fewer resources is always bringing a lot of satisfaction.</p><p>As time goes by, I think, like everyone else, I try to simplify every aspect of my life I can. Upgrading the dependencies of my personal website every couple of weeks didn&#39;t work in any way with this approach.</p><p>I already have a good amount of work with my open-source projects; it was not necessary to top that with extra maintenance that wasn&#39;t adding any real value to my website.</p><blockquote><p><strong>31/07/2025 edit:</strong> This article has been featured in <em><a href="https://thisweekinreact.com/newsletter/244" target="_blank"  rel='noopener noreferrer'>This Week in React</a></em><strong>#244</strong>. Thank you Sébastien Lorber! :)</p></blockquote>]]></content:encoded>
</item>
<item>
  <title>Lazy load SVG icons with 'use' in React.js</title>
  <link>https://www.jeantinland.com/blog/lazy-load-svg-icons-with-use-react-js/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/lazy-load-svg-icons-with-use-react-js/</guid>
  <description><![CDATA[Learn how to optimize your React.js applications by lazy loading SVG icons using the use tag. This article covers different methods of incorporating SVG icons, including inline injection, sprite files, and splitting sprites into individual files. It also provides a step-by-step guide to implementing lazy loading for SVG icons with the Intersection Observer API, ensuring efficient network requests and improved performance.]]></description>
  <content:encoded><![CDATA[<p>Using icons in your website or app almost always brings up the question of optimization.</p><p><em>Icons used in this article are extracted from the Remix Icon library.</em></p><h2 id="the-inline-way">The inline way <a class="heading-anchor" href="#the-inline-way">#</a></h2><p>Maybe you are using a popular icon library like <code>react-icon</code>, or <code>@remixicon/react</code>. In that case, each icon you import will likely be <strong>inline injected</strong> in your final HTML. Depending on the complexity of these icons or their number, this can lead to a <strong>significant increase in your bundle size</strong>.</p><h2 id="the-sprite-way">The sprite way <a class="heading-anchor" href="#the-sprite-way">#</a></h2><p>Another approach that I find interesting is to use SVG icons with the <code>&lt;use&gt;</code> tag. This way, you can reference the same SVG file multiple times in your HTML without having to duplicate the SVG code.</p><p>Downloading, optimizing (with SVGOMG), and importing icons inside your project can be a lot of work if you need numerous icons for your application but starting from scratch you&#39;ll generally add icons one by one when needed so it doesn&#39;t feel like a big deal.</p><p>The basic setup for this usage it to create a single <code>.svg</code> sprite file that will reference several icons inside <symbol> elements.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/2000/svg"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns:xlink</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/1999/xlink"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> style</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"display: none"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#E78482;--shiki-dark:#E78482"> id</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"arrow-right"</span><span style="color:#E78482;--shiki-dark:#E78482"> viewBox</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0 0 24 24"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">path</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> d</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"m16.172 11-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2h12.172Z"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#E78482;--shiki-dark:#E78482"> id</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"arrow-left"</span><span style="color:#E78482;--shiki-dark:#E78482"> viewBox</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0 0 24 24"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">path</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> d</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11Z"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>If your <code>sprite.svg</code> is located in <code>/images/icons/sprite.svg</code>, you can call your icons like this:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/images/icons/sprite.svg#arrow-right"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>Doing this will allow you to <strong>reference the same SVG file multiple times</strong> without increasing your bundle size.</p><p>However, if you use only one or two icons in a specific page, you&#39;ll load the entire sprite file just for these icons. Depending on its size, it can be acceptable or not.</p><h2 id="splitting-your-sprite-into-individual-files">Splitting your sprite into individual files <a class="heading-anchor" href="#splitting-your-sprite-into-individual-files">#</a></h2><p>To avoid loading the entire sprite file, you can split your sprite into individual files.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">&#x3C;!-- arrow-left.svg --></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/2000/svg"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns:xlink</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/1999/xlink"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> style</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"display: none"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#E78482;--shiki-dark:#E78482"> id</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"icon"</span><span style="color:#E78482;--shiki-dark:#E78482"> viewBox</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0 0 24 24"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">path</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> d</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11Z"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">&#x3C;!-- arrow-right.svg --></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/2000/svg"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> xmlns:xlink</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"http://www.w3.org/1999/xlink"</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> style</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"display: none"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#E78482;--shiki-dark:#E78482"> id</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"icon"</span><span style="color:#E78482;--shiki-dark:#E78482"> viewBox</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"0 0 24 24"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">path</span></span><span class="line"><span style="color:#E78482;--shiki-dark:#E78482"> d</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"m16.172 11-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2h12.172Z"</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">symbol</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>These icons can be used like this:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"/images/icons/arrow-right.svg#icon"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>Browser cache is doing the rest: icons used in several places will be loaded instantly after their first use.</p><h2 id="usage-in-reactjs">Usage in React.js <a class="heading-anchor" href="#usage-in-reactjs">#</a></h2><p>In order to reduce boilerplate, you can create a component that will handle the loading of your icons.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="tsx"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// @/components/icon.tsx</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">type</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#39465E;--shiki-dark:#FFD484"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGProps</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">&#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGSVGElement</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">></span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">&#x26;</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> code</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> string</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">};</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> default</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> function</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> Icon</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">({ </span><span style="color:#39465E;--shiki-dark:#FFF9EE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ...</span><span style="color:#39465E;--shiki-dark:#FFF9EE">props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> width</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#E78482;--shiki-dark:#E78482"> height</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> {</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">...</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">props</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{</span><span style="color:#74829B;--shiki-dark:#98A8C5">`/images/icons/</span><span style="color:#74829B;--shiki-dark:#F5F5F5">${</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">code</span><span style="color:#74829B;--shiki-dark:#F5F5F5">}</span><span style="color:#74829B;--shiki-dark:#98A8C5">.svg#icon`</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// Usage:</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#6DB3CE">Icon</span><span style="color:#E78482;--shiki-dark:#E78482"> code</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"arrow-right"</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span></code></pre></div><h2 id="now-with-lazy-loading">Now with lazy loading <a class="heading-anchor" href="#now-with-lazy-loading">#</a></h2><p>I find it interesting to lazy load these icons because it allows for a reduced number of request during the initial page load: only icons that are in view are requested over the network.</p><p>You&#39;ll need to adjust your <code>icon.tsx</code> component like this:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="tsx"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// @/components/icon.tsx</span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">// This code implements lazy loading for SVG icons using the Intersection Observer API.</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">type</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#39465E;--shiki-dark:#FFD484"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGProps</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">&#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGSVGElement</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">></span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">&#x26;</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> code</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#1E2737;--shiki-dark:#6DB3CE"> string</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">};</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> default</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> function</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> Icon</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">({ </span><span style="color:#39465E;--shiki-dark:#FFF9EE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ...</span><span style="color:#39465E;--shiki-dark:#FFF9EE">props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Creates a ref to track the SVG element</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> ref</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useRef</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">&#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGSVGElement</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">>(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">null</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Uses useState to track if the icon is in viewport</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">] </span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useState</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">false</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useEffect</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(() </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Checks if IntersectionObserver is supported by the browser</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> isCompatible</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "IntersectionObserver"</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> in</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> window</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">isCompatible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> svg</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> ref</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">current</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Checks if not already inView before setting the observer</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x26;&#x26;</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> !</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Creates an observer that triggers when icon enters viewport</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> new</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> IntersectionObserver</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> ([</span><span style="color:#39465E;--shiki-dark:#FFF9EE">entry</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]) </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">entry</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">isIntersecting</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">true</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Adds a root margin to trigger the observer a bit earlier: 24px before svg enters the viewport</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> { </span><span style="color:#D1AB66;--shiki-dark:#FFD484">rootMargin</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "24px"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Sets up observation of the SVG element on mount</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">observe</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> () </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Cleans up by unobserving when icon is inView or unmounted</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">unobserve</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> };</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> } </span><span style="color:#AD82CB;--shiki-dark:#AD82CB">else</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Falls back to always showing the icon</span></span><span class="line"><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">true</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]);</span></span><span class="line"></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Only sets the SVG reference when icon is in view</span></span><span class="line"><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic"> // Prevents unnecessary loading of SVG icons outside viewport</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> inView</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ?</span><span style="color:#74829B;--shiki-dark:#98A8C5"> `/images/icons/</span><span style="color:#74829B;--shiki-dark:#F5F5F5">${</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">code</span><span style="color:#74829B;--shiki-dark:#F5F5F5">}</span><span style="color:#74829B;--shiki-dark:#98A8C5">.svg#icon`</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> :</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> undefined</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> ref</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">ref</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#E78482;--shiki-dark:#E78482"> width</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#E78482;--shiki-dark:#E78482"> height</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> {</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">...</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">props</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> {</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x26;&#x26;</span><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">href</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>Here the result in video:</p><p><video src="/public/images/blog/svg-lazy-loading/svg_use_lazy_loading.mp4#t=0.001" controls muted loop></video></p><p>Below the code I use on my website without comments and with the addition of a <code>IconCode</code> type more specific allowing for auto completion for the <code>code</code> property when using the <code>&lt;Icon /&gt;</code> components.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="tsx"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#98A8C5">"use client"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">import</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> *</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> as</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> from</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "react"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> type</span><span style="color:#39465E;--shiki-dark:#FFD484"> IconCode</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "arrow-left"</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> |</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "arrow-right"</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE">type</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#39465E;--shiki-dark:#FFD484"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGProps</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">&#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGSVGElement</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">></span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">&#x26;</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> code</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#39465E;--shiki-dark:#FFD484"> IconCode</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">};</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB">export</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> default</span><span style="color:#39465E;--shiki-dark:#FFF9EE"> function</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> Icon</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">({ </span><span style="color:#39465E;--shiki-dark:#FFF9EE">code</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ...</span><span style="color:#39465E;--shiki-dark:#FFF9EE">props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">:</span><span style="color:#39465E;--shiki-dark:#FFD484"> Props</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> ref</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useRef</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">&#x3C;</span><span style="color:#39465E;--shiki-dark:#FFD484">SVGSVGElement</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">>(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">null</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">] </span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useState</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">false</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> React</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">useEffect</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(() </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> isCompatible</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "IntersectionObserver"</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> in</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> window</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">isCompatible</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> svg</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> ref</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">current</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x26;&#x26;</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> !</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> new</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> IntersectionObserver</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> ([</span><span style="color:#39465E;--shiki-dark:#FFF9EE">entry</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]) </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> if</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">entry</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">isIntersecting</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">) {</span></span><span class="line"><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">true</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-dark:#FFD484"> rootMargin</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "24px"</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">observe</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> () </span><span style="color:#39465E;--shiki-dark:#FFF9EE">=></span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#FFF9EE"> observer</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">.</span><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold">unobserve</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">svg</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> };</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> } </span><span style="color:#AD82CB;--shiki-dark:#AD82CB">else</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#D1AB66;--shiki-light-font-weight:bold;--shiki-dark:#FFD484;--shiki-dark-font-weight:bold"> setInView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">true</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">);</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> }</span><span style="color:#39465E;--shiki-dark:#98A8C5">,</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> [</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">inView</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">]);</span></span><span class="line"></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> const</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> =</span><span style="color:#1E2737;--shiki-dark:#FFF9EE"> inView</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> ?</span><span style="color:#74829B;--shiki-dark:#98A8C5"> `/images/icons/</span><span style="color:#74829B;--shiki-dark:#F5F5F5">${</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">code</span><span style="color:#74829B;--shiki-dark:#F5F5F5">}</span><span style="color:#74829B;--shiki-dark:#98A8C5">.svg#icon`</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> :</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> undefined</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">;</span></span><span class="line"></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> return</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> (</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#E78482;--shiki-dark:#E78482"> ref</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">ref</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#E78482;--shiki-dark:#E78482"> width</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#E78482;--shiki-dark:#E78482"> height</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{24}</span><span style="color:#AD82CB;--shiki-dark:#AD82CB"> {</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">...</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">props</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#AD82CB;--shiki-dark:#AD82CB"> {</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE"> &#x26;&#x26;</span><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">use</span><span style="color:#E78482;--shiki-dark:#E78482"> href</span><span style="color:#6DB3CE;--shiki-dark:#6DB3CE">=</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">{</span><span style="color:#1E2737;--shiki-dark:#FFF9EE">href</span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span><span style="color:#39465E;--shiki-dark:#98A8C5"> /></span><span style="color:#AD82CB;--shiki-dark:#AD82CB">}</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">svg</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB"> );</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>Of course, this is only an approach to this specific problematic, this code can be adjusted or completely rewritten if necessary.</p><blockquote><p>The logic of detecting and rendering the Icon was fixed thanks to <a href="https://github.com/mohammadazeemwani" target="_blank"  rel='noopener noreferrer'>@mohammadazeemwani</a>!</p></blockquote>]]></content:encoded>
</item>
<item>
  <title>Trying modelism again</title>
  <link>https://www.jeantinland.com/blog/trying-modelism-again/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/trying-modelism-again/</guid>
  <description><![CDATA[This article recounts my journey back into modelism, focusing on the assembly and painting of a French Somua S35 tank model. It covers the author's history with modelism, the choice of the model, the building process, and the final painting and detailing steps.]]></description>
  <content:encoded><![CDATA[<p><strong>A small story about modelism and French WW2 tank</strong>. It is as much a story as a simple trace of the process of producing the model presented in this article.</p><h2 id="return-to-my-roots">Return to my roots <a class="heading-anchor" href="#return-to-my-roots">#</a></h2><p>I think my appeal for modelism - simply put, painting minis - is one of the many things I inherited from my father.</p><p>As far as I can remember, <strong>I have always been fascinated by these recreations of war or everyday life scenes</strong>. I started gluing and “painting” some WW2 planes before I was ten. These were not what I would call successes, but it was a blast simply to try. It was also way too fragile for a child liking to break things apart.</p><p>Soon enough, and with some friends, <strong>I discovered the dark and heavy world of Warhammer 40000…</strong> I spent years going through cycles of frenetically assembling and painting only to stop and going at it again later. Each time I improved a bit on my “technique” without attaining an excellent level. That said, I&#39;m quite satisfied with the result of my latest try.</p><p>To this day, I&#39;m stuck with a big box of painted and unpainted minis from Games Workshop. I&#39;m also stuck with this urge to paint something from time to time.</p><p>Yeah, I could simply pick some of these and paint them but this Sci-Fi world is not appealing to me anymore as it used to be. I&#39;m more into the historical side of things now.</p><h2 id="finding-an-excuse-to-buy-a-new-model">Finding an excuse to buy a new model <a class="heading-anchor" href="#finding-an-excuse-to-buy-a-new-model">#</a></h2><p>With Christmas approaching, gifts were bought and packed. Among them, I thought it would be cool if I bought a tank model for my father.</p><p>Coming back home I was wondering if it was in fact a good idea… I may be more patient than my dad when it comes to these practices.</p><p>I then decided that I would graciously assemble and paint the tank and offer it as is. Yeah, yeah, I know, very generous and not a gift to myself at all!</p><h2 id="about-the-model">About the model <a class="heading-anchor" href="#about-the-model">#</a></h2><p>I settled on a <strong>French Somua S35 tank at 1/35 scale</strong> produced by Tamiya. This should provide a lot of details and have an acceptable size : <strong>not to small, not to big</strong>.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/0.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/0.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/0.jpg" alt="" loading="eager" width="797" height="523"/></picture></div></div></div></div></p><p>For the historical bits, the <strong>Somua S35 was a medium tank</strong> produced by France between 1936 and 1940. It was an excellent tank providing speed, maneuverability, protection!</p><p><strong>The hull was made of cast steel</strong> which enhanced durability and prevented bolts and rivets to fly everywhere inside on heavy impact. This phenomenon observed on other tanks of this era could make huge damages to the crew.</p><p>One of its <strong>main weaknesses</strong>, other than the quantity produced that wouldn&#39;t meet the army&#39;s need at the time, was that it was <strong>a one-person turret</strong>. This meant that the commander had to handle commanding, loading, and firing at the same time. Demographics can partly explain this design choice as <strong>France was less populated than Germany at the time</strong>: fewer people to man those tanks.</p><h2 id="some-pictures-of-the-build-process">Some pictures of the build process <a class="heading-anchor" href="#some-pictures-of-the-build-process">#</a></h2><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/1.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/1.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/1.jpg" alt="" loading="eager" width="1600" height="1200"/></picture></div></div></div></div></p><blockquote><p>It begins with the bottom part of the tank.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/2.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/2.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/2.jpg" alt="" loading="eager" width="1600" height="1200"/></picture></div></div></div></div></p><blockquote><p>Once the main part assembled, it&#39;s time to add the tracks.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/3.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/3.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/3.jpg" alt="" loading="eager" width="1600" height="1200"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/4.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/4.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/4.jpg" alt="" loading="eager" width="1600" height="1200"/></picture></div></div></div></div></p><blockquote><p>These sets of tracks are each composed of 102 chain links. That&#39;s a lot of microscopic parts to assemble…</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/5.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/5.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/5.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/6.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/6.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/6.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/7.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/7.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/7.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/8.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/8.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/8.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>The hull is done.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/9.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/9.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/9.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>Tools are adding a nice level of details.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/10.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/10.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/10.jpg" alt="" loading="eager" width="2621" height="1966"/></picture></div></div></div></div></p><blockquote><p>Some other details are added.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/11.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/11.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/11.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>The turret is now assembled.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/12.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/12.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/12.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>The tank commander is ready to hop in the turret.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/13.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/13.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/13.jpg" alt="" loading="eager" width="2000" height="1358"/></picture></div></div></div></div></p><blockquote><p>This tank needs some optics!</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/14.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/14.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/14.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/15.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/15.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/15.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>Tank is fully assembled!</p></blockquote><h2 id="paiting-time">Paiting time! <a class="heading-anchor" href="#paiting-time">#</a></h2><p>Tamiya provides a really nice painting guide in the box. I choose to reproduce the camo of the <strong>Somua S35 No. 20 of the 4th Reg. de Cuirassiers</strong>.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/16.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/16.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/16.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>Impossible to paint correctly without a light subcoating.</p></blockquote><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/17.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/17.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/17.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/18.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/18.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/18.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/19.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/19.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/19.jpg" alt="" loading="eager" width="2000" height="1501"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/20.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/20.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/20.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>After two successive coats, my paint is finally covering enough.</p></blockquote><h2 id="decals">Decals <a class="heading-anchor" href="#decals">#</a></h2><p>I must say I was apprehending this step a lot as I remembered childhood “traumas” when my decals were almost systematically torn while trying to apply them. I don&#39;t know if decals improved with time or if I simply am more precise and focused now.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/21.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/21.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/21.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><blockquote><p>I&#39;m pretty okay with this!</p></blockquote><h2 id="final-result">Final result <a class="heading-anchor" href="#final-result">#</a></h2><p>In order to age the appearance of this tank and make it look less pristine, I used a contrast paint on its entirerity.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/22.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/22.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/22.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/23.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/23.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/23.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/24.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/24.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/24.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/trying-modelism-again/25.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/trying-modelism-again/25.webp" type="image/webp"/><img src="/_generated/bare/images/blog/trying-modelism-again/25.jpg" alt="" loading="eager" width="2000" height="1500"/></picture></div></div></div></div></p><p>That&#39;s all! It is certainly not a piece of art, but I hope my father will like it as is.</p>]]></content:encoded>
</item>
<item>
  <title>CSS counters to the rescue</title>
  <link>https://www.jeantinland.com/blog/css-counters-to-the-rescue/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Sun, 06 Oct 2024 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/css-counters-to-the-rescue/</guid>
  <description><![CDATA[Of usage of CSS counters]]></description>
  <content:encoded><![CDATA[<p>As I work with a huge fleet of websites, I externalized every bit of code I could into a mutualized library. Obviously, the main goal is to avoid redundancy between projects. In order to preserve flexibility, some components are simply providing markup and logic. Doing so, each website can provide its own styles when sourcing code from this library.</p><h2 id="my-use-case-a-carousel-with-numbered-slides">My use case: a carousel with numbered slides <a class="heading-anchor" href="#my-use-case-a-carousel-with-numbered-slides">#</a></h2><p>One of the first externalized components was the infamous carousel. Time after time I added more personalization options to the markup and logic: horizontal sliding transition, previous and next controls, current element indicators with dots, touch support, etc. The source file of this specific component was becoming a bit of a mess.</p><p>With the complete revamp of <a href="https://www.valraiso.net/" target="_blank"  rel='noopener noreferrer'>Valraiso&#39;s website</a>, our graphic designer added a nice carousel on the home page.</p><p>The one from our common library was to be put to good use!</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/css-counters/valraiso_homepage_carousel.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/css-counters/valraiso_homepage_carousel.webp" type="image/webp"/><img src="/_generated/bare/images/blog/css-counters/valraiso_homepage_carousel.jpg" alt="" loading="eager" width="1212" height="556"/></picture></div></div></div></div></p><p>As you can see in this picture, almost everything was already taken care of; the only missing part was on the current slide counter.</p><p>Instead of implementing the feature right away, I left the classic, too much used <code>// TODO</code>. As expected, this comment stayed in place way too long.</p><h2 id="a-css-only-approach">A CSS only approach <a class="heading-anchor" href="#a-css-only-approach">#</a></h2><p>After joking for two years about this with our graphic designer, I finally asked an intern to take care of the problem, as it seems to be a great exercise.</p><p>While he was starting to add more and more logic to the javascript, I thought it would be interesting to go another way: after all, only this website needed this specific feature; why add weight to all websites code bases?</p><p>I think it is easy to say it was a good call: <strong>problem was solved with only 3 lines of CSS</strong>.</p><p>Here is what we had to do in order to make it work.</p><p>Our markup was looking like this. For a variable number of sections, our component is spawning a corresponding dot. The current dot is identified with a specific CSS class: <code>carousel__dot--current</code>.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="html"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__inner"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#E78482;--shiki-dark:#E78482"> data-current</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">""</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">section</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dots"</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dot"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dot carousel__dot--current"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dot"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dot"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#E78482;--shiki-dark:#E78482"> class</span><span style="color:#39465E;--shiki-dark:#98A8C5">=</span><span style="color:#74829B;--shiki-dark:#98A8C5">"carousel__dot"</span><span style="color:#39465E;--shiki-dark:#98A8C5">>&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">button</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5"> &#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span><span class="line"><span style="color:#39465E;--shiki-dark:#98A8C5">&#x3C;/</span><span style="color:#2FC2C3;--shiki-dark:#7EDDDE">div</span><span style="color:#39465E;--shiki-dark:#98A8C5">></span></span></code></pre></div><p>With that in mind, we only had to declare a counter and increment it for each <code>.carousel__dot</code>:</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.carousel__dot</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> counter-increment</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> home-carousel-dots;</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p>Now, by using this value at two different places, it was done.</p><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.carousel__dot--current</span><span style="color:#E78482;--shiki-dark:#E78482">::before</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "0"</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> counter</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#39465E;--shiki-dark:#FFF9EE">home-carousel-dots</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">); </span><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">/* in our case 02 */</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><copy-button></copy-button><div class="code-block :brd-grd" data-label="css"><pre class="shiki shiki-themes day-shift night-shift" style="background-color:#ffffff;--shiki-dark-bg:#1b222d;color:#1e2737;--shiki-dark:#bbbbbb" tabindex="0" custom="[object Object]"><code><span class="line"><span style="color:#74829B;--shiki-dark:#7EDDDE">.carousel__dots</span><span style="color:#E78482;--shiki-dark:#E78482">::after</span><span style="color:#1E2737;--shiki-dark:#BBBBBB"> {</span></span><span class="line"><span style="color:#39465E;--shiki-dark:#FFF9EE"> content</span><span style="color:#39465E;--shiki-dark:#98A8C5">:</span><span style="color:#74829B;--shiki-dark:#98A8C5"> "/0"</span><span style="color:#8FC8BB;--shiki-dark:#8FC8BB"> counter</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">(</span><span style="color:#39465E;--shiki-dark:#FFF9EE">home-carousel-dots</span><span style="color:#1E2737;--shiki-dark:#BBBBBB">); </span><span style="color:#98A8C5;--shiki-light-font-style:italic;--shiki-dark:#74829B;--shiki-dark-font-style:italic">/* /05 */</span></span><span class="line"><span style="color:#1E2737;--shiki-dark:#BBBBBB">}</span></span></code></pre></div><p><strong>The secret resides in the fact that depending on the scope in which you use your counter, it doesn&#39;t have the same value.</strong></p><p>At the <code>.carousel__dot--current</code> level, the counter is equal to the current count of <code>.carousel__dot</code> in the HTML. But above, at the <code>.carousel__dots</code> level, a count has been made, so the counter is equal to the total of <code>.carousel__dot</code> in the HTML.</p><p>The rest was simply positioning and styling.</p><p>I&#39;m always amazed by the capabilities of basic CSS over javascript solutions. We tend to often overlook other, simpler viable options.</p>]]></content:encoded>
</item>
<item>
  <title>How I accidentally reimplemented the Windows taskbar in macOS</title>
  <link>https://www.jeantinland.com/blog/how-i-accidentally-reimplemented-the-windows-taskbar-in-macos/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Sat, 20 Jul 2024 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/how-i-accidentally-reimplemented-the-windows-taskbar-in-macos/</guid>
  <description><![CDATA[How I accidentally reimplemented the Windows taskbar in macOS. The journey of creating simple-bar, a customizable status bar for macOS using Übersicht and React.]]></description>
  <content:encoded><![CDATA[<p>Experiencing the first lockdown from my father&#39;s house in 2020, I&#39;ve been able to work efficiently and embrace the WFH (Work From Home) paradigm as many of my fellow developers have.</p><p>On top of that, I had a lot of free time, allowing me to tweak my workflow between two fir tree cuttings.</p><p><strong>While working on a Mac</strong>, I was missing tools that would allow me to have better control over my windows. <strong>I had already tried</strong> to use <em>yabai</em> as <strong>a window manager</strong> in the past but couldn&#39;t stick to it as <strong>I was missing</strong> an important piece: <strong>the hotkey daemon that would articulate everything</strong>.</p><p>Installing both <a href="https://github.com/asmvik/yabai" target="_blank"  rel='noopener noreferrer'>yabai</a> and <a href="https://github.com/asmvik/skhd" target="_blank"  rel='noopener noreferrer'>skhd</a> (both developed by <a href="https://github.com/asmvik" target="_blank"  rel='noopener noreferrer'>asmvik</a>) then became the foundation of my setup.</p><p>One thing that was lacking in my new setup was <strong>a status bar displaying</strong> some basic information like the <strong>current focused workspace</strong>, the <strong>current process title</strong>, or simply <strong>today&#39;s date and time</strong>. This feature was originally implemented out of the box in <em>yabai</em> but was stripped in the later version as it wasn&#39;t really in the scope of the project.</p><h2 id="taking-the-matter-into-my-own-hands">Taking the matter into my own hands <a class="heading-anchor" href="#taking-the-matter-into-my-own-hands">#</a></h2><p>At the time, there was already a status bar that would answer this need, but I wasn&#39;t satisfied by the offer. I wanted to have total control over the feature set and the appearance of it.</p><p>Working mainly with Javascript and React and stumbling on <strong><a href="http://tracesof.net/uebersicht/" target="_blank"  rel='noopener noreferrer'>Übersicht</a></strong>, my choice was quickly made.</p><blockquote><p><a href="http://tracesof.net/uebersicht/" target="_blank"  rel='noopener noreferrer'>Übersicht</a> is a macOS utility that allows you to run widgets written in React on your desktop. These widgets are fed with output from shell scripts.</p></blockquote><h2 id="first-foundation">First foundation <a class="heading-anchor" href="#first-foundation">#</a></h2><p><em><a href="https://github.com/Jean-Tinland/simple-bar" target="_blank"  rel='noopener noreferrer'>simple-bar</a></em> was born with, at first, a read-only role. It was a simple display of numbered workspaces with an indicator on the focused one. At its center, it was showing the currently focused window title. And finally, on its right side, it was displaying the current date and time with a battery level indicator.</p><p>I open-sourced it, and after a few months, people started slowly to hit the &quot;star&quot; button. It made me realize I wasn&#39;t the only one working on macOS with this feeling of missing a piece of interface on my desktop.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_1.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_1.webp" type="image/webp"/><img src="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_1.jpg" alt="" loading="eager" width="1800" height="1125"/></picture></div></div></div></div></p><h2 id="interactivity">Interactivity <a class="heading-anchor" href="#interactivity">#</a></h2><p>Soon enough, Übersicht allowed for <strong>direct interaction</strong> with its widgets. Thanks to that, workspaces became clickable and allowed users to swiftly land on any workspace.</p><p>The battery widget could toggle <code>caffeinate</code> on click, the date display would open the calendar app by default, and wifi could also be toggled on click, among a lot of other possible interactions.</p><p>It was at this time that people started to suggest more and more features that I was happy to implement. <strong>I also had a lot of pull requests</strong>; to this day, 63 users contributed to <em>simple-bar</em>, helping me a lot in perfecting the product.</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_2.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_2.webp" type="image/webp"/><img src="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_2.jpg" alt="" loading="eager" width="2000" height="1250"/></picture></div></div></div></div></p><h2 id="more-and-more-features">More and more features <a class="heading-anchor" href="#more-and-more-features">#</a></h2><p>With almost <strong>150 customization options</strong> added, a settings module was a must. When the <strong>19 default data widgets</strong> were not enough and after having several people suggest it, I implemented a <strong>custom widget system</strong> allowing the user to display its own shell script output in <em>simple-bar</em>.</p><p>One of the latest upgrades of the project was the creation of an http node server coupled with a websocket server, allowing the user to plug <em>simple-bar</em> into it and send <code>curl</code> enabling or refreshing specific widgets or parts of the bar.</p><p>This solution eliminates one of the original weaknesses of <em>simple-bar</em> : its slowness when asked to be refreshed. It also enabled the option to synchronize the default and custom widgets with users&#39; workflows.</p><h2 id="the-realization">The realization <a class="heading-anchor" href="#the-realization">#</a></h2><p>Oh, the irony...</p><p>It was only a few days ago, as I switched my main display to a <strong>big</strong> external screen and needed to move <em>simple-bar</em> to the bottom of it, that it clicked.</p><p><strong>I spent four years trying to create a replacement for the default macOS status bar, only to end up with a magnificent Windows task bar</strong> (plus the workspaces list).</p><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_3.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_3.webp" type="image/webp"/><img src="/_generated/bare/images/blog/accidental-windows-taskbar/simple_bar_article_preview_3.jpg" alt="" loading="eager" width="2560" height="1440"/></picture></div></div></div></div></p><p>Working on this project and dealing with more than 240 issues and 160 pull requests was an incredibly formative experience.</p><p>The difficult part comes from accepting to let people down with some requests and bug fixes I couldn&#39;t implement due to lack of time and sometimes motivation.</p><p>At the same time, I worked really hard on the structuration and code quality of the project in order to make it easily <em>forkable</em>, and I think it worked: I saw more than 110 forks after four years of existence.</p><p>I think the choice of Übersicht, thus developing <em>simple-bar</em> as a simple React app, guaranteed appreciable accessibility: working on this project is almost the same as working with <code>HTML</code> and <code>CSS</code> only. It allowed more participation to the project compared to others written in <code>C</code> for example.</p><p>Even if I know that, performance wise, <em>simple-bar</em> is lagging far behind <em><a href="https://github.com/FelixKratz/SketchyBar" target="_blank"  rel='noopener noreferrer'>SketchyBar</a></em>, its accessibility and extensibility are keeping it a viable alternative.</p><p>I love to think I also indirectly contributed to <code>SketchyBar</code> thanks to <a href="https://github.com/kvndrsslr" target="_blank"  rel='noopener noreferrer'>@kvndrsslr</a>, who created <em><a href="https://github.com/kvndrsslr/sketchybar-app-font" target="_blank"  rel='noopener noreferrer'>sketchybar-app-font</a></em> based on a forked icon set used in <em>simple-bar</em>.</p>]]></content:encoded>
</item>
<item>
  <title>The ultimate note app quest</title>
  <link>https://www.jeantinland.com/blog/the-ultimate-note-app/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Tue, 07 May 2024 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/the-ultimate-note-app/</guid>
  <description><![CDATA[This note app is a simple and efficient solution for taking random notes. With features like post-it style notes, drag-and-drop functionality, resizable notes, category assignment, markdown preview, and date association, this app provides a streamlined and intuitive note-taking experience.]]></description>
  <content:encoded><![CDATA[<p>Taking <strong>random notes</strong> and <strong>keeping track of it</strong> is not an easy task. I tried on several occasion to create <em>my</em> perfect notes app but I failed the first 3 times over the last 7 years as <strong>I was falling into the trap of adding more and more features</strong> that were in fact less and less useful.</p><p>We are here to see what path I followed before arriving at a satisfactory and <strong>basic solution</strong>.</p><p>If you are not here for the blabla, you can skip the &quot;origins&quot; and sarcasme part an go directly to the <a href="#a-fresh-start" target="_self"  >presentation of the app</a>.</p><h2 id="early-attempts">Early attempts <a class="heading-anchor" href="#early-attempts">#</a></h2><p>I <a href="/portfolio/perso/notepad/" target="_self"  >started simple</a> with a <strong>PHP application</strong> linked to a <strong>MySQL database</strong>. My notes were organized inside categories that I could manage directly in the application. My <strong>plain text notes</strong> were soon enough holding me back as I was manipulating a lot of <strong>code snippets</strong> that were looking way better with some syntax highlighting so I created a second type of notes in order to handle theses.</p><p>But that wasn&#39;t the end of it: for some reason I thought it would be better if my notes could contain some <strong>rich text</strong>, <strong>images</strong>, <strong>anchors</strong>... A third type of note was added with an floating TinyMCE action bar allowing text formatting and image upload.</p><p>After that I find the need to add <strong>shareable version of my notes</strong> in read-only mode. Next came the <strong>tchat feature</strong> as the app was used by several people. Finally, all my work was literally <strong>burnt to the ground</strong> as the server hosting the app was lost in a fire and no backup were to be found (git you said? Nope, coding through <em>FTP</em> like a pro!).</p><p>I took this hit as a time to reflect and came to the following conclusion: what was I thinking trying to build from ground up the entire set of features offered by Google Docs?</p><p>Only months later I was going again with a <a href="/portfolio/perso/notepad-2/" target="_self"  >new project</a> that was meant to be a simplified version of the previous iteration: <strong>1 type of rich text notes</strong> all <strong>sorted inside manageable categories</strong>. That was it. But here again I was simply recreating the exact set of features anyone could find inside the &quot;Notes&quot; app made by Apple. Except for the little knowledge in Javascript and PHP that it brought me, it was a bit of a waste of time.</p><p>I let it die and moved on... for a while. COVID hit and I couldn&#39;t think of any better way to spend my time than to <a href="/portfolio/perso/notes/" target="_self"  >develop a <em>revolutionary app</em></a> that would be a mix between the Apple Note app and the Apple Finder! This time it was <strong>rich text notes</strong> kept inside <strong>unnamed but colored</strong> categories. Before being done I had a realization: <strong>where this madness will end</strong>?</p><p>I wasn&#39;t inventing anything at all and taking aside the small knowledge gain I pretty much wasted my time, again.</p><h2 id="reflecting-on-these-quotmistakesquot">Reflecting on these &quot;mistakes&quot; <a class="heading-anchor" href="#reflecting-on-these-quotmistakesquot">#</a></h2><p>Several years later, I was collecting my thoughts on these attempts to create a viable and durable note app and I realized the following:</p><ul><li>I don&#39;t need plenty artifices to keep track of my ideas: <strong>plain text</strong> - or at max markdown - would largely suffice.</li><li><strong>The simpler, the better</strong>: I can&#39;t burn so much time in random pet projects.</li><li>Why keep trying to organize ideas that comes and go as they want, I like <strong>wild post-its</strong>!</li></ul><p>Based on these points, I was ready for a last try.</p><div id="a-fresh-start"></div><h2 id="a-fresh-start">A fresh start <a class="heading-anchor" href="#a-fresh-start">#</a></h2><p>Now better equipped with all my thinking and experience with notes, I decided to set 2 ground rules before beginning:</p><ul><li>This is an express project: it must be done in <strong>2 work days</strong>.</li><li>The feature set must be kept to a minimum.</li></ul><p>And here are the selected features:</p><ul><li>Post-its like notes <strong>dropped on a canvas</strong>.</li><li>Post-its can be <strong>moved and resized</strong> as desired.</li><li><strong>Position of post-its is persistant</strong> and relative to the viewport.</li><li>Notes can also be <strong>displayed in an orderly manner</strong>.</li><li><strong>Categories are identified by a color</strong>, they a created directly in database as I don&#39;t need new categories every day.</li><li>Plain text that can be previewed as <strong>markdown</strong>.</li><li>Each post-it can be download as <code>.md</code>.</li><li>Notes can be associated to a date.</li><li>Bonus: system synced theme.</li></ul><p>Used tools:</p><ul><li><strong>Next.js</strong> with app router: useful for rendering the notes on the server and clean refresh of the app on content update with the revalidation helper.</li><li><strong>Framer Motion</strong>: perfect for easy implementation of dragging and resizing of notes.</li><li><strong>Postgres</strong> database hosted on Vercel linked to the Vercel project.</li><li><strong>React Markdown</strong> for markdown previewing.</li></ul><h2 id="result">Result <a class="heading-anchor" href="#result">#</a></h2><p>Capitalizing on an <strong>existing basic design system</strong>, I was able to work swiftly on core features.</p><p>Based on those components (buttons, inputs, etc...) and a full theme composed by CSS variables, each UI element was rapidly in place.</p><p>As for the demo, a series of videos will speak louder than words demonstrating theses functionalities.</p><h3 id="note-creation">Note creation <a class="heading-anchor" href="#note-creation">#</a></h3><p><video src="/public/images/blog/draftpad/012_new_draft.mp4#t=0.001" controls muted loop></video></p><blockquote><p>I can create a new note by simply clicking on the &quot;New draft&quot; button or dragging and dropping it anywhere on the canvas.</p></blockquote><h3 id="note-resizing">Note resizing <a class="heading-anchor" href="#note-resizing">#</a></h3><p><video src="/public/images/blog/draftpad/02_resize_draft.mp4#t=0.001" controls muted loop></video></p><blockquote><p>Each note can be resized from its edges.</p></blockquote><h3 id="organizing-post-its">Organizing post-its <a class="heading-anchor" href="#organizing-post-its">#</a></h3><p><video src="/public/images/blog/draftpad/03_organize_drafts.mp4#t=0.001" controls muted loop></video></p><blockquote><p>Notes can be moved (drag mode is activated by pressing <code>cmd</code> or <code>ctrl</code>), stacking order is determined by the last time each one has been modified.</p></blockquote><h3 id="categories">Categories <a class="heading-anchor" href="#categories">#</a></h3><p><video src="/public/images/blog/draftpad/04_draft_categories.mp4#t=0.001" controls muted loop></video></p><blockquote><p>Each note can be assigned to a specific category.</p></blockquote><h3 id="basic-search-engine">Basic search engine <a class="heading-anchor" href="#basic-search-engine">#</a></h3><p><video src="/public/images/blog/draftpad/05_search_drafts.mp4#t=0.001" controls muted loop></video></p><blockquote><p>A simple search bar is available to filter notes by title.</p></blockquote><h3 id="organized-grid-view">Organized grid view <a class="heading-anchor" href="#organized-grid-view">#</a></h3><p><video src="/public/images/blog/draftpad/06_grid_view.mp4#t=0.001" controls muted loop></video></p><blockquote><p>All notes can be displayed in a grid view.</p></blockquote><h3 id="notes-amp-dates">Notes &amp; dates <a class="heading-anchor" href="#notes-amp-dates">#</a></h3><p><video src="/public/images/blog/draftpad/07_draft_with_past_date.mp4#t=0.001" controls muted loop></video></p><blockquote><p>Notes can be associated with a date. If this date has passed, the relevant note will be displayed with a red border.</p></blockquote><h3 id="light-and-dark-themes">Light and dark themes <a class="heading-anchor" href="#light-and-dark-themes">#</a></h3><p><video src="/public/images/blog/draftpad/08_themes.mp4#t=0.001" controls muted loop></video></p><blockquote><p>The application is automatically synced with the system theme. This one was really easy with a simple switch of shades of grey and the use of the <code>prefers-color-scheme</code> media query.</p></blockquote><h3 id="overview">Overview <a class="heading-anchor" href="#overview">#</a></h3><p><div class="image-gallery image-gallery--free image-gallery--1"><div class="image-gallery__inner"><div class="image-gallery__item"><div class="image-gallery__backdrop" tabIndex="-1"><picture class="image-gallery__image" tabIndex="0"><source srcset="/_generated/bare/images/blog/draftpad/09_result.avif" type="image/avif"/><source srcset="/_generated/bare/images/blog/draftpad/09_result.webp" type="image/webp"/><img src="/_generated/bare/images/blog/draftpad/09_result.jpg" alt="" loading="eager" width="2000" height="1138"/></picture></div></div></div></div></p><blockquote><p>You can see I embraced the mess with all theses stacked notes but this is not a problem as everything can be quickly sorted and filtered.</p></blockquote><h2 id="now-that-its-done">Now that its done <a class="heading-anchor" href="#now-that-its-done">#</a></h2><p>I think I came to a result that suits perfectly to my needs. I <strong>developed it in less than two day</strong> as I planned to. Of course I had to patch some things and adjust some CSS after deploying it but those were quick patches!</p><p>In technical terms, the <strong>Next.js/Vercel environment</strong> allowed me to <strong>deploy the app more quickly</strong> than any other solution I could think of: <strong>it was done in minutes</strong>. <strong>Framer Motion was perfect</strong>: it made disappear the hassle of <strong>handling dragging events</strong>.</p><p>To my knowledge, no notes app was offering the same set of features as mine. I&#39;m pretty content with the idea of having developed something relatively unique.</p><p>I hope you&#39;ll find at least some inspiration in this project.</p>]]></content:encoded>
</item>
<item>
  <title>This is a simple blog</title>
  <link>https://www.jeantinland.com/blog/this-is-a-simple-blog/</link>
  <dc:creator><![CDATA[Jean Tinland]]></dc:creator>
  <pubDate>Mon, 06 May 2024 00:00:00 GMT</pubDate>
  <category><![CDATA[Blog]]></category>
  <guid isPermaLink="false">https://www.jeantinland.com/blog/this-is-a-simple-blog/</guid>
  <description><![CDATA[A brief introduction to this blog.]]></description>
  <content:encoded><![CDATA[<p>Welcome to my <strong>personal blog</strong> where you&#39;ll find some <strong>stories about things I develop</strong>. I may also address <strong>other various topics</strong>.</p><p>I’m beginning to enjoy writing about things, and I do it as it comes. Don’t expect crazy stories; these articles will be dull for most people, but the point is to have a basic place of expression for myself.</p><p>That being said, feel free to <a href="/contact/" target="_self"  >contact me</a> if you want me to speak about a specific subject.</p><p>I hope you enjoy your reading!</p>]]></content:encoded>
</item>
  </channel>
</rss>