Create text change hover effect with JavaScript
Tags: javascript, webdev, beginners, tutorial
Published: 2023-03-21 18:25:30

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.

{% stackblitz https://stackblitz.com/edit/web-platform-99k2qs?file=index.html&view=preview %}

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.

```html
<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.

```css
* {
  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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oen1q97q44svv2g8ctgv.png)

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.

```javascript
// 🔡 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.

```javascript
// 🎁 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.

```javascript
// 🛠 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u2dubtbn73o2on3ze5je.png)

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](https://developer.mozilla.org/en-US/docs/Web/API/Event)-based object will be passed as an argument to the function.

```javascript
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.

> Keep in mind that [innerHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) gets or sets the HTML markup contained within the element. If your hovered element has other HTML tags as children, they will be serialized and 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/986jkzoto8x8b8308pq3.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2eja2wmymr8nh3qipb2p.png)

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring).

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.

```javascript
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cczb9gffynithx17vjrz.png)

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:

```javascript
// 🏭 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) 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.

```javascript
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.

---

Check out other articles on [my blog](https://ramomujagic.com/blog/).