[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)