Top
Best
New

Posted by ChiptuneIsCool 3/30/2025

Minimal CSS-only blurry image placeholders(leanrada.com)
470 points | 75 comments
esprehn 4/3/2025|
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_ 4/3/2025||
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">
cendyne 4/4/2025||
I can't wait for the day when attr has this implemented. It would eliminate the need for so many inline styles
miragecraft 4/5/2025||
Is this really a performance concern though?
WorldMaker 4/3/2025||
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.
matthberg 4/3/2025||
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).

mubou 4/3/2025||
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.)

mattdesl 4/3/2025||
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.

emsixteen 4/3/2025||
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.
diiiimaaaa 4/3/2025||
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 4/3/2025|||
> 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.

WorldMaker 4/4/2025|||
There are certainly legitimate cases where the placeholders come from a separate request. Most CRDTs and similar sync engines you don’t want to (and/or are not allowed to) store binary images directly inside them, so you need to store references to some other blob storage. But Blurhash is a simple short string (and LQIP here is a simple integer) and those store well in CRDTs and other sync engines, so you can pair that with your reference pointer (which might not even be a URL depending on your blob storage engine and its sync mechanics and authorization schemes and whatever else) and whatever other metadata you want/need to include like width/height or aspect ratio and alt/title/caption.

When the CRDT or document sync engine inevitably sync much faster than your blobs you have something to show in that placeholder. If the blob sync fails for some reason, you still have something to show more interesting than your browser’s old broken image logo under your “Sync is slow or broken” warning.

I think placeholders help a bunch in situations like that where your image fetch is a lot more complicated than a URL that you can add in a `src` attribute. It’s also really easy to get into situations where such blob fetching is complex: In cases where you have to respect user and/or tenant privacy and need complex OAuth flows. In cases where you need end-to-end photo encryption. In cases where you need peer-to-peer sync and only P2P sync because you’ve been mandated to reduce touch points and likelihood of accidentally storing photos at rest in middle layers. Situations like images of HIPAA data, PII, PIFI, etc.

On a static site with public (or cookie sessioned) images direct linked by URL, yeah the placeholders don’t do much other than check certain design boxes. There’s lots of other places images (and their metadata) come from, and placeholders are a useful fallback in the worst cases.

JimDabell 4/3/2025|||
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.
simonw 4/3/2025||
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...
throwaway2016a 4/3/2025||
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 4/3/2025|
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 4/3/2025||
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 4/3/2025|||
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/4a562082-b681-4f0c-909c-3c32c34fd050

throwaway2016a 4/3/2025|||
I could tell and I really appreciate it. It's really helpful to see both the good and the bad.

Great work!

Cieric 4/3/2025||
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.
chmod775 4/3/2025||
Cool hack, but performance is terrible. That page makes scrolling on my phone laggy.
bufferoverflow 4/3/2025|
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 4/3/2025|
Cool app but no maintained library to use it in our own apps and scripts.
bufferoverflow 4/3/2025||
https://github.com/GoogleChromeLabs/squoosh
Lord_Zero 4/5/2025||
If you go to NPM the first thing it says in bold is "no longer maintained" and it links to the Google Chrome labs GitHub as well.

https://www.npmjs.com/package/@squoosh/cli#project-no-longer...

So to clarify, is the GitHub maintained but the npm distribution is not? Or is none of it maintained unless you use the website/app?

More comments...