Tailwind CSS Typing Text Animation
The Context
I had seen a few personal portfolio sites which had titles that used a typing animation that made it look like they had just typed out their name. I thought this was cool and wanted to rebuild it.
This was just for the title of my website, which you can see here.
The Solution (don’t use CSS)
I used an animation that I called
blink
to animate the caret blinking on the screen, which was pretty straightforward and I took from here.The problem that I had with a purely CSS solution for revealing letters was:
- I wanted the curser/caret to move along with the text appearing and the solution would become more complex to factor this in
- Monospace fonts are preferred as revealing letter by letter as you can reveal a fixed amount of space with each tick, but the font I wanted to use was not monospace.
To keep things simple, showing words appearing letter-by-letter with a trailing caret, I used a simple JS approach–instead of using CSS tricks to reveal each letter, just split the string by letters and don’t render all letters to the page until a timeout has been waited for (using
setTimeout
).I’m sure this is less efficient (i.e. it triggers many re-renders) and has the disadvantage that when the component is remounted it replays the animation, however for my use-case (a small amount of text as a title high in the VDOM hierarchy), it seems to be working well-enough.
The Code
// tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { // ... theme: { extend: { // ... keyframes: { // ... blink: { "0%": { opacity: "0", }, "0.1%": { opacity: "1", }, "50%": { opacity: "1", }, "50.1%": { opacity: "0", }, "100%": { opacity: "0", }, }, }, animation: { // ... blink: "blink infinite 2s", }, }, }, // ... }; // TypewriterEffect.tsx "use client"; import { useEffect, useState } from "react"; export const TypewriterEffect = ({ text = "", keyPressDelay = 75 }) => { const [displayedText, setDisplayedText] = useState(""); const [currentIndex, setCurrentIndex] = useState(0); useEffect(() => { if (currentIndex < text.length) { const timeout = setTimeout(() => { setDisplayedText(text.slice(0, currentIndex + 1)); // Always take a full substring setCurrentIndex(currentIndex + 1); }, keyPressDelay); return () => clearTimeout(timeout); } }, [currentIndex, text, keyPressDelay]); return ( <> {displayedText} <Caret /> </> ); }; export const Caret = () => ( <span className="ml-1 h-fit inline text-ice-800 animate-blink">|</span> ); // Usage ... <h1 className="font-bold text-3xl md:text-4xl mb-8 tracking-tight text-gray-700 dark:text-white"> <TypewriterEffect text="James Haworth Wheatman" /> </h1> ...