carousel – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 16 Dec 2022 14:58:17 +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 carousel – CSS-Tricks https://css-tricks.com 32 32 45537868 CSS Infinite 3D Sliders https://css-tricks.com/css-infinite-3d-sliders/ https://css-tricks.com/css-infinite-3d-sliders/#comments Fri, 16 Dec 2022 14:58:08 +0000 https://css-tricks.com/?p=375621 In this series, we’ve been making image sliders with nothing but HTML and CSS. The idea is that we can use the same markup but different CSS to get wildly different results, no matter how many images we toss …


CSS Infinite 3D Sliders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In this series, we’ve been making image sliders with nothing but HTML and CSS. The idea is that we can use the same markup but different CSS to get wildly different results, no matter how many images we toss in. We started with a circular slider that rotates infinitely, sort of like a fidget spinner that holds images. Then we made one that flips through a stack of photos.

This time around, we’re diving into the third dimension. It’s going to look tough at first, but lots of the code we’re looking at is exactly what we used in the first two articles in this series, with some modifications. So, if you’re just now getting into the series, I’d suggest checking out the others for context on the concepts we’re using here.

CSS Sliders series

This is what we’re aiming for:

At first glance, it looks like we have a rotating cube with four images. But in reality, we’re dealing with six images in total. Here is the slider from a different angle:

Now that we have a good visual for how the images are arranged, let’s dissect the code to see how we get there.

The basic setup

Same HTML as the rest of the sliders we’ve used for the other sliders:

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

And once again, we’re using CSS Grid to place the images in a stack, one on top of another:

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 160px;
  aspect-ratio: 1;
  object-fit: cover;
}

The animation

The logic for this slider is very similar to the circular slider from the first article. In fact, if you check the video above again, you can see that the images are placed in a way that creates a polygon. After a full rotation, it returns to the first image.

We relied on the CSS transform-origin and animation-delay properties for that first slider. The same animation is applied to all of the image elements, which rotate around the same point. Then, by using different delays, we correctly place all the images around a big circle.

The implementation will be a bit different for our 3D slider. Using transform-origin won’t work here because we’re working in 3D, so we will use transform instead to correctly place all the images, then rotate the container.

We’re reaching for Sass again so we can loop through the number of images and apply our transforms:

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
     transform: 
       rotate(#{360*($i - 1) / $n}deg) /* 1 */
       translateY(50% / math.tan(180deg / $n)) /* 2 */ 
       rotateX(90deg); /* 3 */
  }
}

You might be wondering why we’re jumping straight into Sass. We started with a fixed number of images using vanilla CSS in the other articles before generalizing the code with Sass to account for any number (N) of images. Well, I think you get the idea now and we can cut out all that discovery work to get to the real implementation.

The transform property is taking three values, which I’ve illustrated here:

Showing the three phases of the image slider layout.

We first rotate all the images above each other. The angle of rotation depends on the number of images. For N images, we have an increment equal to 360deg/N. Then we translate all of the images by the same amount in a way that makes their center points meet on the sides.

Showing the stack of images arranged flat in a circle with a red line running through the center point of the images.

There’s some boring geometry that helps explain how all this works, but the distance is equal to 50%/tan(180deg/N). We dealt with a similar equation when making the circular slider ( transform-origin: 50% 50%/sin(180deg/N) ).

Finally, we rotate the images around the x-axis by 90deg to get the arrangement we want. Here is a video that illustrates what the last rotation is doing:

Now all we have to do is to rotate the whole container to create our infinite slider.

.gallery {
  transform-style: preserve-3d;
  --_t: perspective(280px) rotateX(-90deg);
  animation: r 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@keyframes r {
  0%, 3% {transform: var(--_t) rotate(0deg); }
  @for $i from 1 to $n {
    #{($i/$n)*100 - 2}%, 
    #{($i/$n)*100 + 3}% {
      transform: var(--_t) rotate(#{($i / $n) * -360}deg);
    }  
  }
  98%, 100% { transform: var(--_t) rotate(-360deg); }
}

That code might be hard to understand, so let’s actually step back a moment and revisit the animation we made for the circular slider. This is what we wrote in that first article:

.gallery {
  animation: m 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@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 keyframes are almost identical. We have the same percentage values, the same loop, and the same rotation.

Why are both the same? Because their logic is the same. In both cases, the images are arranged around a circular shape and we need to rotate the whole thing to show each image. That’s how I was able to copy the keyframes from the circular slider and use that same code for our 3D slider. The only difference is that we need to rotate the container by -90deg along the x-axis to see the images since we have already rotated them by 90deg on the same axis. Then we add a touch of perspective to get the 3D effect.

That’s it! Our slider is done. Here is the full demo again. All you have to do is to add as many images as you want and update one variable to get it going.

Vertical 3D slider

Since we are playing in the 3D space, why not make a vertical version of the previous slider? The last one rotates along the z-axis, but we can also move along the x-axis if we want.

If you compare the code for both versions of this slider, you might not immediately spot the difference because it’s only one character! I replaced rotate() with rotateX() inside the keyframes and the image transform. That’s it!

It should be noted that rotate() is equivalent to rotateZ(), so by changing the axis from Z to X we transform the slider from the horizontal version into the vertical one.

Cube slider

We cannot talk about 3D in CSS without talking about cubes. And yes, that means we are going to make another version of the slider.

The idea behind this version of the slider is to create an actual cube shape with the images and rotate the full thing in around the different axis. Since it’s a cube, we’re dealing with six faces. We’ll use six images, one for each face of the cube. So, no Sass but back to vanilla CSS.

That animation is a little overwhelming, right? Where do you even start?

We have six faces, so we need to perform at least six rotations so that each image gets a turn. Well, actually, we need five rotations — the last one brings us back to the first image face. If you go grab a Rubik’s Cube — or some other cube-shaped object like dice — and rotate it with your hand, you’ll have a good idea of what we’re doing.

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

  transform-style: preserve-3d;
  --_p: perspective(calc(2.5*var(--s)));
  animation: r 9s infinite cubic-bezier(.5, -0.5, .5, 1.5);
}

@keyframes r {
  0%, 3%   { transform: var(--_p); }
  14%, 19% { transform: var(--_p) rotateX(90deg); }
  31%, 36% { transform: var(--_p) rotateX(90deg) rotateZ(90deg); }
  47%, 52% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
  64%, 69% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg); }
  81%, 86% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg); }
  97%, 100%{ transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
}

The transform property starts with zero rotations and, on each state, we append a new rotation on a specific axis until we reach six rotations. Then we are back to the first image.

Let’s not forget the placement of our images. Each one is applied to a face of the cube using transform:

.gallery img {
  grid-area: 1 / 1;
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
  transform: var(--_t,) translateZ(calc(var(--s) / 2));
}
.gallery img:nth-child(2) { --_t: rotateX(-90deg); }
.gallery img:nth-child(3) { --_t: rotateY( 90deg) rotate(-90deg); }
.gallery img:nth-child(4) { --_t: rotateX(180deg) rotate( 90deg); }
.gallery img:nth-child(5) { --_t: rotateX( 90deg) rotate( 90deg); }
.gallery img:nth-child(6) { --_t: rotateY(-90deg); }

You are probably thinking there is weird complex logic behind the values I’m using there, right? Well, no. All I did was open DevTools and play with different rotation values for each image until I got it right. It may sound stupid but, hey, it works — especially since we have a fixed number of images and we are not looking for something that supports N images.

In fact, forget the values I’m using and try to do the placement on your own as an exercise. Start with all the images stacked on top of each other, open the DevTools, and go! You will probably end up with different code and that’s totally fine. There can be different ways to position the images.

What’s the trick with the comma inside the var()? Is it a typo?

It’s not a typo so don’t remove it! If you do remove it, you will notice that it affects the placement of the first image. You can see that in my code I defined --_t for all the images except the first one because I only need a translation for it. That comma makes the variable fall back to a null value. Without the comma, we won’t have a fallback and the whole value will be invalid.

From the specification:

Note: That is, var(--a,) is a valid function, specifying that if the --a custom property is invalid or missing, the var()` should be replaced with nothing.

Random cube slider

A little bit of randomness can be a nice enhancement for this sort of animation. So, rather than rotate the cube in sequential order, we can roll the dice so to speak, and let the cube roll however it will.

Cool right? I don’t know about you, but I like this version better! It’s more interesting and the transitions are satisfying to watch. And guess what? You can play with the values to create your own random cube slider!

The logic is actual not random at all — it just appears that way. You define a transform on each keyframe that allows you to show one face and… well, that’s really it! You can pick any order you want.

@keyframes r {
  0%, 3%   { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
  14%,19%  { transform: var(--_p) rotate3d(-1, 1, 0,180deg); }
  31%,36%  { transform: var(--_p) rotate3d( 0,-1, 0, 90deg); }
  47%,52%  { transform: var(--_p) rotate3d( 1, 0, 0, 90deg); }
  64%,69%  { transform: var(--_p) rotate3d( 1, 0, 0,-90deg); }
  81%,86%  { transform: var(--_p) rotate3d( 0, 1, 0, 90deg); }
  97%,100% { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
}

I am using rotate3d() this time but am still relying on DevTools to find the values that feel “right” to me. Don’t try to find a relationship between the keyframes because there simply isn’t one. I’m defining separate transforms and then watching the “random” result. Make sure the first image is the first and last frames, respectively, and show a different image on each of the other frames.

You are not obligated to use a rotate3d() transform as I did. You can also chain different rotations like we did in the previous example. Play around and see what you can come up with! I will be waiting for you to share your version with me in the comments section!

Wrapping up

I hope you enjoyed this little series. We built some fun (and funny) sliders while learning a lot about all kinds of CSS concepts along the way — from grid placement and stacking order, to animation delays and transforms. We even got to play with a dash of Sass to loop through an array of elements.

And we did it all with the exact same HTML for each and every slider we made. How cool is that? CSS is dang powerful and capable of accomplishing so much without the aid of JavaScript.


CSS Infinite 3D Sliders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-infinite-3d-sliders/feed/ 2 375621
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
A Super Flexible CSS Carousel, Enhanced With JavaScript Navigation https://css-tricks.com/a-super-flexible-css-carousel-enhanced-with-javascript-navigation/ https://css-tricks.com/a-super-flexible-css-carousel-enhanced-with-javascript-navigation/#comments Fri, 05 Mar 2021 15:47:52 +0000 https://css-tricks.com/?p=335385 Not sure about you, but I often wonder how to build a carousel component in such a way that you can easily dump a bunch of items into the component and get a nice working carousel — one that allows …


A Super Flexible CSS Carousel, Enhanced With JavaScript Navigation originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Not sure about you, but I often wonder how to build a carousel component in such a way that you can easily dump a bunch of items into the component and get a nice working carousel — one that allows you to scroll smoothly, navigate with the dynamic buttons, and is responsive. If that is the thing you’d like to build, follow along and we’ll work on it together!

This is what we’re aiming for:

We’re going to be working with quite a bit of JavaScript, React and the DOM API from here on out.

First, let’s spin up a fresh project

Let’s start by bootstrapping a simple React application with styled-components tossed in for styling:

npx create-react-app react-easy-carousel

cd react-easy-carousel
yarn add styled-components
yarn install

yarn start

Styling isn’t really the crux of what we’re doing, so I have prepared aa bunch of predefined components for us to use right out of the box:

// App.styled.js
import styled from 'styled-components'

export const H1 = styled('h1')`
  text-align: center;
  margin: 0;
  padding-bottom: 10rem;
`
export const Relative = styled('div')`
  position: relative;
`
export const Flex = styled('div')`
  display: flex;
`
export const HorizontalCenter = styled(Flex)`
  justify-content: center;
  margin-left: auto;
  margin-right: auto;
  max-width: 25rem;
`
export const Container = styled('div')`
  height: 100vh;
  width: 100%;
  background: #ecf0f1;
`
export const Item = styled('div')`
  color: white;
  font-size: 2rem;
  text-transform: capitalize;
  width: ${({size}) => `${size}rem`};
  height: ${({size}) => `${size}rem`};
  display: flex;
  align-items: center;
  justify-content: center;
`

Now let’s go to our App file, remove all unnecessary code, and build a basic structure for our carousel:

// App.js
import {Carousel} from './Carousel'

function App() {
  return (
    <Container>
      <H1>Easy Carousel</H1>
      <HorizontalCenter>
        <Carousel>
        {/* Put your items here */}
        </Carousel>
      </HorizontalCenter>
    </Container>
  )
}
export default App

I believe this structure is pretty straightforward. It’s the basic layout that centers the carousel directly in the middle of the page.

Let’s talk about the structure of our component. We’re gonna need the main <div> container which as our base. Inside that, we’re going to take advantage of native scrolling and put another block that serves as the scrollable area.

// Carousel.js 
<CarouserContainer>
  <CarouserContainerInner>
    {children}
  </CarouserContainerInner>
</CarouserContainer>

You can specify width and height on the inner container, but I’d avoid strict dimensions in favor of some sized component on top of it to keep things flexible.

Scrolling, the CSS way

We want that scroll to be smooth so it’s clear there’s a transition between slides, so we’ll reach for CSS scroll snapping, set the scroll horizontally along the x-axis, and hide the actual scroll bar while we’re at it.

export const CarouserContainerInner = styled(Flex)`
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  -ms-overflow-style: none;
  scrollbar-width: none;

  &::-webkit-scrollbar {
    display: none;
  }

  & > * {
    scroll-snap-align: center;
  }
`

Wondering what’s up with scroll-snap-type and scroll-snap-align? That’s native CSS that allows us to control the scroll behavior in such a way that an element “snaps” into place during a scroll. So, in this case, we’ve set the snap type in the horizontal (x) direction and told the browser it has to stop at a snap position that is in the center of the element.

In other words: scroll to the next slide and make sure that slide is centered into view. Let’s break that down a bit to see how it fits into the bigger picture.

Our outer <div> is a flexible container that puts it’s children (the carousel slides) in a horizontal row. Those children will easily overflow the width of the container, so we’ve made it so we can scroll horizontally inside the container. That’s where scroll-snap-type comes into play. From Andy Adams in the CSS-Tricks Almanac:

Scroll snapping refers to “locking” the position of the viewport to specific elements on the page as the window (or a scrollable container) is scrolled. Think of it like putting a magnet on top of an element that sticks to the top of the viewport and forces the page to stop scrolling right there.

Couldn’t say it better myself. Play around with it in Andy’s demo on CodePen.

But, we still need another CSS property set on the container’s children (again, the carousel slides) that tells the browser where the scroll should stop. Andy likens this to a magnet, so let’s put that magnet directly on the center of our slides. That way, the scroll “locks” on the center of a slide, allowing to be full in view in the carousel container.

That property? scroll-snap-align.

& > * {
  scroll-snap-align: center;
}

We can already test it out by creating some random array of items:

const colors = [
  '#f1c40f',
  '#f39c12',
  '#e74c3c',
  '#16a085',
  '#2980b9',
  '#8e44ad',
  '#2c3e50',
  '#95a5a6',
]
const colorsArray = colors.map((color) => (
  <Item
    size={20}
    style={{background: color, borderRadius: '20px', opacity: 0.9}}
    key={color}
  >
    {color}
  </Item>
))

And dumping it right into our carousel:

// App.js
<Container>
  <H1>Easy Carousel</H1>
  <HorizontalCenter>
    <Carousel>{colorsArray}</Carousel>
  </HorizontalCenter>
</Container>

Let’s also add some spacing to our items so they won’t look too squeezed. You may also notice that we have unnecessary spacing on the left of the first item. We can add a negative margin to offset it.

export const CarouserContainerInner = styled(Flex)`
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  -ms-overflow-style: none;
  scrollbar-width: none;
  margin-left: -1rem;

  &::-webkit-scrollbar {
    display: none;
  }

  & > * {
    scroll-snap-align: center;
    margin-left: 1rem;
  }
`

Take a closer look at the cursor position while scrolling. It’s always centered. That’s the scroll-snap-align property at work!

And that’s it! We’ve made an awesome carousel where we can add any number of items, and it just plain works. Notice, too, that we did all of this in plain CSS, even if it was built as a React app. We didn’t really need React or styled-components to make this work.

Bonus: Navigation

We could end the article here and move on, but I want to take this a bit further. What I like about what we have so far is that it’s flexible and does the basic job of scrolling through a set of items.

But you may have noticed a key enhancement in the demo at the start of this article: buttons that navigate through slides. That’s where we’re going to put the CSS down and put our JavaScript hats on to make this work.

First, let’s define buttons on the left and right of the carousel container that, when clicked, scrolls to the previous or next slide, respectively. I’m using simple SVG arrows as components:

// ArrowLeft
export const ArrowLeft = ({size = 30, color = '#000000'}) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width={size}
    height={size}
    viewBox="0 0 24 24"
    fill="none"
    stroke={color}
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <path d="M19 12H6M12 5l-7 7 7 7" />
  </svg>
)

// ArrowRight
export const ArrowRight = ({size = 30, color = '#000000'}) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width={size}
    height={size}
    viewBox="0 0 24 24"
    fill="none"
    stroke={color}
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <path d="M5 12h13M12 5l7 7-7 7" />
  </svg>
)

Now let’s position them on both sides of our carousel:

// Carousel.js
<LeftCarouselButton>
  <ArrowLeft />
</LeftCarouselButton>

<RightCarouselButton>
  <ArrowRight />
</RightCarouselButton>

We’ll sprinkle in some styling that adds absolute positioning to the arrows so that the left arrow sits on the left edge of the carousel and the right arrow sits on the right edge. A few other things are thrown in to style the buttons themselves to look like buttons. Also, we’re playing with the carousel container’s :hover state so that the buttons only show when the user’s cursor hovers the container.

// Carousel.styled.js

// Position and style the buttons
export const CarouselButton = styled('button')`
  position: absolute;
  cursor: pointer;
  top: 50%;
  z-index: 1;
  transition: transform 0.1s ease-in-out;
  background: white;
  border-radius: 15px;
  border: none;
  padding: 0.5rem;
`

// Display buttons on hover
export const LeftCarouselButton = styled(CarouselButton)`
  left: 0;
  transform: translate(-100%, -50%);

  ${CarouserContainer}:hover & {
    transform: translate(0%, -50%);
  }
`
// Position the buttons to their respective sides
export const RightCarouselButton = styled(CarouselButton)`
  right: 0;
  transform: translate(100%, -50%);

  ${CarouserContainer}:hover & {
    transform: translate(0%, -50%);
  }
`

This is cool. Now we have buttons, but only when the user interacts with the carousel.

But do we always want to see both buttons? It’d be great if we hide the left arrow when we’re at the first slide, and hide the right arrow when we’re at the last slide. It’s like the user can navigate past those slides, so why set the illusion that they can?

I suggest creating a hook that’s responsible for all the scrolling functionality we need, as we’re gonna have a bunch of it. Plus, it’s just good practice to separate functional concerns from our visual component.

First, we need to get the reference to our component so we can get the position of the slides. Let’s do that with ref:

// Carousel.js
const ref = useRef()
const position = usePosition(ref)

<CarouserContainer>
  <CarouserContainerInner ref={ref}>
    {children}
  </CarouserContainerInner>
  <LeftCarouselButton>
    <ArrowLeft />
  </LeftCarouselButton>
  <RightCarouselButton>
    <ArrowRight />
  </RightCarouselButton>
</CarouserContainer>

The ref property is on <CarouserContainerInner> as it contains all our items and will allow us to do proper calculations.

Now let’s implement the hook itself. We have two buttons. To make them work, we need to keep track of the next and previous items accordingly. The best way to do so is to have a state for each one:

// usePosition.js
export function usePosition(ref) {
  const [prevElement, setPrevElement] = useState(null)
  const [nextElement, setNextElement] = useState(null)
}

The next step is to create a function that detects the position of the elements and updates the buttons to either hide or display depending on that position.

Let’s call it the update function. We’re gonna put it into React’s useEffect hook because, initially, we want to run this function when the DOM mounts the first time. We need access to our scrollable container which is available to use under the ref.current property. We’ll put it into a separate variable called element and start by getting the element’s position in the DOM.

We’re gonna use getBoundingClientRect() here as well. This is a very helpful function because it gives us an element’s position in the viewport (i.e. window) and allows us to proceed with our calculations.

// usePosition.js
 useEffect(() => {
  // Our scrollable container
  const element = ref.current

  const update = () => {
    const rect = element.getBoundingClientRect()
}, [ref])

We’ve done a heck of a lot positioning so far and getBoundingClientRect() can help us understand both the size of the element — rect in this case — and its position relative to the viewport.

Credit: Mozilla Developer Network

The following step is a bit tricky as it requires a bit of math to calculate which elements are visible inside the container.

First, we need to filter each item by getting its position in the viewport and checking it against the container boundaries. Then, we check if the child’s left boundary is bigger than the container’s left boundary, and the same thing on the right side.

If one of these conditions is met means that our child is visible inside the container. Let’s convert it into the code step-by-step:

  1. We need to loop and filter through all container children. We can use the children property available on each node. So, let’s convert it into an array and filter:
const visibleElements = Array.from(element.children).filter((child) => {}
  1. After that, we need to get the position of each element by using that handy getBoundingClientRect() function once again:
const childRect = child.getBoundingClientRect()
  1. Now let’s bring our drawing to life:
rect.left <= childRect.left && rect.right >= childRect.right

Pulling that together, this is our script:

// usePosition.js
const visibleElements = Array.from(element.children).filter((child) => {
  const childRect = child.getBoundingClientRect()

  return rect.left <= childRect.left && rect.right >= childRect.right
})

Once we’ve filtered out items, we need to check whether an item is the first or the last one so we know to hide the left or right button accordingly. We’ll create two helper functions that check that condition using previousElementSibling and nextElementSibling. This way, we can see if there is a sibling in the list and whether it’s an HTML instance and, if it is, we will return it.

To receive the first element and return it, we need to take the first item from our visible items list and check if it contains the previous node. We’ll do the same thing for the last element in the list, however, we need to get the last item in the list and check if it contains the next element after itself:

// usePosition.js
function getPrevElement(list) {
  const sibling = list[0].previousElementSibling

  if (sibling instanceof HTMLElement) {
    return sibling
  }

  return sibling
}

function getNextElement(list) {
  const sibling = list[list.length - 1].nextElementSibling
  if (sibling instanceof HTMLElement) {
    return sibling
  }
  return null
}

Once we have those functions, we can finally check if there are any visible elements in the list, and then set our left and right buttons into the state:

// usePosition.js 
if (visibleElements.length > 0) {
  setPrevElement(getPrevElement(visibleElements))
  setNextElement(getNextElement(visibleElements))
}

Now we need to call our function. Moreover, we want to call this function each time we scroll through the list — that’s when we want to detect the position of the element.

// usePosition.js
export function usePosition(ref) {
  const [prevElement, setPrevElement] = useState(null)
  const [nextElement, setNextElement] = useState(null)
  useEffect(() => {
    const element = ref.current
    const update = () => {
      const rect = element.getBoundingClientRect()
      const visibleElements = Array.from(element.children).filter((child) => {
        const childRect = child.getBoundingClientRect()
        return rect.left <= childRect.left && rect.right >= childRect.right
      })
      if (visibleElements.length > 0) {
        setPrevElement(getPrevElement(visibleElements))
        setNextElement(getNextElement(visibleElements))
      }
    }

    update()
    element.addEventListener('scroll', update, {passive: true})
    return () => {
      element.removeEventListener('scroll', update, {passive: true})
    }
  }, [ref])

Here’s an explanation for why we’re passing {passive: true} in there.

Now let’s return those properties from the hook and update our buttons accordingly:

// usePosition.js
return {
  hasItemsOnLeft: prevElement !== null,
  hasItemsOnRight: nextElement !== null,
}
// Carousel.js 
<LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}>
  <ArrowLeft />
</LeftCarouselButton>

<RightCarouselButton hasItemsOnRight={hasItemsOnRight}>
  <ArrowRight />
</RightCarouselButton>
// Carousel.styled.js
export const LeftCarouselButton = styled(CarouselButton)`
  left: 0;
  transform: translate(-100%, -50%);
  ${CarouserContainer}:hover & {
    transform: translate(0%, -50%);
  }
  visibility: ${({hasItemsOnLeft}) => (hasItemsOnLeft ? `all` : `hidden`)};
`
export const RightCarouselButton = styled(CarouselButton)`
  right: 0;
  transform: translate(100%, -50%);
  ${CarouserContainer}:hover & {
    transform: translate(0%, -50%);
  }
  visibility: ${({hasItemsOnRight}) => (hasItemsOnRight ? `all` : `hidden`)};
`

So far, so good. As you’ll see, our arrows show up dynamically depending on our scroll location in the list of items.

We’ve got just one final step to go to make the buttons functional. We need to create a function that’s gonna accept the next or previous element it needs to scroll to.

const scrollRight = useCallback(() => scrollToElement(nextElement), [
  scrollToElement,
  nextElement,
])
const scrollLeft = useCallback(() => scrollToElement(prevElement), [
  scrollToElement,
  prevElement,
])

Don’t forget to wrap functions into the useCallback hook in order to avoid unnecessary re-renders.

Next, we’ll implement the scrollToElement function. The idea is pretty simple. We need to take the left boundary of our previous or next element (depending on the button that’s clicked), sum it up with the width of the element, divided by two (center position), and offset this value by half of the container width. That will give us the exact scrollable distance to the center of the next/previous element.

Here’s that in code:

// usePosition.js  
const scrollToElement = useCallback(
  (element) => {
    const currentNode = ref.current

    if (!currentNode || !element) return

    let newScrollPosition

    newScrollPosition =
      element.offsetLeft +
      element.getBoundingClientRect().width / 2 -
      currentNode.getBoundingClientRect().width / 2

    currentNode.scroll({
      left: newScrollPosition,
      behavior: 'smooth',
    })
  },
  [ref],
)

scroll actually does the scrolling for us while passing the precise distance we need to scroll to. Now let’s attach those functions to our buttons.

// Carousel.js  
const {
  hasItemsOnLeft,
  hasItemsOnRight,
  scrollRight,
  scrollLeft,
} = usePosition(ref)

<LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft} onClick={scrollLeft}>
  <ArrowLeft />
</LeftCarouselButton>

<RightCarouselButton hasItemsOnRight={hasItemsOnRight} onClick={scrollRight}>
  <ArrowRight />
</RightCarouselButton>

Pretty nice!

Like a good citizen, we ought to clean up our code a bit. For one, we can be more in control of the passed items with a little trick that automatically sends the styles needed for each child. The Children API is pretty rad and worth checking out.

<CarouserContainerInner ref={ref}>
  {React.Children.map(children, (child, index) => (
    <CarouselItem key={index}>{child}</CarouselItem>
  ))}
</CarouserContainerInner>

Now we just need to update our styled components. flex: 0 0 auto preserves the original sizes of the containers, so it’s totally optional

export const CarouselItem = styled('div')`
  flex: 0 0 auto;

  // Spacing between items
  margin-left: 1rem;
`
export const CarouserContainerInner = styled(Flex)`
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  -ms-overflow-style: none;
  scrollbar-width: none;
  margin-left: -1rem; // Offset for children spacing

  &::-webkit-scrollbar {
    display: none;
  }

  ${CarouselItem} & {
    scroll-snap-align: center;
  }
`

Accessibility

We care about our users, so we need to make our component not only functional, but also accessible so folks feel comfortable using it. Here are a couple things I’d suggest:

  • Adding role='region' to highlight the importance of this area.
  • Adding an area-label as an identifier.
  • Adding labels to our buttons so screen readers could easily identify them as “Previous” and “Next” and inform the user which direction a button goes.
// Carousel.js
<CarouserContainer role="region" aria-label="Colors carousel">

  <CarouserContainerInner ref={ref}>
    {React.Children.map(children, (child, index) => (
      <CarouselItem key={index}>{child}</CarouselItem>
    ))}
  </CarouserContainerInner>
  
  <LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}
    onClick={scrollLeft}
    aria-label="Previous slide
  >
    <ArrowLeft />
  </LeftCarouselButton>
  
  <RightCarouselButton hasItemsOnRight={hasItemsOnRight}
    onClick={scrollRight}
    aria-label="Next slide"
   >
    <ArrowRight />
  </RightCarouselButton>

</CarouserContainer>

Feel free to add additional carousels to see how it behaves with the different size items. For example, let’s drop in a second carousel that’s just an array of numbers.

const numbersArray = Array.from(Array(10).keys()).map((number) => (
  <Item size={5} style={{color: 'black'}} key={number}>
    {number}
  </Item>
))

function App() {
  return (
    <Container>
      <H1>Easy Carousel</H1>
      <HorizontalCenter>
        <Carousel>{colorsArray}</Carousel>
      </HorizontalCenter>

      <HorizontalCenter>
        <Carousel>{numbersArray}</Carousel>
      </HorizontalCenter>
    </Container>
  )
}

And voilà, magic! Dump a bunch of items and you’ve got fully workable carousel right out of the box.


Feel free to modify this and use it in your projects. I sincerely hope that this is a good starting point to use as-is, or enhance it even further for a more complex carousel. Questions? Ideas? Contact me on Twitter, GitHub, or the comments below!


A Super Flexible CSS Carousel, Enhanced With JavaScript Navigation originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-super-flexible-css-carousel-enhanced-with-javascript-navigation/feed/ 11 335385
How to Make a CSS-Only Carousel https://css-tricks.com/how-to-make-a-css-only-carousel/ https://css-tricks.com/how-to-make-a-css-only-carousel/#comments Fri, 24 Apr 2020 19:29:00 +0000 https://css-tricks.com/?p=306971 We mentioned a way to make a CSS-only carousel in a recent issue of the newsletter and I thought that a more detailed write up would be interesting and capture some of my thoughts on making one.

So, here’s what …


How to Make a CSS-Only Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We mentioned a way to make a CSS-only carousel in a recent issue of the newsletter and I thought that a more detailed write up would be interesting and capture some of my thoughts on making one.

So, here’s what we’re making today:

There’s no JavaScript here, whatsoever! No jQuery plugins. No trickiness. Just a couple of new-ish CSS properties that I’ve been experimenting with as well as some basic HTML.

OK to start, we need to focus on the markup. The design includes a left navigation made up of images and a large image gallery on the right that lets us scroll through each image individually. We’ll also need a wrapper to help us organize the layout:

<div class="wrapper">
  <nav class="lil-nav"></nav>
  <div class="gallery"></div>
</div>

Next, we can add images! For this little example, I checked out our list of sites with high quality images that you can use for free and went with Unsplash.

After saving images with the CodePen asset manager, I started adding the URLs to the nav element:

<nav class="lil-nav">
  <a href="#image-1">
    <img class="lil-nav__img" src="..." alt="Yosemite" />
  </a>
  <a href="#image-2">
    <img class="lil-nav__img" src="..." alt="Basketball hoop" />
  </a>
  <!-- more images go here --> 
</nav>

See that the href to each of these links is pointing to an ID? That’s because if we look at the demo again, we want to be able to click an image and then we want to it to hop to the larger version of that image in the gallery to the right.

So, now we can start to add these images to the large gallery, too…

<div class="gallery">
  <img class="gallery__img" id="image-1" src="..." alt="Yosemite" />
  <img class="gallery__img" id="image-2" src="..." alt="Basketball hoop" />
  <!-- more images go here --> 
</div>

Nifty. Next is the fun part: styling this bad boy. We can use a grid layout the parent .wrapper and set some smart defaults for the img element:

img {
  display: block;
  max-width: 100%;
}

.wrapper {
  display: grid;
  grid-template-columns: 1fr 5fr;
  grid-gap: 20px;
}

So far, we have our layout sorted and our links set up. Next, let’s account for overflow that might spill outside our wrapper and make sure that the nav and the gallery are scrollable:

.wrapper {
  display: grid;
  grid-template-columns: 1fr 5fr;
  grid-gap: 10px;
  overflow: hidden;
  height: 100vh; 
}

.gallery {
  overflow: scroll;
}

.lil-nav {
  overflow-y: scroll;
  overflow-x: hidden;
}

We can scroll through each image in the gallery now, but if this was a production website we’d probably want to make sure that folks can scroll passed this carousel a bit more easily. Trent Walton wrote about this very problem several years ago and I think it’s always worth keeping in mind.

Next up, let’s focus on the carousel snap of each image in the gallery. To do that we’ll need to use the scroll-snap-type and scroll-snap-align property like this:

.gallery {
  overflow: scroll;
  scroll-snap-type: x mandatory;
}

.gallery__img {
  scroll-snap-align: start;
  margin-bottom: 10px;
}

Now try scrolling through the gallery on the right-hand side again:

If you want to learn more about these properties I’d highly recommend this piece about practical CSS scroll snapping which digs into the nitty-gritty of these properties.

We have a pretty dang usable carousel! From here, all we have to do is tidy up the design because the gallery image isn’t the full height of the screen. To do that we can use object-fit and give each image a min-height with the vh unit, just like this:

.gallery__img {
  scroll-snap-align: start;
  margin-bottom: 10px;
  min-height: 100vh;
  object-fit: cover;
}

Now the big gallery images will always be the full size of the screen and will scale to take up the width and height. Let’s move on and tackle the style of the little navigation images:

.lil-nav {
  overflow-y: scroll;
  overflow-x: hidden;
}

.lil-nav a {
  height: 200px;
  display: flex;
  margin-bottom: 10px;
}

.lil-nav__img {
  object-fit: cover;
}

At first, I made this little nav act like a carousel too, but it felt really weird. I’m just keeping the default scroll behavior for now. In that demo above, though, try clicking an image. Notice how it jumps to that image in the carousel immediately? It would be nice if we could animate that transition a bit — and we can!

.gallery {
  overflow: scroll;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}

That scroll-behavior CSS property is super handy for this and so now the whole thing will animate if you click one of the nav items:

Nifty, eh? One more tiny thing we could do here is throw a filter on the nav items to make them black and white and then animate them on hover:

.lil-nav__img {
  object-fit: cover;
  filter: saturate(0);
  transition: 0.3s ease all;
}

.lil-nav__img:hover {
  transform: scale(1.05);
  filter: saturate(1);
}

I’m sure there’s a lot more we could do here but I think this works quite nicely!

We could even throw a tiny bit of JavaScript into the mix to show which image is active, but I reckon that folks know that just from looking at the gallery.

That’s it! We now have a carousel that’s pretty dang good for progressive enhancement and it means we don’t have to load a library of JavaScript or write a bunch more code than we really need to.

Let’s go one step further though and make this chap responsive. What we want to do is reverse the order of our grid by moving all of our current styles into a media query that is only activated at larger screens.

You might want to open up this demo in a new tab and decrease/increase the size of the browser to see the changes take place:

If you load this demo on a mobile device you should see how the layout switches between the two modes. This is done by using a single media query on the .wrapper element. Note that we’re using Sass:

$large: 1200px;

.wrapper {
  overflow: hidden;
  height: 100vh;
  display: grid;
  grid-template-rows: 2fr 1fr;
  grid-gap: 10px;

  @media screen and (min-width: $large) {
    grid-template-columns: 1fr 5fr;
    grid-template-rows: auto;
  }
}

Let’s add one on the navigation, too. But this time, we need to tell the navigation to start on the second row so it moves to the bottom of the screen:

.lil-nav {
  overflow-x: scroll;
  overflow-y: hidden;
  display: flex;
  grid-row-start: 2;

  @media screen and (min-width: $large) {
    overflow-y: scroll;
    overflow-x: hidden;
    display: block;
    grid-row-start: auto;
  }
}

With the gallery we need to switch around the scroll-type for larger screens and reverse the overflow property as well:

.gallery {
  overflow-x: scroll;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  display: flex;

  @media screen and (min-width: $large) {
    display: block;
    overflow-y: scroll;
    overflow-x: hidden;
    scroll-snap-type: y mandatory;
  }
}

That’s the bulk of the changes we’ve had to make and I quite like it! If we wanted to make this production-ready, we would think about accessibility (e.g. we probably don’t want screen readers to read out all the images in both the nav and gallery). Then there’s performance — we might consider lazy-loading so the images are only rendered when they’re needed.

Either way, this is a good start !


How to Make a CSS-Only Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-make-a-css-only-carousel/feed/ 16 https://css-tricks.com/wp-content/uploads/2020/04/fancy-carousel-1.mp4 carousel Archives - CSS-Tricks nonadult 306971
Creating a Modal Image Gallery With Bootstrap Components https://css-tricks.com/creating-a-modal-image-gallery-with-bootstrap-components/ https://css-tricks.com/creating-a-modal-image-gallery-with-bootstrap-components/#comments Fri, 06 Mar 2020 15:23:22 +0000 https://css-tricks.com/?p=304193 Have you ever clicked on an image on a webpage that opens up a larger version of the image with navigation to view other photos?

Some folks call it a pop-up. Others call it a lightbox. Bootstrap calls it a


Creating a Modal Image Gallery With Bootstrap Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Have you ever clicked on an image on a webpage that opens up a larger version of the image with navigation to view other photos?

Some folks call it a pop-up. Others call it a lightbox. Bootstrap calls it a modal. I mention Bootstrap because I want to use it to make the same sort of thing. So, let’s call it a modal from here on out.

Why Bootstrap? you might ask. Well, a few reasons:

  • I’m already using Bootstrap on the site where I want this effect, so there’s no additional overhead in terms of loading resources.
  • I want something where I have complete and easy control over aesthetics. Bootstrap is a clean slate compared to most modal plugins I’ve come across.
  • The functionality I need is fairly simple. There isn’t much to be gained by coding everything from scratch. I consider the time I save using the Bootstrap framework to be more beneficial than any potential drawbacks.

Here’s where we’ll end up:

Let’s go through that, bit by bit.

Let’s start with the markup for a grid layout of images. We can use Bootstrap’s grid system for that.

<div class="row" id="gallery">
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-1">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-2">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-3">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-4">
  </div>
</div>

Now we need data attributes to make those images interactive. Bootstrap looks at data attributes to figure out which elements should be interactive and what they should do. In this case, we’ll be creating interactions that open the modal component and allow scrolling through the images using the carousel component.

About those data attributes:

  1. We’ll add data-toggle="modal"  and data-target="#exampleModal" to the parent element (#gallery). This makes it so clicking anything in the gallery opens the modal. We should also add the data-target value (#exampleModal) as the ID of the modal itself, but we’ll do that once we get to the modal markup.
  2. Let’s add data-target="#carouselExample"  and a data-slide-to attribute to each image. We could add those to the image wrappers instead, but we’ll go with the images in this post. Later on, we’ll want to use the data-target value (#carouselExample) as the ID for the carousel, so note that for when we get there. The values for data-slide-to are based on the order of the images.

Here’s what we get when we put that together:

<div class="row" id="gallery" data-toggle="modal" data-target="#exampleModal">
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-1.jpg" data-target="#carouselExample" data-slide-to="0">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-2.jpg" data-target="#carouselExample" data-slide-to="1">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-3.jpg" data-target="#carouselExample" data-slide-to="2">
  </div>
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-4.jpg" data-target="#carouselExample" data-slide-to="3">
  </div>
</div>

Interested in knowing more about data attributes? Check out the CSS-Tricks guide to them.

Step 2: Make the modal work

This is a carousel inside a modal, both of which are standard Bootstrap components. We’re just nesting one inside the other here. Pretty much a straight copy-and-paste job from the Bootstrap documentation.

Here’s some important parts to watch for though:

  1. The modal ID should match the data-target of the gallery element.
  2. The carousel ID should match the data-target of the images in the gallery.
  3. The carousel slides should match the gallery images and must be in the same order.

Here’s the markup for the modal with our attributes in place:

<!-- Modal markup: https://getbootstrap.com/docs/4.4/components/modal/ -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        
      <!-- Carousel markup goes here -->


      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

We can drop the carousel markup right in there, Voltron style!

<!-- Modal markup: https://getbootstrap.com/docs/4.4/components/modal/ -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        
      <!-- Carousel markup: https://getbootstrap.com/docs/4.4/components/carousel/ -->
      <div id="carouselExample" class="carousel slide" data-ride="carousel">
          <div class="carousel-inner">
            <div class="carousel-item active">
              <img class="d-block w-100" src="/image-1.jpg">
            </div>
            <div class="carousel-item">
              <img class="d-block w-100" src="/image-2.jpg">
            </div>
            <div class="carousel-item">
              <img class="d-block w-100" src="/image-3.jpg">
            </div>
            <div class="carousel-item">
              <img class="d-block w-100" src="/image-4.jpg">
            </div>
          </div>
          <a class="carousel-control-prev" href="#carouselExample" role="button" data-slide="prev">
            <span class="carousel-control-prev-icon" aria-hidden="true"></span>
            <span class="sr-only">Previous</span>
          </a>
          <a class="carousel-control-next" href="#carouselExample" role="button" data-slide="next">
            <span class="carousel-control-next-icon" aria-hidden="true"></span>
            <span class="sr-only">Next</span>
          </a>
        </div>
      </div>

      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

Looks like a lot of code, right? Again, it’s basically straight from the Bootstrap docs, only with our attributes and images.

Step 3: Deal with image sizes

This isn’t necessary, but if the images in the carousel have different dimensions, we can crop them with CSS to keep things consistent. Note that we’re using Sass here.

// Use Bootstrap breakpoints for consistency.
$bootstrap-sm: 576px;
$bootstrap-md: 768px;
$bootstrap-lg: 992px;
$bootstrap-xl: 1200px;


// Crop thumbnail images.
#gallery {
  img {
    height: 75vw;
    object-fit: cover;
    
    @media (min-width: $bootstrap-sm) {
      height: 35vw;
    }
    
    @media (min-width: $bootstrap-lg) {
      height: 18vw;
    }
  }
}


// Crop images in the coursel
.carousel-item {
  img {
    height: 60vw;
    object-fit: cover;
    
    @media (min-width: $bootstrap-sm) {
      height: 350px;
    }
  }
}

Step 4: Optimize the images

You may have noticed that the markup uses the same image files in the gallery as we do in the modal. That doesn’t need to be the case. In fact, it’s a better idea to use smaller, more performant versions of the images for the gallery. We’re going to be blowing up the images to their full size version anyway in the modal, so there’s no need to have the best quality up front.

The good thing about Bootstrap’s approach here is that we can use different images in the gallery than we do in the modal. They’re not mutually exclusive where they have to point to the same file.

So, for that, I’d suggest updating the gallery markup with lower-quality images:

<div class="row" id="gallery" data-toggle="modal" data-target="#exampleModal">
  <div class="col-12 col-sm-6 col-lg-3">
    <img class="w-100" src="/image-1-small.jpg" data-target="#carouselExample" data-slide-to="0">
  
  <!-- and so on... -->
</div>

That’s it!

The site where I’m using this has already themed Bootstrap. That means everything is already styled to spec. That said, even if you haven’t themed Bootstrap you can still easily add custom styles! With this approach (Bootstrap vs. plugins), customization is painless because you have complete control over the markup and Bootstrap styling is relatively sparse.

Here’s the final demo:


Creating a Modal Image Gallery With Bootstrap Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-a-modal-image-gallery-with-bootstrap-components/feed/ 16 304193
CSS-Only Carousel https://css-tricks.com/css-only-carousel/ https://css-tricks.com/css-only-carousel/#comments Fri, 10 Jan 2020 16:06:41 +0000 https://css-tricks.com/?p=300964 It’s kind of amazing how far HTML and CSS will take you when building a carousel/slideshow.

  1. Setting some boxes in a horizontal row with CSS Flexbox is easy.
  2. Showing only one box at a time with overflow and making it


CSS-Only Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
It’s kind of amazing how far HTML and CSS will take you when building a carousel/slideshow.

  1. Setting some boxes in a horizontal row with CSS Flexbox is easy.
  2. Showing only one box at a time with overflow and making it swipable with overscroll-behavior is easy.
  3. You can make the “slides” line up nicely with scroll-snap-type.
  4. A couple of #jump-links is all you need to make navigation for it, which you can make all nice and smooth with scroll-behavior.

Christian Schaefer has taken it a little further with next and previous buttons, plus an auto-play feature that stops playing once interaction starts.

About that auto-play thing — it’s a bonafide CSS trick:

  1. First I slowly offset the scroll snap points to the right, making the scroll area follow along due to being snapped to them.
  2. After having scrolled the width of a whole slide, I deactivate the snapping. The scroll area is now untied from the scroll snap points.
  3. Now I let the scroll snap points jump back to their initial positions without them “snap-dragging” the scroll area back with them
  4. Then I re-engage the snapping which now lets the scroll area snap to a different snap point 🤯

Cool.

JavaScript-powered slideshows (e.g. with Flickty) can be real nice, too. There is just something neat about getting it done with so little code.

And speaking of how far we can get with HTML and CSS, here are a few related takes on image carousels and galleries.


CSS-Only Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-only-carousel/feed/ 20 300964
Carousels Don’t Have to be Complicated https://css-tricks.com/carousels-dont-complicated/ Fri, 28 Oct 2016 14:45:10 +0000 http://css-tricks.com/?p=247145 Over on the MediaTemple blog, I show you how you can make a pretty decent little carousel with just a few lines of code. Here’s the entire premise:

Every time a carousel is mentioned in a blog post, it’s a


Carousels Don’t Have to be Complicated originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Over on the MediaTemple blog, I show you how you can make a pretty decent little carousel with just a few lines of code. Here’s the entire premise:

Every time a carousel is mentioned in a blog post, it’s a requirement to mention that… say it with me now:

You probably shouldn’t use a carousel.

Jared Smith made a microsite just for it, with research and rhetoric supporting the idea that you shouldn’t use them. Most of that information focuses on the fact that there is low engagement with the non-first-slide(s).

I’m not here to argue with data, but I would argue with the dogmatism of “never user them, EVER.” Swiping on mobile is an extremely common action, and what is a swipe that reveals more content? Basically a carousel. What about a carousel that doesn’t demand much interaction? Perhaps just a way to swipe through an artist’s recent work. This seems like a perfectly nice way to do that, so long as the UI is clear and accessibility is implemented.

What I am here to talk about is the situation where you do want a carousel and to resist the temptation to reach for a wheelbarrow full of code to do so. I guarantee there are people who’ve picked an entire CMS because they thought they needed it to make a carousel. No shame. We’re all learning.

I have good news: Carousels don’t have to be complicated. They don’t have to require a ton of code or do anything that you can’t wrap your head around with basic HTML, CSS, and JavaScript knowledge.

It’s not just my idea, I link out to all the smart people who have tackled this subject before throughout the years and made similarly simple and awesome demos.

To Shared LinkPermalink on CSS-Tricks


Carousels Don’t Have to be Complicated originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
247145