Some easy to use Javascript function to randomly animate individual characters from any given HTML container element.
Background
For my about page I've tried to keep the page really clean but I've struggled to make it interesting. After some research on other peoples websites it became clear that text animations is the way to go in 2024.
When I created the slide-up animation the page immediately became alive. But it was still hard coded for specific elements within the text which is still boring to look at.
So thats when I looked in the javascript/html methods to iterate within the container en pick a letter at random to animate.
Tutorial
Let's start by initializing a new Next.js application.
You can do that by inserting the following npx command in your terminal by choice.
After initialization we can delete everything in the page.tsx
, globals.css
, page.module.css
to start with a nice blank application.
- npx create-next-app@latest
- What is your project named? demo
- Would you like to use TypeScript? Yes
- Would you like to use ESLint? Yes
- Would you like to use Tailwind CSS? No
- Would you like to use `src/` directory? No
- Would you like to use App Router? (recommended) Yes
- Would you like to customize the default import alias (@/*)?) No
The basics
To make it easy to show we want a full-screen page with 100vh
and some text centered in the middle of the screen, note that I've added some formatting elements to make sure that our function works with nested elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function Home() {
return (
<main className="main">
<div className="center">
<p>
Sed augue ipsum, <strong>egestas</strong> nec, vestibulum et,
malesuada adipiscing, dui. Mauris turpis nunc, blandit et, volutpat
molestie, <u>porta ut</u>, ligula. Suspendisse feugiat. Maecenas
egestas arcu quis ligula <em>mattis</em> placerat. Donec elit libero,
sodales nec, volutpat a, suscipit non, turpis.
</p>
</div>
</main>
);
}
Component
Let's start by wrapping our p
tag into a new component called LetterJuggler. This component will contain all the business logic.
To select a random letter from an element tree is quite difficult but happily for us the browser provides us with a useful utility method .innerText
which parses the contents of that element and returns the readable text as a string.
From this string we want to select a random letter and store its position and make sure that the selected index is not a blank space otherwise the animation will show nothing and that defeats the purpose of this function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import LetterJuggler from "@/components/LetterJuggler";
export default function Home() {
return (
<main className="main">
<div className="center">
<LetterJuggler>
Sed augue ipsum, <strong>egestas</strong> nec, vestibulum et,
malesuada adipiscing, dui. Mauris turpis nunc, blandit et, volutpat
molestie, <u>porta ut</u>, ligula. Suspendisse feugiat. Maecenas
egestas arcu quis ligula <em>mattis</em> placerat. Donec elit libero,
sodales nec, volutpat a, suscipit non, turpis.
</LetterJuggler>
</div>
</main>
);
}
Wrapping
To apply a animation to our selected letter we want to wrap it with a span
so that we can apply our own animations through CSS classes. Unfortunately the container contains multiple elements so we can't simply find and replace the text.
Therefore we need to loop through the child nodes and count every character until we find the matching index.
To keep our HTML valid we need to wrap the beginning middle and end with a span
. So if we want to wrap the letter"a" from this string "this is a test" we need to get:
<span>this is </span><span>a</span><span>test</span>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
body {
margin: 0;
}
main {
display: flex;
height: 100vh;
}
.center {
max-width: 700px;
margin: auto;
}
p {
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
}
.animate-bottom-to-top {
color: red;
}
Animation
To make our component juggle we need to wrap our useEffect
with a interval which activates every second.
For the bottom-to-top animation we can use the clip-path property to draw a rectangle over the target element. And translate the offset with CSS keyframes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
body {
margin: 0;
}
main {
display: flex;
height: 100vh;
}
.center {
max-width: 700px;
margin: auto;
}
p {
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
}
.animate-bottom-to-top {
animation: bottom-to-top 500ms linear both;
}
@keyframes bottom-to-top {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0),
}
}