Making Stagger Reveal Animations for Text

A short tutorial on how to recreate a letter stagger animation with GSAP and Splitting.js.

Thibaud Allie made this wonderful animation which you can see live on the site of Dani Morales. It happens when you click on “About” (and then “Close”). This kind of show/hide animation on the typographic elements is being used in many designs lately. At Codrops we call it a “reveal” animation.

I fell in love with that lettering effect and wanted to reimplement it using GSAP and Splitting.js. So I made a similar typography based layout and move the lines of text by staggering the letter animations.

The simplified markup looks like this:

<section class="content__item content__item--home content__item--current">
	<p class="content__paragraph" data-splitting>Something</p>
	<p class="content__paragraph" data-splitting>More</p>
<section class="content__item content__item--about">
	<p class="content__paragraph" data-splitting>Something</p>
	<p class="content__paragraph" data-splitting>Else</p>

Note that the necessary style for the content__paragraph element is overflow: hidden so that the reveal/unreveal animation works.

All elements with the “data-splitting” attribute will be split up into spans that we can then animate individually. So let’s have a look at the JavaScript for that.

Initially, we need to import some libraries and styles:

import "splitting/dist/splitting.css";
import "splitting/dist/splitting-cells.css";
import Splitting from "splitting";
import { gsap } from 'gsap';
import { preloadFonts } from './utils';
import Cursor from "./cursor";

I’m using some Adobe Fonts here (Freight Big Pro and Tenon) so let’s preload them:

preloadFonts('lwc3axy').then(() => document.body.classList.remove('loading'));

Then it’s time to split all the texts into spans:


The design has a custom cursor that we call as follows:

new Cursor(document.querySelector('.cursor'))

Let’s get all relevant DOM elements:

let DOM = {
    content: {
        home: {
            section: document.querySelector('.content__item--home'),
            get chars() {
                return this.section.querySelectorAll('.content__paragraph .word > .char, .whitespace');
            isVisible: true
        about: {
            section: document.querySelector('.content__item--about'),
            get chars() {
                return this.section.querySelectorAll('.content__paragraph .word > .char, .whitespace')
            get picture() {
                return this.section.querySelector('.content__figure');
            isVisible: false
    links: {
        about: {
            anchor: document.querySelector('a.frame__about'),
            get stateElement() {
                return this.anchor.children;
        home: document.querySelector('a.frame__home')

Let’s have a look at the GSAP timeline now where all the magic happens (and also some default settings):

const timelineSettings = {
    staggerValue: 0.014,
    charsDuration: 0.5
const timeline = gsap.timeline({paused: true})
    // Stagger the animation of the home section chars
    .staggerTo( DOM.content.home.chars, timelineSettings.charsDuration, {
        ease: 'Power3.easeIn',
        y: '-100%',
        opacity: 0
    }, timelineSettings.staggerValue, 'start')
    // Here we do the switch
    // We need to toggle the current class for the content sections
    .add( () => {
    // Change the body's background color
    .to(document.body, {
        duration: 0.8,
        ease: 'Power1.easeInOut',
        backgroundColor: '#c3b996'
    }, 'switchtime-=timelineSettings.charsDuration/4')
    // Start values for the about section elements that will animate in
    .set(DOM.content.about.chars, {
        y: '100%'
    }, 'switchtime')
    .set(DOM.content.about.picture, {
        y: '40%',
        rotation: -4,
        opacity: 0
    }, 'switchtime')
    // Stagger the animation of the about section chars
    .staggerTo( DOM.content.about.chars, timelineSettings.charsDuration, {
        ease: 'Power3.easeOut',
        y: '0%'
    }, timelineSettings.staggerValue, 'switchtime')
    // Finally, animate the picture in
    .to( DOM.content.about.picture, 0.8, {
        ease: 'Power3.easeOut',
        y: '0%',
        opacity: 1,
        rotation: 0
    }, 'switchtime+=0.6');

When we click on the “About” or logo/home link we want to toggle the current content by playing and reversing the timeline. We also want to toggle the current state of the “About” and “Close” link:

const switchContent = () => {
    DOM.links.about.stateElement[0].classList[DOM.content.about.isVisible ? 'add' : 'remove']('frame__about-item--current');
    DOM.links.about.stateElement[1].classList[DOM.content.about.isVisible ? 'remove' : 'add']('frame__about-item--current');
    timeline[DOM.content.about.isVisible ? 'reverse' : 'play']();
    DOM.content.about.isVisible = !DOM.content.about.isVisible;
    DOM.content.home.isVisible = !DOM.content.about.isVisible;

DOM.links.about.anchor.addEventListener('click', () => switchContent());
DOM.links.home.addEventListener('click', () => {
    if ( DOM.content.home.isVisible ) return;

And that’s all the magic! It’s not too complicated and you can do many nice effects with this kind of letter animation.

Thank you for reading and hopefully this was relevant and helpful 🙂

Please reach out to me @codrops or @crnacura if you have any questions or suggestions.


Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!