0

So I have a span element whose textContent changes when I hover over it. I want the old text to fade out, then fade in the new text. Also, since the new text is longer than the old one, the width of the span needs to increase smoothly to the new value.

The solution below basically does what I want, but I am not satisfied with it for two reasons:

  1. It splits the animation effect in two Animations that are played one after another. This is not elegant and it causes a slight glitch in some browsers (Chrome and Edge, but not Firefox) at the point where the first animation finishes.
  2. Determining the old and new width "manually", by simply changing the text and using getComputedStyle, also seems quite ugly. Also I'm not sure it will always work because getComputedStyle might not return the width in pixels.

Concerning (1), there is a different solution where I only have one Animation and the text gets changed in the middle using setTimeout. This doesn't have the glitch, but it is likewise not elegant.

Question: What is the best way to achieve the effect described above?

Below is my current solution:

const initialText = "Hover here";
const finalText = "Text changed on hover";
const dur = 1500; // in milliseconds

const element = document.getElementById("my-element");

element.textContent = finalText;
const finalWidth = parseFloat(window.getComputedStyle(element).width);

element.textContent = initialText;
const initialWidth = parseFloat(window.getComputedStyle(element).width);

const intermediateWidth = ( initialWidth + finalWidth ) / 2;

const keyframes_0 = new KeyframeEffect(
  element,
  [{ opacity: 1, width: initialWidth + "px" }, { opacity: 0, width: intermediateWidth + "px" }],
  { duration: dur / 2 }
);

const keyframes_1 = new KeyframeEffect(
  element,
  [{ opacity: 0, width: intermediateWidth + "px" }, { opacity: 1, width: finalWidth + "px" }],
  { duration: dur / 2 }
);

const animation_0 = new Animation( keyframes_0, document.timeline );
const animation_1 = new Animation( keyframes_1, document.timeline );

element.addEventListener('mouseenter', () => {
  animateTextChange(element, animation_1, animation_0, finalText, 1.0);
});

element.addEventListener('mouseleave', () => {
  animateTextChange(element, animation_0, animation_1, initialText, -1.0);
});

function animateTextChange(elem, anim0, anim1, text, rate) {
  if( anim0.playState === "running" ) {
    anim0.onfinish = () => { };
    anim0.playbackRate = rate;
    anim0.play();
  } else {
    anim0.onfinish = () => { };
    anim1.onfinish = () => {
      elem.textContent = text;
      anim0.playbackRate = rate;
      anim0.play();
    };
    anim1.playbackRate = rate;
    anim1.play();
  }
}
<span id="my-element" style="display:inline-block; white-space:nowrap; overflow:clip">
Hover here
</span>
&hellip; more text
pgraf
  • 133
  • 4

0 Answers0