# How to Animate a “Twisting Text Effect” With CSS and JavaScript

In this tutorial, we’ll learn how to split an element’s text into separate characters, which we’ll then animate to give us a twisting effect.

## What We’ll be Building

Without further intro, let’s check out what we’ll be building:

In our case, there will be two types of animations:

• The first animation will happen each time a heading comes into view.
• The second animation will happen each time the user hovers over a link.

## 1. Begin With the HTML Markup

We’ll define three sections. Each section will have a heading, a paragraph, and a link.

We’ll add the data-split attribute to the elements that will be animated. An additional data-split-type attribute will determine the animation type. Possible values are hover and scroll.

Here’s the required markup:

 1 
 2 
 3 

...

 4 

...

 5  ...  6 
 7 
 8 9 
 10 
 11 

...

 12 

...

 13  ...  14 
 15 
 16 17 
 18 
 19 

...

 20 

...

 21  ...  22 
 23 


## 2. Split Text

The splitCharacters() function will be responsible for splitting the text of all elements with the data-split attribute and wrapping each of their characters into a span element.

So, assuming that initially, we have this element:

 1 Hover me → 

After running the function, the aforementioned element’s markup will turn into this:

The end result will appear the same as before running the function.

Here are a few things to note:

• Inside the target element we’ll define the .inner element which will contain the .front and .back elements. We add this wrapper span to isolate the styles of the target element (e.g. link) and avoid any inconsistencies (e.g. if we add paddings to the link).
• Both the .front and .back elements will contain the element’s initial text wrapped into span elements.
• By default, the contents of the .front element will be visible. Depending on the animation type, the contents of the .back one will appear when either we hover over the target element or scroll till it comes into view.
• As we’ll see in a bit, we’ll use the transition-delay property to sequentially animate the characters. To create different delays between them, we’ll use the index CSS variable. Each character, apart from the empty one (whitespace), will receive as a value of this variable an incremented number which will denote the character’s position/index inside their parent.

With all these in mind, here’s the complete declaration of the splitCharacters() function:

 1 function splitCharacters() {  2  const targets = document.querySelectorAll("[data-split]");  3 4  for (const target of targets) {  5  let string = '';  6  let counter = 0;  7 8  const targetContent = target.textContent;  9  const words = targetContent.trim().split(" ");  10 11  words.forEach(function (word, wordIndex, wordArray) {  12  const chars = word.split("");  13  chars.forEach(function (char, charIndex, charArray) {  14  string += ${char};  15 16  if (  17  wordIndex === wordArray.length - 1 &&  18  charIndex === charArray.length - 1  19  ) {  20  counter = 0;  21  }  22  });  23  if (wordIndex !== wordArray.length - 1) {  24  string += " ";  25  }  26  });  27  string += "";  28 29  string += '';  30  words.forEach(function (word, wordIndex, wordArray) {  31  const chars = word.split("");  32  chars.forEach(function (char) {  33  string += ${char};  34  });  35  if (wordIndex !== wordArray.length - 1) {  36  string += " ";  37  }  38  });  39 40  string += "";  41  string += "";  42  target.innerHTML = string;  43  }  44 } 

For simplicity, we’ll only focus our attention on the main styles. Besides, you can check them all by clicking on the CSS tab of the demo.

Here are the noteworthy things:

• The .back element will be an absolute element.
• By default, there will be a 0.015s difference between the animation of each character. That said, the first character of the .front and .back elements will have a transition delay of 0.015s, the second one 0.03s, the third one 0.045s, and so on. The whitespace won’t have any delay.
• The characters inside the .back element will be hidden by default and sit underneath the text like this:

The associated styles:

 1 [data-split],  2 [data-split] span {  3  display: inline-block;  4 }  5 6 [data-split] .inner {  7  display: block;  8  position: relative;  9  overflow: hidden;  10 }  11 12 [data-split] .back {  13  position: absolute;  14  top: 0;  15  right: 0;  16  bottom: 0;  17  left: 0;  18 }  19 20 [data-split] .char {  21  transition: all 0.4s cubic-bezier(0.2, 0.63, 0.4, 1.02);  22  transition-delay: calc(0.015s * var(--index));  23 }  24 25 [data-split] .back .char {  26  opacity: 0;  27  transform: translateY(101%) skewX(55deg);  28 }  29 30 [data-split-type="hover"] .char {  31  transition-duration: 0.25s;  32 } 

### Hover Animation

As we hover over an element with the [data-split-type="hover"] attribute, the characters of the .back element will appear while the ones of the .front element will become hidden by moving upwards like this:

Here are the corresponding styles:

 1 [data-split-type="hover"]:hover .back .char {  2  opacity: 1;  3  transform: none;  4 }  5 6 [data-split-type="hover"]:hover .front .char {  7  opacity: 0;  8  transform: translateY(-101%) skewX(-55deg);  9 } 

As we scroll the page, all elements with the [data-split-type="scroll"] attribute will be animated as soon as they become visible in the viewport. In our example, only the headings will have this behavior.

To accomplish this task, we’ll borrow some code from this tutorial that uses the Intersection Observer API.

So, when at least 50% of each heading comes into view, it’ll receive the is-animated class. Otherwise, it’ll lose this class, and the animation will return to its initial state.

Below we declare the function responsible for this stuff:

 1 animateOnScroll();  2 3 function animateOnScroll() {  4  const targets = document.querySelectorAll('[data-split-type="scroll"]');  5  const isAnimatedClass = "is-animated";  6  const threshold = 0.5;  7 8  function callback(entries, observer) {  9  entries.forEach((entry) => {  10  const elem = entry.target;  11  if (entry.intersectionRatio >= threshold) {  12  elem.classList.add(isAnimatedClass);  13  } else {  14  elem.classList.remove(isAnimatedClass);  15  }  16  });  17  }  18 19  const observer = new IntersectionObserver(callback, { threshold });  20  for (const target of targets) {  21  observer.observe(target);  22  }  23 } 

And here are the styles that kick in during this condition:

 1 [data-split-type="scroll"].is-animated .back .char {  2  opacity: 1;  3  transform: none;  4 }  5 6 [data-split-type="scroll"].is-animated .front .char {  7  opacity: 0;  8  transform: translateY(-101%) skewX(-55deg);  9 } 

## Conclusion

That’s all for today, folks! During this exercise, we covered a way to split an element’s text into individual characters and animate them on scroll and hover. I hope you gained some new knowledge that you’ll use to enhance this demo or create similar text effects in your projects. If so, don’t forget to give our demo some love :)

Let’s look at our creation once again:

If you need a more complete and robust solution to animate words, characters, lines, etc. you can try a JavaScript library like Splitting.js or GSAP's SplitText.js (although it isn’t free).

As always, thanks a lot for reading!