Making a Text Scramble Animation with JavaScript
Tags: webdev, tailwindcss, javascript, tutorial
Published: 2023-11-28 09:51:28

####**[Live Demo](https://cruip-tutorials.vercel.app/text-scramble/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/text-scramble/text-scramble.js)**

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](https://evervault.com/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](https://cruip.com).

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:

```html
<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](https://cruip.com/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:

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

```js
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:

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

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

```js
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 the `links` array.
*   Then we created the `handleActiveLink` method, whose task is updating the active link whenever a menu link is clicked. It removes the `aria-current` attribute from the active link, sets the new active link, and assigns it the `aria-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, the `init` method's execution is halted with a `return`. Otherwise, we call the `handleActiveLink` 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.

```js
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 to `false` and changes to `true` 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 using `clearInterval(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 the `isScrambling` property to `true` 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 the `isScrambling` property to `false` and stops the time interval by using `clearInterval(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:

1.  ""(`this.prefix`) + "**spfjert**" (`this.suffix`)
2.  "**G**" (`this.prefix`) + "**pfjfqr**" (`this.suffix`)
3.  "**Ge**" (`this.prefix`) + "**jdqll**" (`this.suffix`)
4.  "**Gen**" (`this.prefix`) + "**rmnb**" (`this.suffix`)
5.  "**Gene**" (`this.prefix`) + "**swt**" (`this.suffix`)
6.  "**Gener**" (`this.prefix`) + "**oe**" (`this.suffix`)
7.  "**Genera**" (`this.prefix`) + "**z**" (`this.suffix`)
8.  "**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.

```js
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:

```js
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:

```js
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!