Atelier UI®

Read the docsGithub
Docs 0.7.0

Getting started

  • Browse Catalog
  • Installation
  • How to contribute
  • Code of conduct

Components (19)

  • Dither Flow
    pro
  • Glowing Fog
    pro
  • Halftone Glow
    pro
  • Fluid Distortion
    new
  • Image Trail
    new
  • Liquid Image
    new
  • Magnetic Dot Grid
    new
  • Pixel Trail
    new
  • Pixelated Text
    new
  • Simple Scramble
    new
  • Text Bounce
    new
  • Text Fluid
    pro
  • Text Roll
    new
  • Curve Image
    new
  • Elastic Stick
    pro
  • Infinite Gallery
    new
  • Infinite Parallax
    new
  • Infinite Zoom
    new
  • Scattered Scroll
    new
Atelier UI 0.7.0 ©2026
Star on githubBuy me a coffeellms.txt
  1. Docs
  2. /
  3. Components
  4. /
  5. Text Bounce

Text Bounce

Letters bounce away from the cursor on hover and spring back into place.

motion
Tailwind CSS
https://atelier-ui.com/text-bounce

Settings

pause
0.00
out-duration
0.35
in-duration
0.80
bounce
0.50
distance
35
rotation
25
See the documentation below for more options.

CLI Install

npx atelier-ui add text-bounce

Manual Install

npm install motion
text-bounce.tsx
import { animate } from "motion"
import type React from "react"
import { type ComponentRef, useRef } from "react"
import { TextSplit } from "../text-split/text-split"

export type TextBounceProps = {
    children: string
    pause?: number
    outDuration?: number
    inDuration?: number
    bounce?: number
    distance?: number
    rotation?: number
}

type LetterProps = {
    char: string
    pause: number
    outDuration: number
    inDuration: number
    bounce: number
    distance: number
    rotation: number
}

function Letter({ char, pause, outDuration, inDuration, bounce, distance, rotation }: LetterProps) {
    const ref = useRef<ComponentRef<"span">>(null)

    const handlePointerEnter = async (event: React.PointerEvent<HTMLSpanElement>) => {
        const el = ref.current
        if (!el) return

        const rect = el.getBoundingClientRect()

        const cx = rect.left + rect.width / 2
        const cy = rect.top + rect.height / 2
        const dx = cx - event.clientX
        const dy = cy - event.clientY
        const dist = Math.sqrt(dx * dx + dy * dy) || 1
        const nx = dx / dist
        const ny = dy / dist

        await animate([
            [
                el,
                { x: nx * distance, y: ny * distance, rotate: nx * rotation },
                { duration: outDuration, ease: "circOut" },
            ],
            [
                el,
                { x: 0, y: 0, rotate: 0 },
                { type: "spring", duration: inDuration, bounce, delay: pause },
            ],
        ])
    }

    return (
        <span
            ref={ref}
            className="inline-block will-change-transform whitespace-pre"
            onPointerEnter={handlePointerEnter}
        >
            {char}
        </span>
    )
}

export function TextBounce({
    children,
    pause = 0,
    outDuration = 0.35,
    inDuration = 0.8,
    bounce = 0.5,
    distance = 35,
    rotation = 25,
}: TextBounceProps) {
    return (
        <TextSplit
            showMask={false}
            splitBy="letters"
            renderItems={(char, index) => {
                return (
                    <Letter
                        key={index}
                        char={char}
                        pause={pause}
                        outDuration={outDuration}
                        inDuration={inDuration}
                        bounce={bounce}
                        distance={distance}
                        rotation={rotation}
                    />
                )
            }}
        >
            {children}
        </TextSplit>
    )
}
text-split.tsx
import React from "react"

type SplitBy = "letters" | "words"

type SplitTextProps = {
    children: string
    splitBy?: SplitBy
    showMask?: boolean
    side?: "x" | "y"
    renderItems?: (char: string, index: number) => React.ReactNode
}

type MaskProps = {
    children: React.ReactNode
    showMask: boolean
} & React.HTMLAttributes<HTMLSpanElement>

function splitText(text: string, splitBy: SplitBy) {
    if (splitBy === "letters") return text.split("")
    if (splitBy === "words") return text.split(" ")
    return []
}

const Mask = ({ children, showMask, ...props }: MaskProps) => {
    if (!showMask) return children
    return (
        <span className="overflow-clip" {...props}>
            {children}
        </span>
    )
}

export function TextSplit({
    children,
    splitBy = "letters",
    showMask = true,
    renderItems,
    ...props
}: SplitTextProps & React.HTMLAttributes<HTMLSpanElement>) {
    if (typeof children !== "string") throw new Error("SplitText only accepts string children")

    const element = splitText(children, splitBy)

    return (
        <>
            {element.map((part, index) => {
                const spacer = index < element.length - 1 && " "
                const elements = splitText(part, splitBy)
                return (
                    <Mask {...props} showMask={showMask} key={index}>
                        {elements.map((char, i) => {
                            return (
                                <React.Fragment key={i}>
                                    {renderItems ? renderItems(char, index) : char}
                                </React.Fragment>
                            )
                        })}

                        {splitBy !== "letters" && spacer}
                    </Mask>
                )
            })}
        </>
    )
}

API

NameTypeDefaultDescription
childrenstring—Text content to render. Required.
pausenumber0Delay in seconds before springing back.
outDurationnumber0.35Duration in seconds for the letter drifting away.
inDurationnumber0.8Duration in seconds for the spring-back animation.
bouncenumber0.5Bounciness of the spring-back. 0 = no overshoot, 1 = maximum oscillation.
distancenumber35How many pixels the letter drifts from its origin.
rotationnumber25Max rotation in degrees applied along the horizontal drift axis. 0 disables it.
  • CLI Install
  • Manual Install
  • API
Star on githubBuy me a coffeellms.txt