Docs
Type Writer

Type Writer

A Type Writer effect to your texts.

Install the following dependencies:

pnpm add gsap @gsap/react

Make a file for cn and mergerRef functions and match the import afterwards

import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))
 
export function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
  return (node: T | null) => {
    refs.forEach((ref) => {
      if (typeof ref === "function") {
        ref(node)
      } else if (ref && "current" in ref) {
        ;(ref as React.MutableRefObject<T | null>).current = node
      }
    })
  }
}

Add this configurations to tailwind.config.js file

 extend:{
    keyframes:{
      'blink-border':{
        '0%,70%,100%': {
  		    "borderColor": 'white'
  		  },
  			'20%,50%': {
  				"borderColor": 'black'
  			}
			}
    },
    animation:{
      'blink-border' : 'blink-border 1s infinite'
    }
 }

Make a file and copy paste this code in a file with name type-writer.tsx

"use client"
 
import { forwardRef, useRef, HTMLAttributes, useEffect, useState } from "react"
import gsap from "gsap"
import { cn, mergeRefs } from "@/lib/utils"
import { useGSAP } from "@gsap/react"
import { ScrollTrigger } from "gsap/ScrollTrigger"
 
gsap.registerPlugin(ScrollTrigger)
type TextAndClass = {
  text: string
  className?: string
}
 
interface TypeWriterProps extends HTMLAttributes<HTMLDivElement> {
  staticText: TextAndClass[]
  textArray?: TextAndClass[]
  delay?: number
  duration?: number
  ease?: gsap.EaseString | gsap.EaseFunction
  className?: string
  start?: string | number | ((tag?: ScrollTrigger) => string | number)
  end?: string | number | ((tag?: ScrollTrigger) => string | number)
  arrayInterval?: number
  deleteSpeed?: number
}
 
const TypeWriter = forwardRef<HTMLDivElement, TypeWriterProps>(
  (
    {
      staticText,
      textArray,
      children,
      delay = 0,
      start = "top 90%",
      end = "top",
      duration = 0.5,
      ease = "none",
      className = "",
      arrayInterval = 3000,
      deleteSpeed = 0.1,
      ...props
    },
    ref
  ) => {
    const containerRef = useRef<HTMLDivElement>(null)
    const textRef = useRef<HTMLDivElement>(null)
    const [currentArrayText, setCurrentArrayText] = useState("")
    const [arrayIndex, setArrayIndex] = useState(0)
    const [isDeleting, setIsDeleting] = useState(false)
    const [mainTextComplete, setMainTextComplete] = useState(false)
 
    // Handle the main text animation
    useGSAP(
      () => {
        if (!textRef.current) return
 
        const animation = gsap.from(textRef.current, {
          width: 0,
          duration: duration || staticText.length * 0.3,
          delay,
          ease,
          onComplete: () => setMainTextComplete(true),
          scrollTrigger: {
            trigger: textRef.current,
            start,
            end,
            toggleActions: "play none none reset",
            onLeaveBack: () => setMainTextComplete(false),
          },
        })
 
        return () => {
          animation.kill()
        }
      },
      {
        dependencies: [staticText, delay, start, end, duration, ease],
        scope: containerRef,
      }
    )
 
    // Handle the textArray animations
    useEffect(() => {
      if (!textArray?.length || !mainTextComplete) return
 
      let timeout: ReturnType<typeof setTimeout>
      let isActive = true // Flag to prevent state updates after unmount
 
      const animateText = () => {
        if (!isActive) return
 
        if (isDeleting) {
          if (currentArrayText.length > 0) {
            setCurrentArrayText((prev) => prev.slice(0, -1))
            timeout = setTimeout(animateText, deleteSpeed * 1000)
          } else {
            setIsDeleting(false)
            setArrayIndex((prev) => (prev + 1) % textArray.length)
          }
        } else {
          const targetText = textArray[arrayIndex].text
          if (currentArrayText.length < targetText.length) {
            setCurrentArrayText((prev) => targetText.slice(0, prev.length + 1))
            timeout = setTimeout(animateText, deleteSpeed * 1000)
          } else {
            timeout = setTimeout(
              () => isActive && setIsDeleting(true),
              arrayInterval
            )
          }
        }
      }
 
      timeout = setTimeout(
        animateText,
        currentArrayText.length === 0 ? 0 : deleteSpeed * 1000
      )
 
      return () => {
        isActive = false
        clearTimeout(timeout)
      }
    }, [
      textArray,
      arrayIndex,
      currentArrayText,
      isDeleting,
      arrayInterval,
      deleteSpeed,
      mainTextComplete,
    ])
 
    // Reset array text when main animation resets
    useEffect(() => {
      if (!mainTextComplete) {
        setCurrentArrayText("")
        setArrayIndex(0)
        setIsDeleting(false)
      }
    }, [mainTextComplete])
 
    return (
      <div
        ref={mergeRefs(ref, containerRef)}
        className={cn("flex flex-col gap-2", className)}
        {...props}
      >
        <div className="flex items-center animate-blink-border border-r-2 pr-1">
          <div ref={textRef} className="flex  text-nowrap overflow-hidden ">
            {staticText.map((data, i) => (
              <span key={`staticText-${i}`} className={cn(data.className)}>
                {data.text}&nbsp;
              </span>
            ))}
          </div>
          {textArray && (
            <div className={cn(textArray[arrayIndex]?.className)}>
              {currentArrayText}
            </div>
          )}
        </div>
      </div>
    )
  }
)
 
TypeWriter.displayName = "TypeWriter"
 
export { TypeWriter }

Props

NameTypeDescription
staticTextArray<{ text: string, className?: string }>An array of objects containing text and optional class names.
textArrayArray<{ text: string, className?: string }>An array of strings used for dynamic text rendering.
childrenReactNodeReact children to be rendered inside the component.
delaynumberDelay before the animation starts (in seconds). Default is 0.
startstring | number | () => string | numberAnimation start point, GSAP docs. Default is "top 90%".
endstring | number | () => string | numberAnimation end point, GSAP docs. Default is "top".
durationnumberDuration of the animation (in seconds). Default is 0.5.
easegsap.EaseString| gsap.EaseFunctionEasing function for animation, GSAP docs. Default is "none".
classNamestringAdditional CSS classes for styling. Default is an empty string.
arrayIntervalnumberTime interval (in milliseconds) between each text change. Default is 3000.
deleteSpeednumberSpeed (in seconds) at which text is deleted during animations. Default is 0.1.
propsobjectAny additional props passed to the component.