A simple text scramble effect that reveals characters from left to right.
npx atelier-ui add simple-scramblenpm install import { type HtmlHTMLAttributes, useCallback, useEffect, useRef } from "react"
import { useFrameLoop } from "@/registry/hooks/use-frame-loop"
export type SimpleScrambleProps = {
duration?: number
playOnMount?: boolean
playOnHover?: boolean
characters?: string
as?: React.ElementType
} & HtmlHTMLAttributes<HTMLElement>
export function SimpleScramble({
children,
duration = 1,
playOnMount = true,
playOnHover = true,
characters = "abcdefghijklmnopqrstuvwxyz@!#*$%^&+_[]",
as,
...rest
}: SimpleScrambleProps) {
// biome-ignore lint/suspicious/noExplicitAny: Polymorphic component
const Tag = (as || "span") as any
const text = typeof children === "string" ? children : ""
const ref = useRef<HTMLElement>(null)
const startTime = useRef(0)
const isAnimating = useRef(false)
const play = useCallback(() => {
startTime.current = 0
isAnimating.current = true
}, [])
useEffect(() => {
if (playOnMount) play()
}, [text, playOnMount, play])
useFrameLoop((time) => {
if (!isAnimating.current) return
if (!ref.current) return
if (!startTime.current) startTime.current = time
const elapsed = time - startTime.current
const resolved = Math.floor((elapsed / duration) * text.length)
if (resolved >= text.length) {
ref.current.textContent = text
isAnimating.current = false
return
}
let next = ""
for (let i = 0; i < text.length; i++) {
if (i < resolved) next += text[i]
else if (text[i] === " ") next += " "
else next += characters[Math.floor(Math.random() * characters.length)]
}
ref.current.textContent = next
})
return (
<Tag
ref={ref}
onTouchStart={playOnHover ? play : undefined}
onMouseEnter={playOnHover ? play : undefined}
{...rest}
>
{text}
</Tag>
)
}
import { useEffect, useRef } from "react"
const DELTA_MAX = 0.1
type FrameLoopCallback = (time: number, delta: number) => void
export function useFrameLoop(callback: FrameLoopCallback, interval?: number) {
const ref = useRef(callback)
ref.current = callback
useEffect(() => {
let frameId = 0
let lastTime = 0
let lastTick = 0
const tick = (now: number) => {
frameId = requestAnimationFrame(tick)
if (interval && now - lastTick < interval) return
if (interval) lastTick = now
const time = now * 0.001
const delta = lastTime ? Math.min(time - lastTime, DELTA_MAX) : 0
lastTime = time
ref.current(time, delta)
}
frameId = requestAnimationFrame(tick)
return () => {
cancelAnimationFrame(frameId)
}
}, [interval])
}
| Name | Type | Default | Description |
|---|---|---|---|
| duration | number | 1 | Total duration of the reveal in seconds. |
| playOnMount | boolean | true | Plays the scramble animation on mount. |
| playOnHover | boolean | true | Replays the scramble animation on hover. |
| as | React.ElementType | "span" | Polymorphic tag: render as any HTML element. |
| characters | string | "abcdefghijkl mnopqrstuv wxyz!@#$%^&*()_+" | Characters to scramble. |
Yugop Nakamura
Original text scramble effect
use-scramble
A react hook for text scrambling by tolis