Skip to content
Advertisement

Unexpected DOM Ordering Behaviour With Element.prepend on Infinite Slider-Track Animation

I’m working on an infinite slider but I’m experiencing a strange bug with DOM ordering.

At the end of each animation iteration, the last child element is supposed to be prepended to the div.slider-track element so it renders at the beginning of the slider and pushes up the other cards before the next iteration. It works as expected until the 11th iteration, where Card1 is prepended two iterations in a row. Card1 is being selected as the lastChild property twice. Shouldn’t Card10 be selected on the second 11th iteration? What gives?

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => {
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card ${count}`
  label.className = "label";
  card.append(label)
  
  return card
}

const populateCards = (element) => {
  for (let i = 1; i <= 10; i++) {
  element.append(newCard(i))
  }
}

sliderTrack.addEventListener('animationiteration', () => {
  sliderTrack.prepend(sliderTrack.lastChild);
});

populateCards(sliderTrack)
body {
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;
}

.slider {
  overflow: hidden;
}

@keyframes slider {
  to {
    transform: translate(10%);
  }
}

.slider-track {
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;
}

.card {
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
}
<div class="slider">
  <div class="slider-track">
  </div>
</div>

Advertisement

Answer

Your issue is caused by a slightly subtle and frequently annoying aspect of the DOM. And this is that “text nodes” – ie any plain text that you have as part of your HTML – count as nodes as well, and therefore as children of their parent element.

In this case, because your HTML for the slider track is written across 2 separate lines:

<div class="slider-track">
</div>

then, believe it or not, you actually have a newline character, the string "n" – as one of the child notes of the slider-track element.

So when you fill it with Card 1 up to Card 10 on initialisation, you actually have 11 children, with that text node first. After 10 iterations of your animation, it ends up with that text node as the last element.

And at that point, sliderTrack.prepend(sliderTrack.lastChild) just moves that innocent newline text node from the end to the front of the DOM children of the slidertrack. That has no noticeable effect – but because this is happening after the animation which takes 1 second, it means that on that particular “tick”, nothing appears to happen, which is the strange and unwanted behaviour you’re observing.

The fix, thankfully, is simple, when you realise this is the problem. You could of course simply remove the newline text, by putting the closing tag onto the same line as the opening tag, without even any space between:

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => {
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card ${count}`
  label.className = "label";
  card.append(label)
  
  return card
}

const populateCards = (element) => {
  for (let i = 1; i <= 10; i++) {
  element.append(newCard(i))
  }
}

sliderTrack.addEventListener('animationiteration', () => {
  sliderTrack.prepend(sliderTrack.lastChild);
});

populateCards(sliderTrack)
body {
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;
}

.slider {
  overflow: hidden;
}

@keyframes slider {
  to {
    transform: translate(10%);
  }
}

.slider-track {
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;
}

.card {
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
}
<div class="slider">
  <div class="slider-track"></div>
</div>

But while that’s not too painful here, it could certainly be very annoying in other cases, forcing you to format your HTML in a less-than-readable way.

So there’s another solution for situations exactly like this – as well as lastChild, there is also the more specific lastElementChild which specifically ignores text nodes. That will also work perfectly here, and is probably the better solution in general:

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => {
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card ${count}`
  label.className = "label";
  card.append(label)
  
  return card
}

const populateCards = (element) => {
  for (let i = 1; i <= 10; i++) {
  element.append(newCard(i))
  }
}

sliderTrack.addEventListener('animationiteration', () => {
  sliderTrack.prepend(sliderTrack.lastElementChild);
});

populateCards(sliderTrack)
body {
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;
}

.slider {
  overflow: hidden;
}

@keyframes slider {
  to {
    transform: translate(10%);
  }
}

.slider-track {
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;
}

.card {
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
}
<div class="slider">
  <div class="slider-track">
  </div>
</div>
User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement