Create text change hover effect

I stumbled upon one website that had this hover effect where the original text would change to some random characters and start revealing itself over time.

It looked like a fun exercise, so I decided to recreate it. Below you can see how it looks and check out the source code as well. This is a beginner-friendly article and includes a step-by-step guide. If you completely understand the source code, feel free to skip reading so you do not waste your time.

In case you decided to continue reading, you might be interested in what we are going to cover here. We will start with a basic HTML structure and mention some CSS styles. However, the main focus will be on JavaScript. You can learn how to listen for and handle events, a little bit of string manipulation, and how to leverage the power of closure. If you are ready, let's get started.

HTML structure

As for any hover effect, you need to have something that you want to hover over. In this case, let's create a couple of links using <a> tag and put them into a body tag, just for demonstration purposes.

<body>
<a class="text-hover-effect" href="#">pricing</a>
<a class="text-hover-effect" href="#">projects</a>
<a class="text-hover-effect" href="#">about us</a>
<a class="text-hover-effect" href="#">subscribe!</a>
</body>

Notice that every <a> tag has a class attribute with a value of text-hover-effect. We will need it later so we can target those HTML elements with JavaScript.

CSS styles

Since the HTML structure is ready, let's add some styles. In this case, I only want to do some simple things, like changing the background color and making sure that links are centered on the page. We can also add styles to links so they look a bit nicer.

* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: #18122b;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
flex: 1;
height: 100vh;
}
a {
font-family: sans-serif;
color: #635985;
text-transform: uppercase;
letter-spacing: 0.1rem;
font-size: 2.6rem;
font-weight: bold;
box-shadow: 0 0 2rem 1rem rgba(0, 0, 0, 0.2);
background: transparent;
padding: 0.6rem;
border-radius: 0.4rem;
min-width: 370px;
border: 1px solid #635985;
text-decoration: none;
display: block;
margin-bottom: 2rem;
}

I will not go into details about what every CSS declaration does, you can always play around with them in the sample project. However, I want to bring your attention to one thing.

Even though we added some CSS, it is actually not required. Notice that there is not even a :hover pseudo-class. This is because the hover effect will be handled entirely by JavaScript.

Understanding the effect

To implement the desired effect and behavior, we first need to understand it. Take a look at the following image.

Illustration of required actions to reproduce the hover effect
Illustration of required actions to reproduce the hover effect

In the default state, the original text is visible. When the cursor hovers over the text, it will immediately be populated with random characters. Only the first character from the original text should remain the same.

After some artificial delay, the character in the second position from the original text will replace the randomly generated character in the same position. This delay and replace process will continue until the last character is reached and the original text is completely revealed.

I hope this was clear enough for you to understand what we are trying to create here. If not, do not worry; we will split it into manageable chunks and cover every chunk separately.

JavaScript code

This is the part where we will spend the most time. First of all, we need to somehow generate random characters for our effect. We can do that by creating a simple array of characters.

// ๐Ÿ”ก Characters to cycle trough
let allowedCharacters = ['X', '$', 'Y', '#', '?', '*', '0', '1', '+'];

The variable allowedCharacters holds a list of the allowed characters. Feel free to add or remove them from your version.

We also need a function that can randomly select one of the characters from the previously created list.

// ๐ŸŽ Function to return random character
function getRandomCharacter() {
const randomIndex = Math.floor(Math.random() * allowedCharacters.length);
return allowedCharacters[randomIndex];
}

Function getRandomCharacter uses Math object to randomly generate a number, index, between 0 and the length of the allowedCharacters array. The generated index is then used to get the character from the allowedCharacters.

To handle hover events, we need to get all <a> tags and attach event listeners to all of them.

// โš™๏ธ Event handler
function handleHoverEvent(e) {
// TODO: Add implementation
}
// Attach an event listener to elements
document.querySelectorAll('.text-hover-effect').forEach((element) => {
element.addEventListener('mouseover', handleHoverEvent);
});

Here, document.querySelectorAll is used to get all elements that have a value of text-hover-effect in the class attribute. Since we only added this class to <a> tags, it will return the iterable collection of Anchor, <a>, elements.

Using addEventListener, we can add mouseover event listener to every element and pass the function handleHoverEvent to handle the events. Now, whenever a user hovers over any of <a> tags, handleHoverEvent function will be executed.

So far, so good. The groundwork is complete; it's time to work on the hover effect implementation now. As we already said, we will split the task into multiple chunks. For the beginning, let's focus on the following part:

First step of the hover effect
First step of the hover effect

On hover, we have to replace the original text, except for the first character, with a randomly generated one.

When an event is triggered, the handleHoverEvent function gets called and an Event-based object will be passed as an argument to the function.

function handleHoverEvent(e) {
const text = e.target.innerHTML;
const randomizedText = text.split('').map(getRandomCharacter).join('');
e.target.innerHTML = `${text.substring(0,1)}${randomizedText.substring(1)}`;
}

The function will receive the e, the event object, when the event is triggered. First, we have to save the current text value from the <a> tag that was hovered. To do so, we can use e.target.innerHTML and save the value into the text variable.

Download more icon variants from https://tabler-icons.io/i/info-circle

Keep in mind that innerHTML gets or sets the HTML markup contained within the element. If your hovered element has other HTML tags as children, they will be returned as well.

Next, we need to generate randomized text with the same length as the original. This is where we can use the getRandomCharacter function that was created before.

The value saved in the text variable is a string that includes multiple characters. But getRandomCharacter generates only one character per call. To solve this problem, we can convert the original text into an array of characters using the split('') method.

Now that we have an array, we can chain the map method and pass the getRandomCharacter function to it. Now, for every original character, a random one will be generated. Finally, to convert the resulting array back to a string, the join('') method is used.

Generate random characters from the original text
Generate random characters from the original text

The "subscribe" string is used as an example to illustrate how the text is being processed. You can see that at the end, we have randomly generated text that is the same length as the original one.

Good, we now have two strings, one original and one randomly generated. The last step is to combine them and update the e.target.innerHTML property. To get the first character from the original text, we can use text.substring(0,1) and to get the rest of the characters from the randomized text, we can use randomizedText.substring(1).

Combine substrings into the final string
Combine substrings into the final string

We could have done this in a different way as well, but I think using the substring method is totally fine, and, if interested, you can find more details here.

After some delay, the next character from the original text must be shown instead of the random one, and it needs to be repeated until the last character is revealed. We can achieve this using for loop and the setTimeout web API method.

function handleHoverEvent(e) {
const text = e.target.innerHTML;
const randomizedText = text.split('').map(getRandomCharacter).join('');
for (let i = 0; i < text.length; i++) {
setTimeout(() => {
const nextIndex = i + 1;
e.target.innerHTML = `${text.substring(0, nextIndex)}${randomizedText.substring(nextIndex)}`;
}, i * 70);
}
}

String values are iterable, so we can use the for loop on them as well. We will loop through the original text saved in the text variable. In every iteration, we invoke the setTimeout function. Notice that the delay for setTimeout is multiplied by the i value. This allows us to properly schedule updates, one after another, using the same wait time of 70 milliseconds between each update.

Lastly, we just needed to modify the assignment part of e.target.innerHTML to include nextIndex value instead of hardcoded ones. We need the nextIndex to ensure that substring method works properly and produces the expected result.

Illustration of scheduled tasks
Illustration of scheduled tasks

We used the for loop and setTimeout to create scheduled tasks. When the delay time expires, setTimeout will invoke the task function, which will update the innerHTML value of the hovered element. This will be repeated for every scheduled task, generated by the for loop.

Our effect should work fine now, but there is one more problem. If you hover over the element really fast, the handleHoverEvent function could be triggered multiple times. This is obviously not what we want. We need to make sure that handleHoverEvent cannot be triggered unless the currently running effect is over.

To make this happen, we can rely on the power of closure. Consider the following code:

// ๐Ÿญ Creates new event handler with a private variable
function createEventHandler() {
// ๐Ÿƒโ€โ™‚๏ธ Private variable: Keep track of the event in progress
let isInProgress = false;
// ๐Ÿ‘‡ Event handler implementation
return function handleHoverEvent(e) {
if (isInProgress) {
return;
}
const text = e.target.innerHTML;
const randomizedText = text.split('').map(getRandomCharacter).join('');
for (let i = 0; i < text.length; i++) {
isInProgress = true;
setTimeout(() => {
const nextIndex = i + 1;
e.target.innerHTML = `${text.substring(0, nextIndex)}${randomizedText.substring(nextIndex)}`;
if (nextIndex === text.length) {
isInProgress = false;
}
}, i * 70);
}
};
}

Most of the code is the same, but there are a couple of important differences. First of all, we have put our handleHoverEvent function inside another function called createEventHandler. Also, we make sure that createEventHandler returns handleHoverEvent. In doing so, we have created the conditions for closure to occur.

Why do we even need closure in the first place? In this particular case, we need to create a variable where we can store information if the effect is in progress. This variable must be private to the handleHoverEvent function since only this function should be able to update it. When it comes to creating private variables, closure is the way to go.

In createEventHandler, we have defined the variable isInProgress and initialized it with false. This variable is used to keep track of whether the current effect is still in progress.

In the function handleHoverEvent, as soon as the loop starts, we will set isInProgress to true, indicating that the effect has started. When the last task is executed, isInProgress is set to false, indicating that the effect has finished.

In the same function, at the beginning, we will check if isInProgress is set to true. In case it's set, we can assume that the effect is still running and return immediately, effectively skipping the handling of the event.

Now we need to modify the code responsible for attaching an event handler to the <a> tag, Anchor elements.

document.querySelectorAll('.text-hover-effect').forEach((element) => {
const eventHandler = createEventHandler();
element.addEventListener('mouseover', eventHandler);
});

For every Anchor, <a>, element new handleHoverEvent function will be created using the createEventHandler function. The resulting function is then saved into the eventHandler and passed to the addEventListener. Since we have leveraged the power of closure to keep track of the event in progress, new events will not be handled until the currently running one is finished.

Conclusion

Initially, I did not plan to make this tutorial too big, but it turned out to be quite lengthy.

I wanted to give a detailed explanation of how this hover effect could be achieved and to show that it is not difficult at all if you split it into smaller pieces. You should be able to do this for every taskโ€”split and conquer.

This was also a nice opportunity to show just how useful the concept of closure is in JavaScript and how it can be leveraged in a practical example.

ยฉ 2023 Ramo Mujagic. Thanks for visiting.