Live Demo / Download
The text scramble effect is a cool animation that rapidly unveils text by randomly changing characters - just like those scenes in movies where hackers decode strings of text! Inspired by Evervault's blog, we'll make a navigation menu with that kind of effect when you click on links. Plus, we'll give you both light and dark versions of the menu, so you can integrate this example into any of our Tailwind templates.
Getting started with the HTML
To get started, we can define the structure of our navigation menu in HTML. We'll create a <nav>
element that contains a list of links. And for styling, we'll make use of Tailwind CSS:
<nav class="text-sm flex flex-col space-y-2" role="navigation">
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">General</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Work Preferences</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Integrations</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Billing</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Subscription</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Security</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Data Sources</a>
</nav>
Creating a JS class for the animation
We'll create a JavaScript class, just as we use to do in our tutorials. This way, we can easily apply the animation to various elements within the page. So, create a file called text-scramble.js
and include it in the HTML document, just before the closing </body>
tag:
<script src="./text-scramble.js"></script>
Now, in the JS file, we're going to create a class called TextScramble
. Inside this class there will be an init
method that initializes the animation:
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.init();
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
// Run the scramble animation
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
To use the TextScramble
class, simply add the data-text-scramble
attribute to the navigation menu:
<nav class="text-sm flex flex-col space-y-2" role="navigation" data-text-scramble>
Next, the class selects all the links within <nav>
and triggers the animation when each link is clicked.
Managing the active link
Now, in our HTML, mark the first link with the attribute aria-current="page"
. This attribute informs the browser which link is currently "active" and allows you to apply specific styles tto differentiate it from the other links.
<nav class="text-sm flex flex-col space-y-2" role="navigation">
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400" aria-current="page">General</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Work Preferences</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Integrations</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Billing</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Subscription</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Security</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Data Sources</a>
</nav>
To make the active link more prominent, we used the aria-[current]:
prefix to give it a different color (aria-[current]:text-indigo-500
). We also added a vertical line to the left of each link using the ::before
pseudo-element. By default, this line is transparent, but it turns indigo when the link is active (aria-[current]:before:bg-indigo-400
). Great! Now let's take care of the active link while navigating.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
The key steps we took are as follows:
- We added an
activeLink
property to the class, which finds the active link within thelinks
array. - Then we created the
handleActiveLink
method, whose task is updating the active link whenever a menu link is clicked. It removes thearia-current
attribute from the active link, sets the new active link, and assigns it thearia-current="page"
attribute. - Lastly, we added a condition inside the
init
method to check if the clicked link is already active -if (link === this.activeLink)
. If it is, theinit
method's execution is halted with areturn
. Otherwise, we call thehandleActiveLink
method to update the active link.
Handling the scramble effect
Now comes the more intricate part. We'll introduce a couple of new properties and methods to handle the shuffle effect.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.isScrambling = false;
this.intervalId = null;
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.intervalId = setInterval(() => {
// Do stuff...
console.log('Scrambling...');
}, 50);
}
stopScramble = (link) => {
this.isScrambling = false;
clearInterval(this.intervalId);
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Let's begin with the newly added properties:
- The boolean property
isScrambling
indicates whether the animation is currently in progress. By default, it is set tofalse
and changes totrue
when the animation starts. This property is useful for stopping the ongoing animation when a different link is clicked. - The variable
intervalId
is used to store the ID of the interval used for the animation. By saving the interval ID, we can later clear the interval usingclearInterval(this.intervalId)
when the animation is completed or aborted.
Now, the new methods:
- The method
startScramble
is responsible for initiating the animation. It is called when a menu link is clicked. This method first checks if the animation is already in progress and stops it if necessary. Then, it sets theisScrambling
property totrue
and starts a time interval that executes a function every 50 milliseconds - currently, this function is empty, but we will fill it in shortly. - The method
stopScramble
is used to stop the animation. It is called when the animation is completed or aborted. This method sets theisScrambling
property tofalse
and stops the time interval by usingclearInterval(this.intervalId)
.
Handling prefix and suffix
First, we'll create a property called this.input
; it will store the text of the link that is going to be animated. Next, we'll add two more properties: this.prefix
and this.suffix
. These properties will be used to replace the link text during the animation. Let's take a practical example to understand how it works. Suppose you click on a link with the text "General" (i.e. this.input
). The animation will go through the following iterations:
- ""(
this.prefix
) + "spfjert" (this.suffix
) - "G" (
this.prefix
) + "pfjfqr" (this.suffix
) - "Ge" (
this.prefix
) + "jdqll" (this.suffix
) - "Gen" (
this.prefix
) + "rmnb" (this.suffix
) - "Gene" (
this.prefix
) + "swt" (this.suffix
) - "Gener" (
this.prefix
) + "oe" (this.suffix
) - "Genera" (
this.prefix
) + "z" (this.suffix
) - "General" (
this.prefix
) + "" (this.suffix
)
At the beginning, the prefix is an empty string, while the suffix consists of random characters. In each iteration, the prefix will be the same as the first letter of this.input
, and the suffix will contain all the subsequent characters. This pattern continues until the prefix matches this.input
, and the suffix becomes an empty string. Now, let's see how we can integrate this logic into our class.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.input = null;
this.prefix = '';
this.suffix = '';
this.isScrambling = false;
this.intervalId = null;
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.input = link.textContent;
this.prefix = '';
this.suffix = 'xxx';
this.intervalId = setInterval(() => this.scrambleIteration(link), 50);
}
scrambleIteration = (link) => {
let nextChar = this.input.charAt(this.prefix.length);
if (nextChar === '') {
this.stopScramble(link);
} else {
this.prefix += nextChar;
this.suffix = 'xxx';
link.textContent = this.prefix;
link.setAttribute('data-scramble-suffix', this.suffix);
}
}
stopScramble = (link) => {
link.textContent = this.input;
link.setAttribute('data-scramble-suffix', '');
this.isScrambling = false;
clearInterval(this.intervalId);
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
In addition to adding this.input
, this.prefix
, and this.suffix
, we also integrated the startScramble
and stopScramble
methods to update the values of this.prefix
and this.suffix
with each iteration. Moreover, while scrambling, the scrambleIteration
method appends a data-scramble-suffix
attribute to the link, which contains the value of the this.suffix
property. This attribute will be used to display the suffix using the link's ::after
pseudo-element - with Tailwind, all you have to do is add the class after:content-[attr(data-scramble-suffix)]
to all menu links.
Generating random characters
The last step is to generate the appropriate length of random characters for the suffix. To do this, we'll create a method called randomChars
returning a string of random letters:
randomChars = (length) => {
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * this.charsetLength);
result += `${this.charset[randomIndex]}`;
}
return result;
}
Now, we just need to use this method in the startScramble
and scrambleIteration
methods. So, the final code for the class will look like this:
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.input = null;
this.prefix = '';
this.suffix = '';
this.isScrambling = false;
this.intervalId = null;
this.charset = 'abcdefghijklmnopqrstuvwxyz';
this.charsetLength = this.charset.length;
this.scrambleIteration = this.scrambleIteration.bind(this);
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.input = link.textContent;
this.prefix = '';
this.suffix = this.randomChars(this.input.length);
this.intervalId = setInterval(() => this.scrambleIteration(link), 50);
}
scrambleIteration = (link) => {
let nextChar = this.input.charAt(this.prefix.length);
if (nextChar === '') {
this.stopScramble(link);
} else {
this.prefix += nextChar;
this.suffix = this.randomChars(this.input.length - this.prefix.length);
link.textContent = this.prefix;
link.setAttribute('data-scramble-suffix', this.suffix);
}
}
stopScramble = (link) => {
link.textContent = this.input;
link.setAttribute('data-scramble-suffix', '');
this.isScrambling = false;
clearInterval(this.intervalId);
}
randomChars = (length) => {
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * this.charsetLength);
result += `${this.charset[randomIndex]}`;
}
return result;
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Conclusions
Creating appealing effects and animations that don't compromise a website's usability can be challenging. In this tutorial, we tried to add a bit of fun to the menu interaction without hurting the overall experience. Did we achieve our goal? Reach out and let us know what you think!
Top comments (0)