Build a Scroll Float Text Animation in React
Animate text so it floats upward and fades in as the user scrolls, using Framer Motion's scroll-linked animation hooks.
Scroll-linked animations feel modern and considered. When text appears to float up as you scroll down the page, it creates a sense of depth, like content is emerging from beneath the surface. It is subtle, but users notice.
The final result
ScrollFloat splits text into individual characters, then animates each one into place as the element enters the viewport. The animation is scrubbed by scroll position, meaning it pauses if you stop scrolling and reverses if you scroll back up.
Setting up
This component uses GSAP with ScrollTrigger. The scrub: true option is what ties the animation to scroll position rather than running it on a timer.
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);Building the component
The component accepts children rather than a text prop directly, so it can be composed naturally inside JSX. We extract the string from children and split it into character spans.
const splitText = useMemo(() => {
const text = typeof children === 'string' ? children : '';
return text.split('').map((char, index) => (
<span className='inline-block word' key={index}>
{char === ' ' ? '\u00A0' : char}
</span>
));
}, [children]);Note the \u00A0 (non-breaking space) for space characters. Regular spaces inside inline-block spans collapse, so we use a non-breaking space to preserve them.
The animation runs in useEffect after mount. We query all the .inline-block spans and animate them from a heavily distorted state to their natural position.
gsap.fromTo(
charElements,
{
willChange: 'opacity, transform',
opacity: 0,
yPercent: 120,
scaleY: 2.3,
scaleX: 0.7,
transformOrigin: '50% 0%',
},
{
duration: animationDuration,
ease: ease,
opacity: 1,
yPercent: 0,
scaleY: 1,
scaleX: 1,
stagger: stagger,
scrollTrigger: {
trigger: el,
scroller,
start: scrollStart,
end: scrollEnd,
scrub: true,
},
}
);The starting scaleY: 2.3 and scaleX: 0.7 combined with yPercent: 120 creates a squashed, tall character that appears to spring upward as it snaps to normal proportions. It looks much more dynamic than a simple fade-up.
The scrollContainerRef prop lets you use a custom scroll container instead of the window, which is useful inside modals or scrollable panels.
const scroller = scrollContainerRef?.current ?? window;How to use it
<ScrollFloat
scrollStart="center bottom+=50%"
scrollEnd="bottom bottom-=40%"
stagger={0.03}
animationDuration={1}
ease="back.inOut(2)"
>
Scroll to reveal this text
</ScrollFloat>Adjust scrollStart and scrollEnd to control over what scroll range the animation plays out.
Key takeaways
scrub: trueon the ScrollTrigger is the key detail. It ties animation progress to scroll position rather than playing the animation on a timer.- The squash-and-stretch start values (
scaleY: 2.3, scaleX: 0.7) are what give each character that satisfying snap into place. - Replacing spaces with non-breaking spaces before wrapping in
inline-blockspans prevents layout collapse that would otherwise mess up word spacing.