<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jake Holman</title>
    <description>The latest articles on Forem by Jake Holman (@jakeisonline).</description>
    <link>https://forem.com/jakeisonline</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1510990%2Fa21d4ea6-93e2-4b35-a68d-9dc431c6570b.jpeg</url>
      <title>Forem: Jake Holman</title>
      <link>https://forem.com/jakeisonline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jakeisonline"/>
    <language>en</language>
    <item>
      <title>How to create and serve image blur placeholders</title>
      <dc:creator>Jake Holman</dc:creator>
      <pubDate>Wed, 25 Jun 2025 07:59:05 +0000</pubDate>
      <link>https://forem.com/jakeisonline/how-to-create-and-server-image-blur-placeholders-4g8n</link>
      <guid>https://forem.com/jakeisonline/how-to-create-and-server-image-blur-placeholders-4g8n</guid>
      <description>&lt;p&gt;&lt;a href="https://jakeisonline.com/nodejs/creating-and-serving-user-uploaded-image-blur-placeholders" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;📌 Originally posted with live interactive demos on jakeisonline.com&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Imagine a page with a large grid of images, you don't want your user staring at blank spaces slowly being filled in as the images load. &lt;em&gt;Gross&lt;/em&gt;. Instead, give them a quick visual indication that something is loading, with a hint of what's to come.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmownpg8x5l2m9wmvyee.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmownpg8x5l2m9wmvyee.gif" alt="Thanks to Karsten Winegeart from Unsplash for the amazing pups"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Thanks to &lt;a href="https://unsplash.com/@karsten116" rel="noopener noreferrer"&gt;Karsten Winegeart from Unsplash&lt;/a&gt; for the amazing pups&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're not familiar with blur placeholders, it's a technique of creating a blurred version of the original image at a fraction of the file size, and then encoding that blurred version directly into the HTML. The blurred image loads as immediately as the page, whilst the original image is swapped in later.&lt;/p&gt;

&lt;p&gt;This technique allows for a really fast, optimised initial loading experience for visitors with any level of internet connection. Those with a fast connection likely won't even see the blurred image, whilst those on slower connections won't be jarred by a sudden blank space being filled in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating the blurred image
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fllwk5u44oblrqsqcok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8fllwk5u44oblrqsqcok.png" alt="Partly due to a lack of shame, we'll use my face from here on out."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to create this blurred image, we'll need to perform two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We should resize the original image to be much smaller than the original. Not only will this of reduce the final size of the blurred image, it will keep the encoding performant. If you care about neither of these things, you can skip this step.&lt;/li&gt;
&lt;li&gt;We're going to need to create a hash of the blurred image (known as a "blurhash"), which we'll later convert to a base64 image data string to serve to the client.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Resizing the image
&lt;/h3&gt;

&lt;p&gt;In the interests of performance and reducing the amount of data we need to store, we'll want to resize the image to be as small as possible.&lt;/p&gt;

&lt;p&gt;Because we're going to be blurring the image, it means we can resize and store a very tiny thumbnail of the image, and then upscale to the desired size when rendering it. The blurring that the blurhash library performs will mean that the upscaled image will look great, and the user won't be able to tell it's being upscaled.&lt;/p&gt;

&lt;p&gt;Doing this in Node is simple, we'll use the popular &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;sharp&lt;/a&gt; library to resize the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sharp&lt;/code&gt; gives us a powerful, performant, and easy way to do all sorts of image operations. In this case, we're going to be using it to resize the image to be much smaller than the original.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Resolve the path to the image&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path/to/image.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Read the image file into a buffer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Calculate optimal dimensions for blurhash&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aspectRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minDimension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;

&lt;span class="c1"&gt;// Calculate dimensions ensuring minimum of 32 pixels on&lt;/span&gt;
&lt;span class="c1"&gt;// the smaller side&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;blurWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blurHeight&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aspectRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Landscape or square&lt;/span&gt;
  &lt;span class="nx"&gt;blurHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;minDimension&lt;/span&gt;
  &lt;span class="nx"&gt;blurWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minDimension&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Portrait&lt;/span&gt;
  &lt;span class="nx"&gt;blurWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;minDimension&lt;/span&gt;
  &lt;span class="nx"&gt;blurHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minDimension&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Give sharp the buffer, and then call the resize method,&lt;/span&gt;
&lt;span class="c1"&gt;// and make sure to return that data as raw bytes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageMeta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blurWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blurHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inside&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ensureAlpha&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;resolveWithObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// We'll use the raw bytes in a bit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But wait, won't resizing the image to be smaller result in a loss of quality? Not really. To illustrate both the resized image and the results of upscaling with blurhash, take a look at the following example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqp4jf8dffwzqvng6c7bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqp4jf8dffwzqvng6c7bg.png" alt="Running blurhash on the smaller image and then upscaling it shows almost no loss of quality.&amp;lt;br&amp;gt;
"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can resize the original image to be much smaller, blurhash it, and then upscale to the desired size when rendering it. Almost no detail it lost despite the upscaling.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating the blurhash
&lt;/h3&gt;

&lt;p&gt;Once we have our image in the desired size, we'll use a fantastic library called &lt;a href="https://www.npmjs.com/package/blurhash" rel="noopener noreferrer"&gt;blurhash&lt;/a&gt; to create a hash of the blurred image (a string of seemingly random characters that represents the image). This hash can then be stored, and later decoded to get the image data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;blurhash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;blurhash&lt;/code&gt; installed, we can create a hash from the resized image data we generated earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create a new array with the correct format&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rgbaData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8ClampedArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageMeta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imageMeta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rgbaData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rgbaData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, what's all this extra code about? While &lt;code&gt;sharp&lt;/code&gt; returns the image data in a format, &lt;code&gt;blurhash&lt;/code&gt; expects the image data to be in a specific format called &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray" rel="noopener noreferrer"&gt;&lt;code&gt;Uint8ClampedArray&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So we simply need to create a new array (&lt;code&gt;rgbaData&lt;/code&gt;) that's exactly the right size for the image data (width &lt;code&gt;x&lt;/code&gt; height &lt;code&gt;x&lt;/code&gt; 4, where the 4 represents the RGBA channels - Red, Green, Blue, and Alpha/transparency), and then copy over all the pixel data from the original format to this new format.&lt;/p&gt;

&lt;p&gt;We end up with a hash that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-VHd]Lt7TH.7xuR+.le?t5xu%2t6OZxar?S4XSoy%MOTV[spoMn,%MV@aeafjuWqkqsUNaWBt6WXogtPafjut6ofs;oea#kBoeof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's my face at 32x32, blurred, and represented as a string. You're welcome!&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlling the blur
&lt;/h3&gt;

&lt;p&gt;Blurhash gives us two parameters to control the blur, called &lt;code&gt;componentX&lt;/code&gt; and &lt;code&gt;componentY&lt;/code&gt;. Roughly, &lt;code&gt;componentX&lt;/code&gt; controls the horizontal blur, and &lt;code&gt;componentY&lt;/code&gt; controls the vertical blur.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:4321/nodejs/creating-and-serving-user-uploaded-image-blur-placeholders#controlling-the-blur" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;📌 See an interactive blur control on jakeisonline.com&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Personally, I've found that &lt;code&gt;componentX = 5&lt;/code&gt; and &lt;code&gt;componentY = 4&lt;/code&gt; gives a good balance between blur and detail, but you should play around with the values to get your desired sweet spot.&lt;/p&gt;

&lt;p&gt;Note though, "The more components you pick, the more information is retained in the placeholder, but the longer the BlurHash string will be", so you'll need to balance that with the amount of detail you want to retain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save the blurred placeholder
&lt;/h3&gt;

&lt;p&gt;Now that you have your blurhash string, you'll need to store it somewhere. Where you store the blurhash is entirely up to you. You could store it in the database, or in a file, or in a cache, or wherever you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So long as&lt;/strong&gt; you can quickly retrieve for decoding and server side rendering later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why not just store the base64 encoded image data?
&lt;/h4&gt;

&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; at this point decode the blurhash into a base64 encoded image data string, and store that in the database.&lt;/p&gt;

&lt;p&gt;This would mean no decoding is required to display the image, but at the expense of increased storage requirements.&lt;/p&gt;

&lt;p&gt;In my example here, the base64 encoded image data is significantly larger than the blurhash string. My example image is &lt;code&gt;~5,500 bytes&lt;/code&gt; in string length as base64, but only &lt;code&gt;100 bytes&lt;/code&gt; as the blurhash string. That's a 190%+ difference!&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the blurred placeholder
&lt;/h2&gt;

&lt;p&gt;Once we have our image resized, compressed into a blurhash, and then stored somewhere, we'll inevitably want to display it somewhere.&lt;/p&gt;

&lt;p&gt;To do that, we'll need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the blurhash from a string to an array of raw pixel data&lt;/li&gt;
&lt;li&gt;Convert the decoded pixel data to an image format, generally a PNG&lt;/li&gt;
&lt;li&gt;Convert that image into a base64 encoded string&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thankfully we have a library that can do all of this for us: &lt;a href="https://www.npmjs.com/package/blurhash-base64" rel="noopener noreferrer"&gt;blurhash-base64&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;blurhash-base64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now all we need to do is decode the blurhash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;blurhashToBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we can display the image!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{hashBase64}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blurhash-base64&lt;/code&gt; gives us a base64 encoded image data string that we can use to display the image.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why not just use CSS?
&lt;/h4&gt;

&lt;p&gt;Rather than doing all this fussy stuff with encoding and decoding, you could simply stretch the resized image to the desired size, use CSS to apply &lt;code&gt;blur&lt;/code&gt; to it, and then wrap it in a container with the same size as the original with &lt;code&gt;overflow-hidden&lt;/code&gt; to ensure the edges are crisp (CSS &lt;code&gt;blur&lt;/code&gt; feathers anything it is applied to).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4ccwx556jwd7gdhbg6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4ccwx556jwd7gdhbg6p.png" alt="Showing the difference of CSS only and the fullstack blurhash approach"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While this would result in a smaller file size for the client, there are a few downsides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You'll need to store that image data somewhere, in order to include it in the initial HTML&lt;/li&gt;
&lt;li&gt;Storing the image data will result in significantly larger storage requirements. My example here would result in needing to store &lt;code&gt;~940 B&lt;/code&gt; vs only &lt;code&gt;100 B&lt;/code&gt; for the blurhash, and that's with a relatively small image.&lt;/li&gt;
&lt;li&gt;Now you've got to mess with markups &lt;em&gt;and&lt;/em&gt; CSS to get the desired effect, and you'll still need JavaScript to swap the placeholder for the original image.&lt;/li&gt;
&lt;li&gt;I personally feel the CSS version looks a bit meh, like I'm looking through a dirty smeared lense instead of the frosted glass effect of blurhash.&lt;/li&gt;
&lt;li&gt;The original pixelated image is still there, and you're at the mercy of the browser's rendering engine to decide how good the blur effect is going to be.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even without file storage concerns, I think the blurhash version is the better option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swapping the placeholder for the original image
&lt;/h2&gt;

&lt;p&gt;Now that we have our blurred placeholder, and we're rendering it on the server, we'll need to swap it for the original (non-blurred) image when it's loaded on the client.&lt;/p&gt;

&lt;p&gt;Executing the following script in the client will swap the placeholder for the original image only when the image is within 50px of the viewport.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Select all images with a data-original-src attribute&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blurredImages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img[data-original-src]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Create an observer to load the images when they're in the viewport&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;loadHighQualityImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Begin loading image when it's within 50px of&lt;/span&gt;
    &lt;span class="c1"&gt;// the viewport for perceived performance&lt;/span&gt;
    &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;50px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadHighQualityImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Disconnect observer to prevent any accidental&lt;/span&gt;
  &lt;span class="c1"&gt;// loading due to erratic scrolling&lt;/span&gt;
  &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Set the src directly, which will trigger browser&lt;/span&gt;
  &lt;span class="c1"&gt;// to load the image&lt;/span&gt;
  &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalSrc&lt;/span&gt;

  &lt;span class="c1"&gt;// May as well clean up after ourselves&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalSrc&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Begin observing all images with a data-original-src attribute&lt;/span&gt;
&lt;span class="nx"&gt;blurredImages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we're done! Now you can take any image, resize it, blur it, save it, and then display it on your page with a placeholder that's instantly rendered for users, and then swapped for the original image when it's loaded.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>ux</category>
      <category>learning</category>
    </item>
    <item>
      <title>Detecting When a Sticky Element Becomes Sticky</title>
      <dc:creator>Jake Holman</dc:creator>
      <pubDate>Tue, 03 Jun 2025 09:30:00 +0000</pubDate>
      <link>https://forem.com/jakeisonline/detecting-when-a-sticky-element-becomes-sticky-38eg</link>
      <guid>https://forem.com/jakeisonline/detecting-when-a-sticky-element-becomes-sticky-38eg</guid>
      <description>&lt;p&gt;Ever noticed a sticky header that changes style as you scroll—like the event dates on &lt;a href="https://lu.ma/climate" rel="noopener noreferrer"&gt;Luma's site&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6gyh6zf6wg088mvk3g1w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6gyh6zf6wg088mvk3g1w.gif" alt="Animated GIF showing date headings on Luma event listings becoming sticky and having a hovering badge effect applied"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They elegantly transition to a different visual treatment once they become sticky. It’s a small interaction, but it makes the UI feel polished and intentional.&lt;/p&gt;

&lt;p&gt;I wanted to figure out how they were doing it.&lt;/p&gt;

&lt;p&gt;CSS handles the stickiness (with &lt;code&gt;position: sticky&lt;/code&gt;), but there’s no built-in way to detect when it happens.&lt;/p&gt;

&lt;p&gt;No CSS selector*. No JavaScript event.&lt;/p&gt;

&lt;p&gt;I love figuring out how to replicate cool interactions I see on the web—so here’s how to do this one using the magical &lt;code&gt;IntersectionObserver&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;It’s widely supported, lightweight, and once you know the trick, you’ll start seeing opportunities to use it everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;a href="https://jakeisonline.com/javascript/detecting-sticky-elements" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;🧠 Read the full guide with live interactive demos on jakeisonline.com&lt;/a&gt;

&lt;/h2&gt;




&lt;p&gt;Have you tried this approach, or come across similar patterns in the wild?&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Good news:&lt;/strong&gt; We’ll soon be able to use &lt;a href="https://caniuse.com/mdn-css_at-rules_container_scroll-state_queries_stuck" rel="noopener noreferrer"&gt;container queries to detect when an element becomes sticky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But as of this article’s publishing, &lt;a href="https://caniuse.com/?search=scroll-state" rel="noopener noreferrer"&gt;it’s only available in Chrome&lt;/a&gt;, so you’re stuck with this for now&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ux</category>
      <category>learning</category>
    </item>
    <item>
      <title>You should overcommunicate pending state</title>
      <dc:creator>Jake Holman</dc:creator>
      <pubDate>Wed, 28 May 2025 15:15:51 +0000</pubDate>
      <link>https://forem.com/jakeisonline/overcommunicating-pending-state-a-small-ux-tweak-that-developers-undervalue-214n</link>
      <guid>https://forem.com/jakeisonline/overcommunicating-pending-state-a-small-ux-tweak-that-developers-undervalue-214n</guid>
      <description>&lt;p&gt;Ever clicked “Save” and wondered: &lt;em&gt;Did it actually do anything?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That tiny moment of ambiguity — where the UI doesn’t clearly communicate what's happening — can erode user trust fast.&lt;/p&gt;

&lt;p&gt;In this post, I make the case for &lt;strong&gt;overcommunicating pending state&lt;/strong&gt;, especially in async interfaces. It's a small but powerful UX pattern that developers can (and should) own.&lt;/p&gt;

&lt;p&gt;I cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why this matters more than we think (even for “small” interactions)&lt;/li&gt;
&lt;li&gt;The subtle difference between “visible feedback” and &lt;em&gt;timely&lt;/em&gt; feedback&lt;/li&gt;
&lt;li&gt;A set of interactive examples to experience both good and bad handling of pending state&lt;/li&gt;
&lt;li&gt;Practical tips on what to overcommunicate (and what not to)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;a href="https://jakeisonline.com/blog/you-should-overcommunicate-pending-state" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;🧠 Read the full post with live interactive demos on jakeisonline.com&lt;/a&gt;

&lt;/h2&gt;




&lt;p&gt;💬 I'd love to hear how &lt;em&gt;you&lt;/em&gt; handle loading states in your apps — spinners? disabled buttons? toast messages? Let's swap notes.&lt;/p&gt;

&lt;p&gt;👋 This is my first post on dev.to — I write about UX patterns from a frontend developer’s perspective. If you're into interaction design, feel free to follow along!&lt;/p&gt;

</description>
      <category>ux</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>design</category>
    </item>
  </channel>
</rss>
