Vitaly Protasov Vitaly Protasov - 5 months ago 12
CSS Question

image sliders overscroll

I'm trying to implement some nice sliders of 9 images. Here is a link to a sandbox with the code: https://jsfiddle.net/gq5r5o8r/

The problem is that if I hover over the first slide on the left and quickly move the cursor to last slide on the right, the images squeeze together and get out of their place, and an empty space comes in between the last slide on the right and the edge of the container.

Construction is based on the list, so the width is 980px, the width of active "hovered" picture is 640px, and 340px is left over for the remaining 8 squeezed images.

I'm guessing the problem has to do with this:

.wb_sliders_menu ul:hover li {
width: calc( 340px / 8 );
}


Can it be solved by sticking the first and last slide edges to the left and right sides of the container somehow?

Thanks in advance!

Answer

TL;DR

Use flex-box:

.wb_sliders_menu ul { display: flex; }
.wb_sliders_menu ul li:hover { flex: 100; }
.wb_sliders_menu ul li {
  min-width: 42.5px; /* or calc(340px / 8) */
  max-width: 640px;
  flex: 1;
}

https://jsfiddle.net/g4nw310b/4/


Why is this happening?

The problem seems to be that the transitions are not all ending at the same time.

Let's look at the math. To make it easier, let's assume a linear transition (y = x). Also, since I'm rounding the numbers the math doesn't add up 100%, but enough to get the idea.

0.00s

The elements all start out at 108.89px (980px / 9). Eight of them will go to 42.50px (340px / 8). The other one will go to 980px. All of them will take 0.5s to get there. That means the shrinking ones will be going at a rate of (42.50px-108.89px)/0.5s = -132.78px/s. The growing one will be going at a rate of (640px-108.89px)/0.5s = 1062.22px/s. If you don't move the mouse, they stay at these rates, which cancel each other out so you don't see any difference in movement: (8 * -132.78px/s) + 1062.22px/s) = -.02 ≈ 0.

0.25s

Now let's move forward 0.25 seconds. The one image is halfway between 108.89px and 640px (i.e. 374.44px), and the others are halfway between 108.89px and 42.50px (i.e. 75.70px). That's 374.44px + (75.70px * 8) = 980px. So far so good...

Now let's assume you are able to instantaneously move the mouse from the first image to the last, while staying hovered over the ul element. This triggers .wb_sliders_menu ul li:hover to toggle the width for the first and last elements, to 340px/8 and 640px respectively, while the rest remain the same. This resets the transition timer for these two elements, while the others remain in the original transition.

The first image is now going from 374.44px to 42.50px at a rate of (42.50px-374.44px)/0.5s = -663.88px/s. The last image will be going from 75.7px to 640px at a rate of (640px-75.70px)/0.5s = 1128.60px/s. The rest are still in the original transition, currently at 75.7px, on their way from 108.88px to 42.50px at a rate of -132.78px/s. That gives us vectors of (7 * -132.78px/s) + -663.88px/s + 1128.60px/s = -464.74px/s. Oops - now we're shrinking!

0.50s

Flash forward another 0.25s. The first image is now halfway between 374.44px and 42.50px (i.e. 208.47px). The last image is halfway between 640px and 75.70px (i.e. 357.85px). The other 7 images have finished their transitions, and are now at 42.50px. That gives us 208.47px + 357.85px + (42.50px * 7) = 863.82px, giving us a 980px - 863.82px = 116.18px gap on the right.

The first and last continue transitioning at rates of -663.88px/s and 1128.60px, giving us the 464.72px/s ≈ 464.74px/s to make up for before. We're now filling the gap back up.

0.75s

After another 0.25s, at the rate of 464.72px/s, we've filled up the gap (0.25s * 464.72px/s = 116.18px) we had before, and everything looks good again.

How do we fix it?

If, at the 0.25s mark, the 7 slides in the middle were to reset their transitions, they would instead be going at a rate of (42.50-75.70px)/0.5s = -66.40px/s. At the 0.5s mark, they would then be halfway between 75.70px and 42.50px (i.e. 59.10px). That gives us a new sum of 208.47px + 357.85px + (59.1px * 7) = 980.02px ≈ 980px. They would also still be transitioning at rates that cancel each other out ((7 * -66.40px/s) + -663.88px/s + 1128.60px/s = -0.08px/s ≈ 0px/s).

Ultimately, the velocities of the transitions need to cancel each other out for a smooth effect. Probably the easiest way to do this is to make sure all of the elements start and end at the same time, using the same timing function.

Javascript

We can do this in Javascript by manually setting hover classes. However, it takes some weird tweaks to the css as well. The JS code shown here is using jQuery, as it's a bit easier to write - but it could be done in pure JS as well.

$(".wb_sliders_menu > ul").on("mouseenter mouseleave", "> li", function(e){
  var $li = $(this);
  var $ul = $li.parent();

  if (e.type === "mouseleave") {
    // Remove the hover class to reset everything to it's initial width
    var $lis = $("> li", $ul);
    $lis.removeClass("hover");
    $ul.removeClass("hover");
  } else {
    // Wait until the next animation frame, then add back the classes.
    // This will effectively reset the timer
    requestAnimationFrame(function() {
      $ul.addClass("hover");
      $li.addClass("hover");
    })
  }
});

You'll need to change the following css rules to use .hover instead of :hover.

.wb_sliders_menu ul.hover li { width: calc( 340px / 8 ); }
.wb_sliders_menu ul li.hover {width: 640px;}

Sometimes the far-right item will disappear, because it's actually moving onto the second line as it doesn't fit in the container. So you'll need to add the following css rules:

.wb_sliders_menu ul { white-space: nowrap; }
.wb_sliders_menu li { display: inline-block; float: none; }

And finally, you'll have a weird gap due to inline-block showing the white-space. To fix this you would need to make sure you have </li><li> instead of </li> <li>.

Here's a working example: https://jsfiddle.net/6zkfewga/1/

CSS

Now here's the fun part. Flex-box! A solution without using any javascript.

.wb_sliders_menu ul { display: flex; }
.wb_sliders_menu ul li:hover { flex: 100; }
.wb_sliders_menu ul li {
  min-width: 42.5px; /* or calc(340px / 8) */
  max-width: 640px;
  flex: 1;
}

You can get rid of the .wb_sliders_menu ul:hover li rule and the float: left.

Here's a working example: https://jsfiddle.net/g4nw310b/4/

Comments