Text spinner effect

webtailwindreactstylingcssanimation

In a previous post I discussed implementing a text reveal effect with Tailwind CSS. It involved using both text overflow and animation properties to provide a dynamic flair to text. In this post I'd like to expand on these techniques to implement a text spinner effect. The goal is to have multiple words transition from top-to-bottom in a cyclic manner, one at a time. In combination with preceding text effect can be used to capture the attention of readers.

To begin let's define a component that accepts props preText and spinnerWords - the preceding text and words that will spin in the component. We place these within a h1 wrapper tag. Using the map function, for word in spinnerWords we render it within a span tag.

type TextSpinnerProps = {
  preText: string,
  spinnerWords: string[],
}

export const TextSpinnerSnippetOne = ({
  preText,
  spinnerWords
}: TextSpinnerProps) => {
  return (
    <div>
      <h1 className="flex items-center text-sm md:text-md font-md my-2">
        {preText} 
        <span>
          {spinnerWords.map((word, index) => (
            <span 
              key={index}
            >{word}</span>
          ))}
        </span>
      </h1>
    </div>
  );
}

Bringing a decade of experience inanimationdesignengineering

We now define the styles and animations. First within tailwind.config.ts file we define both the animation text-spinner and its key frames. We define the animation to repeat infinitely with a duration of three seconds. For the keyframes we have the initial frame at -100% y-axis translation then move to 100% to implement the spinner effect. Note that since there are three spinner words we have the 0% translation (i.e. when the work is visible) to be at one-third through the animation.
Finally finish the effect by updating the opacity at for each frame we've defined.

  ...,
  animation: {
    "text-spinner": "text-spinner 3.0s linear infinite",
  },
  ...,
  "text-spinner": { 
    "0%": { transform: "translateY(-100%)", opacity: "0.1" },
    "10%": { transform: "translateY(0)", opacity: "1" },
    "33%": { transform: "translateY(0)", opacity: "1" },
    "45%": { transform: "translateY(100%)", opacity: "0.1" },
    "100%": { transform: "translateY(100%)", opacity: "0.1" },
  }

We now define the component's styles. As part of getting the spinner effect we set the wrapper span's display to absolute in each of the spinner word's span elements to relative display. For each word we apply an animation delay based on the index of the word within the array. In combination with overflow-hidden we complete the spinner effect.

    <h1 className="flex items-center text-xs sm:text-md md:text-lg font-md my-2">
      {preText} 
      <span className="relative overflow-hidden">
        {spinnerWords.map((word, index) => (
          <span 
            key={index}
            className='absolute translate-y-full [animation-delay:${index}.0s] animate-text-spinner font-md'
            >{word}</span>
        ))}
      </span>
    </h1>

Bringing a decade of experience inanimationdesignengineering

Hmm... not the result we wanted. After a bit of time investigating this, I relearned (perhaps the third time 😅) the following about Tailwind CSS and dynamic class names:

The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files.

Therefore, using each word's index value to specifyanimation-delay is insufficient. After spending a few hour searching for a workaround I remembered the KISS principle and decided a workaround wasn't worth it. Instead we can hardcode the span tags for the spinner words directly. With some additional styling classes from Tailwind we have:

  <h1 className="flex items-center text-xs sm:text-md md:text-lg font-md my-2">
    {preText} 
    <span className="relative ml-1 h-[1.1em] font-bold w-28 overflow-hidden my-2">
      <span className='absolute h-full w-full translate-y-full leading-none [animation-delay:0.0s] animate-text-spinner'>animation</span>
      <span className='absolute h-full w-full translate-y-full leading-none [animation-delay:1s] animate-text-spinner'>design</span>
      <span className='absolute h-full w-full translate-y-full leading-none [animation-delay:2s] animate-text-spinner'>engineering</span>
    </span>
  </h1>

Bringing a decade of experience inanimationdesignengineering

While this was a fun exercise to learn more about how CSS animations with Tailwind works, it was a reminder to myself about not over optimizing components in a way to make them more generic and extensible when in reality the likelihood of needing this extensibility is low. Thanks for reading!