Horizontal scroll using keyframes in Firefox

Viewed 316

I am working on a CSS-only carousel with an autoplay feature.

A keyframe animation sets a margin-left to .slide-snapper (div which controls scroll-snap-align and the animations) which causes the slide to move to the left. It works totally fine in Chrome, but in Firefox setting a margin-left (or left alternatively) has no effect on the scroll. If inspected you can see the div element moving but without the expected effect.

Why is this behavior different in Firefox?

*, *:after, *:before {
    box-sizing: border-box;
}
/* Example images */
#slide_1 {
    background-image: url(https://images.unsplash.com/photo-1579353977828-2a4eab540b9a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_2 {
    background-image: url(https://images.unsplash.com/photo-1561336313-0bd5e0b27ec8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_3 {
    background-image: url(https://images.unsplash.com/photo-1600716051809-e997e11a5d52?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_4 {
    background-image: url(https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHNhbXBsZXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60);
}
.li_slide {
    background-size: cover;
    background-position: center;
}
/* Animations */
@keyframes tonext {
    75% {
        margin-left: 0;
    }
    95% {
        margin-left: calc(100% + 150px);
    }
    98% {
        margin-left: calc(100% + 150px);
    }
    99% {
        margin-left: 0;
    }
}
@keyframes snap {
    96% {
        scroll-snap-align: start;
    }
    97% {
        scroll-snap-align: none;
    }
    99% {
        scroll-snap-align: none;
    }
    100% {
        scroll-snap-align: start;
    }
}
@keyframes tostart {
    75% {
        margin-left: 0;
    }
    95% {
        margin-left: calc(-300% - 450px);
    }
    98% {
        margin-left: calc(-300% - 450px);
    }
    99% {
        margin-left: 0;
    }
}
/* Reset */
ol, li {
    list-style: none;
    margin: 0;
    padding: 0;
}
#pSlider {
    width: 300px;
    height: 300px;
    position: relative;
    margin: 0 auto;
    /* Left, Right Navigation Arrows */
}
#pSlider #slider-container {
    display: flex;
    height: 100%;
    width: 100%;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
}
#pSlider #slider-container .li_slide {
    position: relative;
    width: 100%;
    border-radius: 10px;
    background-color: khaki;
    flex: none;
    margin-right: 150px;
}
#pSlider #slider-container .li_slide:nth-child(even) {
    background-color: mediumaquamarine;
}
#pSlider #slider-container .li_slide:last-child {
    margin-right: 0;
}
#pSlider #slider-container .li_slide .slide-snapper {
    /* Auto play */
    scroll-snap-align: start;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    animation: tonext 4s infinite ease, snap 4s infinite ease;
}
@media (prefers-reduced-motion: reduce) {
    #pSlider #slider-container .li_slide .slide-snapper {
        animation-name: none;
    }
}
#pSlider #slider-container .li_slide:last-child .slide-snapper {
    animation-name: tostart, snap;
}
#pSlider #slider-container:hover .li_slide .slide-snapper, #pSlider #slider-container:focus-within .li_slide .slide-snapper {
    animation-name: none;
}
#pSlider:before, #pSlider:after, #pSlider .prev_slide, #pSlider .next_slide {
    display: inline-block;
    position: absolute;
    top: 0;
    width: 30px;
    height: 100%;
    z-index: 2;
    cursor: pointer;
}
#pSlider:before, #pSlider:after {
    background-repeat: no-repeat;
    background-position: center;
    background-size: 70%;
}
#pSlider .next_slide {
    border-top-right-radius: 10px;
    border-bottom-right-radius: 10px;
    padding: 20px;
}
#pSlider .prev_slide {
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    padding: 20px;
}
#pSlider:before, #pSlider .prev_slide {
    content: "";
    left: 0px;
}
#pSlider:after, #pSlider .next_slide {
    content: "";
    right: 0px;
}
#pSlider:after {
    z-index: 1;
    /* set underneath .next_slide (z: 2)*/
    background-image: url("data:image/svg+xml,%3Csvg width='40' height='60' xmlns='http://www.w3.org/2000/svg' version='1.1'%3E%3Cpolyline points='10 10 30 30 10 50' stroke='rgba(255,255,255,0.9)' stroke-width='4' stroke-linecap='butt' fill='none' stroke-linejoin='round'%3E<%3C/polyline%3E%3C/svg%3E");
}
#pSlider:before {
    background-image: url("data:image/svg+xml,%3Csvg width='40' height='60' xmlns='http://www.w3.org/2000/svg' version='1.1'%3E%3Cpolyline points='30 10 10 30 30 50' stroke='rgba(255,255,255,0.9)' stroke-width='4' stroke-linecap='butt' fill='none' stroke-linejoin='round'%3E>%3C/polyline%3E%3C/svg%3E");
}
/* Scrollbar */
#pSlider #slider-container::-webkit-scrollbar {
    display: none;
    height: 5px;
}
#pSlider #slider-container::-webkit-scrollbar-thumb {
    background-color: #000;
    border-radius: 50px;
}
#pSlider #slider-container::-webkit-scrollbar-track {
    background-color: transparent;
}

/* Input */

#sliderSettings {
  text-align: center;
}

#slider-width, #slider-height {
  text-align: center;
  margin-bottom: 20px; 
}

h2 {
  text-align: center;
}
<h2>CSS Autoplay Carousel</h2>
<div id="sliderSettings">
  <label for="slider-width">Slider width in px:</label>
  <input type="text" id="slider-width" name="slider-width" minlength="2" maxlength="4" size="6" placeholder="300">
  <label for="slider-height">Slider height in px:</label>
  <input type="text" id="slider-height" name="slider-height" minlength="2" maxlength="4" size="6" placeholder="300">
</div>

<div id="pSlider">
  <ol id="slider-container" dir="ltr">
    <li id="slide_1" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_4"></a> <a class="next_slide" href="#slide_2"></a> </li>
    <li id="slide_2" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_1"></a> <a class="next_slide" href="#slide_3"></a> </li>
    <li id="slide_3" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_2"></a> <a class="next_slide" href="#slide_4"></a> </li>
    <li id="slide_4" class="li_slide">
      <div  class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_3"></a> <a class="next_slide" href="#slide_1"></a> </li>
  </ol>
</div>

Simplified version:

@keyframes tonext {
    75% {
        margin-left: 0;
    }
    95% {
        margin-left: calc(100% + 150px);
    }
    98% {
        margin-left: calc(100% + 150px);
    }
    99% {
        margin-left: 0;
    }
}

@keyframes snap {
    96% {
        scroll-snap-align: start;
    }
    97% {
        scroll-snap-align: none;
    }
    99% {
        scroll-snap-align: none;
    }
    100% {
        scroll-snap-align: start;
    }
}

#pSlider {
    width: 300px;
    height: 300px;
    position: relative;
    margin: 0 auto;
    /* Left, Right Navigation Arrows */
}
#pSlider #slider-container {
    display: flex;
    height: 100%;
    width: 100%;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
}
#pSlider #slider-container .li_slide {
    position: relative;
    width: 100%;
    border-radius: 10px;
    background-color: khaki;
    flex: none;
    margin-right: 150px;
}
#pSlider #slider-container .li_slide:nth-child(even) {
    background-color: mediumaquamarine;
}
#pSlider #slider-container .li_slide:last-child {
    margin-right: 0;
}
#pSlider #slider-container .li_slide .slide-snapper {
    /* Auto play */
    scroll-snap-align: start;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    animation: tonext 4s infinite ease, snap 4s infinite ease;
}

#pSlider #slider-container .li_slide:last-child .slide-snapper {
  animation: none;
}
<div id="pSlider">
  <div id="slider-container">
    <div id="slide_1" class="li_slide">
      <div class="slide-snapper"></div>
    </div>
    <div id="slide_2" class="li_slide">
      <div class="slide-snapper"></div>
    </div>
  </div>
</div>

2 Answers

One thing I noticed, unless you interact with the container Firefox doesn't snap the scroll bar. Couldn't find anything related on the Bugzilla.
If you run following snippet in Firefox. The scroll will snap after you click anywhere on the container.

if ("scrollRestoration" in history) {
  history.scrollRestoration = "manual";
}
#pSlider {
  width: 250px;
  height: 250px;
  position: relative;
  margin: 0 auto;
  /* Left, Right Navigation Arrows */
}

#pSlider #slider-container {
  display: flex;
  height: 100%;
  width: 100%;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}

#pSlider #slider-container .li_slide {
  overflow: visible;
  position: relative;
  width: 100%;
  border-radius: 10px;
  abackground-color: khaki;
  flex: none;
  margin-right: 150px;
  border: 1px solid green;
}

#pSlider #slider-container .li_slide .slide-snapper {
  /* Auto play */
  scroll-snap-align: start;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin-left: calc(100% + 150px);
  border: 1px dashed red;
  background-color: rgb(246, 209, 130);
}
<div id="pSlider">
  <div id="slider-container">
    <div id="slide_1" class="li_slide">
      Slide
      <div class="slide-snapper">Snapper</div>
    </div>
  </div>
</div>
Open in Firefox and click anywhere on the container.


I created a basic CSS only carousel without scroll snapping a while ago:

:root {
  /* no of slides excluding last duplicate */
  --slide-count: 4;
  /* time that a slide stays idle and slides left */
  /* 70% idle, 30% sliding animation */
  --slide-time: 3s;
  --carousel-width: 300px;
  --carousel-height: 150px;
}

body {
  height: 95vh;
  width: 95vw;
  display: flex;
  flex-flow: column;
  justify-content: center;
}

.carousel {
  margin: 0 auto;
  width: var(--carousel-width);
  height: var(--carousel-height);
  white-space: nowrap;
  contain: layout style;
  overflow: hidden;
  display: flex;
  flex-direction: row;
  counter-reset: count;
}

.slide {
  width: 100%;
  height: 100%;
  position: relative;
  left: 0;
  flex: none;
  animation-name: shiftMe, slideMe;
  animation-duration: calc(var(--slide-time) * var(--slide-count)), var(--slide-time);
  animation-timing-function: steps(var(--slide-count), end), ease-out;
  animation-iteration-count: infinite;
  counter-increment: count;
  position: relative;
}

.slide::before {
  content: counter(count);
  position: absolute;
  top: 0px;
  font-size: 1rem;
}

.slide:last-child::before {
  content: "1";
}

@keyframes shiftMe {
  0% {
    left: 0px;
  }
  100% {
    left: calc(var(--carousel-width) * var(--slide-count) * -1);
  }
}

@keyframes slideMe {
  0% {
    transform: translateX(0px);
  }
  70% {
    transform: translateX(0px);
  }
  100% {
    transform: translateX(calc(var(--carousel-width) * -1));
  }
}

.carousel:hover .slide {
  animation-play-state: paused;
}

.slide:nth-child(even) {
  background-color: rgba(173, 216, 230, 0.7);
}

.slide:nth-child(odd),
.slide:last-child {
  background-color: rgba(144, 238, 144, 0.7);
}

img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
<div class="carousel">
  <div class=slide><img src="https://developer.mozilla.org/static/media/chrome.4c570865.svg"></div>
  <div class=slide><img src="https://developer.mozilla.org/static/media/safari.3679eb31.svg"></div>
  <div class=slide><img src="https://developer.mozilla.org/static/media/firefox.51d8a59c.svg"></div>
  <div class=slide><img src="https://developer.mozilla.org/static/media/edge.40018f6a.svg"></div>
  <div class=slide><img src="https://developer.mozilla.org/static/media/chrome.4c570865.svg"></div>

It is a stateful carousel so you can't jump slides. You have to duplicate first slide and put it at the end to make it seamless.

Vertical version

I'm guessing you'd prefer a CSS-only solution.

But a mere 7 lines of javascript will ensure this slideshow's autoplay feature works cross-browser (including in Firefox):

let currentSlideIndex = 1;
const numberOfSlides = document.querySelectorAll('.li_slide').length;

const moveToNextSlide = () => {
  let currentSlide = document.querySelector('#slide_' + currentSlideIndex);
  currentSlide.querySelector('.next_slide').click();
  currentSlideIndex = (currentSlideIndex === numberOfSlides) ? 1 : currentSlideIndex + 1;
}

const autoRunSlideshow = setInterval(moveToNextSlide, 4000);

Working Example:

let currentSlideIndex = 1;
const numberOfSlides = document.querySelectorAll('.li_slide').length;

const moveToNextSlide = () => {
 
  let currentSlide = document.querySelector('#slide_' + currentSlideIndex);
  currentSlide.querySelector('.next_slide').click();
  currentSlideIndex = (currentSlideIndex === numberOfSlides) ? 1 : currentSlideIndex + 1;
}

const autoRunSlideshow = setInterval(moveToNextSlide, 4000);
*, *:after, *:before {
    box-sizing: border-box;
}
/* Example images */
#slide_1 {
    background-image: url(https://images.unsplash.com/photo-1579353977828-2a4eab540b9a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_2 {
    background-image: url(https://images.unsplash.com/photo-1561336313-0bd5e0b27ec8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_3 {
    background-image: url(https://images.unsplash.com/photo-1600716051809-e997e11a5d52?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8c2FtcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60);
}
#slide_4 {
    background-image: url(https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHNhbXBsZXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60);
}
.li_slide {
    background-size: cover;
    background-position: center;
}
/* Animations */
@keyframes tonext {
    75% {
        margin-left: 0;
    }
    95% {
        margin-left: calc(100% + 150px);
    }
    98% {
        margin-left: calc(100% + 150px);
    }
    99% {
        margin-left: 0;
    }
}
@keyframes snap {
    96% {
        scroll-snap-align: start;
    }
    97% {
        scroll-snap-align: none;
    }
    99% {
        scroll-snap-align: none;
    }
    100% {
        scroll-snap-align: start;
    }
}
@keyframes tostart {
    75% {
        margin-left: 0;
    }
    95% {
        margin-left: calc(-300% - 450px);
    }
    98% {
        margin-left: calc(-300% - 450px);
    }
    99% {
        margin-left: 0;
    }
}
/* Reset */
ol, li {
    list-style: none;
    margin: 0;
    padding: 0;
}
#pSlider {
    width: 300px;
    height: 300px;
    position: relative;
    margin: 0 auto;
    /* Left, Right Navigation Arrows */
}
#pSlider #slider-container {
    display: flex;
    height: 100%;
    width: 100%;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
}
#pSlider #slider-container .li_slide {
    position: relative;
    width: 100%;
    border-radius: 10px;
    background-color: khaki;
    flex: none;
    margin-right: 150px;
}
#pSlider #slider-container .li_slide:nth-child(even) {
    background-color: mediumaquamarine;
}
#pSlider #slider-container .li_slide:last-child {
    margin-right: 0;
}
#pSlider #slider-container .li_slide .slide-snapper {
    /* Auto play */
    scroll-snap-align: start;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    animation: tonext 4s infinite ease, snap 4s infinite ease;
}
@media (prefers-reduced-motion: reduce) {
    #pSlider #slider-container .li_slide .slide-snapper {
        animation-name: none;
    }
}
#pSlider #slider-container .li_slide:last-child .slide-snapper {
    animation-name: tostart, snap;
}
#pSlider #slider-container:hover .li_slide .slide-snapper, #pSlider #slider-container:focus-within .li_slide .slide-snapper {
    animation-name: none;
}
#pSlider:before, #pSlider:after, #pSlider .prev_slide, #pSlider .next_slide {
    display: inline-block;
    position: absolute;
    top: 0;
    width: 30px;
    height: 100%;
    z-index: 2;
    cursor: pointer;
}
#pSlider:before, #pSlider:after {
    background-repeat: no-repeat;
    background-position: center;
    background-size: 70%;
}
#pSlider .next_slide {
    border-top-right-radius: 10px;
    border-bottom-right-radius: 10px;
    padding: 20px;
}
#pSlider .prev_slide {
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    padding: 20px;
}
#pSlider:before, #pSlider .prev_slide {
    content: "";
    left: 0px;
}
#pSlider:after, #pSlider .next_slide {
    content: "";
    right: 0px;
}
#pSlider:after {
    z-index: 1;
    /* set underneath .next_slide (z: 2)*/
    background-image: url("data:image/svg+xml,%3Csvg width='40' height='60' xmlns='http://www.w3.org/2000/svg' version='1.1'%3E%3Cpolyline points='10 10 30 30 10 50' stroke='rgba(255,255,255,0.9)' stroke-width='4' stroke-linecap='butt' fill='none' stroke-linejoin='round'%3E&lt;%3C/polyline%3E%3C/svg%3E");
}
#pSlider:before {
    background-image: url("data:image/svg+xml,%3Csvg width='40' height='60' xmlns='http://www.w3.org/2000/svg' version='1.1'%3E%3Cpolyline points='30 10 10 30 30 50' stroke='rgba(255,255,255,0.9)' stroke-width='4' stroke-linecap='butt' fill='none' stroke-linejoin='round'%3E&gt;%3C/polyline%3E%3C/svg%3E");
}
/* Scrollbar */
#pSlider #slider-container::-webkit-scrollbar {
    display: none;
    height: 5px;
}
#pSlider #slider-container::-webkit-scrollbar-thumb {
    background-color: #000;
    border-radius: 50px;
}
#pSlider #slider-container::-webkit-scrollbar-track {
    background-color: transparent;
}

/* Input */

#sliderSettings {
  text-align: center;
}

#slider-width, #slider-height {
  text-align: center;
  margin-bottom: 20px; 
}

h2 {
  text-align: center;
}
<h2>CSS Autoplay Carousel</h2>
<div id="sliderSettings">
  <label for="slider-width">Slider width in px:</label>
  <input type="text" id="slider-width" name="slider-width" minlength="2" maxlength="4" size="6" placeholder="300">
  <label for="slider-height">Slider height in px:</label>
  <input type="text" id="slider-height" name="slider-height" minlength="2" maxlength="4" size="6" placeholder="300">
</div>

<div id="pSlider">
  <ol id="slider-container" dir="ltr">
    <li id="slide_1" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_4"></a> <a class="next_slide" href="#slide_2"></a> </li>
    <li id="slide_2" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_1"></a> <a class="next_slide" href="#slide_3"></a> </li>
    <li id="slide_3" class="li_slide">
      <div class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_2"></a> <a class="next_slide" href="#slide_4"></a> </li>
    <li id="slide_4" class="li_slide">
      <div  class="slide-snapper"></div>
      <a class="prev_slide" href="#slide_3"></a> <a class="next_slide" href="#slide_1"></a> </li>
  </ol>
</div>

Related