# Text Flip

Animated text that cycles through items with a smooth flip transition.

```tsx
"use client"

import { useRef } from "react"
import { motion, useInView, usePageInView } from "motion/react"

import { TextFlip } from "@/components/text-flip"

const WORDS = ["Developer", "Designer", "Creator", "Builder"]

export default function TextFlipDemo() {
  const ref = useRef<HTMLDivElement>(null)
  const isPageInView = usePageInView()
  const isInView = useInView(ref)
  const play = isPageInView && isInView

  return (
    <div
      ref={ref}
      className="flex items-center gap-1.5 text-2xl font-medium text-muted-foreground"
    >
      <span>I am a</span>
      <span className="inline-grid">
        {/* Placeholder for the tallest word */}
        <span className="invisible col-start-1 row-start-1" aria-hidden>
          {WORDS.reduce((a, b) => (a.length >= b.length ? a : b))}
        </span>
        <TextFlip
          as={motion.span}
          className="col-start-1 row-start-1 text-foreground"
          play={play}
        >
          {WORDS.map((word) => (
            <span key={word}>{word}</span>
          ))}
        </TextFlip>
      </span>
    </div>
  )
}

```

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn@latest add @ncdai/text-flip
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install motion clsx tailwind-merge
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" 
      import type { ClassValue } from "clsx"
      import { clsx } from "clsx"
      import { twMerge } from "tailwind-merge"

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs))
      }

      export function absoluteUrl(path: string) {
        return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
      }

      ```

      <Step>Copy and paste the following code into your project</Step>

      ```tsx title="components/text-flip.tsx" 
      "use client"

      import { Children, useEffect, useState } from "react"
      import type { Transition, Variants } from "motion/react"
      import { AnimatePresence, motion } from "motion/react"

      import { cn } from "@/lib/utils"

      const defaultVariants: Variants = {
        initial: { y: -8, opacity: 0 },
        animate: { y: 0, opacity: 1 },
        exit: { y: 8, opacity: 0 },
      }

      type MotionElement = typeof motion.p | typeof motion.span | typeof motion.code

      export type TextFlipProps = {
        /**
         * Motion element to render.
         * @defaultValue motion.p
         * */
        as?: MotionElement
        className?: string
        /** Array of children to cycle through. */
        children: React.ReactNode[]

        /**
         * Time in seconds between each flip.
         * @defaultValue 2
         * */
        interval?: number
        /**
         * Motion transition configuration.
         * @defaultValue { duration: 0.3 }
         * */
        transition?: Transition
        /** Motion variants for enter/exit animations. */
        variants?: Variants

        /** Controls whether the flip animation runs. */
        play?: boolean

        /** Called with the new index after each flip. */
        onIndexChange?: (index: number) => void
      }

      export function TextFlip({
        as: Component = motion.p,
        className,
        children,

        interval = 2,
        transition = { duration: 0.3 },
        variants = defaultVariants,
        play = true,

        onIndexChange,
      }: TextFlipProps) {
        const [currentIndex, setCurrentIndex] = useState(0)

        const items = Children.toArray(children)

        useEffect(() => {
          if (!play) return

          const timer = setInterval(() => {
            setCurrentIndex((prev) => {
              const next = (prev + 1) % items.length
              onIndexChange?.(next)
              return next
            })
          }, interval * 1000)

          return () => clearInterval(timer)
        }, [play, interval, items.length, onIndexChange])

        return (
          <AnimatePresence mode="wait" initial={false}>
            <Component
              key={currentIndex}
              className={cn("inline-block", className)}
              initial="initial"
              animate="animate"
              exit="exit"
              transition={transition}
              variants={variants}
            >
              {items[currentIndex]}
            </Component>
          </AnimatePresence>
        )
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

```tsx
import { TextFlip } from "@/components/text-flip"
```

```tsx
<TextFlip>
  <span>Developer</span>
  <span>Designer</span>
  <span>Creator</span>
</TextFlip>
```

## API Reference

### TextFlip

<TypeTable
  id="type-table-text-flip.tsx-TextFlipProps"
  type={{
  "id": "text-flip.tsx-TextFlipProps",
  "name": "TextFlipProps",
  "description": "",
  "entries": [
    {
      "name": "as",
      "description": "Motion element to render.",
      "tags": [
        {
          "name": "defaultValue",
          "text": "motion.p"
        }
      ],
      "type": "MotionElement | undefined",
      "simplifiedType": "union",
      "required": false,
      "deprecated": false
    },
    {
      "name": "className",
      "description": "",
      "tags": [],
      "type": "string | undefined",
      "simplifiedType": "string",
      "required": false,
      "deprecated": false
    },
    {
      "name": "children",
      "description": "Array of children to cycle through.",
      "tags": [],
      "type": "React.ReactNode[]",
      "simplifiedType": "array",
      "required": true,
      "deprecated": false
    },
    {
      "name": "interval",
      "description": "Time in seconds between each flip.",
      "tags": [
        {
          "name": "defaultValue",
          "text": "2"
        }
      ],
      "type": "number | undefined",
      "simplifiedType": "number",
      "required": false,
      "deprecated": false
    },
    {
      "name": "transition",
      "description": "Motion transition configuration.",
      "tags": [
        {
          "name": "defaultValue",
          "text": "{ duration: 0.3 }"
        }
      ],
      "type": "Transition | undefined",
      "simplifiedType": "union",
      "required": false,
      "deprecated": false
    },
    {
      "name": "variants",
      "description": "Motion variants for enter/exit animations.",
      "tags": [],
      "type": "Variants | undefined",
      "simplifiedType": "object",
      "required": false,
      "deprecated": false
    },
    {
      "name": "play",
      "description": "Controls whether the flip animation runs.",
      "tags": [],
      "type": "boolean | undefined",
      "simplifiedType": "union",
      "required": false,
      "deprecated": false
    },
    {
      "name": "onIndexChange",
      "description": "Called with the new index after each flip.",
      "tags": [],
      "type": "((index: number) => void) | undefined",
      "simplifiedType": "function",
      "required": false,
      "deprecated": false
    }
  ]
}}
/>

<DocSponsors />


Last updated on February 20, 2026