Creating a chameleon text effect

I recently created a new page on this website. From an early time in my frontend journey, I have been amazed by the websites that are featured on awwwards and I wanted to create something fancy like them.

Here's how I did it.

What we want to build

Let's take a look again at what we're trying to build here:

Hello there!

let's break down what's happening here:

  • text color transitions from one color to another
  • as soon as it reaches the "target" color, we change it again to keep it changing

Changing colors

We can animate the color changing of the text by using transition:

CSS
1transition-property: color;
2transition-duration: 5s;
3transition-timing-function: linear;
4
5/* or in one line */
6transition: color 5s linear;

And since I'm using styled-components, we can put this in a re-usable CSS string:

JSX
1import { css } from "styled-components";
2
3const WaveMixin = css`
4 transition-property: color;
5 transition-duration: 5s;
6 transition-timing-function: linear;
7`;

But this doesn't do anything at the moment — to actually change the color, we will need to update the color of the <span> that we are targeting after it has been rendered to the screen.

Let's start writing our little wrapper component for this:

JSX
1// usage
2<Highlight>Hello there!</Highlight>;
3
4const Highlight = ({ children }) => {
5 return <Wave>{children}</Wave>;
6};
7
8// import styled, { css } from "styled-components";
9const WaveMixin = css`
10 transition-property: color;
11 transition-duration: 5s;
12 transition-timing-function: linear;
13`;
14
15const Wave = styled.span`
16 ${WaveMixin}
17`;

And now we change the color once the component has mounted:

JSX
1<Highlight>Hello there!</Highlight>;
2
3let root: HTMLElement;
4
5const getNewColor = () => {
6 const h = random(1, 360);
7 const s = random(80, 90);
8 const l = random(50, 60);
9
10 return `hsl(${h}, ${s}%, ${l}%)`;
11};
12
13const Highlight = ({ children }) => {
14 const changeColor = () => {
15 const newColor = getNewColor();
16
17 if (root === undefined) root = document?.documentElement;
18 root.style.setProperty("--color-chameleon", newColor);
19 };
20
21 useEffect(() => {
22 changeColor();
23 }, []);
24
25 return <Wave>{children}</Wave>;
26};
27
28const WaveMixin = css`
29 color: var(--color-chameleon);
30 transition-property: color;
31 transition-duration: 5s;
32 transition-timing-function: linear;
33`;
34
35const Wave = styled.span`
36 ${WaveMixin}
37`;
Hello there!

Huh, that didn't seem to work out 🤔 but that's because the transition already ran since the above live example was already "mounted" by the time you scrolled down to it.

A wild custom hook appeared!

To keep changing the color after a pre-defined interval, we'll be defining our very own useInterval custom hook:

JSX
1const useInterval = (callback, delay) => {
2 const savedCallback = useRef();
3
4 useEffect(() => {
5 savedCallback.current = callback;
6 }, [callback]);
7
8 useEffect(() => {
9 const handler = (...args) => savedCallback.current(...args);
10
11 if (delay !== null) {
12 const intervalId = setInterval(handler, delay);
13 return () => clearInterval(intervalId);
14 }
15 }, [delay]);
16};

Let's now use this hook to change the color of our text just as it completes its transition.

JSX
1import { useInterval } from "utils/hooks";
2
3const Highlight = ({ children }) => {
4 const changeColor = () => {
5 const newColor = getNewColor();
6
7 if (root === undefined) root = document?.documentElement;
8 root.style.setProperty("--color-chameleon", newColor);
9 };
10
11 useEffect(() => {
12 changeColor();
13 }, []);
14
15 useInterval(() => {
16 changeColor();
17 }, 5000);
18
19 return <Wave>{children}</Wave>;
20};
Hello there!

Voila! ✨

There we go. What's great is that you can also use this to highlight link, like so:

This is a link.

Wrapping up

I'm really glad with how the effect turned out, and it was also easier to accomplish because I was using CSS-in-JS. It's great to see how powerful tools like styled-components can be! You can check the effect out in action and see it in all its glory 😄

Have a good one 👋