Gradient-Revealed Text: An Alternative Approach to Gsap's SplitText

I was working on a client project recently that featured a section of text that was supposed to start at a very low-contrast color, and then as the section scrolled into view, the text was supposed to transition, character-by-character, to a high-contrast color. This is something I've seen before, and I'm sure you have too. I immediately thought: "GSAP's SplitText."

And that's exactly what I turned to. I put together a component that used GSAP's ScrollTrigger plugin to handle the triggering of the event when the section was at the right position in the viewport, and GSAP's SplitText plugin to handle the character-by-character color animation. 

This all worked well, and was easy to implement, but then I pulled up Deque's AXE Devtools to test the page for simple accessibility concerns, and it flagged an error on my new component: Elements must only use permitted ARIA attributes

See the Pen GSAP SplitText A11y Error by Matt Soria (@itsmattsoria) on CodePen.

If you have Deque's AXE Devtools browser extension installed and you run it on that demo, you'll see that same error. 

I could see what the issue was: I had initiated SplitText on a div, which is the element SplitText slapped aria-label onto with the text content of the inner HTML, and a div, without a given role, can't have aria-label. I was using a div for a good reason, though: the text I wanted to animate was coming from a rich text field of a CMS, which could contain multiple <p> elements, and some styled <span> elements as well, so a generic container made the most sense. I could futz around with stripping that data of the tags as long as it was all going to be communicated by aria-label anyway, but whenever I find myself getting into the territory of stripping semantics and structural meaning, it's a clear red flag begging the question, "Is there not a simpler way?"

GSAP has this very scenario covered in their docs, though, and the proposed solution is to tell GSAP to leave off all of the aria attributes and let you handle it by duplicating the text markup in question, and visually hiding it, while adding aria-hidden="true" to the element to be animated by GSAP:

<!-- Visually hidden, but semantically meaningful content -->
<div class="visually-hidden">
  <p>Animated text...</p>
  <p>Some <strong>more</strong> animated text.</p>
<div>

<!-- The element that will be targeted by GSAP to split + animate -->
<div data-animated-text aria-hidden="true">
  <p>Animated text...</p>
  <p>Some <strong>more</strong> animated text.</p>
<div>

My coworker Abigail actually shared this post by Adrian Roselli urging you to just not do it, the day after I encountered this. I was already unsatisfied with the look of the animation (seen in the above CodePen) and this encouraged me to take another look and see if I couldn't find another approach.

That's when I decided to try and try it without SplitText and just go straight CSS. For the purposes of the design I was implementing, it actually wasn't important to be able to animate each letter individually since the letters themselves were meant to stay stationary, and the starting point of the animation was the same text, just at a lighter color.

Here's what I came up with:

I like the smoothness of the gradient spreading across the text over the abruptness of the letter-by-letter color change that happens when using SplitText.

And aesthetics aside, I love this approach because it's accessible without needing to do anything hacky (it's just plain markup), and it's JavaScript-free sparkles emoji.

Here's what the markup looks like:

<div class="animated-text">
  <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Minima maiores dicta <strong>vero voluptatem architecto</strong> quos quo saepe, est error quisquam. Exercitationem quas similique at perferendis voluptate repudiandae consectetur quis itaque!</p>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Excepturi officia eum corporis! Quaerat atque, consequuntur nulla, fuga deleniti obcaecati rem blanditiis reiciendis ipsum facilis dolorum voluptatum id tempore eligendi nemo!</p>
</div>

That's it! It's just a container of semantic text markup with a class on it. 

And here's what the CSS looks like:

:root {
  --color-start: #BBAC9B;
  --color-end: #121110;
}

.animated-text {
  /* Set this as the end/legible color in case a visitor prefers reduced-motion */
  color: var(--color-end);
}

/* Wrap all of the animation styles in check for the visitor's motion preferences */
@media (prefers-reduced-motion: no-preference) {
  .animated-text {
    /* Scroll-driven Animation Properties */
    view-timeline-name: --animated-text;
    view-timeline-axis: block;
    animation: linear animatedTextBackground forwards;
    animation-timeline: --animated-text;
    animation-range: entry 45% cover 60%;
    /* The background gradient styles */
    color: transparent;
    background-clip: text;
    background-size: 100% 300%;
    background-position: 50% 100%;
    will-change: background-position;
    /* The gradient that creates the effect */
    background-image: linear-gradient(172deg, var(--color-end) 45%, var(--color-start) 55%);
  }
}

/* Animation that moves the background gradient into position */
@keyframes animatedTextBackground {
  from {
    background-position: 50% 90%;
  }
  to {
    background-position: 50% 0%;
  }
}

That's it! The markup is inherently accessible, and on top of that, we can take a no-motion-first approach, ensuring the text is legible by default, and sprinkling on the animation if it's (assumed to be) acceptable to the visitor.

No Hate for GSAP

I just want to make clear that this post isn't meant as a dump on GSAP, or to convince you to not use GSAP—I think it's an incredibly powerful and useful tool, and I find a use for it on nearly every project I take on. It's just that for this particular design challenge, it seemed like a less-is-more approach was worth considering, and avoided the potential issues that Adrian outlined in his post.