Interesting examples
Masked text, scroll reveal in CSS, a magnetic button, a counter, a pinned section, layoutId, View Transitions.
Updated
Examples that make a site feel "premium"
Each example below is short, reusable and builds on what you learned in Animation basics, Motion and GSAP. All of them must respect prefers-reduced-motion.
1. A title that appears word by word
'use client'
import { motion, stagger } from 'motion/react'
export function RevealTitle({ text }: { text: string }) {
return (
<motion.h1 initial="hidden" animate="show" variants={{ show: { transition: { delayChildren: stagger(0.05) } } }}>
{text.split(' ').map((word, i) => (
<span key={i} className="inline-block overflow-hidden pr-[0.25em]">
<motion.span className="inline-block" variants={{ hidden: { y: '110%' }, show: { y: 0, transition: { ease: [0.22, 1, 0.36, 1], duration: 0.7 } } }}>
{word}
</motion.span>
</span>
))}
</motion.h1>
)
}The trick: each word sits in a span with overflow-hidden; the word rises from outside it → a "mask" effect.
2. Cards that enter on scroll, CSS only
@supports (animation-timeline: view()) {
.card {
animation: card-in linear both;
animation-timeline: view();
animation-range: entry 10% cover 30%;
}
}
@keyframes card-in {
from { opacity: 0; transform: translateY(40px) scale(0.96); }
}Zero JavaScript — the animation is tied directly to the element's position on screen. @supports leaves older browsers without the effect, but with visible content.
3. A "magnetic" button
The button moves slightly toward the cursor:
'use client'
import { motion, useSpring } from 'motion/react'
export function MagneticButton({ children }: { children: React.ReactNode }) {
const x = useSpring(0, { stiffness: 200, damping: 15 })
const y = useSpring(0, { stiffness: 200, damping: 15 })
return (
<motion.button
style={{ x, y }}
onPointerMove={e => {
const r = e.currentTarget.getBoundingClientRect()
x.set((e.clientX - (r.left + r.width / 2)) * 0.3) // 30% of the distance from the center
y.set((e.clientY - (r.top + r.height / 2)) * 0.3)
}}
onPointerLeave={() => (x.set(0), y.set(0))}
>
{children}
</motion.button>
)
}4. A counter that counts up to the value
'use client'
import { animate, useInView, useMotionValue, useTransform, motion } from 'motion/react'
import { useEffect, useRef } from 'react'
export function Counter({ to }: { to: number }) {
const ref = useRef(null)
const inView = useInView(ref, { once: true })
const value = useMotionValue(0)
const rounded = useTransform(value, v => Math.round(v).toLocaleString('en-US'))
useEffect(() => {
if (inView) animate(value, to, { duration: 1.4, ease: 'easeOut' })
}, [inView, to, value])
return <motion.span ref={ref}>{rounded}</motion.span>
}5. A "pinned" section with GSAP
The image stays fixed while the text changes as you scroll — classic scroll storytelling:
useGSAP(
() => {
const tl = gsap.timeline({ scrollTrigger: { trigger: '.story', start: 'top top', end: '+=2000', scrub: true, pin: true } })
tl.from('.step-1', { opacity: 0, y: 40 })
.to('.step-1', { opacity: 0, y: -40 })
.from('.step-2', { opacity: 0, y: 40 }, '<')
.to('.visual', { scale: 1.15, rotate: 3 }, 0)
},
{ scope: container },
)6. A list → detail transition with layoutId
// in the list
<motion.img layoutId={`photo-${p.id}`} src={p.src} />
// in the detail modal
<motion.img layoutId={`photo-${p.id}`} src={p.src} className="w-full" />The image "flies" from the card into the modal. Combined with intercepted routes, the modal gets its own URL too.
7. Page transitions with View Transitions
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 250ms;
}
.product-image { view-transition-name: product-hero; }document.startViewTransition(() => updateTheDOM())The browser takes a "snapshot" before and after, then animates the difference — including a morph between elements with the same view-transition-name.
A checklist before publishing
- Does it run at 60 fps on an average phone? (DevTools → Performance, 4× CPU throttling)
- Is the content visible without the animation too (JS disabled, reduced motion)?
- Do the animations have a purpose, not just decoration?
- Is the intro's total duration under ~1.2 s?
Summary
- Masked text + stagger, scroll reveals in pure CSS, a magnetic button, a counter, a pinned section,
layoutId, View Transitions. - Every example is a combination of: states, easing, stagger, scroll.
- Test on a phone and with reduced motion.