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:
- It splits the animation effect in two
Animation
s 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. - 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 becausegetComputedStyle
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>
… more text