<?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>Fri Apr 10 2026 14:23:22 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 to the availability of my websites and services, I created a really basic setup allowing me 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 set up a monitoring tool on the same infrastructure in case of a 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 had deployed many things on Vercel&#39;s platform. I also knew a free tier was available that allowed 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 easy task scheduling.</p><h2 id="the-api">The API <a class="heading-anchor" href="#the-api">#</a></h2><p>The synergy between Next.js and Vercel led me to spin up a minimalist Next.js app exposing a single <code>/api/check</code> endpoint.</p><p>When this route, protected by a secret, is called, a simple <code>.json</code> configuration file is parsed. From it, a collection of URLs to check is iterated over. On success, everything is fine and the script continues. On failure, it retries a configurable number of times. Definitive errors are listed in a simple report sent via email through <code>nodemailer</code>, alerting me to any app failure relatively quickly. <em>I couldn&#39;t think of a simpler solution.</em></p><p>Below is 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 that checks whether things are working properly only once 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 like the right interval 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 closed the lid.</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 that.</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, one could also eliminate the intermediate API and use only 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 the number of checks required for each app throughout the day.</p><p>The API allows me to centralize everything in a single place and handle check 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>Then I stumbled upon 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 to 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>id</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 tooltip 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 HTML <code>data-*</code> attributes and the <em>WAI</em> standard. That means you&#39;ll see a lot of <code>aria-label</code> and <code>data-tooltip</code> 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 lay the foundations of our tooltip system. The <code>--offset</code> variable can be adjusted to fit your needs. The same goes for the <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 ignores 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 with both <code>aria-label</code> and <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. This 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 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 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 great! They provide a way of building reusable (or not) pieces of interface that will stay solid over time. Being a web standard brings a certain peace of mind when using them. On top of that, total control can be achieved over the level of isolation they need 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 following 3 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&#39;s system theme. Once the component is loaded, it detects if the user has already chosen a theme, in which case their 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 that 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 an <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 handles styles with its <code>class</code> attribute; that way it will be displayed correctly 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 over 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 simpler 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 asset 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 spending quite some time rebuilding my website from the ground up and trying to spare users from downloading every useless byte, I found out that I was forcing them to download <strong>120kb</strong> just to display a really discreet grain effect over the background.</p><p><strong>If planning and orchestrating a full optimization pass on a system or a website seems easy, preventing long-term degradation is another matter entirely</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 I was done with every tweak I could think of to keep the payload as light as possible, I introduced, on second thought, a film-like grain effect over the background of my website. It was a simple <code>.png</code> file of <code>250px</code> by <code>250px</code>, and 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 alternative way to create 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 same grain when shown on its own like that, but when used as an overlay, the effect is pretty much the same using this bit of <code>CSS</code> and adjusting the 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>Below, you&#39;ll find 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 can be toggled for preview purposes.</p><grain-generator><noscript><div class=":alrt"><code>&lt;grain-generator &#x2F;&gt;</code> needs JavaScript to be 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 necessary to take things slow and reflect on every change if I want to keep things clean.</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 bytes 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 images 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>Lastly, the <code>img</code> is added inside this backdrop with a <code>tabindex=&quot;0&quot;</code>, allowing it to be focused 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 design suggestion; it can be adjusted to fit your needs perfectly.</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 the image backdrop in <code>position: fixed</code> when its child image is 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 the image on top of the backdrop in <code>position: fixed</code> when it is 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 on the image and focus the backdrop. As a result, 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 can 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 that allow them 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 targeting only devices using a mouse, which generally implies a keyboard.</p><blockquote><p>This bit will only work in browsers supporting the <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 with <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 didn&#39;t cost me a dime!</p><p>So why part ways?</p><p>Working with <em>Next.js</em> on a website as simple as mine always felt like using a war machine where a much 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 felt 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> against a background of vendor lock-in, 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 build 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;s 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 with <em>CSS</em>, leveraging the new layer system and allowing full control over specificity in the 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 JavaScript 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 I wanted a website working with <em>HTML</em> and <em>CSS</em> only, there were some things that were 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 exist 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 more compatible with environments without <em>JavaScript</em>:</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 a 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 into 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 compatible image format.</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 come from France.</p><p>Reducing the size of all the assets - 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 these rules, every hovered internal link triggers a pre-render of the targeted page, allowing for 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 always brings 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 when starting from scratch, you&#39;ll generally add icons one by one as needed, so it doesn&#39;t feel like a big deal.</p><p>The basic setup for this usage is to create a single <code>.svg</code> sprite file that will reference several icons inside <code>&lt;symbol&gt;</code> 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 caching does 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 requests 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 is the result in a 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 is the code I use on my website, without comments and with the addition of a more specific <code>IconCode</code> type, allowing autocompletion for the <code>code</code> property when using the <code>&lt;Icon /&gt;</code> component.</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 problem, and 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>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[On the use 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, overused <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 website codebases?</p><p>I think it is easy to say it was a good call: <strong>the 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 generates 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> elements 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 number of <code>.carousel__dot</code> elements 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 often tend to 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 was able to work efficiently and embrace the WFH (Work From Home) paradigm as many of my fellow developers did.</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>currently 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 a 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 with the available options. I wanted to have total control over the feature set and its appearance.</p><p>Working mainly with JavaScript and React and stumbling upon <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 slowly started hitting 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 have contributed to <em>simple-bar</em>, helping me a lot to perfect 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 several people suggested it, I implemented a <strong>custom widget system</strong> allowing users to display their 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> commands to enable or refresh 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 refresh. 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 structure 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> lags far behind <em><a href="https://github.com/FelixKratz/SketchyBar" target="_blank"  rel='noopener noreferrer'>SketchyBar</a></em>, its accessibility and extensibility keep 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 them</strong> is not an easy task. I tried on several occasions 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 lore, you can skip the &quot;origins&quot; and sarcasm part and 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 holding me back, as I was manipulating a lot of <strong>code snippets</strong> that looked much better with some syntax highlighting, so I created a second type of note in order to handle these.</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 a floating TinyMCE action bar allowing text formatting and image upload.</p><p>After that I found the need to add <strong>shareable versions of my notes</strong> in read-only mode. Next came the <strong>chat 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 backups 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 the 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 note</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 would this madness end</strong>?</p><p>I wasn&#39;t inventing anything at all and, setting aside the small amount of knowledge I gained, 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 many artifices to keep track of my ideas: <strong>plain text</strong> - or at most 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 come 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 two 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 persistent</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 are created directly in the 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 downloaded as <code>.md</code>.</li><li>Notes can be associated with 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 of CSS variables, each UI element was rapidly in place.</p><p>As for the demo, a series of videos will speak louder than words in demonstrating these 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 these stacked notes, but this is not a problem as everything can be quickly sorted and filtered.</p></blockquote><h2 id="now-that-it39s-done">Now that it&#39;s done <a class="heading-anchor" href="#now-that-it39s-done">#</a></h2><p>I think I came to a result that suits my needs perfectly. I <strong>developed it in less than two days</strong> as I had 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 the hassle of <strong>handling dragging events</strong> disappear.</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>various other 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 reading!</p>]]></content:encoded>
</item>
  </channel>
</rss>