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:
<section> <div> <h2 data-split data-split-type="scroll">...</h2> <p>...</p> <a href="" data-split data-split-type="hover">...</a> </div> </section> <section> <div> <h2 data-split data-split-type="scroll">...</h2> <p>...</p> <a href="" data-split data-split-type="hover">...</a> </div> </section> <section> <div> <h2 data-split data-split-type="scroll">...</h2> <p>...</p> <a href="" data-split data-split-type="hover">...</a> </div> </section>
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:
<a href="" data-split data-split-type="hover">Hover me →</a>
After running the function, the aforementioned element’s markup will turn into this:
<a href="" data-split data-split-type="hover"> <span class="inner"> <span class="front"> <span class="char" style="--index: 1;">H</span> <span class="char" style="--index: 2;">o</span> <span class="char" style="--index: 3;">v</span> <span class="char" style="--index: 4;">e</span> <span class="char" style="--index: 5;">r</span> <span> </span> <span class="char" style="--index: 6;">m</span> <span class="char" style="--index: 7;">e</span> <span> </span> <span class="char" style="--index: 8;">→</span> </span> <span class="back"> <span class="char" style="--index: 1;">H</span> <span class="char" style="--index: 2;">o</span> <span class="char" style="--index: 3;">v</span> <span class="char" style="--index: 4;">e</span> <span class="char" style="--index: 5;">r</span> <span> </span> <span class="char" style="--index: 6;">m</span> <span class="char" style="--index: 7;">e</span> <span> </span> <span class="char" style="--index: 8;">→</span> </span> </span> </a>
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 wrapperspan
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 intospan
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 theindex
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:
function splitCharacters() { const targets = document.querySelectorAll("[data-split]"); for (const target of targets) { let string = '<span class="inner"><span class="front">'; let counter = 0; const targetContent = target.textContent; const words = targetContent.trim().split(" "); words.forEach(function (word, wordIndex, wordArray) { const chars = word.split(""); chars.forEach(function (char, charIndex, charArray) { string += `<span class="char" style="--index: ${++counter};">${char}</span>`; if ( wordIndex === wordArray.length - 1 && charIndex === charArray.length - 1 ) { counter = 0; } }); if (wordIndex !== wordArray.length - 1) { string += "<span> </span>"; } }); string += "</span>"; string += '<span class="back">'; words.forEach(function (word, wordIndex, wordArray) { const chars = word.split(""); chars.forEach(function (char) { string += `<span class="char" style="--index: ${++counter};">${char}</span>`; }); if (wordIndex !== wordArray.length - 1) { string += "<span> </span>"; } }); string += "</span>"; string += "</span>"; target.innerHTML = string; } }
3. Add Styles
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:
[data-split], [data-split] span { display: inline-block; } [data-split] .inner { display: block; position: relative; overflow: hidden; } [data-split] .back { position: absolute; top: 0; right: 0; bottom: 0; left: 0; } [data-split] .char { transition: all 0.4s cubic-bezier(0.2, 0.63, 0.4, 1.02); transition-delay: calc(0.015s * var(--index)); } [data-split] .back .char { opacity: 0; transform: translateY(101%) skewX(55deg); } [data-split-type="hover"] .char { transition-duration: 0.25s; }
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:
[data-split-type="hover"]:hover .back .char { opacity: 1; transform: none; } [data-split-type="hover"]:hover .front .char { opacity: 0; transform: translateY(-101%) skewX(-55deg); }
4. Animate on Scroll
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:
animateOnScroll(); function animateOnScroll() { const targets = document.querySelectorAll('[data-split-type="scroll"]'); const isAnimatedClass = "is-animated"; const threshold = 0.5; function callback(entries, observer) { entries.forEach((entry) => { const elem = entry.target; if (entry.intersectionRatio >= threshold) { elem.classList.add(isAnimatedClass); } else { elem.classList.remove(isAnimatedClass); } }); } const observer = new IntersectionObserver(callback, { threshold }); for (const target of targets) { observer.observe(target); } }
And here are the styles that kick in during this condition:
[data-split-type="scroll"].is-animated .back .char { opacity: 1; transform: none; } [data-split-type="scroll"].is-animated .front .char { opacity: 0; transform: translateY(-101%) skewX(-55deg); }
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!