Lexington has been awarded a grant from Astro, to celebrate. Get a 30% discount. Apply code LEXINGTON30 ( uppercase ) at checkout.

← Back to all tutorials

How to create a particle text effect with Tailwind CSS and JavaScript

#
Published and written on Oct 10 2024 by Michael Andreuzza

Hello everyone, today we are going to create a simple particle text effect with Tailwind CSS and JavaScript. This effect is great for creating a fun and playful animation.

What is a particle text effect?

A particle text effect is an animation where individual particles move and come together to form text. These particles can start off scattered randomly across a container and gradually reform into specific characters, giving a dynamic visual impact. This technique often involves JavaScript to control particle movement and a framework like Tailwind CSS for styling the particles and text.

Use Cases

  • Landing Pages: To grab attention with an interactive and engaging animation as part of the hero section.
  • Loading Screens: To keep users entertained while a page or resource is loading.
  • Text Reveals: For cool text reveals in digital presentations, banners, or promotional content.
  • Interactive Typography: For adding playful animations to headlines or call-to-action buttons.

To the code!

The container

ID’s

  • id="text-container": This line of code will define the id of the container. This id will be used to target the container in the JavaScript code. Classes
  • h-96: This class will make the container element have a height of 96 pixels.
  • lg:h-32: This class will make the container element have a height of 32 pixels on screens with a width of 1024 pixels or greater.

We need to add a height to the container so that the particles can be positioned correctly.

  • flex: This class will make the container element use flexbox.
  • items-center: This class will center the container element vertically.
<div
    id="text-container"
    class="h-96 lg:h-32 flex items-center">
</div>

Time for the script!

The constructor

Parameters

  • containerId: This parameter will be used to get the container element with the id of “text-container” from the document.
  • initialText: This parameter will be used to set the initial text of the particles.
  • finalText: This parameter will be used to set the final text of the particles.
  • particleCount: This parameter will be used to set the number of particles. Return Value
  • this.container: This line of code will return the container element with the id of “text-container” from the document.
  • this.initialText: This line of code will return the initial text of the particles.
  • this.finalText: This line of code will return the final text of the particles.
  • this.particleCount: This line of code will return the number of particles.
  • this.particles: This line of code will return an empty array.
  • this.animationFrame: This line of code will return null. Initialization
  • this.init(): This line of code will call the init function.
  • this.createParticles(): This line of code will call the createParticles function.
  • this.addEventListeners(): This line of code will call the addEventListeners function.
  • this.startAnimation(): This line of code will call the startAnimation function.
constructor(containerId, initialText, finalText, particleCount = 500) {
    this.container = document.getElementById(containerId);
    this.initialText = initialText;
    this.finalText = finalText;
    this.particleCount = particleCount;
    this.particles = [];
    this.animationFrame = null;

    this.init();
}

init() {
    this.createParticles();
    this.addEventListeners();
    this.startAnimation();
}

The createParticles function

Variables

  • const fragment: This line of code will define a variable called fragment and set its value to document.createDocumentFragment().
  • const containerRect: This line of code will define a variable called containerRect and set its value to this.container.getBoundingClientRect().

Loop

  • for (let i = 0; i < this.particleCount; i++): This line of code will create a loop that will iterate this.particleCount times. Variables
  • const particle: This line of code will define a variable called particle and set its value to document.createElement("span").
  • particle.textContent: This line of code will set the textContent of the particle element to this.getRandomChar().
  • particle.className: This line of code will set the className of the particle element to "absolute opacity-0 transition-all duration-1000 ease-out".
  • fragment.appendChild(particle): This line of code will append the particle element to the fragment element.
  • this.particles.push({ element: particle, x: Math.random() * containerRect.width, y: Math.random() * containerRect.height, speedX: Math.random() * 2 - 1, speedY: Math.random() * 2 - 1 });: This line of code will push an object with the element property set to the particle element, the x property set to a random number between 0 and the width of the containerRect, the y property set to a random number between 0 and the height of the containerRect, the speedX property set to a random number between -1 and 1, and the speedY property set to a random number between -1 and 1.
  • this.container.appendChild(fragment);: This line of code will append the fragment element to the this.container element.
createParticles() {
    const fragment = document.createDocumentFragment();
    const containerRect = this.container.getBoundingClientRect();

    for (let i = 0; i < this.particleCount; i++) {
        const particle = document.createElement("span");
        particle.textContent = this.getRandomChar();
        particle.className =
            "absolute opacity-0 transition-all duration-1000 ease-out";
        fragment.appendChild(particle);

        this.particles.push({
            element: particle,
            x: Math.random() * containerRect.width,
            y: Math.random() * containerRect.height,
            speedX: Math.random() * 2 - 1,
            speedY: Math.random() * 2 - 1,
        });
    }

    this.container.appendChild(fragment);
}

Random character

  • getRandomChar(): This line of code will define a function called getRandomChar that will be called when the createParticles function is called. Return Value
  • return this.initialText[Math.floor(Math.random() * this.initialText.length)]: This line of code will return a random character from the this.initialText array.
getRandomChar() {
    return this.initialText[
        Math.floor(Math.random() * this.initialText.length)
    ];
}

Animating the particles

Variables

  • const containerRect: This line of code will define a variable called containerRect and set its value to this.container.getBoundingClientRect(). Loop
  • this.particles.forEach((particle) => {: This line of code will iterate over each particle in the this.particles array and pass the particle to the anonymous function.
  • particle.x += particle.speedX;: This line of code will add the speedX property of the particle object to the x property of the particle object.
  • particle.y += particle.speedY;: This line of code will add the speedY property of the particle object to the y property of the particle object.
  • particle.x < 0 || particle.x > containerRect.width: This line of code will check if the x property of the particle object is less than 0 or greater than the width of the containerRect.
  • particle.y < 0 || particle.y > containerRect.height: This line of code will check if the y property of the particle object is less than 0 or greater than the height of the containerRect.
  • Object.assign(particle.element.style, { transform: translate(${particle.x}px, ${particle.y}px), opacity: "1" });: This line of code will assign the transform and opacity styles to the particle element. Return Value
  • this.animationFrame = requestAnimationFrame(this.animateParticles.bind(this));: This line of code will set the animationFrame property of the this object to the result of calling the requestAnimationFrame function with the animateParticles function as an argument.
animateParticles() {
    const containerRect = this.container.getBoundingClientRect();

    this.particles.forEach((particle) => {
        particle.x += particle.speedX;
        particle.y += particle.speedY;

        if (particle.x < 0 || particle.x > containerRect.width)
            particle.speedX *= -1;
        if (particle.y < 0 || particle.y > containerRect.height)
            particle.speedY *= -1;

        Object.assign(particle.element.style, {
            transform: `translate(${particle.x}px, ${particle.y}px)`,
            opacity: "1",
        });
    });

    this.animationFrame = requestAnimationFrame(
        this.animateParticles.bind(this)
    );
}

The reformText function

Variables

  • const containerRect: This line of code will define a variable called containerRect and set its value to this.container.getBoundingClientRect().
  • const fontSize: This line of code will define a variable called fontSize and set its value to window.innerWidth < 1024 ? 16 : 54. This line of code will check if the window.innerWidth is less than 1024 pixels and if so, it will set the fontSize to 16.
  • const letterSpacing: This line of code will define a variable called letterSpacing and set its value to fontSize * 0 .8. This line of code will multiply the fontSize by 0.8 to get the letterSpacing.
  • const textWidth: This line of code will define a variable called textWidth and set its value to this.finalText.length * letterSpacing.
  • startX: This line of code will define a variable called startX and set its value to (containerRect.width - textWidth) / 2. This line of code will calculate the startX by subtracting the textWidth from the containerRect.width and then dividing by 2.
  • startY: This line of code will define a variable called startY and set its value to containerRect.height / 2. This line of code will calculate the startY by dividing the containerRect.height by 2. Loop
  • this.particles.forEach((particle, index) => {: This line of code will iterate over each particle in the this.particles array and pass the particle and the index to the anonymous function.
  • if (index < this.finalText.length) {: This line of code will check if the index is less than the length of the this.finalText array.
  • const targetX = startX + index * letterSpacing;: This line of code will define a variable called targetX and set its value to startX + index * letterSpacing.
  • const targetY = startY + (Math.random() - 0.5) * (fontSize / 2);: This line of code will define a variable called targetY and set its value to startY + (Math.random() - 0.5) * (fontSize / 2).
  • Object.assign(particle.element.style, { transform: translate(${targetX}px, ${targetY}px), opacity: "1" });: This line of code will assign the transform and opacity styles to the particle element.
  • particle.element.textContent = this.finalText[index];: This line of code will set the textContent of the particle element to the this.finalText[index].
  • } else {: This line of code will close the if statement if index is less than the length of the this.finalText array.
  • particle.element.style.opacity = "0";: This line of code will set the opacity style of the particle element to “0”.
reformText() {
    const containerRect = this.container.getBoundingClientRect();
    const fontSize = window.innerWidth < 1024 ? 16 : 54;
    const letterSpacing = fontSize * 0.8;
    const textWidth = this.finalText.length * letterSpacing;
    const startX = (containerRect.width - textWidth) / 2;
    const startY = containerRect.height / 2;

    this.particles.forEach((particle, index) => {
        if (index < this.finalText.length) {
            const targetX = startX + index * letterSpacing;
            const targetY = startY + (Math.random() - 0.5) * (fontSize / 2);

            Object.assign(particle.element.style, {
                transform: `translate(${targetX}px, ${targetY}px)`,
                opacity: "1",
            });
            particle.element.textContent = this.finalText[index];
        } else {
            particle.element.style.opacity = "0";
        }
    });
}

Start animation function

  • cancelAnimationFrame(this.animationFrame);: This line of code will cancel the animationFrame property of the this object.
  • this.createParticles();: This line of code will call the createParticles function.
  • this.animateParticles();: This line of code will call the animateParticles function.
  • setTimeout(() => {: This line of code will use the setTimeout function to delay the execution of the following code by 2000 milliseconds.
  • cancelAnimationFrame(this.animationFrame);: This line of code will cancel the animationFrame property of the this object.
  • this.reformText();: This line of code will call the reformText function.
  • }, 2000);: This line of code will close the setTimeout function.
startAnimation() {
    cancelAnimationFrame(this.animationFrame);
    this.createParticles();
    this.animateParticles();

    setTimeout(() => {
        cancelAnimationFrame(this.animationFrame);
        this.reformText();
    }, 2000);
}

Handle resize function

  • const containerRect = this.container.getBoundingClientRect();: This line of code will define a variable called containerRect and set its value to this.container.getBoundingClientRect().
  • this.particles.forEach((particle) => {: This line of code will iterate over each particle in the this.particles array and pass the particle to the anonymous function.
  • particle.x = Math.random() * containerRect.width;: This line of code will set the x property of the particle object to a random number between 0 and the width of the containerRect.
  • particle.y = Math.random() * containerRect.height;: This line of code will set the y property of the particle object to a random number between 0 and the height of the containerRect.
  • cancelAnimationFrame(this.animationFrame);: This line of code will cancel the animationFrame property of the this object.
  • this.animateParticles();: This line of code will call the animateParticles function.
 handleResize() {
     const containerRect = this.container.getBoundingClientRect();
     this.particles.forEach((particle) => {
         particle.x = Math.random() * containerRect.width;
         particle.y = Math.random() * containerRect.height;
     });
     cancelAnimationFrame(this.animationFrame);
     this.animateParticles();
 }

The addEventListeners function

  • this.container.addEventListener("click", this.startAnimation.bind(this));: This line of code will add a click event listener to the this.container element that will call the startAnimation function when the element is clicked.
  • window.addEventListener("resize", this.handleResize.bind(this));: This line of code will add a resize event listener to the window object that will call the handleResize function when the window is resized.
 addEventListeners() {
     this.container.addEventListener("click", this.startAnimation.bind(this));
     window.addEventListener("resize", this.handleResize.bind(this));
 }

Event listener for the window

  • new ParticleTextAnimation("text-container", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "HELLO EVERYONE");: This line of code will create a new instance of the ParticleTextAnimation class with the text-container id, the ABCDEFGHIJKLMNOPQRSTUVWXYZ initial text, and the HELLO EVERYONE final text.
document.addEventListener("DOMContentLoaded", () => {
    new ParticleTextAnimation(
        "text-container",
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
        "HELLO EVERYONE"
    );
});

The full script.

This is how the full script will look like when you’re done.

class ParticleTextAnimation {
    constructor(containerId, initialText, finalText, particleCount = 500) {
        this.container = document.getElementById(containerId);
        this.initialText = initialText;
        this.finalText = finalText;
        this.particleCount = particleCount;
        this.particles = [];
        this.animationFrame = null;

        this.init();
    }

    init() {
        this.createParticles();
        this.addEventListeners();
        this.startAnimation();
    }

    createParticles() {
        const fragment = document.createDocumentFragment();
        const containerRect = this.container.getBoundingClientRect();

        for (let i = 0; i < this.particleCount; i++) {
            const particle = document.createElement("span");
            particle.textContent = this.getRandomChar();
            particle.className =
                "absolute opacity-0 transition-all duration-1000 ease-out";
            fragment.appendChild(particle);

            this.particles.push({
                element: particle,
                x: Math.random() * containerRect.width,
                y: Math.random() * containerRect.height,
                speedX: Math.random() * 2 - 1,
                speedY: Math.random() * 2 - 1,
            });
        }

        this.container.appendChild(fragment);
    }

    getRandomChar() {
        return this.initialText[
            Math.floor(Math.random() * this.initialText.length)
        ];
    }

    animateParticles() {
        const containerRect = this.container.getBoundingClientRect();

        this.particles.forEach((particle) => {
            particle.x += particle.speedX;
            particle.y += particle.speedY;

            if (particle.x < 0 || particle.x > containerRect.width)
                particle.speedX *= -1;
            if (particle.y < 0 || particle.y > containerRect.height)
                particle.speedY *= -1;

            Object.assign(particle.element.style, {
                transform: `translate(${particle.x}px, ${particle.y}px)`,
                opacity: "1",
            });
        });

        this.animationFrame = requestAnimationFrame(
            this.animateParticles.bind(this)
        );
    }

    reformText() {
        const containerRect = this.container.getBoundingClientRect();
        const fontSize = window.innerWidth < 1024 ? 16 : 54;
        const letterSpacing = fontSize * 0.8;
        const textWidth = this.finalText.length * letterSpacing;
        const startX = (containerRect.width - textWidth) / 2;
        const startY = containerRect.height / 2;

        this.particles.forEach((particle, index) => {
            if (index < this.finalText.length) {
                const targetX = startX + index * letterSpacing;
                const targetY = startY + (Math.random() - 0.5) * (fontSize / 2);

                Object.assign(particle.element.style, {
                    transform: `translate(${targetX}px, ${targetY}px)`,
                    opacity: "1",
                });
                particle.element.textContent = this.finalText[index];
            } else {
                particle.element.style.opacity = "0";
            }
        });
    }

    startAnimation() {
        cancelAnimationFrame(this.animationFrame);
        this.createParticles();
        this.animateParticles();

        setTimeout(() => {
            cancelAnimationFrame(this.animationFrame);
            this.reformText();
        }, 2000);
    }

    handleResize() {
        const containerRect = this.container.getBoundingClientRect();
        this.particles.forEach((particle) => {
            particle.x = Math.random() * containerRect.width;
            particle.y = Math.random() * containerRect.height;
        });
        cancelAnimationFrame(this.animationFrame);
        this.animateParticles();
    }

    addEventListeners() {
        this.container.addEventListener("click", this.startAnimation.bind(this));
        window.addEventListener("resize", this.handleResize.bind(this));
    }
}

document.addEventListener("DOMContentLoaded", () => {
    new ParticleTextAnimation(
        "text-container",
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
        "HELLO EVERYONE"
    );
});

Conclusion

This is a simple particle text effect that you can recreate with Tailwind CSS and JavaScript. You can customize the colors, number of particles, and text to your liking.

Hope you enjoyed this tutorial and have a good day!

/Michael Andreuzza

Did you like this tutorial? Please share it with your friends!

Get lifetime access to every theme available today for $199 and own them forever. Plus, new themes, lifetime updates, use on unlimited projects and enjoy lifetime support.

— No subscription required!