Skip to main content

> Building react-terminal-typewriter: A Lightweight React Hook for Terminal-Style Effects

4 min read

Every developer has seen those cool terminal-style typewriter effects on landing pages. Text that types itself, character by character, creating an engaging and dynamic user experience. I needed one for my projects, but existing solutions were either too heavy, lacked TypeScript support, or didn't handle React 18+ StrictMode properly.

So I built my own: react-terminal-typewriter.

The Problem

When I started looking for a typewriter hook, I found that most solutions had issues:

  • Heavy dependencies – many packages pulled in entire animation libraries
  • No loop support – text would type once and stop
  • React 18 incompatible – StrictMode caused double animations
  • Poor TypeScript support – missing or incomplete type definitions
  • No cursor control – fixed blink speeds with no customization

I needed something lightweight, type-safe, and flexible.

The Solution

I created react-terminal-typewriter – a single React hook that:

  • Has zero dependencies (only React as peer dependency)
  • Weighs ~1KB minified
  • Supports loop mode with configurable delays
  • Handles React 18+ StrictMode correctly
  • Provides full TypeScript support
  • Offers cursor blink speed control

Quick Start

npm install react-terminal-typewriter
import { useTypewriter } from 'react-terminal-typewriter'
 
function Hero() {
  const { displayText, cursorBlinkSpeed } = useTypewriter({
    text: 'Hello, World!',
    delay: 100,
    loop: true
  })
 
  return (
    <h1>
      {displayText}
      <span 
        className="cursor"
        style={{ '--cursor-blink-speed': `${cursorBlinkSpeed}ms` } as React.CSSProperties}
      />
    </h1>
  )
}

Key Features

Loop Mode

The most requested feature – text that types, pauses, deletes, and repeats:

const { displayText, isDeleting } = useTypewriter({
  text: 'React Terminal Typewriter',
  loop: true,
  loopDelay: 3000,    // Wait 3s before deleting
  deleteDelay: 30      // Delete faster than typing
})
 
// Change styles based on state
<span style={{ color: isDeleting ? 'orange' : 'green' }}>
  {displayText}
</span>

Configurable Speeds

Fine-tune every aspect of the animation:

Option Default Description
delay 100ms Typing speed
startDelay 500ms Initial delay
loopDelay 2000ms Pause before deleting
deleteDelay 50ms Deletion speed
cursorBlinkSpeed 800ms Cursor blink rate

Terminal Style Example

Perfect for command-line aesthetics:

const { displayText, cursorBlinkSpeed } = useTypewriter({
  text: 'npm install react-terminal-typewriter',
  delay: 50,
  startDelay: 1000
})
 
return (
  <div className="terminal">
    <span className="prompt">$ </span>
    {displayText}
    <span className="cursor" />
  </div>
)

Technical Implementation

React 18+ StrictMode Handling

The biggest challenge was handling React 18's StrictMode, which intentionally double-invokes effects in development. My solution uses refs to track running state:

const isRunningRef = useRef(false)
 
useEffect(() => {
  if (isRunningRef.current) return
  isRunningRef.current = true
  
  // Animation logic...
  
  return () => {
    isRunningRef.current = false
    clearTimeout(timeoutRef.current)
  }
}, [text])

Recursive setTimeout Pattern

Instead of setInterval, I use recursive setTimeout for precise timing control:

const tick = () => {
  if (isDeletingRef.current) {
    // Delete character
    setDisplayText(prev => prev.slice(0, -1))
    timeoutRef.current = setTimeout(tick, deleteDelay)
  } else {
    // Type character
    setDisplayText(text.slice(0, indexRef.current + 1))
    indexRef.current++
    timeoutRef.current = setTimeout(tick, delay)
  }
}

This approach ensures:

  • Consistent timing regardless of React re-renders
  • Clean cleanup on unmount
  • Proper state synchronization

What I Learned

Building this package taught me:

  • npm publishing workflow – from package.json setup to GitHub Actions CI/CD
  • Library bundling – using tsup for ESM/CJS dual output
  • React internals – deep understanding of useEffect lifecycle
  • TypeScript best practices – proper type exports and declarations
  • Documentation importance – README, examples, and live demo

Try It Yourself

🔗 Live Demo: vitalii4reva.github.io/react-terminal-typewriter
💻 Source Code: github.com/vitalii4reva/react-terminal-typewriter
📦 npm Package: npmjs.com/package/react-terminal-typewriter

The package is MIT licensed and open for contributions!

What's Next?

Planned features:

  • Multiple text strings – cycle through an array of texts
  • Pause/Resume control – programmatic animation control
  • Custom easing – variable typing speeds for more natural effect
  • onComplete callback – trigger actions when typing finishes

Final Thoughts

Sometimes the best solution is the simplest one. By focusing on a single hook with zero dependencies, I created something that's easy to use, maintain, and extend.

Need a typewriter effect in your React app? Give react-terminal-typewriter a try. And if you find bugs or have feature requests, open an issue on GitHub!


What's your favorite UI animation? Do you prefer building your own solutions or using libraries? Let me know!