images – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 09 Dec 2022 14:26:40 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 images – CSS-Tricks https://css-tricks.com 32 32 45537868 CSS Infinite Slider Flipping Through Polaroid Images https://css-tricks.com/css-infinite-slider-flipping-through-polaroid-images/ https://css-tricks.com/css-infinite-slider-flipping-through-polaroid-images/#respond Fri, 09 Dec 2022 14:26:39 +0000 https://css-tricks.com/?p=375537 In the last article, we made a pretty cool little slider (or “carousel” if that’s what you prefer) that rotates in a circular direction. This time we are going to make one that flips through a stack of Polaroid …


CSS Infinite Slider Flipping Through Polaroid Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In the last article, we made a pretty cool little slider (or “carousel” if that’s what you prefer) that rotates in a circular direction. This time we are going to make one that flips through a stack of Polaroid images.

Cool right? Don’t look at the code quite yet because there’s a lot to unravel. Join me, will ya?

CSS Sliders series

The basic setup

Most of the HTML and CSS for this slider is similar to the circular one we made last time. In fact, we’re using the exact same markup:

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

And this is the basic CSS that sets our parent .gallery container as a grid where all the images are stacked one on top of one another:

.gallery  {
  display: grid;
  width: 220px; /* controls the size */
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 10px solid #f2f2f2;
  box-shadow: 0 0 4px #0007;
}

Nothing complex so far. Even for the Polaroid-like style for the images, all I’m using is some border and box-shadow. You might be able to do it better, so feel free to play around with those decorative styles! We’re going to put most of our focus on the animation, which is the trickiest part.

What’s the trick?

The logic of this slider relies on the stacking order of the images — so yes, we are going to play with z-index. All of the images start with the same z-index value (2) which will logically make the last image on the top of the stack.

We take that last image and slide it to the right until it reveals the next image in the stack. Then we decrease the image’s z-index value then we slide it back into the deck. And since its z-index value is lower than the rest of the images, it becomes the last image in the stack.

Here is a stripped back demo that shows the trick. Hover the image to activate the animation:

Now, imagine the same trick applied to all the images. Here’s the pattern if we’re using the :nth-child() pseudo-selector to differentiate the images:

  • We slide the last image (N). The next image is visible (N - 1).
  • We slide the next image (N - 1). The next image is visible (N - 2)
  • We slide the next image (N - 2). The next image is visible (N - 3)
  • (We continue the same process until we reach the first image)
  • We slide the first image (1). The last image (N) is visible again.

That’s our infinite slider!

Dissecting the animation

If you remember the previous article, I defined only one animation and played with delays to control each image. We will be doing the same thing here. Let’s first try to visualize the timeline of our animation. We will start with three images, then generalize it later for any number (N) of images.

Diagramming the three parts of the animation.

Our animation is divided into three parts: “slide to right”, “slide to left” and “don’t move”. We can easily identify the delay between each image. If we consider that the first image starts at 0s, and the duration is equal to 6s, then the second one will start at -2s and the third one at -4s.

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 6s / 3 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 6s / 3 */

We can also see that the “don’t move” part takes two-thirds of the whole animation (2*100%/3) while the “slide to right” and “slide to left” parts take one-third of it together — so, each one is equal to 100%/6 of the total animation.

We can write our animation keyframes like this:

@keyframes slide {
  0%     { transform: translateX(0%); }
  16.67% { transform: translateX(120%); }
  33.34% { transform: translateX(0%); }
  100%   { transform: translateX(0%); } 
}

That 120% is an arbitrary value. I needed something bigger than 100%. The images need to slide to the right away from the rest of the images. To do that, it needs to move by at least 100% of its size. That’s why I went 120% — to gain some extra space.

Now we need to consider the z-index. Don’t forget that we need to update the image’s z-index value after it slides to the right of the pile, and before we slide it back to the bottom of the pile.

@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  100%   { transform: translateX(0% );  z-index: 1; }  
}

Instead of defining one state at the 16.67% (100%/6) point in the timeline, we are defining two states at nearly identical points (16.66% and 16.67%) where the z-index value decreases before we slide back the image back to the deck.

Here’s what happens when we pull of all that together:

Hmmm, the sliding part seems to work fine, but the stacking order is all scrambled! The animation starts nicely since the top image is moving to the back… but the subsequent images don’t follow suit. If you notice, the second image in the sequence returns to the top of the stack before the next image blinks on top of it.

We need to closely follow the z-index changes. Initially, all the images have are z-index: 2. That means the stacking order should go…

Our eyes 👀 --> 3rd (2) | 2nd (2) | 1st (2)

We slide the third image and update its z-index to get this order:

Our eyes 👀 --> 2nd (2) | 1st (2) | 3rd (1)

We do the same with the second one:

Our eyes 👀 --> 1st (2) | 3rd (1) | 2nd (1)

…and the first one:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

We do that and everything seems to be fine. But in reality, it’s not! When the first image is moved to the back, the third image will start another iteration, meaning it returns to z-index: 2:

Our eyes 👀 --> 3rd (2) | 2nd (1) | 1st (1)

So, in reality we never had all the images at z-index: 2 at all! When the images aren’t moving (i.e., the “don’t move” part of the animation) the z-index is 1. If we slide the third image and update its z-index value from 2 to 1, it will remain on the top! When all the images have the same z-index, the last one in the source order — our third image in this case — is on top of the stack. Sliding the third image results in the following:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

The third image is still on the top and, right after it, we move the second image to the top when its animation restarts at z-index: 2:

Our eyes 👀 --> 2nd (2) | 3rd (1) | 1st (1)

Once we slide it, we get:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

Then the first image will jump on the top:

Our eyes 👀 --> 1st(2) | 3rd (1) | 2nd (1)

OK, I am lost. All the logic is wrong then?

I know, it’s confusing. But our logic is not completely wrong. We only have to rectify the animation a little to make everything work the way we want. The trick is to correctly reset the z-index.

Let’s take the situation where the third image is on the top:

Our eyes 👀 -->  3rd (2) | 2nd (1) | 1st (1)

We saw that sliding the third image and changing its z-index keeps it on top. What we need to do is update the z-index of the second image. So, before we slide the third image away from the deck, we update the z-index of the second image to 2.

In other words, we reset the z-index of the second image before the animation ends.

Diagramming the parts of the animation with indicators for where z-index is increased or decreased.

The green plus symbol represents increasing z-index to 2, and the red minus symbol correlates to z-index: 1. The second image starts with z-index: 2, then we update it to 1 when it slides away from the deck. But before the first image slides away from the deck, we change the z-index of the second image back to 2. This will make sure both images have the same z-index, but still, the third one will remain on the top because it appears later in the DOM. But after the third image slides and its z-index is updated, it moves to the bottom.

This two-thirds through the animation, so let’s update our keyframes accordingly:

@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  66.33% { transform: translateX(0%);   z-index: 1; }
  66.34% { transform: translateX(0%);   z-index: 2; } /* and also here */
  100%   { transform: translateX(0%);   z-index: 2; }  
}

A little better, but still not quite there. There’s another issue…

Oh no, this will never end!

Don’t worry, we are not going to change the keyframes again because this issue only happens when the last image is involved. We can make a “special” keyframe animation specifically for the last image to fix things up.

When the first image is on the top, we have the following situation:

Our eyes 👀 -->  1st (2) | 3rd (1) | 2nd (1)

Considering the previous adjustment we made, the third image will jump on the top before the first image slides. It only happens in this situation because the next image that moves after the first image is the last image which has a higher order in the DOM. The rest of the images are fine because we have N, then N - 1, then we go from 3 to 2, and 2 to 1… but then we go from 1 to N.

To avoid that, we will use the following keyframes for the last image:

@keyframes slide-last {
  0%     { transform: translateX(0%);   z-index: 2;}
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  83.33% { transform: translateX(0%);   z-index: 1; }
  83.34% { transform: translateX(0%);   z-index: 2; } /* and also here */
  100%   { transform: translateX(0%);   z-index: 2; }
}

We reset the z-index value 5/6 through the animation (instead of two-thirds) which is when the first image is out of the pile. So we don’t see any jumping!

TADA! Our infinite slider is now perfect! Here’s our final code in all its glory:

.gallery > img {
  animation: slide 6s infinite;
}
.gallery > img:last-child {
  animation-name: slide-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } 
  33.34% { transform: translateX(0%); z-index: 1; }
  66.33% { transform: translateX(0%); z-index: 1; }
  66.34% { transform: translateX(0%); z-index: 2; } 
  100% { transform: translateX(0%); z-index: 2; }
}
@keyframes slide-last {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; }
  33.34% { transform: translateX(0%); z-index: 1; }
  83.33% { transform: translateX(0%); z-index: 1; }
  83.34% { transform: translateX(0%); z-index: 2; } 
  100%  { transform: translateX(0%); z-index: 2; }
}

Supporting any number of images

Now that our animation works for three images, let’s make it work for any number (N) of images. But first, we can optimize our work a little by splitting the animation up to avoid redundancy:

.gallery > img {
  z-index: 2;
  animation: 
    slide 6s infinite,
    z-order 6s infinite steps(1);
}
.gallery > img:last-child {
  animation-name: slide, z-order-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}
@keyframes z-order {
  16.67%,
  33.33% { z-index: 1; }
  66.33% { z-index: 2; }
}
@keyframes z-order-last {
  16.67%,
  33.33% { z-index: 1; }
  83.33% { z-index: 2; }
}

Way less code now! We make one animation for the sliding part and another one for the z-index updates. Note that we use steps(1) on the z-index animation. That’s because I want to abruptly change the z-index value, unlike the sliding animation where we want smooth movement.

Now that the code is easier to read and maintain, we have a better view for figuring out how to support any number of images. What we need to do is update the animation delays and the percentages of the keyframes. The delay are easy because we can use the exact same loop we made in the last article to support multiple images in the circular slider:

@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 - $i)/$n}*6s);
  }
}

That means we’re moving from vanilla CSS to Sass. Next, we need to imagine how the timeline scale with N images. Let’s not forget that the animation happens in three phases:

Showing the three parts of the animation in a series of lines with arrows.

After “slide to right” and “slide to left”, the image should stay put until the rest of the images go through the sequence. So the “don’t move” part needs to take the same amount of time as (N - 1) as “slide to right” and “slide to left”. And within one iteration, N images will slide. So, “slide to right” and “slide to left” both take 100%/N of the total animation timeline. The image slides away from the pile at (100%/N)/2 and slides back at 100%/N .

We can change this:

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}

…to this:

@keyframes slide {
  #{50/$n}%  { transform: translateX(120%); }
  #{100/$n}% { transform: translateX(0%); }
}

If we replace N with 3, we get 16.67% and 33.33% when there are 3 images in the stack. It’s the same logic with the stacking order where we will have this:

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  66.33% { z-index: 2; }
}

We still need to update the 66.33% point. That’s supposed to be where the image resets its z-index before the end of the animation. At that same time, the next image starts to slide. Since the sliding part takes 100%/N, the reset should happen at 100% - 100%/N:

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 - 100/$n}% { z-index: 2; }
}

But for our z-order-last animation to work, it should happen a bit later in the sequence. Remember the fix we did for the last image? Resetting the z-index value needs to happen when the first image is out of the pile and not when it starts sliding. We can use the same reasoning here in our keyframes:

@keyframes z-order-last {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 - 50/$n}% { z-index: 2; }
}

We are done! Here’s what we get when using five images:

We can add a touch of rotation to make things a bit fancier:

All I did is append rotate(var(--r)) to the transform property. Inside the loop, --r is defined with a random angle:

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    --r: #{(-20 + random(40))*1deg}; /* a random angle between -20deg and 20deg */
  }
}

The rotation creates small glitches as we can sometimes see some of the images jumping to the back of the stack, but it’s not a big deal.

Wrapping up

All that z-index work was a big balancing act, right? If you were unsure how stacking order work before this exercise, then you probably have a much better idea now! If you found some of the explanations hard to follow, I highly recommend you to take another read of the article and map things out with pencil and paper. Try to illustrate each step of the animation using a different number of images to better understand the trick.

Last time, we used a few geometry tricks to create a circular slider that rotates back to the first image after a full sequence. This time, we accomplished a similar trick using z-index. In both cases, we didn’t duplicate any of the images to simulate a continuous animation, nor did we reach for JavaScript to help with the calculations.

Next time, we will make 3D sliders. Stay tuned!


CSS Infinite Slider Flipping Through Polaroid Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-infinite-slider-flipping-through-polaroid-images/feed/ 0 375537
CSS Infinite and Circular Rotating Image Slider https://css-tricks.com/css-only-infinite-and-circular-image-slider/ https://css-tricks.com/css-only-infinite-and-circular-image-slider/#comments Fri, 02 Dec 2022 14:12:57 +0000 https://css-tricks.com/?p=375308 Image sliders (also called carousels) are everywhere. There are a lot of CSS tricks to create the common slider where the images slide from left to right (or the opposite). It’s the same deal with the many JavaScript libraries out


CSS Infinite and Circular Rotating Image Slider originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Image sliders (also called carousels) are everywhere. There are a lot of CSS tricks to create the common slider where the images slide from left to right (or the opposite). It’s the same deal with the many JavaScript libraries out there that create fancy sliders with complex animations. We are not going to do any of that in this post.

Through a little series of articles, we are going to explore some fancy and uncommon CSS-only sliders. If you are of tired seeing the same ol’ classic sliders, then you are in the right place!

CSS Sliders series

For this first article, we will start with something I call the “circular rotating image slider”:

Cool right? let’s dissect the code!

The HTML markup

If you followed my series of fancy image decorations or CSS grid and custom shapes, then you know that my first rule is to work with the smallest HTML possible. I always try hard to find CSS solutions before cluttering my code with a lot <div>s and other stuff.

The same rule applies here — our code is nothing but a list of images in a container.

Let’s say we’re working with four images:

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

That’s it! Now let’s move to the interesting part of the code. But first, we’re going to dive into this to understand the logic of how our slider works.

How does it work?

Here is a video where I remove overflow: hidden from the CSS so we can better understand how the images are moving:

It’s like our four images are placed on a large circle that rotates counter-clockwise.

All the images have the same size (denoted by S in the figure). Note the blue circle which is the circle that intersects with the center of all the images and has a radius (R). We will need this value later for our animation. R is equal to 0.707 * S. (I’m going to skip the geometry that gives us that equation.)

Let’s write some CSS!

We will be using CSS Grid to place all the images in the same area above each other:

.gallery  {
  --s: 280px; /* control the size */

  display: grid;
  width: var(--s);
  aspect-ratio: 1;
  padding: calc(var(--s) / 20); /* we will see the utility of this later */
  border-radius: 50%;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

Nothing too complex so far. The tricky part is the animation.

We talked about rotating a big circle, but in reality, we will rotate each image individually creating the illusion of a big rotating circle. So, let’s define an animation, m, and apply it to the image elements:

.gallery > img {
  /* same as before */
  animation: m 8s infinite linear;
  transform-origin: 50% 120.7%;
}

@keyframes m {
  100% { transform: rotate(-360deg); }
}

The main trick relies on that highlighted line. By default, the CSS transform-origin property is equal to center (or 50% 50%) which makes the image rotate around its center, but we don’t need it to do that. We need the image to rotate around the center of the big circle that contains our images hence the new value for transform-origin.

Since R is equal to 0.707 * S, we can say that R is equal to 70.7% of the image size. Here’s a figure to illustrate how we got the 120.7% value:

Let’s run the animation and see what happens:

I know, I know. The result is far from what we want, but in reality we are very close. It may looks like there’s just one image there, but don’t forget that we have stacked all the images on top of each other. All of them are rotating at the same time and only the top image is visible. What we need is to delay the animation of each image to avoid this overlap.

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 8s / 4 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 8s / 4 */
.gallery > img:nth-child(4) { animation-delay: -6s; } /* -3 * 8s / 4 */

Things are already getting better!

If we hide the overflow on the container we can already see a slider, but we will update the animation a little so that each image remains visible for a short period before it moves along.

We’re going to update our animation keyframes to do just that:

@keyframes m {
  0%, 3% { transform: rotate(0); }
  22%, 27% { transform: rotate(-90deg); }
  47%, 52% { transform: rotate(-180deg); }
  72%, 77% { transform: rotate(-270deg); }
  98%, 100% { transform: rotate(-360deg); }
}

For each 90deg (360deg/4, where 4 is the number of images) we will add a small pause. Each image will remain visible for 5% of the overall duration before we slide to the next one (27%-22%, 52%-47%, etc.). I’m going to update the animation-timing-function using a cubic-bezier() function to make the animation a bit fancier:

Now our slider is perfect! Well, almost perfect because we are still missing the final touch: the colorful circular border that rotates around our images. We can use a pseudo-element on the .gallery wrapper to make it:

.gallery {
  padding: calc(var(--s) / 20); /* the padding is needed here */
  position: relative;
}
.gallery::after {
  content: "";
  position: absolute;
  inset: 0;
  padding: inherit; /* Inherits the same padding */
  border-radius: 50%;
  background: repeating-conic-gradient(#789048 0 30deg, #DFBA69 0 60deg);
  mask: 
    linear-gradient(#fff 0 0) content-box, 
    linear-gradient(#fff 0 0);
  mask-composite: exclude;
}
.gallery::after,
.gallery >img {
  animation: m 8s infinite cubic-bezier(.5, -0.2, .5, 1.2);
}

I have created a circle with a repeating conic gradient for the background while using a masking trick that only shows the padded area. Then I apply to it the same animation we defined for the images.

We are done! We have a cool circular slider:

Let’s add more images

Working with four images is good, but it would be better if we can scale it to any number of images. After all, this is the purpose of an image slider. We should be able to consider N images.

For this, we are going to make the code more generic by introducing Sass. First, we define a variable for the number of images ($n) and we will update every part where we hard-coded the number of images (4).

Let’s start with the delays:

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 8s / 4 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 8s / 4 */
.gallery > img:nth-child(4) { animation-delay: -6s; } /* -3 * 8s / 4 */

The formula for the delay is (1 - $i)*duration/$n, which gives us the following Sass loop:

@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 - $i) / $n} * 8s);
  }
}

We can make the duration a variable as well if we really want to. But let’s move on to the animation:

@keyframes m {
  0%, 3% { transform: rotate(0); }
  22%, 27% { transform: rotate(-90deg); }
  47%, 52% { transform: rotate(-180deg); }
  72%, 77% { transform: rotate(-270deg); }
  98%, 100% {transform: rotate(-360deg); }
}

Let’s simplify it to get a better view of the pattern:

@keyframes m {
  0% { transform: rotate(0); }
  25% { transform: rotate(-90deg); }
  50% { transform: rotate(-180deg); }
  75% { transform: rotate(-270deg); }
  100% { transform: rotate(-360deg); }
}

The step between each state is equal to 25% — which is 100%/4 — and we add a -90deg angle — which is -360deg/4. That means we can write our loop like this instead:

@keyframes m {
  0% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100}% { transform: rotate(#{($i / $n) * -360}deg); }  
  }
  100% { transform: rotate(-360deg); }
}

Since each image takes 5% of the animation, we change this:

#{($i / $n) * 100}%

…with this:

#{($i / $n) * 100 - 2}%, #{($i / $n) * 100 + 3}%

It should be noted that 5% is an arbitrary value I choose for this example. We can also make it a variable to control how much time each image should stay visible. I am going to skip that for the sake of simplicity, but for homework, you can try to do it and share your implementation in the comments!

@keyframes m {
  0%,3% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100 - 2}%, #{($i / $n) * 100 + 3}% { transform: rotate(#{($i / $n) * -360}deg); }  
  }
  98%,100% { transform: rotate(-360deg); }
}

The last bit is to update transform-origin. We will need some geometry tricks. Whatever the number of images, the configuration is always the same. We have our images (small circles) placed inside a big circle and we need to find the value of the radius, R.

You probably don’t want a boring geometry explanation so here’s how we find R:

R = S / (2 * sin(180deg / N))

If we express that as a percentage, that gives us:

R = 100% / (2 * sin(180deg / N)) = 50% / sin(180deg / N)

…which means the transform-origin value is equal to:

transform-origin: 50% (50% / math.sin(180deg / $n) + 50%);

We’re done! We have a slider that works with any number images!

Let’s toss nine images in there:

Add as many images as you want and update the $n variable with the total number of images.

Wrapping up

With a few tricks using CSS transforms and standard geometry, we created a nice circular slider that doesn’t require a lot of code. What is cool about this slider is that we don’t need to bother duplicating the images to keep the infinite animation since we have a circle. After a full rotation, we will get back to the first image!


CSS Infinite and Circular Rotating Image Slider originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-only-infinite-and-circular-image-slider/feed/ 7 375308
CSS Grid and Custom Shapes, Part 3 https://css-tricks.com/css-grid-and-custom-shapes-part-3/ https://css-tricks.com/css-grid-and-custom-shapes-part-3/#comments Fri, 11 Nov 2022 14:42:04 +0000 https://css-tricks.com/?p=374826 After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image …


CSS Grid and Custom Shapes, Part 3 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image galleries.

CSS Grid and Custom Shapes series

Should I read the previous articles before?

It’s not mandatory but highly recommended to cover as many tricks as possible. You can also read them in any order, but following along in chronological is a good idea to see how we arrived here.

Enough talking, let’s jump straight to our first example.

Before digging into the CSS, let’s check the markup:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
</div>

Nothing but a few <img> tags in a div wrapper, right? Remember, the main challenge for this series is to work with the smallest amount of HTML possible. All the examples we’ve seen throughout this series use the exact same HTML markup. No extra divs, wrappers, and whatnot. All that we need are images contained in a wrapper element.

Let’s check the CSS now:

.gallery {
  --g: 6px; /* the gap */

  display: grid;
  width: 450px; /* the size */
  aspect-ratio: 1; /* equal height */
  grid: auto-flow 1fr / repeat(3, 1fr);
  gap: var(--g);
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: 2 / 1 / span 2 / span 2;
}

Basically, this is a square grid with three equal columns. From there, all that’s happening is the second and third images are explicitly placed on the grid, allowing the first and last images to lay out automatically around them.

This automatic behavior is a powerful feature of CSS Grid called “auto-placement”. Same thing with the number of rows — none of them are explicitly defined. The browser “implicitly” creates them based on the placement of the items. I have a very detailed article that explores both concepts.

You might be wondering what’s going on with those grid and grid-area property values. They look strange and are tough to grok! That’s because I chose the CSS grid shorthand property, which is super useful but accepts an unseemly number of values from its constituent properties. You can see all of them in the Almanac.

But what you really need to know is this:

grid: auto-flow 1fr / repeat(3, 1fr);

…is equivalent to this:

grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
DevTools style rules for the grid property.
You can also use your favorite DevTools for further proof.

Same for the grid-area property. If we open DevTools and inspect our declaration: grid-area: 1/2/span 2/span 2; you will get the following:

grid-area: 1 / 2 / span 2 / span 2;

…that is the same as writing all this out:

grid-row-start: 1; /* 1st row */
grid-column-start: 2; /* 2nd column */
grid-row-end: span 2; /* take 2 rows */
grid-column-end: span 2; /* take 2 columns */

Same deal for the other grid-area declaration. When we put it all together, here’s what we get:

The different images labeled by number on the grid.

Yes, the second and third images are overlapped in the middle. That’s no mistake! I purposely spanned them on top of one another so that I can apply a clip-path to cut a portion from each one and get the final result:

Showing the effect with and without clip-path.

How do we do that? We can cut the bottom-left corner of the second image (img:nth-child(2)) with the CSS clip-path property:

clip-path: polygon(0 0, 100% 0, 100% 100%, calc(50% + var(--g) / 4) 100%, 0 calc(50% - var(--g) / 4))

And the top-right corner of the third one:

clip-path: polygon(0 0, calc(50% - var(--g) / 4) 0, 100% calc(50% + var(--g) / 4), 100% 100%, 0 100%);

I know, I know. That’s a lot of numbers and whatnot. I do have an article that details the technique.

That’s it, we have our first grid of images! I added a grayscale filter on the <img> selector to get that neat little hover effect.

The Split Image Reveal

Let’s try something different. We can take what we learned about clipping the corner of an image and combine it with a nice effect to reveal the full image on hover.

The grid configuration for this one is less intense than the last one, as all we need are two overlapping images:

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 350px; /* the size */
  aspect-ratio: 1; /* equal height */
}

Two images that are the same size are stacked on top of each other (thanks to grid-area: 1 / 1).

The hover effect relies on animating clip-path. We will dissect the code of the first image to see how it works, then plug the same thing into the second image with updated values. Notice, though that we have three different states:

  1. When no images are hovered, half of each image is revealed.
  2. When we hover over the first image, it is more fully revealed but retains a small corner clip.
  3. When we hover over the second image, the first one has only a small triangle visible.
Showing the three clipping states of the hover effect.

In each case, we have a triangular shape. That means we need a three-point polygon for the clip-path value.

What? The second state isn’t a triangle, but more of a square with a cut corner.

You are right, but if we look closely we can see a “hidden” triangle. Let’s add a box-shadow to the images.

A ha! Did you notice it?

Showing the transition between states with the overflow shape revealed to explain how it works.

What sort of magic is this? It’s a little known fact that clip-path accepts values outside the 0%-100% range, which allows us to create “overflowing” shapes. (Yes, I just coined this. You’re welcome.) This way, we only have to work with three points instead of the five it would take to make the same shape from the visible parts. Optimized CSS for the win!

This is the code after we plug in the polygon values into the clip-path property:

.gallery > img:first-child {
  clip-path: polygon(0 0, calc(100% + var(--_p)) 0 , 0 calc(100% + var(--_p)))
}
.gallery > img:last-child {
  clip-path: polygon(100% 100%, 100% calc(0% - var(--_p)), calc(0% - var(--_p)) 100%)
}

Notice the --_p variable. I’m using that to optimize the code a bit as we add the hover transition. Instead of updating the whole clip-path we only update this variable to get the movement. Here is a video to see how the points should move between each state:

We can take slap a transition on the <img> selector, then update the --_p variable on the states to get the final effect:

.gallery {
  --g: 8px; /* the gap */
}
.gallery > img {
  /* etc. */
  --_p: calc(-1 * var(--g));
  transition: .4s .1s;
}
.gallery:hover > img:last-child,
.gallery:hover > img:first-child:hover{
  --_p: calc(50% - var(--g));
}
.gallery:hover > img:first-child,
.gallery:hover > img:first-child:hover + img {
  --_p: calc(-50% - var(--g));
}

If we don’t consider the gap (defined as --g in the code) between the images, then the three values of --_p are 0%, 50%, and -50%. Each one defines one of the states we explained previously.

The Pie Image Reveal

Let’s increase the difficulty level from that last one and try to do the same trick but with four images instead of two.

Cool, right? Each image is a quarter of a circle and, on hover, we have an animation that transforms an image into a full circle that covers the remaining images. The effect may look impossible because there is no way to rotate points and transform them to fill the circle. In reality, though, we are not rotating any points at all. It’s an illusion!

For this example, I will only focus on the clip-path animation since the configuration of the grid is the same as the previous example: four equally-sized images stacked on top of each other.

And a video worth a boring and long explanation:

The clip-path is formed by seven points, where three of them are in a fixed position and the others move as shown in the video. The effect looks less cool when it’s running slowly but we can see how the clip-path morphs between shapes.

The effect is a little better if we add border-radius and we make it faster:

And by making it even faster like in the original example, we get the perfect illusion of one quarter of a circle morphing into a full circle. Here’s the polygon value for our clip-path on the first image in the sequence:

.gallery > img:nth-child(1) {
  clip-path: polygon(50% 50%, calc(50% * var(--_i, 0)) calc(120% * var(--_i, 0)), 0 calc(100% * var(--_i, 0)),0 0, 100% 0, 100% calc(100% * var(--_i, 0)), calc(100% - 50% * var(--_i, 0)) calc(120% * var(--_i, 0)));
}
.gallery > img:hover {
 --_i: 1;
}

As usual, I am using a variable to optimize the code. The variable will switch between 0 and 1 to update the polygon.

The same goes for the others image but with a different clip-path configuration. I know that the values may look hard to decipher but you can always use online tools like Clippy to visualize the values.

The Mosaic of Images

You know mosaics, right? It’s an art style that creates decorative designs out of smaller individual pieces, like colored stones. But it can also be a composite image made up of other smaller images.

And, you guessed it: we can totally do that sort of thing in CSS!

First, let’s imagine what things are like if clip-path were taken out of the mix and all we had were five overlapping images:

I am cheating a little in this video because I am inspecting the code to identify the area of each image, but this is what you need to do in your head. For each image, try to complete the missing part to see the full rectangle and, with this, we can identify the position and size of each one.

We need to find how many columns and rows we need for the grid:

  1. We have two big images placed next to each other that each fill half the grid width and the full grid height. That means will probably need two columns (one for both images) and one row (for the full height of the grid).
  2. We have the image in the middle that overlaps the two other images. That means we actually need four columns instead of two, though we still only need the one row.
  3. The last two images each fill half the grid, just like the first two images. But they’re only half the height of the grid. We can use the existing columns we already have, but we’re going to need two rows instead of one to account for these images being half the grid height.
That leaves us with a tidy 4×2 grid.

I don’t want you to think that the way I sliced this up is the only way to do it. This is merely how I’ve made sense of it. I am sure there are other configurations possible to get the same layout!

Let’s take that information and define our grid, then place the images on it:

.gallery {
  display: grid;
  grid: repeat(2, 1fr) / repeat(4, 1fr); 
  aspect-ratio: 2;
}
.gallery img:nth-child(1) {
  grid-area: 1 / 1 / span 2 / span 2;
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: span 2 / span 2 / -1 / -1;
}
.gallery img:nth-child(4) {
  grid-area: 2 / 1 / span 1 / span 2;
}
.gallery img:nth-child(5) {
  grid-area: span 1 / span 2 / -1 / -1;
}

I think you get the idea of what’s happening here now that we’ve seen a few examples using the same approach. We define a grid and place images on it explicitly, using grid-area so the images overlap.

OK, but the aspect-ratio is different this time.

It is! If you get back to the reasoning we made, we have the first two images that are square next to each other having the same size. This means that the width of the grid needs to be equal to twice its height. Hence, aspect-ratio: 2.

Now it’s time for the clip-path values. We have four triangles and a rhombus.

Showing the three unique shapes and the clip-path values that create them.
We’re only showing the three unique shapes we’re making instead of the five total shapes.

Again, I’m using Clippy for all this math-y stuff. But, honestly, I can write many simple shapes by hand, having spent several years working closely with clip-path, and I know you can too with practice!

The Complex Mosaic of Images

Let’s increase the difficulty and try another mosaic, this time with less symmetry and more complex shapes.

Don’t worry, you will see that it’s the same concept as the one we just made! Again, let’s imagine each image is a rectangle, then go about defining the grid based on what we see.

We’ll start with two images:

They are both squares. The first image is equal to half the size of the second image. The first image takes up less than one half of the grid width, while the second image takes up more than half giving us a total of two columns with a different size (the first one is equal to half the second one). The first image is half the height, so let’s automatically assume we need two rows as well.

Let’s add another image to the layout

This one makes things a bit more complex! We need to draw some lines to identify how to update the grid configuration.

We will move from a 2×2 grid to four columns and three rows. Pretty asymmetric, right? Before we try to figure out that complete sizing, let’s see if the same layout holds up when we add the other images.

Looks like we still need more rows and columns for everything to fall into place. Based on the lines in that image, we’re going to have a total of five columns and four rows.

The logic is simple even though the layout is complex, right? We add the images one by one to find the right configuration that fits everything. Now we need to identify the size of each column and row.

If we say the smallest row/column is equal to one fraction of the grid (1fr) we will get:

grid-template-columns: 1fr 1fr 2fr 3fr 5fr;

…for the columns, and:

grid-template-rows: 3fr 1fr 2fr 2fr;

…for the rows. We can consolidate this using the grid shorthand property again:

grid: 3fr 1fr 2fr 2fr / 1fr 1fr 2fr 3fr 5fr;

You know the drill! Place the images on the grid and apply a clip-path on them:

.gallery img:nth-child(1) {
  grid-area: 1 / 1 /span 2 / span 3;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
.gallery img:nth-child(2) {
  grid-area: 1/2/span 3/span 3;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.gallery img:nth-child(3) {
  grid-area: 1 / span 2 / -1 / -1;
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
.gallery img:nth-child(4) {
  grid-area: span 3 / 1 / -1 / span 3;
  clip-path: polygon(25% 0, 100% 60%, 50% 100%, 0 100%, 0 20%);
}
.gallery img:nth-child(5) {
  grid-area: span 3/span 3/-1/-1;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

We can stop here and our code is fine, but we will do a little more to optimize the clip-path values. Since we don’t have any gaps between our images, we can use the fact that our images overlap to slim things down. Here is a video to illustrate the idea:

As you can see, the image in the middle (the one with the camera) doesn’t need a clip-path. because the other images overlap it, giving us the shape without any additional work! And notice that we can use the same overflowing three-point clip-path concept we used earlier on the image in the bottom-left to keep the code smaller there as well.

In the end, we have a complex-looking grid of images with only four clip-path declarations — all of them are three-point polygons!

Wrapping up

Wow, right? I don’t know about you, but I never get bored of seeing what CSS can do these days. It wasn’t long ago that all of this would have taken verbose hackery and definitely some JavaScript.

Throughout this series, we explored many, many different types of image grids, from the basic stuff to the complex mosaics we made today. And we got a lot of hands-on experience working with CSS clipping — something that you will definitely be able to use on other projects!

But before we end this, I have some homework for you…

Here are two mosaics that I want you to make using what we covered here. One is on the “easier” side, and the other is a bit tricky. It would be really awesome to see your work in the comments, so link them up! I’m curious to see if your approach is different from how I’d go about it!


CSS Grid and Custom Shapes, Part 3 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-3/feed/ 2 374826
Fancy Image Decorations: Single Element Magic https://css-tricks.com/fancy-image-decorations-single-element-magic/ https://css-tricks.com/fancy-image-decorations-single-element-magic/#comments Fri, 14 Oct 2022 15:37:28 +0000 https://css-tricks.com/?p=373965 As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? …


Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? Decorate an image using only the <img> tag and nothing more.

That right, no extra markup, no divs, and no pseudo-elements. Just the one tag.

Sounds difficult, right? But by the end of this article — and the others that make up this little series — I’ll prove that CSS is powerful enough to give us great and stunning results despite the limitation of working with a single element.

Fancy Image Decorations series

Let’s start with our first example

Before digging into the code let’s enumerate the possibilities for styling an <img> without any extra elements or pseudo-elements. We can use border, box-shadow, outline, and, of course, background. It may look strange to add a background to an image because we cannot see it as it will be behind the image — but the trick is to create space around the image using padding and/or border and then draw our background inside that space.

I think you know what comes next since I talked about background, right? Yes, gradients! All the decorations we are going to make rely on a lot of gradients. If you’ve followed me for a while, I think this probably comes as no surprise to you at all. 😁

Let’s get back to our first example:

img {
  --s: 10px; /* control the size */
  padding: var(--s);
  border: calc(2 * var(--s)) solid #0000;
  outline: 1px solid #000;
  outline-offset: calc(-1 * var(--s));
  background: conic-gradient(from 90deg at 1px 1px, #0000 25%, #000 0);
}

We are defining padding and a transparent border using the variable --s to create a space around our image equal to three times that variable.

Why are we using both padding and border instead of one or the other? We can get by using only one of them but I need this combination for my gradient because, by default, the initial value of background-clip is border-box and background-origin is equal to padding-box.

Here is a step-by-step illustration to understand the logic:

Initially, we don’t have any borders on the image, so our gradient will create two segments with 1px of thickness. (I am using 3px in this specific demo so it’s easier to see.) We add a colored border and the gradient still gives us the same result inside the padding area (due to background-origin) but it repeats behind the border. If we make the color of the border transparent, we can use the repetition and we get the frame we want.

The outline in the demo has a negative offset. That creates a square shape at the top of the gradient. That’s it! We added a nice decoration to our image using one gradient and an outline. We could have used more gradients! But I always try to keep my code as simple as possible and I found that adding an outline is better that way.

Here is a gradient-only solution where I am using only padding to define the space. Still the same result but with a more complex syntax.

Let’s try another idea:

For this one, I took the previous example removed the outline, and applied a clip-path to cut the gradient on each side. The clip-path value is a bit verbose and confusing but here is an illustration to better see its points:

Side-by-side comparison of the image with and without using clip-path.

I think you get the main idea. We are going to combine backgrounds, outlines, clipping, and some masking to achieve different kinds of decorations. We are also going to consider some cool hover animations as an added bonus! What we’ve looked at so far is merely a small overview of what’s coming!

The Corner-Only Frame

This one takes four gradients. Each gradient covers one corner and, on hover, we expand them to create a full frame around the image. Let’s dissect the code for one of the gradients:

--b: 5px; /* border thickness */
background: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 90deg, darkblue 0) 0 0;
background-size: 50px 50px; 
background-repeat: no-repeat;

We are going to draw a gradient with a size equal to 50px 50px and place it at the top-left corner (0 0). For the gradient’s configuration, here’s a step-by-step illustration showing how I reached that result.

We tend to think that gradients are only good for transitioning between two colors. But in reality, we can do so much more with them! They are especially useful when it comes to creating different shapes. The trick is to make sure we have hard stops between colors — like in the example above — rather than smooth transitions:

#0000 25%, darkblue 0

This is basically saying: “fill the gradient with a transparent color until 25% of the area, then fill the remaining area with darkblue.

You might be scratching your head over the 0 value. It’s a little hack to simplify the syntax. In reality, we should use this to make a hard stop between colors:

#0000 25%, darkblue 25%

That is more logical! The transparent color ends at 25% and darkblue starts exactly where the transparency ends, making a hard stop. If we replace the second one with 0, the browser will do the job for us, so it is a slightly more efficient way to go about it.

Somewhere in the specification, it says:

if a color stop or transition hint has a position that is less than the specified position of any color stop or transition hint before it in the list, set its position to be equal to the largest specified position of any color stop or transition hint before it.

0 is always smaller than any other value, so the browser will always convert it to the largest value that comes before it in the declaration. In our case, that number is 25%.

Now, we apply the same logic to all the corners and we end with the following code:

img {
  --b: 5px; /* border thickness */
  --c: #0000 90deg, darkblue 0; /* define the color here */
  padding: 10px;
  background:
    conic-gradient(from 90deg  at top    var(--b) left  var(--b), var(--c)) 0 0,
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--c)) 100% 0,
    conic-gradient(from 0deg   at bottom var(--b) left  var(--b), var(--c)) 0 100%,
    conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--c)) 100% 100%;
  background-size: 50px 50px; /* adjust border length here */
  background-repeat: no-repeat;
}

I have introduced CSS variables to avoid some redundancy as all the gradients use the same color configuration.

For the hover effect, all I’m doing is increasing the size of the gradients to create the full frame:

img:hover {
  background-size: 51% 51%;
}

Yes, it’s 51% instead of 50% — that creates a small overlap and avoids possible gaps.

Let’s try another idea using the same technique:

This time we are using only two gradients, but with a more complex animation. First, we update the position of each gradient, then increase their sizes to create the full frame. I also introduced more variables for better control over the color, size, thickness, and even the gap between the image and the frame.

img {
  --b: 8px;  /* border thickness*/
  --s: 60px; /* size of the corner*/
  --g: 14px; /* the gap*/
  --c: #EDC951; 

  padding: calc(var(--b) + var(--g));
  background-image:
    conic-gradient(from  90deg at top    var(--b) left  var(--b), #0000 25%, var(--c) 0),
    conic-gradient(from -90deg at bottom var(--b) right var(--b), #0000 25%, var(--c) 0);
  background-position:
    var(--_p, 0%) var(--_p, 0%),
    calc(100% - var(--_p, 0%)) calc(100% - var(--_p, 0%));
  background-size: var(--s) var(--s);
  background-repeat: no-repeat;
  transition: 
    background-position .3s var(--_i,.3s), 
    background-size .3s calc(.3s - var(--_i, .3s));
}
img:hover {
  background-size: calc(100% - var(--g)) calc(100% - var(--g));
  --_p: calc(var(--g) / 2);
  --_i: 0s;
}

Why do the --_i and --_p variables have an underscore in their name? The underscores are part of a naming convention I use to consider “internal” variables used to optimize the code. They are nothing special but I want to make a difference between the variables we adjust to control the frame (like --b, --c, etc.) and the ones I use to make the code shorter.

The code may look confusing and not easy to grasp but I wrote a three-part series where I detail such technique. I highly recommend reading at least the first article to understand how I reached the above code.

Here is an illustration to better understand the different values:

Showing the same image of two classic cars three times to illustrate the CSS variables used in the code.

The Frame Reveal

Let’s try another type of animation where we reveal the full frame on hover:

Cool, right? And you if you look closely, you will notice that the lines disappear in the opposite direction on mouse out which makes the effect even more fancy! I used a similar effect in a previous article.

But this time, instead of covering all the element, I cover only a small portion by defining a height to get something like this:

This is the top border of our frame. We repeat the same process on each side of the image and we have our hover effect:

img {
  --b: 10px; /* the border thickness*/
  --g: 5px; /* the gap on hover */
  --c: #8A9B0F; 

  padding: calc(var(--g) + var(--b));
  --_g: no-repeat linear-gradient(var(--c) 0 0);
  background: 
    var(--_g) var(--_i, 0%) 0,
    var(--_g) 100% var(--_i, 0%),
    var(--_g) calc(100% - var(--_i, 0%)) 100%,
    var(--_g) 0 calc(100% - var(--_i, 0%));
  background-size: var(--_i, 0%) var(--b),var(--b) var(--_i, 0%);
  transition: .4s, background-position 0s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
}

As you can see, I am applying the same gradient four times and each one has a different position to cover only one side at a time.

Another one? Let’s go!

This one looks a bit tricky and it indeed does require some imagination to understand how two conic gradients are pulling off this kind of magic. Here is a demo to illustrate one of the gradients:

The pseudo-element simulates the gradient. It’s initially out of sight and, on hover, we first change its position to get the top edge of the frame. Then we increase the height to get the right edge. The gradient shape is similar to the ones we used in the last section: two segments to cover two sides.

But why did I make the gradient’s width 200%? You’d think 100% would be enough, right?

100% should be enough but I won’t be able to move the gradient like I want if I keep its width equal to 100%. That’s another little quirk related to how background-position works. I cover this in a previous article. I also posted an answer over at Stack Overflow dealing with this. I know it’s a lot of reading, but it’s really worth your time.

Now that we have explained the logic for one gradient, the second one is easy because it’s doing exactly the same thing, but covering the left and bottom edges instead. All we have to do is to swap a few values and we are done:

img {
  --c: #8A9B0F; /* the border color */
  --b: 10px; /* the border thickness*/
  --g: 5px;  /* the gap */

  padding: calc(var(--g) + var(--b));
  --_g: #0000 25%, var(--c) 0;
  background: 
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--_g))
     var(--_i, 200%) 0 / 200% var(--_i, var(--b))  no-repeat,
    conic-gradient(            at bottom var(--b) left  var(--b), var(--_g))
     0 var(--_i, 200%) / var(--_i, var(--b)) 200%  no-repeat;
  transition: .3s, background-position .3s .3s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
  transition: .3s, background-size .3s .3s;
}

As you can see, both gradients are almost identical. I am simply swapping the values of the size and position.

The Frame Rotation

This time we are not going to draw a frame around our image, but rather adjust the look of an existing one.

You are probably asking how the heck I am able to transform a straight line into an angled line. No, the magic is different than that. That’s just the illusion we get after combining simple animations for four gradients.

Let’s see how the animation for the top gradient is made:

I am simply updating the position of a repeating gradient. Nothing fancy yet! Let’s do the same for the right side:

Are you starting to see the trick? Both gradients intersect at the corner to create the illusion where the straight line is changed to an angled one. Let’s remove the outline and hide the overflow to better see it:

Now, we add two more gradients to cover the remaining edges and we are done:

img {
  --g: 4px; /* the gap */
  --b: 12px; /* border thickness*/
  --c: #669706; /* the color */

  padding: calc(var(--g) + var(--b));
  --_c: #0000 0 25%, var(--c) 0 50%;
  --_g1: repeating-linear-gradient(90deg ,var(--_c)) repeat-x;
  --_g2: repeating-linear-gradient(180deg,var(--_c)) repeat-y;
  background:
    var(--_g1) var(--_p, 25%) 0, 
    var(--_g2) 0 var(--_p, 125%),
    var(--_g1) var(--_p, 125%) 100%, 
    var(--_g2) 100% var(--_p, 25%);
  background-size: 200% var(--b), var(--b) 200%;
  transition: .3s;
}
img:hover {
  --_p: 75%;
}

If we take this code and slightly adjust it, we can get another cool animation:

Can you figure out the logic in this example? That’s your homework! The code may look scary but it uses the same logic as the previous examples we looked at. Try to isolate each gradient and imagine how it animates.

Wrapping up

That’s a lot of gradients in one article!

It sure is and I warned you! But if the challenge is to decorate an image without an extra elements and pseudo-elements, we are left with only a few possibilities and gradients are the most powerful option.

Don’t worry if you are a bit lost in some of the explanations. I always recommend some of my old articles where I go into greater detail with some of the concepts we recycled for this challenge.

I am gonna leave with one last demo to hold you over until the next article in this series. This time, I am using radial-gradient() to create another funny hover effect. I’ll let you dissect the code to grok how it works. Ask me questions in the comments if you get stuck!

Fancy Image Decorations series


Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/fancy-image-decorations-single-element-magic/feed/ 3 373965
Inline Image Previews with Sharp, BlurHash, and Lambda Functions https://css-tricks.com/inline-image-previews-with-sharp-blurhash-and-lambda-functions/ https://css-tricks.com/inline-image-previews-with-sharp-blurhash-and-lambda-functions/#comments Thu, 19 May 2022 14:30:08 +0000 https://css-tricks.com/?p=365784 Don’t you hate it when you load a website or web app, some content displays and then some images load — causing content to shift around? That’s called content reflow and can lead to an incredibly annoying user experience for …


Inline Image Previews with Sharp, BlurHash, and Lambda Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Don’t you hate it when you load a website or web app, some content displays and then some images load — causing content to shift around? That’s called content reflow and can lead to an incredibly annoying user experience for visitors.

I’ve previously written about solving this with React’s Suspense, which prevents the UI from loading until the images come in. This solves the content reflow problem but at the expense of performance. The user is blocked from seeing any content until the images come in.

Wouldn’t it be nice if we could have the best of both worlds: prevent content reflow while also not making the user wait for the images? This post will walk through generating blurry image previews and displaying them immediately, with the real images rendering over the preview whenever they happen to come in.

So you mean progressive JPEGs?

You might be wondering if I’m about to talk about progressive JPEGs, which are an alternate encoding that causes images to initially render — full size and blurry — and then gradually refine as the data come in until everything renders correctly.

This seems like a great solution until you get into some of the details. Re-encoding your images as progressive JPEGs is reasonably straightforward; there are plugins for Sharp that will handle that for you. Unfortunately, you still need to wait for some of your images’ bytes to come over the wire until even a blurry preview of your image displays, at which point your content will reflow, adjusting to the size of the image’s preview.

You might look for some sort of event to indicate that an initial preview of the image has loaded, but none currently exists, and the workarounds are … not ideal.

Let’s look at two alternatives for this.

The libraries we’ll be using

Before we start, I’d like to call out the versions of the libraries I’ll be using for this post:

Making our own previews

Most of us are used to using <img /> tags by providing a src attribute that’s a URL to some place on the internet where our image exists. But we can also provide a Base64 encoding of an image and just set that inline. We wouldn’t usually want to do that since those Base64 strings can get huge for images and embedding them in our JavaScript bundles can cause some serious bloat.

But what if, when we’re processing our images (to resize, adjust the quality, etc.), we also make a low quality, blurry version of our image and take the Base64 encoding of that? The size of that Base64 image preview will be significantly smaller. We could save that preview string, put it in our JavaScript bundle, and display that inline until our real image is done loading. This will cause a blurry preview of our image to show immediately while the image loads. When the real image is done loading, we can hide the preview and show the real image.

Let’s see how.

Generating our preview

For now, let’s look at Jimp, which has no dependencies on things like node-gyp and can be installed and used in a Lambda.

Here’s a function (stripped of error handling and logging) that uses Jimp to process an image, resize it, and then creates a blurry preview of the image:

function resizeImage(src, maxWidth, quality) {
  return new Promise<ResizeImageResult>(res => {
    Jimp.read(src, async function (err, image) {
      if (image.bitmap.width > maxWidth) {
        image.resize(maxWidth, Jimp.AUTO);
      }
      image.quality(quality);

      const previewImage = image.clone();
      previewImage.quality(25).blur(8);
      const preview = await previewImage.getBase64Async(previewImage.getMIME());

      res({ STATUS: "success", image, preview });
    });
  });
}

For this post, I’ll be using this image provided by Flickr Commons:

Photo of the Big Boy statue holding a burger.

And here’s what the preview looks like:

Blurry version of the Big Boy statue.

If you’d like to take a closer look, here’s the same preview in a CodeSandbox.

Obviously, this preview encoding isn’t small, but then again, neither is our image; smaller images will produce smaller previews. Measure and profile for your own use case to see how viable this solution is.

Now we can send that image preview down from our data layer, along with the actual image URL, and any other related data. We can immediately display the image preview, and when the actual image loads, swap it out. Here’s some (simplified) React code to do that:

const Landmark = ({ url, preview = "" }) => {
    const [loaded, setLoaded] = useState(false);
    const imgRef = useRef<HTMLImageElement>(null);
  
    useEffect(() => {
      // make sure the image src is added after the onload handler
      if (imgRef.current) {
        imgRef.current.src = url;
      }
    }, [url, imgRef, preview]);
  
    return (
      <>
        <Preview loaded={loaded} preview={preview} />
        <img
          ref={imgRef}
          onLoad={() => setTimeout(() => setLoaded(true), 3000)}
          style={{ display: loaded ? "block" : "none" }}
        />
      </>
    );
  };
  
  const Preview: FunctionComponent<LandmarkPreviewProps> = ({ preview, loaded }) => {
    if (loaded) {
      return null;
    } else if (typeof preview === "string") {
      return <img key="landmark-preview" alt="Landmark preview" src={preview} style={{ display: "block" }} />;
    } else {
      return <PreviewCanvas preview={preview} loaded={loaded} />;
    }
  };

Don’t worry about the PreviewCanvas component yet. And don’t worry about the fact that things like a changing URL aren’t accounted for.

Note that we set the image component’s src after the onLoad handler to ensure it fires. We show the preview, and when the real image loads, we swap it in.

Improving things with BlurHash

The image preview we saw before might not be small enough to send down with our JavaScript bundle. And these Base64 strings will not gzip well. Depending on how many of these images you have, this may or may not be good enough. But if you’d like to compress things even smaller and you’re willing to do a bit more work, there’s a wonderful library called BlurHash.

BlurHash generates incredibly small previews using Base83 encoding. Base83 encoding allows it to squeeze more information into fewer bytes, which is part of how it keeps the previews so small. 83 might seem like an arbitrary number, but the README sheds some light on this:

First, 83 seems to be about how many low-ASCII characters you can find that are safe for use in all of JSON, HTML and shells.

Secondly, 83 * 83 is very close to, and a little more than, 19 * 19 * 19, making it ideal for encoding three AC components in two characters.

The README also states how Signal and Mastodon use BlurHash.

Let’s see it in action.

Generating blurhash previews

For this, we’ll need to use the Sharp library.


Note

To generate your blurhash previews, you’ll likely want to run some sort of serverless function to process your images and generate the previews. I’ll be using AWS Lambda, but any alternative should work.

Just be careful about maximum size limitations. The binaries Sharp installs add about 9 MB to the serverless function’s size.

To run this code in an AWS Lambda, you’ll need to install the library like this:

"install-deps": "npm i && SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm i --arch=x64 --platform=linux sharp"

And make sure you’re not doing any sort of bundling to ensure all of the binaries are sent to your Lambda. This will affect the size of the Lambda deploy. Sharp alone will wind up being about 9 MB, which won’t be great for cold start times. The code you’ll see below is in a Lambda that just runs periodically (without any UI waiting on it), generating blurhash previews.


This code will look at the size of the image and create a blurhash preview:

import { encode, isBlurhashValid } from "blurhash";
const sharp = require("sharp");

export async function getBlurhashPreview(src) {
  const image = sharp(src);
  const dimensions = await image.metadata();

  return new Promise(res => {
    const { width, height } = dimensions;

    image
      .raw()
      .ensureAlpha()
      .toBuffer((err, buffer) => {
        const blurhash = encode(new Uint8ClampedArray(buffer), width, height, 4, 4);
        if (isBlurhashValid(blurhash)) {
          return res({ blurhash, w: width, h: height });
        } else {
          return res(null);
        }
      });
  });
}

Again, I’ve removed all error handling and logging for clarity. Worth noting is the call to ensureAlpha. This ensures that each pixel has 4 bytes, one each for RGB and Alpha.

Jimp lacks this method, which is why we’re using Sharp; if anyone knows otherwise, please drop a comment.

Also, note that we’re saving not only the preview string but also the dimensions of the image, which will make sense in a bit.

The real work happens here:

const blurhash = encode(new Uint8ClampedArray(buffer), width, height, 4, 4);

We’re calling blurhash‘s encode method, passing it our image and the image’s dimensions. The last two arguments are componentX and componentY, which from my understanding of the documentation, seem to control how many passes blurhash does on our image, adding more and more detail. The acceptable values are 1 to 9 (inclusive). From my own testing, 4 is a sweet spot that produces the best results.

Let’s see what this produces for that same image:

{
  "blurhash" : "UAA]{ox^0eRiO_bJjdn~9#M_=|oLIUnzxtNG",
  "w" : 276,
  "h" : 400
}

That’s incredibly small! The tradeoff is that using this preview is a bit more involved.

Basically, we need to call blurhash‘s decode method and render our image preview in a canvas tag. This is what the PreviewCanvas component was doing before and why we were rendering it if the type of our preview was not a string: our blurhash previews use an entire object — containing not only the preview string but also the image dimensions.

Let’s look at our PreviewCanvas component:

const PreviewCanvas: FunctionComponent<CanvasPreviewProps> = ({ preview }) => {
    const canvasRef = useRef<HTMLCanvasElement>(null);
  
    useLayoutEffect(() => {
      const pixels = decode(preview.blurhash, preview.w, preview.h);
      const ctx = canvasRef.current.getContext("2d");
      const imageData = ctx.createImageData(preview.w, preview.h);
      imageData.data.set(pixels);
      ctx.putImageData(imageData, 0, 0);
    }, [preview]);
  
    return <canvas ref={canvasRef} width={preview.w} height={preview.h} />;
  };

Not too terribly much going on here. We’re decoding our preview and then calling some fairly specific Canvas APIs.

Let’s see what the image previews look like:

In a sense, it’s less detailed than our previous previews. But I’ve also found them to be a bit smoother and less pixelated. And they take up a tiny fraction of the size.

Test and use what works best for you.

Wrapping up

There are many ways to prevent content reflow as your images load on the web. One approach is to prevent your UI from rendering until the images come in. The downside is that your user winds up waiting longer for content.

A good middle-ground is to immediately show a preview of the image and swap the real thing in when it’s loaded. This post walked you through two ways of accomplishing that: generating degraded, blurry versions of an image using a tool like Sharp and using BlurHash to generate an extremely small, Base83 encoded preview.

Happy coding!


Inline Image Previews with Sharp, BlurHash, and Lambda Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/inline-image-previews-with-sharp-blurhash-and-lambda-functions/feed/ 2 365784
Useful Tools for Creating AVIF Images https://css-tricks.com/useful-tools-for-creating-avif-images/ https://css-tricks.com/useful-tools-for-creating-avif-images/#comments Mon, 09 May 2022 15:00:10 +0000 https://css-tricks.com/?p=365579 AVIF (AV1 Image File Format) is a modern image file format specification for storing images that offer a much more significant file reduction when compared to other formats like JPG, JPEG, PNG, and WebP. Version 1.0.0 of the AVIF specification …


Useful Tools for Creating AVIF Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
AVIF (AV1 Image File Format) is a modern image file format specification for storing images that offer a much more significant file reduction when compared to other formats like JPG, JPEG, PNG, and WebP. Version 1.0.0 of the AVIF specification was finalized in February 2019 and released by Alliance for Open Media to the public.

In this article, you will learn about some browser-based tools and command line tools for creating AVIF images.

Why use AVIF over JPGs, PNGS, WebP, and GIF?

  • Lossless compression and lossy compression
  • JPEG suffers from awful banding
  • WebP is much better, but there’s still noticeable blockiness compared to the AVIF
  • Multiple color space
  • 8, 10, 12-bit color depth

Caveats

Jake Archibald, wrote an article a few years back on this new image format and also helped us to identify some disadvantages to compressing images, normally you should look out for these two when compressing to AVIF:

  1. If a user looks at the image in the context of the page, and it strikes them as ugly due to compression, then that level of compression is not acceptable. But, one tiny notch above that boundary is fine.
  2. It’s okay for the image to lose noticeable detail compared to the original unless that detail is significant to the context of the image.

See also: Addy Osmani at Smashing Magazine goes in-depth on using AVIF and WebP.

Data on support for the avif feature across the major browsers from caniuse.com

Browser Solutions

Squoosh

Screenshot of Squoosh.
Screenshot of Squoosh.

Squoosh is a popular image compression web app that allows you to convert images in numerous formats to other widely used compressed formats, including AVIF.

Features
  • File-size limit: 4MB
  • Image optimization settings (located on the right side)
  • Download controls – this includes seeing the size of the resulting file and the percentage reduction from the original image
  • Free to use

AVIF.io

Screenshot of AVIF.io.
Screenshot of AVIF.io.

AVIF.io is an image tool that doesn’t require any form of code. All you need to do is upload your selected images in PNG, JPG, GIF, etc. and it would return compressed versions of them.

Features
  • Conversion settings (located on the top-right of upload banner)
  • Free to use

You can find answers to your questions on the AVIF.io FAQ page.

Command Line Solutions

avif-cli

avif-cli by lovell lets you take your images (PNG, JPEG, etc.) stored in a folder and converts them to AVIF images of your specified reduction size.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install avif

Run the command in your terminal:

npx avif --input="./imgs/*" --output="./output/" --verbose
  • ./imgs/* – represents the location of all your image files
  • ./output/ – represents the location of your output folder
Features
  • Free to use
  • Speed of conversion can be set

You can find out about more commands via the avif-cli GitHub page.

sharp

sharp is another useful tool for converting large images in common formats to smaller, web-friendly AVIF images.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install sharp

Create a JavaScript file named sharp-example.js and copy this code:

const sharp = require('sharp')

const convertToAVIF = () => {
    sharp('path_to_image')
    .toFormat('avif', {palette: true})
    .toFile(__dirname + 'path_to_output_image')
}

convertToAVIF()

Where path_to_image represents the path to your image with its name and extension, i.e.:

./imgs/example.jpg

And path_to_output_image represents the path you want your image to be stored with its name and new extension, i.e.:

/sharp-compressed/compressed-example.avif

Run the command in your terminal:

node sharp-example.js

And there! You should have a compressed AVIF file in your output location!

Features
  • Free to use
  • Images can be rotated, blurred, resized, cropped, scaled, and more using sharp

See also: Stanley Ulili’s article on How To Process Images in Node.js With Sharp.

Conclusion

AVIF is a technology that front-end developers should consider for their projects. These tools allow you to convert your existing JPEG and PNG images to AVIF format. But as with adopting any new tool in your workflow, the benefits and downsides will need to be properly evaluated in accordance with your particular use case.

I hope you enjoyed reading this article as much as I enjoyed writing it. Thank you so much for your time and I hope you have a great day ahead!


Useful Tools for Creating AVIF Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/useful-tools-for-creating-avif-images/feed/ 6 365579
The Search For a Fixed Background Effect With Inline Images https://css-tricks.com/fixed-background-effect/ https://css-tricks.com/fixed-background-effect/#comments Thu, 06 Jan 2022 15:58:55 +0000 https://css-tricks.com/?p=358917 I was working on a client project a few days ago and wanted to create a certain effect on an <img>. See, background images can do the effect I was looking for somewhat easily with background-attachment: fixed;. With …


The Search For a Fixed Background Effect With Inline Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I was working on a client project a few days ago and wanted to create a certain effect on an <img>. See, background images can do the effect I was looking for somewhat easily with background-attachment: fixed;. With that in place, a background image stays in place—even when the page scrolls. It isn’t used all that often, so the effect can look unusual and striking, especially when used sparingly.

Table of Contents

It took me some time to figure out how to achieve the same effect only with an inline image, rather than a CSS background image. This is a video of the effect in action:

The exact code for the above demo is available in this Git repo. Just note that it’s a Next.js project. We’ll get to a CodePen example with raw HTML in a bit.

Why use <img> instead of background-image?

The are a number of reasons I wanted this for my project:

  • It’s easier to lazy load (e.g. <img loading="lazy"… >.
  • It provides better SEO (not to mention accessibility), thanks to alt text.
  • It’s possible to use srcset/sizes to improve the loading performance.
  • It’s possible to use the <picture> tag to pick the best image size and format for the user’s browser.
  • It allows users to download save the image (without resorting to DevTools).

Overall, it’s better to use the image tag where you can, particularly if the image could be considered content and not decoration. So, I wound up landing on a technique that uses CSS clip-path. We’ll get to that in a moment, right after we first look at the background-image method for a nice side-by-side comparison of both approaches.

1. Using CSS background-image

This is the “original” way to pull off a fixed scrolling effect. Here’s the CSS:

.hero-section {
  background-image: url("nice_bg_image.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center; 
  background-attachment: fixed;
}

But as we just saw, this approach isn’t ideal for some situations because it relies on the CSS background-image property to call and load the image. That means the image is technically not considered content—and thus unrecognized by screen readers. If we’re working with an image that is part of the content, then we really ought to make it accessible so it is consumed like content rather than decoration.

Otherwise, this technique works well, but only if the image spans the whole width of the viewport and/or is centered. If you have an image on the right or left side of the page like the example, you’ll run into a whole number of positioning issues because background-position is relative to the center of the viewport.

Fixing it requires a few media queries to make sure it is positioned properly on all devices.

2. Using the clip-path trick on an inline image

Someone on StackOverflow shared this clip-path trick and it gets the job done well. You also get to keep using the<img> tag, which, as we covered above, might be advantageous in some circumstances, especially where an image is part of the content rather than pure decoration.

Here’s the trick:

.image-container {
  position: relative;
  height: 200px;
  clip-path: inset(0);
}

.image {
  object-fit: cover;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

Check it out in action:

Now, before we rush out and plaster this snippet everywhere, it has its own set of downsides. For example, the code feels a bit lengthy to me for such a simple effect. But, even more important is the fact that working with clip-path comes with some implications as well. For one, I can’t just slap a border-radius: 10px; in there like I did in the earlier example to round the image’s corners. That won’t work—it requires making rounded corners from the clipping path itself.

Another example: I don’t know how to position the image within the clip-path. Again, this might be a matter of knowing clip-path really well and drawing it where you need to, or cropping the image itself ahead of time as needed.

Is there something better?

Personally, I gave up on using the fixed scrolling effect on inline images and am back to using a CSS background image—which I know is kind of limiting.

Have you ever tried pulling this off, particularly with an inline image, and managed it well? I’d love to hear!


The Search For a Fixed Background Effect With Inline Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/fixed-background-effect/feed/ 15 358917
A Look at the Cloudinary WordPress Plugin https://css-tricks.com/a-look-at-the-cloudinary-wordpress-plugin/ https://css-tricks.com/a-look-at-the-cloudinary-wordpress-plugin/#comments Tue, 30 Nov 2021 15:23:00 +0000 https://css-tricks.com/?p=357679 (This is a sponsored post.)

Cloudinary (the media hosting and optimization service) has a brand new version (v3) of its WordPress plugin that has really nailed it. First, a high-level look at the biggest things this plugin does:

  • It


A Look at the Cloudinary WordPress Plugin originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
(This is a sponsored post.)

Cloudinary (the media hosting and optimization service) has a brand new version (v3) of its WordPress plugin that has really nailed it. First, a high-level look at the biggest things this plugin does:

  • It takes over your media handling. Images and video are served by Cloudinary instead of your own server, which is good for a whole host of reasons. But don’t worry, your assets are also on your own server, so there is no lock-in.
  • It serves your images and video as performantly as possible. Everything is optimized, served in the best format, and use techniques like responsive images and lazy loading, all while providing a good loading experience. All together, those things are massive for performance.
  • It provides and image gallery block with lots of functionality.

Setting it up is as easy as copy and pasting from your Cloudinary account.

So, yes, you need a Cloudinary account. You can check out the programmable media plans here. There is a free tier that will likely work for many sites and paid plans that will cover sites that have heavy media needs, of which you’ll likely find the pricing amicable. Once you have your account, you pop the connection string (from your dashboard) into this quick onboarding wizard and you’re basically done. The default settings are good.

You could do literally nothing else and the plugin will work its magic, but it’s fun to look through all the settings.

Here are the general settings:

Those two (default) settings are important. Auto sync is nice in that all your images (even your entire existing media library) is synced up to Cloudinary and stays in sync. This is necessary to host your images (and do fancy optional stuff like “transforms”) but you could otherwise think of it as a backup. When you use “Cloudinary and WordPress” as the Storage setting, it means that media will be uploaded to your own server and Cloudinary. That’s what I would highly recommend, but if you’re in a situation where, say, you have very limited or no storage on your WordPress host, you could have the images go straight to Cloudinary (only).

In the Image settings, you can see two of Cloudinary’s most powerful weapons: f_auto and q_auto, standing for “auto image formatting” and “auto quality compression.” Those are defaults I’d highly recommend leaving alone. It means that any browser on any device gets the best possible format of the image, and Cloudinary adjusts the quality as appropriate for that image. Cloudinary has very good tech for doing this, so let it do it.

The “doing images right” checklist is a thing.

Remember, we blogged it just recently. Host them on a CDN. Optimze them. Serve them in the best possible format for the requesting browser. Use responsive images. Lazy load them. None of those things are trivial, and that’s just a partial list. The good news is: this plugin does all that stuff for you, does it well, and does it without you having to think too much about it.

Showing the source HTML code for an image block. It's a lot of code, starting with the image tag and all of the srcset attributes to make the image responsive.

I like seeing the output. This is where the rubber meets the road. From this I can see that responsive images are implemented correctly and lots of different sizes are available. I can see the the image sources are pointing at the Cloudinary CDN. I can see lazy loading is implemented and working. I can see the width and height attributes are there as they should be to ensure space is reserved for the images during loading. This is everything.

It goes the extra mile by hosting the images the used by the theme as well.

Heck, it replaces CSS background-images in your theme’s stylesheet with Cloudinary-hosted versions. That’s… amazing. There must be some real clever WordPress filter stuff going on.

I like seeing this in there:

Screenshot of a Cloudinary screen in the WordPress admin. It provides settings for the Gallery Block, including colors, main viewer options, carousel, and a toggle for advanced settings. A preview of the block is to the right of the settings.

Why? It shows that this plugin is part of modern WordPress. Block editor WordPress. The block itself is simple, but useful. It shows images in a variety of useful layouts with a “lightbox”-like effect (wow, it’s been a long time since I’ve typed the word lightbox). Hey, sometimes you just need a dang image gallery and you might as well use one that is well done.

Who am I to say?

Just a lowly blogger, I suppose. But I can tell you I’ve been watching this evolve for quite a while. A ways back, I had implemented a hand-rolled Cloudinary integration here on CSS-Tricks because I wanted all this stuff. I ultimately had to give up on it as it was more technical debt than I could maintain.

The previous versions of the WordPress plugin were better, but it’s not until now, v3, where this integration is truly nailed.

Shortly after that time I tore down my custom integration, I blogged “Workflow Considerations for Using an Image Management Service” and outlined what I thought the (rather high) bar would be for integrating a third-party image host. It was a lot to ask, and I wasn’t really sure if anyone would find the incentive and motivation to do it all. Well, Cloudinary has done it here. This is as perfect a media management plugin as I could imagine.


A Look at the Cloudinary WordPress Plugin originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-look-at-the-cloudinary-wordpress-plugin/feed/ 3 357679
Frameworks Helping Image Usage https://css-tricks.com/frameworks-helping-image-usage/ https://css-tricks.com/frameworks-helping-image-usage/#comments Thu, 09 Sep 2021 19:44:54 +0000 https://css-tricks.com/?p=350644 I recently blogged about how images are hard and it ended up being a big ol’ checklist of things that you could/should think about and implement when placing images on websites.

I think it’s encouraging to see frameworks — these …


Frameworks Helping Image Usage originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently blogged about how images are hard and it ended up being a big ol’ checklist of things that you could/should think about and implement when placing images on websites.

I think it’s encouraging to see frameworks — these beloved tools that we leverage to help us build websites — offering additional tools within them to help tackle this checklist and take on the hard (but perfectly suited for computers) tasks of displaying images.

Some examples:

I’m not sure I’d give any of them flying colors as far as ease of use. There is stuff to install, configure, and it’s likely you’ll only reach for it if you already know you should be doing it, and your pre-existing knowledge of image performance can help you through the process. It’s not the failing of these frameworks; this stuff is complicated and the audience is developers who are, fair is fair, a little into the idea of control.

I do gotta hand it to my BFF WordPress on this one. You literally do nothing and just get responsive images out of the box. If you need to tap into the filters to control things, you can do that like you can anything else in WordPress: through hooks. If you go for Jetpack (and I highly encourage you to), you flip on the (incredibly, free) Site Accelerator feature, which takes all those images, optimizes them, CDN-hosts them, lazy loads them, and serves them in formats, like WebP, when possible (I would assume more next-gen formats will happen eventually). Jetpack is a sponsor, so full disclosure there, but I use it very much on purpose because the experience makes image handling something I literally don’t have to think about.

Another interesting aspect of frameworks-helping-with-images is that some of it was born out of Google getting involved. Google calls it “Aurora”:

For almost two years, we have worked with some of the most popular frameworks such as Next.js, Nuxt and Angular, working to improve web performance.

The project does all sorts of stuff, including hand out money to help fund open-source tools, and direct help specific initiatives. Like images:

An Image component in Next.js that encapsulates best practices for image loading, followed by a collaboration with Nuxt on the same. Use of this component has resulted in significant improvements to paint times and layout shift (example: 57% reduction in Largest Contentful Paint and 100% reduction in Cumulative Layout Shift on nextjs.org/give).

Cool, right? I think so? What weirds me out about this just a smidge is that it feels meaningful when Google’s squad rolls up to contribute to a framework. They didn’t pick underdog frameworks here, surely on purpose, because they want their work to impact the most people. So, frameworks that are already successful benefit from A-squad contributions. A rich-get-richer situation. I’m not sure it’s a huge problem, but it’s just something I think about.


Frameworks Helping Image Usage originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/frameworks-helping-image-usage/feed/ 2 350644
Your Image Is Probably Not Decorative https://css-tricks.com/your-image-is-probably-not-decorative/ https://css-tricks.com/your-image-is-probably-not-decorative/#respond Tue, 20 Jul 2021 20:02:06 +0000 https://css-tricks.com/?p=344944 Eric doesn’t mince words, especially in the title, but also in the conclusion:

In modern web design and development, displaying an image is a highly intentional act. Alternate descriptions allow us to explain the content of the image, and in


Your Image Is Probably Not Decorative originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Eric doesn’t mince words, especially in the title, but also in the conclusion:

In modern web design and development, displaying an image is a highly intentional act. Alternate descriptions allow us to explain the content of the image, and in doing so, communicate why it is worth including.

Just because an image displays something fanciful doesn’t mean it isn’t worth describing. Announcing its presence ensures that anyone, regardless of ability or circumstance, can fully understand your digital experience.

I like the bit where, even when a CSS background-image is used, you can still use a “spacer GIF” to add alt text. And speaking of alt descriptions, did you know even Open Graph images can have them?

To Shared LinkPermalink on CSS-Tricks


Your Image Is Probably Not Decorative originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/your-image-is-probably-not-decorative/feed/ 0 344944
Images are hard. https://css-tricks.com/images-are-hard/ https://css-tricks.com/images-are-hard/#comments Mon, 19 Jul 2021 20:51:07 +0000 https://css-tricks.com/?p=344404 Putting images on websites is incredibly simple, yes? Actually, yes, it is. You use <img> and link it to a valid source in the src attribute and you’re done. Except that there are (counts fingers) 927 things you could (and …


Images are hard. originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Putting images on websites is incredibly simple, yes? Actually, yes, it is. You use <img> and link it to a valid source in the src attribute and you’re done. Except that there are (counts fingers) 927 things you could (and some you really should) do that often go overlooked. Let’s see…

  • Make sure you use sentence-format alt text on the image to describe what the image depicts.
  • Wrap it in a <figure> and use a <figcaption> if you want visible text that goes with it.
  • Use a <picture> element with a variety of <source> elements if you want to either…
    • Have different source images for different screen sizes and other conditions.
    • Serve different formatted images, to take advantage of next-gen image formats.
  • Know enough about image formats off the bat, so that you can use formats like SVG when appropropriate.
  • Know enough about your audience so that you can default to using the most modern format you can.
  • Use srcset and sizes attributes to serve differently sized images to be bandwidth efficient on differnet devices. Note that to be as efficient as possible, creating these image variations is dependent on the image itself, not pre-determined sizes.
  • Optimize the image and all its variations. Try lots of image optimization software, as they all yield different results. Pick the best. Make sure the optimized versions aren’t accidentally bigger in size or lower quality than the original because that can happen.
  • Lazy load the images so users don’t have to download images they won’t see. Natively, we’ve got loading="lazy" but this is so important that it’s probably worth polyfilling.
  • Make sure to include width and height attributes so that the image reserves the correct amount of space in a layout before it loads (even when the image is of fluid width).
  • Host your images on a fast, cookie-less, global CDN. Probably <link rel="preconnect"> and <link rel="dns-prefetch"> to the CDN domain(s). But don’t do any CDN stuff in dev, only staging or production. The canonical source for your images should be your own servers, so you can move CDNs as needed.
  • The largest image on the page could preload entirely, like <link rel="preload" as="image"> for the best possible LCP.
  • Try to figure out when you should use decoding="async" (I have no idea).
  • Have fallback styling in place for when/if the image doesn’t load.
  • Consider more pleasing before-load or fail states for the images, like small blurred versions of the images.
  • Have performance monitoring in place to make sure rogue giant images don’t slip through and mess with your performance budgets, and can watch for broken image errors.

I’m sure I’m forgetting some. Makes you sweat, doesn’t it?

Addy covered a lot of this in more detail in this Smashing Magazine article and has a whole book on the subject.

If you ask me, it can’t be done. At least, not all of it together while being practical with your time. Fortunately: computers. I know both Eleventy Image and gatsby-plugin-image are well-regarded in how well they automate this stuff and deliver as many of these best practices as they can.

Another reality is sometimes just doing some of these things, perhaps the biggest wins, while being practical with time. In Jake’s “Half the size of images by optimising for high density displays” he writes:

Here’s the technique I use for most images on this blog: I take the maximum size the image can be displayed in CSS pixels, and I multiply that by two, and I encode it at a lower quality, as it’ll always be displayed at a 2x density or greater. Yep. That’s it.

Reminiscent of Dave’s 2013 1.5✕ hack.


Images are hard. originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/images-are-hard/feed/ 7 344404
The Humble `img` Element And Core Web Vitals https://css-tricks.com/the-humble-img-element-and-core-web-vitals/ https://css-tricks.com/the-humble-img-element-and-core-web-vitals/#comments Mon, 17 May 2021 22:38:06 +0000 https://css-tricks.com/?p=340716 Addy Osmani on images in HTML:

The humble <img> element has gained some superpowers over the years. Given how central it is to image optimization on the web, let’s catch up on what it can do and how it can


The Humble `img` Element And Core Web Vitals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Addy Osmani on images in HTML:

The humble <img> element has gained some superpowers over the years. Given how central it is to image optimization on the web, let’s catch up on what it can do and how it can help improve user experience and the Core Web Vitals.

Addy does a good job of translating stuff like this into Instagram posts, so I’ll embed that here:

I’d say this stuff is required knowledge for any HTML developer1. Images affect a site’s performance. Images affect a site’s accessibility. Images affect a site’s UX. Images effect a site’s SEO. That’s not stuff you can sluff off. There is a lot of stuff to know, but that’s the job.

I’m still really curious about this decoding="async" stuff. I’ve read some things that suggest it’s best to have this on all images (though Addy doesn’t suggest this, but doesn’t provide guidance either). I don’t yet understand how best to use this attribute, but if it does turn out that it’s “all the time,” I think we should push browsers to make that behavior the default so we don’t have to bother with the attribute.

I also see Addy suggests loading the hero image as early as possible, but no further trickery. I’m curious if this trick we covered about just not loading the hero at all (until interaction) in an effort to trick CWV into higher numbers will end up being a good practice, or bad.


  1. I don’t hear the term “HTML developer” thrown around much, but I like it. For example, people regularly use “React developer” to describe those who do React development. If you write code that ends up as the DOM, you’re an HTML developer. You’re responsible for the experience that HTML delivers.

To Shared LinkPermalink on CSS-Tricks


The Humble `img` Element And Core Web Vitals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-humble-img-element-and-core-web-vitals/feed/ 1 340716
Maximally optimizing image loading for the web in 2021 https://css-tricks.com/maximally-optimizing-image-loading-for-the-web-in-2021/ Tue, 16 Feb 2021 21:16:54 +0000 https://css-tricks.com/?p=334315 Malte Ubl’s list for:

8 image loading optimization techniques to minimize both the bandwidth used for loading images on the web and the CPU usage for image display.

  1. Fluid width images in CSS, not forgetting the height and width attributes


Maximally optimizing image loading for the web in 2021 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Malte Ubl’s list for:

8 image loading optimization techniques to minimize both the bandwidth used for loading images on the web and the CPU usage for image display.

  1. Fluid width images in CSS, not forgetting the height and width attributes in HTML so you get proper aspect-ratio on first render.
  2. Use content-visibility: auto;
  3. Send AVIF when you can.
  4. Use responsive images syntax.
  5. Set far-out expires headers on images and have a cache-busting strategy (like changing the file name).
  6. Use loading="lazy"
  7. Use decoding="async"
  8. Use inline CSS/SVG for a blurry placeholder.

Apparently, there is but one tool that does it all: eleventy-high-performance-blog.

My thoughts:

  • If you are lazy loading, do you really need to do the content-visibilty thing also? They seem very related.
  • Serving AVIF is usually good, but it seems less cut-and-dry than WebP was. You need to make sure your AVIF version is both better and smaller, which feels like a manual process right now. Update: I’m told that AVIF is actually more-reliably smaller (than JPG at least) than WebP is on the whole. I still think it’s worth being careful. This makes me want to outsource format choice to cloud providers that serve in the smallest format they can produce even moreso.
  • The decoding thing seems weird. I’ll totally use it if it’s a free perf win, but if it’s always a good idea, shouldn’t the browser just always do it?
  • I’m not super convinced blurry placeholders are in the same category of necessary as the rest of this stuff. Feels like a trend.

To Shared LinkPermalink on CSS-Tricks


Maximally optimizing image loading for the web in 2021 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
334315
Add Beautiful Images with the Unsplash API https://css-tricks.com/add-beautiful-images-with-the-unsplash-api/ https://css-tricks.com/add-beautiful-images-with-the-unsplash-api/#comments Thu, 26 Mar 2020 15:03:22 +0000 https://css-tricks.com/?p=305332 (This is a sponsored post.)

Perhaps you know Unsplash? I’d wager it’s the most popular stock photography site out there for two big reasons:

  1. Every photo on there is pretty darn nice
  2. Every photo is entirely free even


Add Beautiful Images with the Unsplash API originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
(This is a sponsored post.)

Perhaps you know Unsplash? I’d wager it’s the most popular stock photography site out there for two big reasons:

  1. Every photo on there is pretty darn nice
  2. Every photo is entirely free even for commercial use. You don’t have to ask permission or even credit it (although that’s appreciated).

Here’s something you might not know though: Unsplash has an API, and it’s unlimited and free. Brass tacks: it’s exactly what you hope it’s going to be. A really clean, well documented, well-performing, JSON API that gives you URLs to photos with metadata.

What would you use the Unsplash API for?

There are lots of examples on Unsplash’s developer area, from Medium to Squarespace to Trello, but here is another one of my favorites!

I use Notion every day. It’s a great app for note-taking, planning, and all sorts of stuff. One of the features it has is giving every document you create within it a custom image header. These give the documents some great personality. Notion has a handful you can choose from or you can upload your own. Or, you can search Unsplash for them!

How does that work? Lemme show you first:


They use the Unsplash API to do it and here’s an article about that. There is a search endpoint as part of the API that makes this quite easy to do.

For example, you’d hit a URL like:

https://api.unsplash.com/search/photos?page=1&query=SEARCH_QUERY

And you’ll get JSON back like:

{
  "total": 133,
  "total_pages": 7,
  "results": [
    {
      "id": "eOLpJytrbsQ",
      "created_at": "2014-11-18T14:35:36-05:00",
      "width": 4000,
      "height": 3000,
      "color": "#A7A2A1",
      "likes": 286,
      "liked_by_user": false,
      "description": "A man drinking a coffee.",
      "user": {
        "id": "Ul0QVz12Goo",
        "username": "ugmonk",
        "name": "Jeff Sheldon",
        "first_name": "Jeff",
        "last_name": "Sheldon",
        "instagram_username": "instantgrammer",
        "twitter_username": "ugmonk",
        "portfolio_url": "http://ugmonk.com/",
        "profile_image": {
          "small": "https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=7cfe3b93750cb0c93e2f7caec08b5a41",
          "medium": "https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=5a9dc749c43ce5bd60870b129a40902f",
          "large": "https://images.unsplash.com/profile-1441298803695-accd94000cac?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=32085a077889586df88bfbe406692202"
        },
        "links": {
          "self": "https://api.unsplash.com/users/ugmonk",
          "html": "http://unsplash.com/@ugmonk",
          "photos": "https://api.unsplash.com/users/ugmonk/photos",
          "likes": "https://api.unsplash.com/users/ugmonk/likes"
        }
      },
      "current_user_collections": [],
      "urls": {
        "raw": "https://images.unsplash.com/photo-1416339306562-f3d12fefd36f",
        "full": "https://hd.unsplash.com/photo-1416339306562-f3d12fefd36f",
        "regular": "https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=92f3e02f63678acc8416d044e189f515",
        "small": "https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&s=263af33585f9d32af39d165b000845eb",
        "thumb": "https://images.unsplash.com/photo-1416339306562-f3d12fefd36f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=8aae34cf35df31a592f0bef16e6342ef"
      },
      "links": {
        "self": "https://api.unsplash.com/photos/eOLpJytrbsQ",
        "html": "http://unsplash.com/photos/eOLpJytrbsQ",
        "download": "http://unsplash.com/photos/eOLpJytrbsQ/download"
      }
    },
    // more photos ...
  ]
}

So to offer a search experience inside an app like Notion, you’d have a little search form and when users submit that search query, you’d hit the API with the value they entered, then loop over response.results using the response.results.urls.thumb to show the images returned. If the user picks one, you can use a higher-res URL to do something with and have access to all that photos metadata.

Hot tip! The URLs to photos are dynamic in that you can resize them, crop them, serve them in different formats, and even change the compression quality all from URL parameters. For example, changing size is like &w=200.

That is exactly what we do on CodePen

The purpose of CodePen Pen Editor is to provide an online code editor that makes it tremendously easy to code something up for the web, save it, and share it. Images are a big part of the web, so it’s very possible that you might want to use a gorgeous image in a Pen. We have Asset Hosting ourselves on CodePen as a PRO feature, but we also offer Unsplash images to everyone for free!

Check out how it works:

A basic example in React

  • Let’s make a search <form>, when submitted, it hits the Unsplash API and returns a bunch of photos.
  • We’ll use Superagent for the Ajax just to make a smidge easier.
  • We’ll track the current search query and returned data in state.

Here is that working!

How might you use that in your own app?

  • Does your app allow users to create anything? If so, could those things be enhanced by great photos? For example, cover images, background images, images for blog posts, etc. Check out existing partners for more ideas.
  • Could this be part of an avatar-choosing experience?
  • Maybe you could build a plugin that enhances some existing app by allowing quicker access to photos.

Feel free to leave comments with more ideas or how you have used the API. And if you haven’t, try it out.


Add Beautiful Images with the Unsplash API originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/add-beautiful-images-with-the-unsplash-api/feed/ 2 305332
Do This to Improve Image Loading on Your Website https://css-tricks.com/do-this-to-improve-image-loading-on-your-website/ https://css-tricks.com/do-this-to-improve-image-loading-on-your-website/#comments Thu, 20 Feb 2020 00:47:12 +0000 https://css-tricks.com/?p=303706 In the video embedded below, Jen Simmons explains how to improve image loading by using width and height attributes. The issue is that there’s a lot of jank when an image is first loaded because an img will naturally have …


Do This to Improve Image Loading on Your Website originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In the video embedded below, Jen Simmons explains how to improve image loading by using width and height attributes. The issue is that there’s a lot of jank when an image is first loaded because an img will naturally have a height of 0 before the image asset has been successfully downloaded by the browser. Then it needs to repaint the page after that which pushes all the content around. I’ve definitely seen this problem a lot on big news websites.

Anyway, Jen is recommending that we should add height and width attributes to images like so:

<img src="dog.png" height="400" width="1000" alt="A cool dog" />

This is because Firefox & Chrome will now take those values into consideration and remove all the jank before the image has loaded, even when you override those values in CSS with a fluid width and thus unknown height. That means content will always stay in the same position, even if the image hasn’t loaded yet. In the past, I’ve worked on a bunch of projects where I’ve placed images lower down the page simply because I want to prevent this sort of jank. I reckon this fixes that problem quite nicely.

To Shared LinkPermalink on CSS-Tricks


Do This to Improve Image Loading on Your Website originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/do-this-to-improve-image-loading-on-your-website/feed/ 7 303706