Custom JS cursor updates on hover – but not when auto-scrolling content moves beneath it (without mouse movement)

I have a custom cursor effect that follows the real browser point around the page. When the mouse hovers over an element with a data-label value set, a class of .cursor-trail--hover is added and the relevant value is displayed along side the cursor*. This is animated in/out of view.

*Note, the position is a bit off in the included example due the the structure of the embed I think.

As you hover over each image in the carousel, you can see this work perfectly.

The problem is if you leave the mouse on the page so there is no movement and the content scrolls/passed under the cursor, it will not update/animate in the same as :hover triggers. The text will eventually change but it changes without any animation and there seems to be a bit of a delay for it to update.

Is there anyway to resolve this and have the text animate/change when the mouse is idle as well?

As an aside if you click/drag on the carousel, the cursor position stops working and stays in the same place even though the cursor has moved – until the ‘click’ is released. Not sure if there is a way to resolve this with the carousel being a 3rd party plugin-in?

Maybe not related to the same issue but I thought I’d mention as it’d be great with anyone has ideas on that as well – incase it’s linked to the core issue.

/* ==========================================================================
   #LAZY LOAD IMAGES
   ========================================================================== */

/**
 * Class to animate/transition an image into view once it has been loaded.
 */

const pixelImage = document.querySelectorAll(".pixel-load")

pixelImage.forEach(div => {
  const img = div.querySelector("img")

  function loaded() {
    div.classList.add("loaded")
  }

  if (img.complete) {
    loaded()
  } else {
    img.addEventListener("load", loaded)
  }
})




/* ==========================================================================
   #KEEN SLIDER
   ========================================================================== */

/**
 * Using Keen-Slider for the infinite looping carousel, which I originally did
 * in pure CSS - but I wanted to make this draggable by the user so made sense
 * to use a 3rd party plug-in to do the heavy lifting.
 */

var animation = {
  duration: 32000,
  easing: (t) => t
}
new KeenSlider("#gallery-slider", {
  dragSpeed: 1,
  loop: true,
  mode: "free",
  slides: {
    perView: 1.5,
    renderMode: "performance",
    spacing: 8
  },
  breakpoints: {
    '(min-width: 768px)': {
      slides: {
        perView: 3,
        spacing: 8
      }
    },
    '(min-width: 1024px)': {
      slides: {
        perView: 4,
        spacing: 8
      }
    }
  },
  created(s) {
    s.moveToIdx(5, true, animation)
  },
  updated(s) {
    s.moveToIdx(s.track.details.abs + 5, true, animation)
  },
  animationEnded(s) {
    s.moveToIdx(s.track.details.abs + 5, true, animation)
  }
})




/* ==========================================================================
   #CURSOR
   ========================================================================== */

/**
 * Two div's are on the page which are used to create a custom cursor effect.
 * `.cursor` follows the pointer whereas the `.cursor-trail` is an outer circle
 * that follows the cursor with a delay.
 * 
 * The original script was based on this CodePen example:
 * https://codepen.io/ntenebruso/pen/QWLzVjY
 *
 * But there were performance (lagging and jumping) issues cross-browser, mainly
 * in Safari (Mac OS). So it was re-written and this version performed better.
 * Previously `calc` was used for sizing and top/left values for positioning.
 * Using fixed values instead of `calc` (as we know the size of the cursor) and
 * `transform` wasn't as much of a performance hit. If we changed the size of
 *  the cursor in the CSS we'd need to update the values here accordingly.
 */

var cursorTrail = document.querySelector(".cursor-trail");
var a = document.querySelectorAll("a");
var timeout;

window.addEventListener(
  "mousemove",
  function(e) {
    var x = e.clientX;
    var y = e.clientY;

    if (!timeout) {
      timeout = setTimeout(function() {
        timeout = null;
        cursorTrail.style.transform = `translate(${x - 4}px, ${y - 4}px)`;
      }, 24);
    }
  },
  false
);

/**
 * Add/remove set classes on hover.
 * 
 * 1. This used to start with `a.forEach((item) => {` but changed to `let` so
 *    that an additional (non-anchor) item could be targeted. `#hello` is for
 *    the image on the 404 page.
 */

// a.forEach((item) => {
let links = document.querySelectorAll('a'); /* [1] */
links.forEach((item) => {
  /* [1] */
  item.addEventListener("mouseover", () => {
    cursorTrail.classList.add("cursor-trail--hover");
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove("cursor-trail--hover");
  });
});

/**
 * Add/remove classes on click (anywhere).
 */

document.addEventListener("mousedown", function() {
  cursorTrail.classList.add("cursor-trail--click");
});

document.addEventListener("mouseup", function() {
  cursorTrail.classList.remove("cursor-trail--click");
});

/**
 * Add custom classes on hover if the cursor needs to be manipulated in a
 * unique way. If an element has a `data-interaction=""` value set. This will
 * be added as a class to the cursor on hover. For example, this is used to
 * style the prev/next arrows on the carousel.
 *
 * This could be set using a specific class but I've just left it targeting all
 * `a` elements for now. Which will add a class of `undefined` if no dataset is
 * specified.
 */

a.forEach((item) => {
  const interaction = item.dataset.interaction;

  item.addEventListener("mouseover", () => {
    cursorTrail.classList.add(interaction);
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove(interaction);
  });
});

/**
 * Text Label
 */

let hasLabel = document.querySelectorAll('.has-label');
var cursorText = document.querySelector('.cursor__label-text');

hasLabel.forEach((item) => {
  const label = item.dataset.label;

  item.addEventListener("mouseover", () => {
    cursorText.textContent = label;
    cursorTrail.classList.add("cursor-trail--label");
  });
  item.addEventListener("mouseleave", () => {
    cursorTrail.classList.remove("cursor-trail--label");
  });
});
/* ==========================================================================
   #BASE
   ========================================================================== */

html {
  font-size: 62.5%;
  margin: 0;
  padding: 0;
}

body {
  font-size: 12px;
  font-family: "Arial", sans-serif;
  margin: 0;
  padding: 64px 0 0;
  text-transform: uppercase;
}

h2 {
  font-size: 12px;
  font-weight: 400;
  margin: 0 16px 16px;
  padding: 0;
}

figure {
  margin: 0;
  padding: 0;
}

img {
  height: auto;
  width: 100%;
  max-width: 100%;
}



/* ==========================================================================
   #CURSOR
   ========================================================================== */

/**
 * Hide the cursor if the `hover` event doesn't exist so the custom cursor isn't
 * displayed on touch devices.
 */

@media (hover: none) {
  .cursor-trail {
    display: none;
  }
}

/**
 * Core `.cursor-trail` styling which is a larger cricle that follows `.cursor`
 * around the screen but with a smooth delay/lag.
 *
 * 1. Fade-in the object only when `body` is hovered over. Otherwise on page
 *    load the cursor is stuck in the top/left of the browser until you move the
 *    mouse, which looks a bit crap. At least this is a bit more graceful.
 * 2. Add a lot transitions for various click/hover states which adjust the size
 *    and colour of the element. `transform` is the one for the smooth lag when
 *    moving the cursor.
 */

.cursor-trail {
  background: white;
  box-sizing: border-box;
  height: 8px;
  margin: 0;
  opacity: 0;
  /* [1] */
  pointer-events: none;
  position: fixed;
  transform-origin: center center;
  transition: height 0.04s ease-out, margin 0.04s ease-out, opacity 0.16s 0.16s, transform 0.32s cubic-bezier(0, 0.48, 0.64, 1), width 0.08s ease-out;
  /* [2] */
  width: 8px;
  mix-blend-mode: difference;
  z-index: 1000;
}

body:hover .cursor-trail {
  opacity: 1;
}

/**
 * A class of `.cursor-trail--hover` is added when the user hovers over a link.
 * When this occurs we shrink the trailing div. The `margin` keeps the div
 * centred with `.cursor` when triggered. Previously a `transform` was used to
 * centre the div - but pixel values just meant less was being calculated at
 * the same time and led to better/smoother performance.
 */

.cursor-trail--hover {
  height: 0;
  margin: 4px 0 0 4px;
  width: 0;
}

/**
 * If an element on the page has a `data-label` set, Javascript gets the value
 * and displays the text within `.cursor__label-text` to a label can be
 * displayed alongside the cursor.
 *
 * 1. The parent of the label which has `overflow: hidden` set, allows the text
 *    to be animated vertically in/out of view on hover.
 */

.cursor__label {
  color: white;
  overflow: hidden;
  /* [1] */
  position: absolute;
  top: -8px;
  left: 16px;
}

/**
 * 1. The label begins out of view, pushed beyond the bottom edge of the parent.
 * 2. When an element with a label is hovered over, a class and animation is
 *    added to the main `.cursor-trail` element. This allows us to animate the
 *    text into view. Only running the animation when the class is added, not
 *    when it is removed...
 * 3. The `transition` only runs when `.cursor-trail--label` is removed from the
 *    parent. As the `animation` only runs when the class is added, it allows us
 *    to `transition` the text out the top edge of it's container without the
 *    animation interfering with it.
 */

.cursor__label-text {
  display: block;
  opacity: 0;
  transform: translateY(-100%);
  /* [1] */
  transition: opacity 0.16s ease-out, transform 0.16s ease-out;
  /* [3] */
  white-space: nowrap;
}

.cursor-trail--label .cursor__label-text {
  /* [2] */
  animation: cursor-label 0.16s ease-out;
  opacity: 1;
  transform: translateY(0);
}

/**
 * Animation which moves the label into view from the bottom of it's parent.
 */

@keyframes cursor-label {
  0% {
    opacity: 0;
    transform: translateY(100%);
  }

  100% {
    opacity: 1;
    transform: translateY(0);
  }
}




/* ==========================================================================
   #MARQUEE
   ========================================================================== */

/**
 * Auto-scrolling gallery displaying images of projects.
 *
 * 1. Animation settings.
 */

:root {
  /* [1] */
  --play: running;
  --direction: normal;
  --duration: 32s;
  --delay: 0s;
  --iteration-count: infinite;
}

/**
 * As we're using a CSS auto-scrolling carousel, we need a duplicate `.marquee`
 * for a seamless, continuous loop. So we need a wrapping div to contain them
 * both.
 *
 * 1. Ensure both `.marquee` elements display in a row.
 * 2. Prevent any horizontal scrolling of the page.
 */

.marquee-wrap {
  display: flex;
  /* [1] */
  flex-direction: row;
  /* [1] */
  margin-bottom: 64px;
  overflow: hidden;
  /* [2] */
}

/**
 * The two `.marquee` elements are styled and behave exactly the same. The only
 * difference is the 2nd one has `aria-hidden="true"` set for SEO.
 *
 * 1. Prevent the content from shrinking.
 */

.marquee {
  display: flex;
  /* [1] */
  flex-shrink: 0;
  /* [1] */
  animation: marquee-scroll var(--duration) linear var(--delay) var(--iteration-count);
  animation-play-state: var(--play);
  animation-delay: var(--delay);
  animation-direction: var(--direction);
  min-width: 100%;
  /* [1] */
}

/**
 * Set the height/width of each item and prevent it from shrinking.
 */

.marquee__item {
  flex-shrink: 0;
  margin-right: 8px;
  width: 320px;
}

/**
 * Animate the content from left to right.
 */

@keyframes marquee-scroll {
  0% {
    transform: translateX(0%);
  }

  100% {
    transform: translateX(-100%);
  }
}



/* ==========================================================================
   #KEEN SLIDER
   ========================================================================== */
/**
 * 1. Removed `overflow: hidden` so I could align the slider with the main grid
 *    but still have it bleed off the edges of the page. To avoid a horizontal
 *    scroll on the site, I've added `overflow: hidden` to a parent div.
 */
.keen-slider:not([data-keen-slider-disabled]) {
  display: flex;
  align-content: flex-start;
  overflow: hidden;
  position: relative;
  touch-action: pan-y;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  width: 100%;
  -webkit-tap-highlight-color: transparent;
}

.keen-slider:not([data-keen-slider-disabled]) .keen-slider__slide {
  min-height: 100%;
  overflow: hidden;
  position: relative;
  width: 100%;
}

.keen-slider:not([data-keen-slider-disabled])[data-keen-slider-v] {
  flex-wrap: wrap;
}

/* ==========================================================================
   #GALLERY
   ========================================================================== */
/**
 * My overrides for the Keen Slider gallery.
 *
 * 1. Remove `overflow: hidden` from the slider and add it to the parent. This
 *    allows the slider to align with the grid but also bleed off the edges of
 *    the page.
 * 2. Align container with the global grid.
 */

.gallery {
  margin-bottom: 64px;
  overflow: hidden;
  /* [1] */
  padding: 0 16px;
  /* [2] */
}

.gallery .keen-slider {
  overflow: visible;
  /* [1] */
}




/* ==========================================================================
   #PIXEL LOAD
   ========================================================================== */

/**
 * Add a pixelated effect to images while the load.
 */

.pixel-load {
  overflow: hidden;
  position: relative;
}

.pixel-load__preload img {
  image-rendering: pixelated;
  position: absolute;
  inset: 0;
  opacity: 1;
  pointer-events: none;
}

.loaded .pixel-load__preload img {
  animation: loaded 0.32s 0.32s steps(1, end) both;
}

@keyframes loaded {
  0% {
    scale: 1.1;
  }

  64% {
    scale: 1.04;
  }

  75% {
    opacity: 0.8;
    scale: 1.02;
  }

  100% {
    opacity: 0;
    z-index: 1;
  }
}
<html>

  <body>
    <div class="cursor-trail">
      <div class="cursor__label"><span class="cursor__label-text"></span></div>
    </div>

    <h2>1. Keen Slider Version</h2>

    <!-- Keen Slider -->
    <div class="gallery">
      <div id="gallery-slider" class="keen-slider">
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 1" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 1</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 2" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 2</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 3" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 3</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 4" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 4</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 5" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 5</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 6" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 6</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 7" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 7</figcaption>
          </figure>
        </div>
        <div class="keen-slider__slide">
          <figure data-label="Hover Label 8" class="has-label">
            <div class="pixel-load">
              <div class="pixel-load__preload">
                <img src="https://placebeard.it/18/24" width="18" height="24">
              </div>
              <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
            </div>
            <figcaption>Slide 8</figcaption>
          </figure>
        </div>
      </div>
    </div>
    <!-- End Keen Slider -->

    <h2>2. CSS Marquee Version</h2>

    <!-- Gallery -->
    <div class="marquee-wrap">

      <div class="marquee">
        <figure data-label="Hover Label 1" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 1</figcaption>
        </figure>
        <figure data-label="Hover Label 2" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 2</figcaption>
        </figure>
        <figure data-label="Hover Label 3" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 3</figcaption>
        </figure>
        <figure data-label="Hover Label 4" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 4</figcaption>
        </figure>
        <figure data-label="Hover Label 5" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 5</figcaption>
        </figure>
        <figure data-label="Hover Label 6" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 6</figcaption>
        </figure>
        <figure data-label="Hover Label 1" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 7</figcaption>
        </figure>
        <figure data-label="Hover Label 8" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Slide 8</figcaption>
        </figure>
      </div>

      <div class="marquee" aria-hidden="true">
        <figure data-label="Hover Label 1" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 1</figcaption>
        </figure>
        <figure data-label="Hover Label 2" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 2</figcaption>
        </figure>
        <figure data-label="Hover Label 3" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 3</figcaption>
        </figure>
        <figure data-label="Hover Label 4" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 4</figcaption>
        </figure>
        <figure data-label="Hover Label 5" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 5</figcaption>
        </figure>
        <figure data-label="Hover Label 6" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 6</figcaption>
        </figure>
        <figure data-label="Hover Label 1" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 7</figcaption>
        </figure>
        <figure data-label="Hover Label 8" class="marquee__item has-label">
          <div class="pixel-load">
            <div class="pixel-load__preload">
              <img src="https://placebeard.it/18/24" width="18" height="24">
            </div>
            <img src="https://placebeard.it/768/1024" width="768" height="1024" loading="lazy" />
          </div>
          <figcaption>Clone Slide 8</figcaption>
        </figure>
      </div>

    </div>
    <!-- End Gallery -->

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/keen-slider.min.js"></script>
  </body>

</html>