rimubhai_
PortfolioBlogComponents

© 2026 rimubhai_ All rights reserved.

ContactPrivacy

Text Writing Effect

A powerful text animation component featuring SVG vector outline path drawing, spring character physics, dynamic viewBox scaling, and 12 Google fonts.

Interactive Studio

Customize animation styles, fonts, speeds, font sizes, colors, and gradient themes.

Speed Duration2s
Font Size64px
Mutasim Fuad RimuMutasim Fuad Rimu

SVG Path Tracing

Calculates dynamic SVG viewBox aspect ratio so long text never cuts off.

8 Motion Styles

Includes Write, Bounce, Wave, Reveal, Typewriter, Fade, Scale, and Slide transitions.

12 Google Fonts

Includes Calligraphy, Cursive, Script, Serif, Sans, Display, and Cinematic typography styles.

Framer Motion

Hardware-accelerated 60fps animations with spring physics and full speed control.

Manual Installation Guide

01

Install Dependencies

Install framer-motion and lucide-react.

pnpm add framer-motion lucide-react
02

Create Component File

Create components/ui/text-writing-effect.tsx.

"use client"

import * as React from "react"
import { motion, HTMLMotionProps, Variants } from "framer-motion"
import { cn } from "@/lib/utils"

export type AnimationStyle =
  | "write"
  | "reveal"
  | "bounce"
  | "wave"
  | "typewriter"
  | "fade"
  | "scale"
  | "slide"

export interface TextWritingEffectProps
  extends Omit<HTMLMotionProps<"div">, "children"> {
  /** The text string to animate */
  text: string
  /** Animation style to apply */
  animationStyle?: AnimationStyle
  /** Font class layout from Next.js google fonts or standard Tailwind */
  fontClassName?: string
  /** Total animation duration in seconds */
  speed?: number
  /** Standard solid color fallback. Defaults to 'currentColor' */
  color?: string
  /** Font size in pixels (number) or CSS clamp/responsive string */
  fontSize?: number | string
  /** An array of colors to build a vibrant linear gradient */
  gradientColors?: string[]
  /** Thickness of the traced vector line (for 'write' style) */
  strokeWidth?: number
  /** Multiplier determining when solid fill fades in relative to speed (0-1) */
  fillDelayRatio?: number
  /** Delay before starting animation in seconds */
  delay?: number
  /** If true, the animation plays when scrolled into view */
  triggerOnView?: boolean
  /** Custom SVG viewBox override for 'write' mode */
  viewBox?: string
}

export const TextWritingEffect = React.forwardRef<
  HTMLDivElement,
  TextWritingEffectProps
>(
  (
    {
      text,
      animationStyle = "write",
      fontClassName,
      speed = 2,
      color = "currentColor",
      fontSize = 64,
      gradientColors,
      strokeWidth = 1.5,
      fillDelayRatio = 0.55,
      delay = 0,
      triggerOnView = false,
      viewBox,
      className,
      ...props
    },
    ref
  ) => {
    const uniqueId = React.useId()
    const gradientId = `text-write-gradient-${uniqueId.replace(/:/g, "")}`
    const isGradient = Array.isArray(gradientColors) && gradientColors.length > 0
    const paintTarget = isGradient ? `url(#${gradientId})` : color

    // Numeric base estimation for SVG calculations
    const numericFontSize = typeof fontSize === "number" ? fontSize : 64

    // Viewport listener options
    const interactionProps = triggerOnView
      ? {
          initial: "initial",
          whileInView: "animate",
          viewport: { once: true, margin: "-10%" },
        }
      : { initial: "initial", animate: "animate" }

    // --- Mode 1: SVG Vector Writing Path Animation ---
    if (animationStyle === "write") {
      const charCount = text.length || 1
      const calculatedWidth = Math.max(
        900,
        Math.round(charCount * (numericFontSize * 0.85) + numericFontSize * 2)
      )
      const calculatedHeight = Math.max(240, Math.round(numericFontSize * 3.5))
      const svgViewBox = viewBox || `0 0 ${calculatedWidth} ${calculatedHeight}`

      const pathLength = Math.max(2400, charCount * numericFontSize * 3)

      const strokeVariants: Variants = {
        initial: {
          strokeDasharray: pathLength,
          strokeDashoffset: pathLength,
        },
        animate: {
          strokeDashoffset: 0,
          transition: {
            duration: Math.max(0.4, speed),
            delay,
            ease: [0.42, 0, 0.58, 1],
          },
        },
      }

      const fillVariants: Variants = {
        initial: {
          opacity: 0,
          scale: 0.985,
        },
        animate: {
          opacity: 1,
          scale: 1,
          transition: {
            duration: 0.6,
            ease: "easeOut",
            delay: delay + speed * fillDelayRatio,
          },
        },
      }

      return (
        <motion.div
          ref={ref}
          className={cn(
            "flex h-auto w-full items-center justify-center overflow-visible p-4 select-none",
            className
          )}
          {...interactionProps}
          {...props}
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox={svgViewBox}
            className="h-full w-full max-w-full overflow-visible"
            preserveAspectRatio="xMidYMid meet"
          >
            <defs>
              {isGradient && (
                <linearGradient
                  id={gradientId}
                  x1="0%"
                  y1="0%"
                  x2="100%"
                  y2="0%"
                >
                  {gradientColors.map((stopColor, idx) => {
                    const calculatedOffset =
                      (idx / (gradientColors.length - 1)) * 100
                    return (
                      <stop
                        key={`${stopColor}-${idx}`}
                        offset={`${calculatedOffset}%`}
                        stopColor={stopColor}
                      />
                    )
                  })}
                </linearGradient>
              )}
            </defs>

            <motion.text
              x="50%"
              y="50%"
              textAnchor="middle"
              dominantBaseline="central"
              className={cn("fill-transparent", fontClassName)}
              style={{
                stroke: paintTarget,
                fontSize:
                  typeof fontSize === "number" ? `${fontSize}px` : fontSize,
              }}
              strokeWidth={strokeWidth}
              strokeLinecap="round"
              strokeLinejoin="round"
              variants={strokeVariants}
            >
              {text}
            </motion.text>

            <motion.text
              x="50%"
              y="50%"
              textAnchor="middle"
              dominantBaseline="central"
              className={cn(
                "pointer-events-none stroke-transparent",
                fontClassName
              )}
              style={{
                fill: paintTarget,
                fontSize:
                  typeof fontSize === "number" ? `${fontSize}px` : fontSize,
              }}
              variants={fillVariants}
            >
              {text}
            </motion.text>
          </svg>
        </motion.div>
      )
    }

    // --- Mode 2: Character-by-Character Animations (Grouped by Words) ---
    const words = text.split(" ")
    const totalChars = text.length || 1

    const getInitial = () => {
      switch (animationStyle) {
        case "reveal":
          return { opacity: 0, y: 22, filter: "blur(12px)" }
        case "bounce":
          return { opacity: 0, y: -40, scale: 0.3 }
        case "wave":
          return { opacity: 0, y: 24, rotateX: -90, scale: 0.8 }
        case "typewriter":
          return { opacity: 0, scale: 0.75, filter: "blur(4px)" }
        case "fade":
          return { opacity: 0, y: 14 }
        case "scale":
          return { opacity: 0, scale: 0.15, rotate: -20 }
        case "slide":
          return { opacity: 0, x: -50, skewX: 25 }
        default:
          return { opacity: 0, y: 12 }
      }
    }

    const getAnimate = () => {
      switch (animationStyle) {
        case "reveal":
          return { opacity: 1, y: 0, filter: "blur(0px)" }
        case "bounce":
          return { opacity: 1, y: 0, scale: 1 }
        case "wave":
          return { opacity: 1, y: 0, rotateX: 0, scale: 1 }
        case "typewriter":
          return { opacity: 1, scale: 1, filter: "blur(0px)" }
        case "fade":
          return { opacity: 1, y: 0 }
        case "scale":
          return { opacity: 1, scale: 1, rotate: 0 }
        case "slide":
          return { opacity: 1, x: 0, skewX: 0 }
        default:
          return { opacity: 1, y: 0 }
      }
    }

    const getTransition = (index: number) => {
      const charDelay = delay + (index / totalChars) * (speed * 0.65)
      const baseDuration = Math.max(0.2, speed * 0.35)

      if (animationStyle === "bounce") {
        return {
          type: "spring" as const,
          stiffness: 350,
          damping: 14,
          delay: charDelay,
        }
      }

      if (animationStyle === "typewriter") {
        return {
          duration: 0.1,
          delay: charDelay,
          ease: "linear" as const,
        }
      }

      return {
        duration: baseDuration,
        delay: charDelay,
        ease: "easeOut" as const,
      }
    }

    const gradientStyle = isGradient
      ? {
          backgroundImage: `linear-gradient(to right, ${gradientColors.join(", ")})`,
          WebkitBackgroundClip: "text",
          WebkitTextFillColor: "transparent",
        }
      : { color }

    const autoPaddingX = "0.15em"
    const autoPaddingY = "0.25em"
    const autoMarginX = "-0.07em"
    const autoMarginY = "-0.12em"

    let globalCharOffset = 0

    return (
      <motion.div
        ref={ref}
        className={cn(
          "flex flex-wrap items-center justify-center overflow-visible p-2 text-center select-none",
          className
        )}
        {...interactionProps}
        {...props}
      >
        {words.map((word, wordIdx) => {
          const wordStartOffset = globalCharOffset
          globalCharOffset += word.length + 1

          return (
            <span
              key={`word-${wordIdx}-${word}`}
              className="inline-flex flex-nowrap items-center whitespace-nowrap overflow-visible"
            >
              {word.split("").map((character, charIdx) => {
                const charIndex = wordStartOffset + charIdx
                return (
                  <motion.span
                    key={`${character}-${charIndex}`}
                    initial={getInitial()}
                    animate={getAnimate()}
                    transition={getTransition(charIndex)}
                    className={cn(
                      "inline-block overflow-visible whitespace-pre",
                      fontClassName
                    )}
                    style={{
                      ...gradientStyle,
                      fontSize:
                        typeof fontSize === "number"
                          ? `${fontSize}px`
                          : fontSize,
                      lineHeight: 1.4,
                      paddingLeft: autoPaddingX,
                      paddingRight: autoPaddingX,
                      paddingTop: autoPaddingY,
                      paddingBottom: autoPaddingY,
                      marginLeft: autoMarginX,
                      marginRight: autoMarginX,
                      marginTop: autoMarginY,
                      marginBottom: autoMarginY,
                    }}
                  >
                    {character}
                  </motion.span>
                )
              })}
              {wordIdx < words.length - 1 && (
                <span
                  className="inline-block"
                  style={{
                    fontSize:
                      typeof fontSize === "number"
                        ? `${fontSize}px`
                        : fontSize,
                    width: "0.25em",
                  }}
                >
                  &nbsp;
                </span>
              )}
            </span>
          )
        })}
      </motion.div>
    )
  }
)

TextWritingEffect.displayName = "TextWritingEffect"
03

Use in Your Application

Import and add the component to your Next.js pages or components.

import { TextWritingEffect } from "@/components/ui/text-writing-effect"

export default function Page() {
  return (
    <TextWritingEffect
      text="Mutasim Fuad Rimu"
      animationStyle="write"
      fontSize={68}
      speed={2}
      gradientColors={["#10b981", "#6366f1"]}
    />
  )
}

Component Code

"use client"

import * as React from "react"
import { motion, HTMLMotionProps, Variants } from "framer-motion"
import { cn } from "@/lib/utils"

export type AnimationStyle =
  | "write"
  | "reveal"
  | "bounce"
  | "wave"
  | "typewriter"
  | "fade"
  | "scale"
  | "slide"

export interface TextWritingEffectProps
  extends Omit<HTMLMotionProps<"div">, "children"> {
  /** The text string to animate */
  text: string
  /** Animation style to apply */
  animationStyle?: AnimationStyle
  /** Font class layout from Next.js google fonts or standard Tailwind */
  fontClassName?: string
  /** Total animation duration in seconds */
  speed?: number
  /** Standard solid color fallback. Defaults to 'currentColor' */
  color?: string
  /** Font size in pixels (number) or CSS clamp/responsive string */
  fontSize?: number | string
  /** An array of colors to build a vibrant linear gradient */
  gradientColors?: string[]
  /** Thickness of the traced vector line (for 'write' style) */
  strokeWidth?: number
  /** Multiplier determining when solid fill fades in relative to speed (0-1) */
  fillDelayRatio?: number
  /** Delay before starting animation in seconds */
  delay?: number
  /** If true, the animation plays when scrolled into view */
  triggerOnView?: boolean
  /** Custom SVG viewBox override for 'write' mode */
  viewBox?: string
}

export const TextWritingEffect = React.forwardRef<
  HTMLDivElement,
  TextWritingEffectProps
>(
  (
    {
      text,
      animationStyle = "write",
      fontClassName,
      speed = 2,
      color = "currentColor",
      fontSize = 64,
      gradientColors,
      strokeWidth = 1.5,
      fillDelayRatio = 0.55,
      delay = 0,
      triggerOnView = false,
      viewBox,
      className,
      ...props
    },
    ref
  ) => {
    const uniqueId = React.useId()
    const gradientId = `text-write-gradient-${uniqueId.replace(/:/g, "")}`
    const isGradient = Array.isArray(gradientColors) && gradientColors.length > 0
    const paintTarget = isGradient ? `url(#${gradientId})` : color

    // Numeric base estimation for SVG calculations
    const numericFontSize = typeof fontSize === "number" ? fontSize : 64

    // Viewport listener options
    const interactionProps = triggerOnView
      ? {
          initial: "initial",
          whileInView: "animate",
          viewport: { once: true, margin: "-10%" },
        }
      : { initial: "initial", animate: "animate" }

    // --- Mode 1: SVG Vector Writing Path Animation ---
    if (animationStyle === "write") {
      const charCount = text.length || 1
      const calculatedWidth = Math.max(
        900,
        Math.round(charCount * (numericFontSize * 0.85) + numericFontSize * 2)
      )
      const calculatedHeight = Math.max(240, Math.round(numericFontSize * 3.5))
      const svgViewBox = viewBox || `0 0 ${calculatedWidth} ${calculatedHeight}`

      const pathLength = Math.max(2400, charCount * numericFontSize * 3)

      const strokeVariants: Variants = {
        initial: {
          strokeDasharray: pathLength,
          strokeDashoffset: pathLength,
        },
        animate: {
          strokeDashoffset: 0,
          transition: {
            duration: Math.max(0.4, speed),
            delay,
            ease: [0.42, 0, 0.58, 1],
          },
        },
      }

      const fillVariants: Variants = {
        initial: {
          opacity: 0,
          scale: 0.985,
        },
        animate: {
          opacity: 1,
          scale: 1,
          transition: {
            duration: 0.6,
            ease: "easeOut",
            delay: delay + speed * fillDelayRatio,
          },
        },
      }

      return (
        <motion.div
          ref={ref}
          className={cn(
            "flex h-auto w-full items-center justify-center overflow-visible p-4 select-none",
            className
          )}
          {...interactionProps}
          {...props}
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox={svgViewBox}
            className="h-full w-full max-w-full overflow-visible"
            preserveAspectRatio="xMidYMid meet"
          >
            <defs>
              {isGradient && (
                <linearGradient
                  id={gradientId}
                  x1="0%"
                  y1="0%"
                  x2="100%"
                  y2="0%"
                >
                  {gradientColors.map((stopColor, idx) => {
                    const calculatedOffset =
                      (idx / (gradientColors.length - 1)) * 100
                    return (
                      <stop
                        key={`${stopColor}-${idx}`}
                        offset={`${calculatedOffset}%`}
                        stopColor={stopColor}
                      />
                    )
                  })}
                </linearGradient>
              )}
            </defs>

            <motion.text
              x="50%"
              y="50%"
              textAnchor="middle"
              dominantBaseline="central"
              className={cn("fill-transparent", fontClassName)}
              style={{
                stroke: paintTarget,
                fontSize:
                  typeof fontSize === "number" ? `${fontSize}px` : fontSize,
              }}
              strokeWidth={strokeWidth}
              strokeLinecap="round"
              strokeLinejoin="round"
              variants={strokeVariants}
            >
              {text}
            </motion.text>

            <motion.text
              x="50%"
              y="50%"
              textAnchor="middle"
              dominantBaseline="central"
              className={cn(
                "pointer-events-none stroke-transparent",
                fontClassName
              )}
              style={{
                fill: paintTarget,
                fontSize:
                  typeof fontSize === "number" ? `${fontSize}px` : fontSize,
              }}
              variants={fillVariants}
            >
              {text}
            </motion.text>
          </svg>
        </motion.div>
      )
    }

    // --- Mode 2: Character-by-Character Animations (Grouped by Words) ---
    const words = text.split(" ")
    const totalChars = text.length || 1

    const getInitial = () => {
      switch (animationStyle) {
        case "reveal":
          return { opacity: 0, y: 22, filter: "blur(12px)" }
        case "bounce":
          return { opacity: 0, y: -40, scale: 0.3 }
        case "wave":
          return { opacity: 0, y: 24, rotateX: -90, scale: 0.8 }
        case "typewriter":
          return { opacity: 0, scale: 0.75, filter: "blur(4px)" }
        case "fade":
          return { opacity: 0, y: 14 }
        case "scale":
          return { opacity: 0, scale: 0.15, rotate: -20 }
        case "slide":
          return { opacity: 0, x: -50, skewX: 25 }
        default:
          return { opacity: 0, y: 12 }
      }
    }

    const getAnimate = () => {
      switch (animationStyle) {
        case "reveal":
          return { opacity: 1, y: 0, filter: "blur(0px)" }
        case "bounce":
          return { opacity: 1, y: 0, scale: 1 }
        case "wave":
          return { opacity: 1, y: 0, rotateX: 0, scale: 1 }
        case "typewriter":
          return { opacity: 1, scale: 1, filter: "blur(0px)" }
        case "fade":
          return { opacity: 1, y: 0 }
        case "scale":
          return { opacity: 1, scale: 1, rotate: 0 }
        case "slide":
          return { opacity: 1, x: 0, skewX: 0 }
        default:
          return { opacity: 1, y: 0 }
      }
    }

    const getTransition = (index: number) => {
      const charDelay = delay + (index / totalChars) * (speed * 0.65)
      const baseDuration = Math.max(0.2, speed * 0.35)

      if (animationStyle === "bounce") {
        return {
          type: "spring" as const,
          stiffness: 350,
          damping: 14,
          delay: charDelay,
        }
      }

      if (animationStyle === "typewriter") {
        return {
          duration: 0.1,
          delay: charDelay,
          ease: "linear" as const,
        }
      }

      return {
        duration: baseDuration,
        delay: charDelay,
        ease: "easeOut" as const,
      }
    }

    const gradientStyle = isGradient
      ? {
          backgroundImage: `linear-gradient(to right, ${gradientColors.join(", ")})`,
          WebkitBackgroundClip: "text",
          WebkitTextFillColor: "transparent",
        }
      : { color }

    const autoPaddingX = "0.15em"
    const autoPaddingY = "0.25em"
    const autoMarginX = "-0.07em"
    const autoMarginY = "-0.12em"

    let globalCharOffset = 0

    return (
      <motion.div
        ref={ref}
        className={cn(
          "flex flex-wrap items-center justify-center overflow-visible p-2 text-center select-none",
          className
        )}
        {...interactionProps}
        {...props}
      >
        {words.map((word, wordIdx) => {
          const wordStartOffset = globalCharOffset
          globalCharOffset += word.length + 1

          return (
            <span
              key={`word-${wordIdx}-${word}`}
              className="inline-flex flex-nowrap items-center whitespace-nowrap overflow-visible"
            >
              {word.split("").map((character, charIdx) => {
                const charIndex = wordStartOffset + charIdx
                return (
                  <motion.span
                    key={`${character}-${charIndex}`}
                    initial={getInitial()}
                    animate={getAnimate()}
                    transition={getTransition(charIndex)}
                    className={cn(
                      "inline-block overflow-visible whitespace-pre",
                      fontClassName
                    )}
                    style={{
                      ...gradientStyle,
                      fontSize:
                        typeof fontSize === "number"
                          ? `${fontSize}px`
                          : fontSize,
                      lineHeight: 1.4,
                      paddingLeft: autoPaddingX,
                      paddingRight: autoPaddingX,
                      paddingTop: autoPaddingY,
                      paddingBottom: autoPaddingY,
                      marginLeft: autoMarginX,
                      marginRight: autoMarginX,
                      marginTop: autoMarginY,
                      marginBottom: autoMarginY,
                    }}
                  >
                    {character}
                  </motion.span>
                )
              })}
              {wordIdx < words.length - 1 && (
                <span
                  className="inline-block"
                  style={{
                    fontSize:
                      typeof fontSize === "number"
                        ? `${fontSize}px`
                        : fontSize,
                    width: "0.25em",
                  }}
                >
                  &nbsp;
                </span>
              )}
            </span>
          )
        })}
      </motion.div>
    )
  }
)

TextWritingEffect.displayName = "TextWritingEffect"

Props Reference

PropTypeDefaultDescription
textstringRequiredThe string of text to animate.
animationStyle"write" | "reveal" | "bounce" | "wave" | "typewriter" | "fade" | "scale" | "slide""write"The motion transition style. write triggers SVG vector path outline tracing.
fontSizenumber64Font size in pixels for all animation styles (including SVG write mode).
speednumber2Total animation duration in seconds.
gradientColorsstring[]undefinedAn array of hex or HSL color strings to construct a smooth linear gradient.
colorstring"currentColor"Solid text color fallback when gradientColors is omitted.

Usage Examples

svg Write
<TextWritingEffect
  text="Mutasim Fuad Rimu"
  animationStyle="write"
  speed={2}
  fontSize={68}
  fontClassName={pacifico.className}
  gradientColors={["#10b981", "#6366f1"]}
/>
character Bounce
<TextWritingEffect
  text="Spring Bounce Motion"
  animationStyle="bounce"
  fontSize={64}
  speed={1.8}
  color="#ec4899"
  fontClassName={lobster.className}
/>
gradient Wave
<TextWritingEffect
  text="3D Fluid Wave Animation"
  animationStyle="wave"
  fontSize={58}
  gradientColors={["#a855f7", "#ec4899", "#f43f5e"]}
  fontClassName={greatVibes.className}
/>