One of the annoying issues this blog had until recently was Cumulative Layout Shift (CLS) caused by responsive images. You probably already encountered a situation, when you arrive on a page and start reading its content while the page is still being loaded. And all of a sudden text jumps. This happens because when an image above the text has fully loaded and the browser rendered it, the page content below the image gets pushed by an amount equal to the height of the image.
In fixed layouts, it can be solved by setting width
and height
attributes on the img
tag, but it wouldn't work for responsive layouts where images are of variable size.
I researched how people solve it these days and ended up with a list of 2 options:
aspect-ratio
CSS propertyEven though option #1 solves the issue perfectly, its browser support is still lacking. So only option #2 remained. Well, it's not that bad. If you're ok with the requirements, it's not that much of a hustle to implement it.
The requirements are:
width
and height
of images must be known.img
tag must be wrapped in a container.width
(either relative or absolute, but not auto
).The trick is to set the height
of the container to zero and then apply a padding-bottom
value equal to img.height / img.width
ratio (in percents).
So a CSS would be:
And a React component:
When rendered, this component takes up all the available space within a parent container with the correct height.
This is an old-known trick that doesn't worth another post. But there was one puzzle I had to solve, that I haven't seen being solved on the internet. So it might be useful for some lost soul out there.
So, in my large screen layout for a blog post I have three kinds of images:
bleed
: image is a little bit wider than the main column.fill
: image's width is equal to the main column's width.center
: image is narrower than the main column.The first two worked perfectly as both had width
property defined:
But the third one — center
— had issues as it had width
set to auto
:
The idea is that if an image is smaller than 90% of the layout's width, it should not be enlarged and have its own width. Otherwise, pixels would show up. And if the image is wider than 90% of the layout's width, it fills exactly this space.
The first step in fixing it was to move width
definition to style
tag.
Then, I had to limit the width
to 90% of the layout's width:
Seems ok, but not really. The problem with this style is that it doesn't consider DPR and on screens with DPR > 1 pixels pop up. I couldn't use window.devicePixelRatio
since the site is statically rendered, so this API isn't applicable here. And I couldn't use media queries in the style
attribute, so I'm out of luck with -webkit-device-pixel-ratio
here as well. After googling around if it's possible to get current DPR via CSS APIs, the answer was "no". I pretty much gave up, shut down my laptop, and went for a dog walk. Where it hit me: I can't get the exact value of current DPR via CSS, but I have -webkit-device-pixel-ratio
, min
CSS function, and CSS variables!
And this worked. Until I started testing responsiveness. On small screens, the max width must be calculated differently. So I had to set a few more CSS vars and do a weird arithmetics:
Odd programming huh. Can't say I enjoyed it much, but the task was solved. No layout shift is happening here anymore.