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.
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 troughlet 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 characterfunction 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 handlerfunction handleHoverEvent(e) {// TODO: Add implementation}// Attach an event listener to elementsdocument.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:
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.
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.
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)
.
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.
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 variablefunction createEventHandler() {// ๐โโ๏ธ Private variable: Keep track of the event in progresslet isInProgress = false;// ๐ Event handler implementationreturn 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.