[HN Gopher] Minimal CSS-only blurry image placeholders
       ___________________________________________________________________
        
       Minimal CSS-only blurry image placeholders
        
       Author : ChiptuneIsCool
       Score  : 443 points
       Date   : 2025-03-30 11:11 UTC (4 days ago)
        
 (HTM) web link (leanrada.com)
 (TXT) w3m dump (leanrada.com)
        
       | ipunchghosts wrote:
       | I know very little of css and to me it seems like a configuration
       | file for rendering text, similar to changing default fonts
       | ornsizes for matplotlib plots using plt.rcParams. How does this
       | do inage blurring then?
        
         | teraflop wrote:
         | If you read the article, it explains exactly how the technique
         | works.
         | 
         | One of the many ways CSS allows you to customize formatting is
         | to change the background style of elements. In addition to just
         | using a solid color or image, you can specify a procedural
         | gradient. And by superimposing several such gradients, you can
         | make a very blurry approximation of an image.
         | 
         | CSS also includes a basic expression language which allows
         | evaluating simple arithmetic expressions. So you can encode all
         | the blurred image's parameters as a packed integer in a single
         | compact CSS property per image, and use rules to define the
         | gradients in terms of that integer.
         | 
         | Note that CSS is not used to compute the blurred image
         | representation itself -- you have to do that separately. (Even
         | if you could do it in pure CSS, the whole point is to show a
         | blurred preview image _before_ the image itself is downloaded
         | to the browser, so doing it in CSS would defeat the purpose.)
        
         | maxbond wrote:
         | > [It] seems like a configuration file for rendering text...
         | 
         | A more accurate mental model might be, "a declarative language
         | for styling HTML elements," where "styling" is very broad. You
         | can make user interfaces that show and hide elements, have
         | animations, etc. triggered by clicking buttons without a single
         | line of JavaScript. It's a lot more powerful than the
         | configuration parameters to plotting functions, in my book it's
         | a programming language rather than a configuration language.
        
       | Reubend wrote:
       | It's a cool solution, and I like that it's CSS only. But the
       | generated placeholders are way too blurry/lossy for my personal
       | preferences.
        
       | mubou wrote:
       | Was expecting the common "background-image: data url + filter:
       | blur" that a lot of static site generators produce, not a binary
       | encoding algorithm implemented in CSS! Very impressive.
       | 
       | I wonder what other things could be encoded this way. Those
       | generic profile pictures, perhaps? (The ones where your email or
       | account id is hashed to produce some unique geometric pattern.)
        
       | seejayseesjays wrote:
       | this is super neat! love your site
        
       | throwaway2016a wrote:
       | Very nice solution!
       | 
       | Definitely very low resolution, but compared to sites that use a
       | solid color this seems much better. And only requiring one
       | variable is really nice.
       | 
       | The article seems very well thought through. Though for both the
       | algorithm and the benchmark algorithm the half blue / half green
       | image with the lake shows the limitations of this technique.
       | Still pretty good considering how light weight it is.
        
         | 8n4vidtmkvmk wrote:
         | The half blue / half green image still looks better with LQIP
         | than BlurHash. I was getting ready to use BlurHash in my app,
         | might try this instead!
         | 
         | In fact, LQIP looks better than most of the BlurHash examples
         | in the gallery (https://leanrada.com/notes/css-only-
         | lqip/gallery/); not sure if these were cherry picked or what.
        
           | Kalabasa wrote:
           | Author here: Definitely cherry picked ;)
           | 
           | I did deliberately pick some "bad" examples like the
           | blue+green image, and other multicolor images.
           | 
           | I wanted to add an upload function so people could test any
           | image, then i realised I'd have to implement the
           | compression/hashing in the client. Maybe i should!
        
             | simonw wrote:
             | I tried getting that working earlier using Claude to
             | convert your script - you can see the result here: https://
             | claude.site/artifacts/b747d94a-2923-4904-8ed1-7330bf...
             | 
             | Here's the transcript and code: https://claude.ai/share/4a5
             | 62082-b681-4f0c-909c-3c32c34fd050
        
             | throwaway2016a wrote:
             | I could tell and I really appreciate it. It's really
             | helpful to see both the good and the bad.
             | 
             | Great work!
        
       | superkuh wrote:
       | I suppose the existence of bad uses does not invalidate the good
       | but it feels like 99% of blurry image placeholder behavior is
       | actually just preventing people from seeing anything unless they
       | also run the ad and spying javascript that monetizes the site.
       | 
       | So a CSS-only way is neat and indisputably better but I think
       | it's missing the point? The point of blurry placeholders isn't to
       | make things easier or display better. The point is to make things
       | worse. This write up is definitely making things better.
        
         | simonw wrote:
         | The point of blurry placeholders is to support loading a page
         | with potentially hundreds of images (maybe with additional lazy
         | loading, which doesn't need JavaScript these days) without
         | blocking display of the page on loading those full images.
         | 
         | I'm not sure why you think it has anything to do with forcing
         | people to execute JavaScript?
        
           | wruza wrote:
           | Can't speak for everyone ofc, but not sure if I ever wanted
           | blurry placeholders when images load fast enough, or found
           | them anything but annoying when not. I think these bells and
           | whistles only serve as designer's self-affirmation.
        
             | simonw wrote:
             | I've definitely wanted them on photo galleries with large
             | numbers of thumbnails, and I appreciate them when they are
             | implemented well, especially if I'm on a slow connection.
        
             | gblargg wrote:
             | Agreed, they just create needless visual activity. How
             | about a page specify where the images appear, and leave it
             | up to the browser to decide how to show them and load them?
             | Is that too simple and workable?
        
           | jasonkester wrote:
           | Indeed. So the visitor need only wait for the 20mb javascript
           | bundle, but not the 600kb of images, before he can see the
           | 1kb of text that he visited the site to read.
        
             | simonw wrote:
             | Sounds like you're in favor of a version of blurry
             | placeholders that's implemented in less than 1KB of CSS.
        
         | recursive wrote:
         | The placeholder is inline in the markup. It can be displayed
         | before the image loads. Which is not inline. I have no idea
         | what 99% thing you're talking about.
        
           | superkuh wrote:
           | Look at literally any "newspaper" website. From the smallest
           | local paper to the NYT.
        
             | recursive wrote:
             | That's a whole separate thing with a different reason for
             | existing.
        
         | nirava wrote:
         | It is really simple to make sure your blurred css placeholder
         | is cosmetic, and a progressive enhancement. I would know, I
         | wrote one a month ago for my personal site.
         | 
         | My goal was to have something that'd transmit all the essential
         | bits of the site in the first 14kB, and worked basically on
         | everything. It wasn't hard, honestly.
         | 
         | It wasn't a particularly complex site but i guess what I'm
         | saying is any well done (and well intentioned) implementation
         | of blurred image placeholders will works with or without JS.
         | That is just sound engineering...
        
       | davidmurdoch wrote:
       | This is brilliant!
        
       | mike2323 wrote:
       | broken on iOS (iPad)
        
         | simonw wrote:
         | Worked for me in Mobile Safari in iOS on my iPhone.
        
           | VladVladikoff wrote:
           | Maybe it's iOS version dependant. I'm a bit out of date (on
           | purpose for jailbreak) and the demo is broken for me.
        
             | whstl wrote:
             | I'm up to date and it's broken for me :/
        
           | wruza wrote:
           | Same setup, didn't work. (Empty space where blur supposed to
           | be.)
        
         | thangngoc89 wrote:
         | Also broken for me:
         | 
         | Safari 18.0 (20619.1.26.31.6), macOS Sequoia 15.0
        
           | tlb wrote:
           | Me too. Thumbnails just appear black.
           | 
           | Safari 17.6 (19618.3.11.11.5), MacOS Sonoma 14.7.3 (23H417)
           | 
           | It works on Chrome on the same machine.
        
           | alwillis wrote:
           | Works fine on macOS Sequoia 15.4 with Safari 18.4.
        
       | esprehn wrote:
       | This is really cool, I love seeing folks use CSS in clever ways.
       | :)
       | 
       | My one feedback would be to avoid using attr selectors on the
       | style attribute like [style*="--lqip:"]. Browsers normally lazy
       | compute that string version of the style attribute [1], but if
       | you use a selector like this then on every style recalc it'll
       | force all new inline styles (ex. element.style.foo = bar) to
       | compute the string version.
       | 
       | Instead if you use a separate boolean attribute (or even faster a
       | class) it'll avoid that perf foot gun. So write <div lqip style="
       | --lqip: ..."> and match on that.
       | 
       | [1]
       | https://source.chromium.org/chromium/chromium/src/+/main:thi...
        
         | cAtte_ wrote:
         | see also the author's last note on the upcoming parsing feature
         | of `attr()`, which would solve both problems (performance and
         | verbosity) at once:                   <img src="..."
         | lqip="192900">
        
       | Zensynthium wrote:
       | Love the website and article! Looks like even with CSS there's
       | always new things to learn and do, good stuff.
        
       | benfortuna wrote:
       | ..or use tailwind - https://tailwindcss.com/docs/filter-blur
        
         | jsheard wrote:
         | That's not at all equivalent to what the OP is doing. The point
         | isn't just to blur an image, which is what those Tailwind
         | classes do, the point is to render a very compact blurry
         | version of an image which _hasn 't loaded yet._
        
       | cynicalsecurity wrote:
       | Why is the page so sluggish on mobile?
        
         | simonw wrote:
         | Probably because of all of the wildly complex CSS calculations
         | it's running, as described by the article.
        
           | Kalabasa wrote:
           | Yep, there are a lot of layers and compositing operations
           | (maybe more than necessary?). I suppose it could be
           | simplified further.
        
       | biker142541 wrote:
       | This works significantly better than I would have expected. I was
       | just exploring extremely simple png strings as an alternative to
       | the hash libraries requiring decoding. I had also explored two
       | color css gradient, based on pregenerated major/minor colors, but
       | too course to be useful (for a fast scrolling gallery). I'll give
       | this a test drive!
        
       | matthberg wrote:
       | Since there're independent Lightness values set for each section
       | (I'd say quadrant but there are 6 of them), I wonder if two bits
       | can be shaved from the `L` value from the base color. It'd take
       | some reshuffling and might not play well with color customization
       | in mainly flat images, but I think it could work.
       | 
       | I'm also curious to see that they're doing solely grayscale
       | radial gradients over the base color instead of tweaking the base
       | color's `L` value and using that as the radial gradient's center,
       | I'd imagine you'd be doing more math that way in the OKLab
       | colorspace which might give prettier results(?).
       | 
       | Tempted to play around with this myself, it's a really creative
       | idea with a lot of potential. Maybe even try moving the centers
       | (picking from a list of pre-defined options with the two bits
       | stolen from the base color's L channel), to account for varying
       | patterns (person portraits, quadrant-based compositions, etc).
        
       | naveed125 wrote:
       | This is pretty neat
        
       | dmitrygr wrote:
       | cool, but the fact that you can now do this with CSS is part of
       | the reason that a new browser engine is so unlikely - one of
       | 100000 things that css can do now and need to be supported :(
       | 
       | Maybe we should have kept CSS simple and JS optional. Maybe we
       | took a few wrong turns...
        
         | cjpearson wrote:
         | It's all additive so each new feature does indeed add
         | complexity, but my impression is that it's often the older
         | features and all their quirks which are the most difficult to
         | implement. Adding a few math functions is much easier than
         | ensuring compatibility with CSS2 floats.
        
       | emsixteen wrote:
       | Forgive my ignorance, feel like it's embarrassing to ask here to
       | be honest, but can someone explain how this helps/works? I've
       | never actually used these placeholders, but I always imagined
       | that they work by processing the image beforehand on the server
       | and using something like a super low quality image or gradient or
       | such as the placeholder. If this is done in pure CSS, does the
       | browser not need to download the image first to figure out what's
       | in it, before then doing the placeholder effect? Perhaps it
       | doesn't help that I've not had my morning coffee yet, but I don't
       | understand.
        
         | JimDabell wrote:
         | It's still computed at build time or dynamically, by a
         | programming language. The "pure CSS" part of it means that the
         | hash is decoded into something visual by CSS without any
         | JavaScript required.
        
         | diiiimaaaa wrote:
         | These placeholders are generated by processing the image on a
         | server beforehand. Generally they create some html, css or svg
         | markup that is served inline. Having to do a separate request
         | for such placeholder is very bad idea.
         | 
         | It's not clear if these placeholders do actually help,
         | especially placeholders with very low quality. In my opinion,
         | they only add visual noise.
         | 
         | I'd focus more on avoiding layout shifts when images load, and
         | serving images in a good format (avif, webp) and size (use
         | `srcset` or `<picture>`).
        
           | biker142541 wrote:
           | > It's not clear if these placeholders do actually help
           | 
           | Well, it depends what you mean by help. It's very dependent
           | on use case and desired UX. Obviously you can prevent layout
           | shifts without them, you can provide feedback on loading
           | status in other ways, and ensure images don't slow load time
           | without colored placeholders. But they can provide a pleasant
           | UX for some use cases, when done right. They can be annoying
           | when not done well.
        
         | simonw wrote:
         | Here's the server-side (Node.js) build script that calculates
         | the integer placeholder image values and adds them to the
         | document:
         | https://github.com/Kalabasa/leanrada.com/blob/7b6739c7c30c66...
        
       | chmod775 wrote:
       | Cool hack, but performance is terrible. That page makes scrolling
       | on my phone laggy.
        
       | rckt wrote:
       | Nice, but... it's not actually minimal. But nice.
       | 
       | Also a bit of nitpicking. While it provides a visual placeholder
       | for an image that's being fetched, it does not reflect its
       | content. So, when it's loaded we can see a completely different
       | color palette and shapes.
        
         | pavlov wrote:
         | What do you mean? In my opinion this library does a very good
         | job of representing the image's color palette considering it's
         | encoded into a single integer. (Even smaller than usual because
         | of CSS limitations, only 20 bits!)
         | 
         | You don't even need JavaScript to decode that integer into the
         | image. The underlying CSS may be complex, but for the user of
         | the library it definitely feels minimal in a good way.
        
           | rckt wrote:
           | In the gallery https://leanrada.com/notes/css-only-
           | lqip/gallery/ there's a good example of what I mean - the
           | bottom right image or 4th from the end. A completely
           | different image in comparison to the gradient.
           | 
           | As for the minimalism, I understand what you mean. But I
           | understood the "minimal" part in regard to implementation,
           | not usage. If we only mean usage, we can say the same about a
           | lot of libs, that they are minimal. Yeah, it's minimal for
           | the end user, but under the hood it is not as minimal. It's
           | not anything bad, it's just how I interpreted the title.
        
         | mary-ext wrote:
         | That's pretty much an issue with every LQIP solutions though,
         | including BlurHash and ThumbHash. The only thing that matters
         | is that it's close enough to the real thing, since they're
         | meant to serve as placeholders.
        
         | tempoponet wrote:
         | I see two issues, let's say "opportunities":
         | 
         | First is the limitation to one hue value. Something like the
         | Sunflower (blue + yellow) is just yellow. Maybe there's a
         | tradeoff that could pack more hue but with less luminescence.
         | 
         | The second is how the primary color is selected. Several images
         | (plant on grey background, street food vendor) appear to be
         | averaging across the image and getting a grey value. By
         | selecting better for the predominant color and its placement,
         | the greys would appear on their own.
        
       | mattdesl wrote:
       | Really like this, nice work!
       | 
       | Something to note is that Color Theif (Quantize) is using median
       | cut on RGB, it would be interesting to try and extract dominant
       | color in OKLab instead.
       | 
       | I also love the idea of a genetic algorithm to find an ideal
       | match for a given image; it should be possible to simulate radial
       | gradients server & client side with webgpu, but probably overkill
       | for such a simple task.
       | 
       | EDIT: Although it works for me in Chrome, it doesn't seem to work
       | in Safari v16.1.
        
       | jbverschoor wrote:
       | That's sexy!
        
       | WorldMaker wrote:
       | It's obviously mostly an aesthetic nitpick for this blog post
       | specifically and not the project itself, because few people are
       | going to be exploring the encoded space outside of the blog post,
       | but the sliders letting you explore the LQIP space would "flash"
       | a lot less if the base color was encoded in the high bits instead
       | of the low bits.
        
       | duffyjp wrote:
       | Years ago before you could do anything this fancy with CSS I
       | experimented with generating 3x2 pixel images server side and
       | then presenting them as base64 encoded pngs in a "scoped" block
       | of CSS to ensure they loaded before the src images.
       | Coincidentally this was the same 3x2 layout as OP did here with
       | CSS. I abandoned it because a 3x2 image scaled up looked
       | terrible, and went with average color instead. This solution
       | looks a lot better visually.
       | 
       | I still do the average color thing today since it's easy to
       | calculate and store server side (I resize the uploaded image to
       | 1x1 px and just record the result as a hex code in the DB).
        
       | ssttoo wrote:
       | Another simple css-only solution as the article mentions is
       | gradients. Like                 background: linear-gradient(
       | to right,         #51463e 0%,         #28241f 100%       );
       | 
       | Tool: https://tools.w3clubs.com/gip/
        
       | layer8 wrote:
       | I read the title as "Minimal CSS -- only blurry image
       | placeholders" first by mistake. ;)
        
       | molszanski wrote:
       | Amazing work! Thanks you for sharing!
        
       | bufferoverflow wrote:
       | We need to embrace WebP v2 for this kind of stuff. I took one of
       | their images, resized it to 24x16px, and compressed it with
       | Squoosh at 65% quality. It compresses to just 144 bytes. And it
       | looks way way way better than these CSS gradients.
       | 
       | https://squoosh.app/
        
         | Lord_Zero wrote:
         | Cool app but no maintained library to use it in our own apps
         | and scripts.
        
           | bufferoverflow wrote:
           | https://github.com/GoogleChromeLabs/squoosh
        
       | thwarted wrote:
       | No one remembers the lowsrc img attribute.
        
       | bmandale wrote:
       | My attempt at the four color approach:
       | 
       | https://0x0.st/820Q.html
        
       | Cieric wrote:
       | Just in case anyone also misses it like I did, dark reader (at
       | least on firefox) appears to apply itself to the final colors
       | causing them to look quite bad and not match the input image at
       | all. I would have discounted this entirely if it wasn't for all
       | the praise I was seeing in the comments here.
        
       | turnsout wrote:
       | Man, it's wild how much you can do with CSS calculations. How
       | long before someone makes a CSS-only Game Boy emulator?
        
       ___________________________________________________________________
       (page generated 2025-04-03 23:01 UTC)