webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Animations · Lesson 4 of 4

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.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.