Building a Smooth Typewriter Animation with React and Framer Motion
Ever wanted to add that classic typewriter effect to your website? You know, the one where text appears letter by letter with a satisfying typing animation? Today, we'll build a React component that does exactly that, complete with smooth animations and theme switching.
What We're Building
Our typewriter component will cycle through different text examples, displaying each one with a realistic typing animation. It includes a theme toggle button and uses Framer Motion for buttery-smooth animations.
The Magic Behind the Animation
The typewriter effect relies on several key animation principles:
- Letter-by-letter reveal: Each character appears with a slight delay
- Cursor simulation: A blinking box effect that follows the typing
- Smooth transitions: Text fades out before the next example appears
- Continuous cycling: Automatically switches between different text examples
Breaking Down the Components
Animation Constants
First, let's define our timing constants:
1const LETTER_DELAY = 0.05;
2const BOX_FADE_DURATION = 0.07;
3const FADE_DELAY = 5;
4const MAIN_FADE_DURATION = 0.25;
5const SWAP_DELAY_IN_MS = 5500;
These control the speed and timing of different parts of our animation:
LETTER_DELAY
: How long to wait between each letter appearingBOX_FADE_DURATION
: How long the cursor effect lastsFADE_DELAY
: When to start fading out the current textMAIN_FADE_DURATION
: How long the fade-out takesSWAP_DELAY_IN_MS
: Total time before switching to the next example
The Text Examples
We define an array of example texts to cycle through:
1const examples = [
2"The only bike I crave is the DAVID PUTRA 2000CC.",
3"I love vibe coding... until I have to fix the simplest bug even AI gives up on.",
4"Why are Tailwind's neutral colors grayer than Gary itself?",
5"Abhi main so jaata hoon, baad mein commit karunga.",
6];
State Management
The component uses React hooks to manage the current example and theme:
1const [exampleIndex, setExampleIndex] = useState(0);
2const { theme, setTheme } = useTheme();
The Animation Loop
We use useEffect
to create an interval that cycles through examples:
1useEffect(() => {
2const intervalId = setInterval(() => {
3 setExampleIndex((pv) => (pv + 1) % examples.length);
4}, SWAP_DELAY_IN_MS);
5
6return () => clearInterval(intervalId);
7}, []);
The Letter Animation Magic
The most interesting part is how we animate each letter. We split the text into individual characters and wrap each one in a motion.span
:
1{examples[exampleIndex].split("").map((l, i) => (
2<motion.span
3 initial={{ opacity: 1 }}
4 animate={{ opacity: 0 }}
5 transition={{
6 delay: FADE_DELAY,
7 duration: MAIN_FADE_DURATION,
8 ease: "easeInOut",
9 }}
10 key={`${exampleIndex}-${i}`}
11 className="relative"
12>
13 {/* Letter reveal animation */}
14 <motion.span
15 initial={{ opacity: 0 }}
16 animate={{ opacity: 1 }}
17 transition={{
18 delay: i * LETTER_DELAY,
19 duration: 0,
20 }}
21 className="text-balance"
22 >
23 {l}
24 </motion.span>
25
26 {/* Cursor effect */}
27 <motion.span
28 initial={{ opacity: 0 }}
29 animate={{ opacity: [0, 1, 0] }}
30 transition={{
31 delay: i * LETTER_DELAY,
32 times: [0, 0.1, 1],
33 duration: BOX_FADE_DURATION,
34 ease: "easeInOut",
35 }}
36 className="absolute top-[3px] right-0 bottom-[3px] left-[1px] bg-neutral-950 dark:bg-neutral-200"
37 />
38</motion.span>
39))}
Each letter has two animations:
- Letter reveal: The actual character fades in
- Cursor effect: A colored box that briefly appears and disappears, simulating a typing cursor
Theme Toggle Feature
We also include a theme toggle button that smoothly transitions between light and dark modes:
1<motion.button
2onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
3className="absolute top-6 right-6 cursor-pointer rounded-full border border-neutral-300 p-2 text-neutral-700 transition-colors duration-200 hover:bg-neutral-200 focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 focus:outline-none dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:ring-offset-neutral-900"
4whileHover={{ scale: 1.1 }}
5whileTap={{ scale: 0.95 }}
6aria-label="Toggle theme"
7>
8<motion.span
9 key={theme}
10 initial={{ rotate: -180, opacity: 0 }}
11 animate={{ rotate: 0, opacity: 1 }}
12 transition={{ duration: 0.3 }}
13 className="flex size-5 items-center justify-center"
14>
15 {theme === "dark" ? <MoonIcon /> : <SunIcon />}
16</motion.span>
17</motion.button>
The button includes hover and tap animations, plus a smooth rotation effect when switching themes.
Styling and Layout
The component uses Tailwind CSS for styling, with a grid background pattern and responsive design:
1<div className="relative flex h-screen w-full flex-col items-center justify-center rounded-2xl">
2<div className="absolute inset-0 h-full w-full rounded-2xl bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:72px_72px]" />
3{/* Component content */}
4</div>
Performance Considerations
Optimization | Implementation | Why It Matters |
---|---|---|
Key prop | key={`${exampleIndex}-${i}`} | Ensures React properly handles animation state |
Cleanup | useEffect return function | Prevents memory leaks from intervals |
Optimized animations | Framer Motion | Automatic animation optimization |
Accessibility
The theme toggle button includes proper ARIA labels and focus states:
aria-label="Toggle theme"
for screen readers- Focus ring styling for keyboard navigation
- Semantic button element
Animation Timing Breakdown
Let's look at how different timing values affect the user experience:
LETTER_DELAY = 0.02 - Creates a rapid typing effect, good for short text
The Complete Code
Here's the full typewriter component:
1"use client";
2
3import React, { useEffect, useState } from "react";
4import { motion } from "framer-motion";
5import { useTheme } from "next-themes";
6import { MoonIcon, SunIcon } from "lucide-react";
7
8const examples = [
9"The only bike I crave is the DAVID PUTRA 2000CC.",
10"I love vibe coding... until I have to fix the simplest bug even AI gives up on.",
11"Why are Tailwind's neutral colors grayer than Gary itself?",
12"Abhi main so jaata hoon, baad mein commit karunga.",
13];
14
15const LETTER_DELAY = 0.05;
16const BOX_FADE_DURATION = 0.07;
17
18const FADE_DELAY = 5;
19const MAIN_FADE_DURATION = 0.25;
20
21const SWAP_DELAY_IN_MS = 5500;
22
23const Typewriter = () => {
24const [exampleIndex, setExampleIndex] = useState(0);
25const { theme, setTheme } = useTheme();
26
27useEffect(() => {
28const intervalId = setInterval(() => {
29setExampleIndex((pv) => (pv + 1) % examples.length);
30}, SWAP_DELAY_IN_MS);
31
32 return () => clearInterval(intervalId);
33
34}, []);
35
36return (
37
38<div className="relative flex h-screen w-full flex-col items-center justify-center rounded-2xl">
39<div className="absolute inset-0 h-full w-full rounded-2xl bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:72px_72px]" />
40<motion.button
41onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
42className="absolute top-6 right-6 cursor-pointer rounded-full border border-neutral-300 p-2 text-neutral-700 transition-colors duration-200 hover:bg-neutral-200 focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 focus:outline-none dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:ring-offset-neutral-900"
43whileHover={{ scale: 1.1 }}
44whileTap={{ scale: 0.95 }}
45aria-label="Toggle theme" >
46<motion.span
47key={theme}
48initial={{ rotate: -180, opacity: 0 }}
49animate={{ rotate: 0, opacity: 1 }}
50transition={{ duration: 0.3 }}
51className="flex size-5 items-center justify-center" >
52{theme === "dark" ? <MoonIcon /> : <SunIcon />}
53</motion.span>
54</motion.button>
55
56 <div className="w-fit px-4 text-center">
57 <p className="mb-4 text-base font-semibold text-neutral-700 uppercase sm:text-2xl md:text-4xl dark:text-neutral-300">
58 <span className="">
59 {examples[exampleIndex].split("").map((l, i) => (
60 <motion.span
61 initial={{
62 opacity: 1,
63 }}
64 animate={{
65 opacity: 0,
66 }}
67 transition={{
68 delay: FADE_DELAY,
69 duration: MAIN_FADE_DURATION,
70 ease: "easeInOut",
71 }}
72 key={`${exampleIndex}-${i}`}
73 className="relative"
74 >
75 <motion.span
76 initial={{
77 opacity: 0,
78 }}
79 animate={{
80 opacity: 1,
81 }}
82 transition={{
83 delay: i * LETTER_DELAY,
84 duration: 0,
85 }}
86 className="text-balance"
87 >
88 {l}
89 </motion.span>
90 <motion.span
91 initial={{
92 opacity: 0,
93 }}
94 animate={{
95 opacity: [0, 1, 0],
96 }}
97 transition={{
98 delay: i * LETTER_DELAY,
99 times: [0, 0.1, 1],
100 duration: BOX_FADE_DURATION,
101 ease: "easeInOut",
102 }}
103 className="absolute top-[3px] right-0 bottom-[3px] left-[1px] bg-neutral-950 dark:bg-neutral-200"
104 />
105 </motion.span>
106 ))}
107 </span>
108 </p>
109 </div>
110 </div>
111
112);
113};
114
115export default Typewriter;
Wrapping Up
This typewriter component combines React state management, Framer Motion animations, and thoughtful timing to create an engaging user experience. The key to smooth animations is getting the timing right - each letter should appear just fast enough to feel natural, but slow enough to be readable.
You can customize the examples array, adjust the timing constants, or modify the styling to match your design needs. The component is fully responsive and works great in both light and dark themes.
Try building this yourself and experiment with different timing values to see how they affect the feel of the animation. Happy coding!