/Components
typography

Text Reveal

Text that reveals character by character with a blur effect when scrolled into view.

textrevealscrollanimationtypographystagger

Preview

Simplicity,Engineered.
Buildbeautifulinterfaceswithoutunnecessarycomplexity.

🖲Usage

example-usage.tsx
import { TextReveal } from "@/components/kanso/text-reveal"

export default function TextRevealDemo() {
  return (
    <TextReveal 
      text="Simplicity, Engineered." 
      className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100" 
    />
  )
}

Installation

1

Create folder & copy source

Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named text-reveal.tsx inside it.

2

Install dependencies

pnpm add framer-motion
3

Required helper files

Ensure your project has the following helper files configured:

  • lib/utils

<>Source Code

text-reveal.tsx
"use client"

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

interface TextRevealProps extends React.HTMLAttributes<HTMLDivElement> {
  /** The text string to reveal */
  text: string
  /** Delay before animation starts in seconds (default: 0) */
  delay?: number
  /** Delay between each character in seconds (default: 0.03) */
  staggerDelay?: number
}

/**
 * TextReveal — Text that reveals character by character when scrolled into view.
 *
 * Uses Framer Motion's `useInView` hook to trigger a staggered opacity animation
 * on each character. Creates an elegant typewriter-like reveal effect.
 *
 * @example
 * ```tsx
 * import { TextReveal } from "@/components/kanso/text-reveal"
 *
 * export default function Demo() {
 *   return (
 *     <TextReveal
 *       text="Simplicity, Engineered."
 *       className="text-4xl font-semibold tracking-tight"
 *     />
 *   )
 * }
 * ```
 */
function TextReveal({
  text,
  className,
  delay = 0,
  staggerDelay = 0.03,
  ...props
}: TextRevealProps) {
  const ref = React.useRef<HTMLDivElement>(null)
  const isInView = useInView(ref, { once: true, margin: "-10% 0px" })

  const words = text.split(" ")

  return (
    <div ref={ref} className={cn("flex flex-wrap", className)} {...props}>
      {words.map((word, wordIndex) => (
        <span key={wordIndex} className="inline-block mr-[0.25em]">
          {word.split("").map((char, charIndex) => {
            // Calculate global index for stagger
            const globalIndex =
              words
                .slice(0, wordIndex)
                .reduce((acc, w) => acc + w.length, 0) + charIndex

            return (
              <motion.span
                key={`${wordIndex}-${charIndex}`}
                className="inline-block"
                initial={{ opacity: 0, y: 8, filter: "blur(4px)" }}
                animate={
                  isInView
                    ? { opacity: 1, y: 0, filter: "blur(0px)" }
                    : { opacity: 0, y: 8, filter: "blur(4px)" }
                }
                transition={{
                  duration: 0.3,
                  delay: delay + globalIndex * staggerDelay,
                  ease: [0.25, 0.1, 0.25, 1],
                }}
              >
                {char}
              </motion.span>
            )
          })}
        </span>
      ))}
    </div>
  )
}

export { TextReveal }
export type { TextRevealProps }

Props

PropTypeDefaultDescription
textrequiredstringThe text string to reveal.
delaynumber0Delay before animation starts in seconds.
staggerDelaynumber0.03Delay between each character reveal in seconds.
classNamestringAdditional CSS classes for styling the text.