Scroll and Swap Text
Scroll and Swap Text is a React and Tailwind CSS text component from Fancy Components, licensed MIT. Copy it and paste it into your project.
Fancy Components
MIT
text
What it is
Scroll and Swap Text is a React and Tailwind CSS text component from Fancy Components, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from Fancy Components and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Scroll and Swap Text
"use client"
import React, { ElementType, useMemo, useRef } from "react"
import { motion, useScroll, useTransform, useSpring } from "motion/react"
import { cn } from "@/lib/utils"
// handy function to extract text from children
const extractTextFromChildren = (children: React.ReactNode): string | undefined => {
// Handle null/undefined
if (children == null) return ""
// Handle string
if (typeof children === "string") return children
// Handle number
if (typeof children === "number") return String(children)
// Handle arrays (including fragments)
if (Array.isArray(children)) {
return children.map(extractTextFromChildren).join("")
}
// Handle React elements
if (React.isValidElement(children)) {
const props = (children as React.ReactElement).props
const childText = (props as any).children as React.ReactNode
// Recursively extract text from children
if (childText != null) {
return extractTextFromChildren(childText)
}
return ""
}
}
interface ScrollAndSwapTextProps {
/**
* The content to be displayed and animated
*/
children: React.ReactNode
/**
* HTML Tag to render the component as
* @default "span"
*/
as?: ElementType
/**
* Reference to the container element for scroll tracking
*/
containerRef: React.RefObject<HTMLElement | null>
/**
* Offset configuration for when the animation should start and end relative to the scroll container. Check motion documentation for more details.
* @default ["0 0", "0 1"]
*/
offset?: [string, string]
/**
* Additional CSS classes for styling the component
*/
className?: string
/**
* Spring animation configuration for smoothing the scroll-based animation
* @default { stiffness: 200, damping: 30 }
*/
springConfig?: {
stiffness?: number
damping?: number
mass?: number
}
}
/**
* ScrollAndSwapText creates a scroll-triggered text animation where text slides vertically
* based on scroll progress.
*/
const ScrollAndSwapText = ({
children,
as = "span",
offset = ["0 0", "0 1"],
className,
containerRef,
springConfig = { stiffness: 200, damping: 30 },
...props
}: ScrollAndSwapTextProps) => {
const ref = useRef<HTMLElement>(null)
// Convert children to string for processing with error handling
const text = useMemo(() => {
try {
return extractTextFromChildren(children)
} catch (error) {
console.error(error)
return ""
}
}, [children])
// Track scroll progress within the specified container and target element
const { scrollYProgress } = useScroll({
container: containerRef,
target: ref,
offset: offset as any, // framer motion doesnt export the type, so we have to cast it, sorry :/
})
// Apply spring physics to smooth the scroll-based animation
const springScrollYProgress = useSpring(scrollYProgress, springConfig)
// Transform scroll progress into vertical translation values
// Original text moves from 0% to -100% (slides up and out)
const top = useTransform(springScrollYProgress, [0, 1], ["0%", "-100%"])
// Replacement text moves from 100% to 0% (slides up from below)
const bottom = useTransform(springScrollYProgress, [0, 1], ["100%", "0%"])
const ElementTag = as
return (
<ElementTag
className={cn("flex overflow-hidden relative items-center justify-center p-0", className)}
ref={ref}
{...props}
>
<span className="relative text-transparent" aria-hidden="true">
{text}
</span>
<motion.span className="absolute" style={{ top: top }}>
{text}
</motion.span>
<motion.span
className="absolute"
style={{ top: bottom }}
aria-hidden="true"
>
{text}
</motion.span>
</ElementTag>
)
}
ScrollAndSwapText.displayName = "ScrollAndSwapText"
export default ScrollAndSwapText