Implementing Scroll-Triggered Animations in Cloned Web Pages with the AI Website Cloner Template

You can implement scroll-triggered animations by creating a reusable useScrollAnimation hook that leverages the native IntersectionObserver API to toggle Tailwind CSS transition classes when cloned elements enter the viewport.

The AI Website Cloner Template from JCodesMore provides a modern Next.js 16 and React 19 stack with Tailwind CSS v4 and shadcn/ui primitives, making it straightforward to add sophisticated scroll-triggered animations to cloned websites. Since the template isolates each page section into reusable components within src/app and src/components, you can integrate viewport detection logic without modifying the build configuration. This guide demonstrates how to implement a type-safe hook that detects scroll position and applies smooth entrance animations using either pure CSS transitions or Framer Motion.

Creating the useScrollAnimation Hook

To detect when elements enter the viewport, create a custom hook in src/hooks/useScrollAnimation.ts that wraps the native IntersectionObserver API. This hook returns a ref to attach to any DOM element and a boolean isVisible flag that triggers your animation classes, with an optional once parameter to prevent replaying animations on scroll up.

import { useEffect, useRef, useState } from 'react'

type Options = {
  rootMargin?: string      // e.g. '0px 0px -100px 0px'
  threshold?: number       // 0 → 1
  once?: boolean           // stop observing after first entry?
}

/**
 * Returns a ref to attach to the element and a boolean `isVisible`.
 * When `isVisible` becomes true you can add any animation class.
 */
export function useScrollAnimation<T extends HTMLElement = HTMLDivElement>(
  options: Options = {}
) {
  const elementRef = useRef<T>(null)
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    const node = elementRef.current
    if (!node) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true)
          if (options.once) observer.unobserve(node)
        } else if (!options.once) {
          setIsVisible(false)
        }
      },
      {
        root: null,
        rootMargin: options.rootMargin ?? '0px',
        threshold: options.threshold ?? 0.1,
      }
    )
    observer.observe(node)
    return () => observer.disconnect()
  }, [elementRef, options])

  return { ref: elementRef, isVisible }
}

Applying Animations to Cloned Components

With the hook implemented, you can animate any cloned section by importing the useScrollAnimation hook and conditionally applying Tailwind classes. The template's cn() utility from src/lib/utils.ts helps compose class strings cleanly without template litter.

Using Tailwind CSS Transitions

Attach the ref from the hook to a wrapper element and toggle opacity and transform classes based on the isVisible state. This approach requires zero JavaScript animation libraries and leverages GPU-accelerated CSS transitions.

import { cn } from '@/lib/utils'               // utility for merging class names
import { useScrollAnimation } from '@/hooks/useScrollAnimation'

export default function FeatureCard() {
  const { ref, isVisible } = useScrollAnimation({ once: true })

  return (
    <div
      ref={ref}
      className={cn(
        'opacity-0 translate-y-8 transition-opacity transition-transform duration-700',
        isVisible && 'opacity-100 translate-y-0'
      )}
    >
      {/* ... cloned markup … */}
    </div>
  )
}

Integrating Framer Motion

For more complex animations, the hook works seamlessly with Framer Motion. Install the library (npm i framer-motion) and pass the isVisible state to a motion.div component to control initial and animate props.

import { motion, MotionProps } from 'framer-motion'
import { useScrollAnimation } from '@/hooks/useScrollAnimation'

export default function AnimatedImage() {
  const { ref, isVisible } = useScrollAnimation({ once: true })

  const motionProps: MotionProps = {
    initial: { opacity: 0, y: 30 },
    animate: { opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 },
    transition: { duration: 0.6, ease: 'easeOut' },
  }

  return (
    <motion.div ref={ref} {...motionProps}>
      {/* cloned image element */}
    </motion.div>
  )
}

Key Files and Architecture

Understanding the file structure in the JCodesMore/ai-website-cloner-template repository helps you place animation logic correctly:

  • src/hooks/useScrollAnimation.ts — The new hook containing the IntersectionObserver logic and TypeScript types.
  • src/lib/utils.ts — Provides the cn() helper function used to conditionally merge Tailwind class names.
  • src/components/ui/button.tsx — Example UI primitive that demonstrates how components receive and spread props, useful for testing the hook.
  • src/app/page.tsx — Main page entry point where cloned sections are assembled and where you can drop animated sections.
  • src/app/layout.tsx — Root layout that wraps every page; suitable for global scroll context providers if needed.

Summary

  • Create a reusable useScrollAnimation hook in src/hooks/useScrollAnimation.ts that wraps IntersectionObserver to detect viewport entry without polling.
  • Use the cn() utility from src/lib/utils.ts to cleanly toggle Tailwind animation classes like opacity-0 and translate-y-8 based on the isVisible state.
  • Apply the hook to any cloned component in src/app/page.tsx or src/components/ui/ to trigger performant scroll-triggered animations.
  • Optionally integrate Framer Motion for complex motion by passing the isVisible boolean to motion.div components for declarative animation control.

Frequently Asked Questions

How do I prevent scroll animations from replaying when scrolling back up?

Set the once option to true when calling the hook: useScrollAnimation({ once: true }). This configuration calls observer.unobserve(node) after the first intersection, ensuring the animation only triggers once per element and preventing unwanted replay effects when users scroll back up the page.

Can I use this approach with Framer Motion instead of Tailwind classes?

Yes. Instead of toggling CSS classes, pass the isVisible boolean returned by the hook to Framer Motion's animate prop, allowing you to specify precise initial and animate states with custom durations and easing curves for more complex motion sequences.

Where should I place scroll-triggered animation logic in a cloned project?

Create the reusable hook in src/hooks/useScrollAnimation.ts, then import it into individual components within src/components/ or sections in src/app/page.tsx. This approach maintains separation of concerns while allowing any cloned section to import animation functionality, consistent with the template's architecture of isolating reusable logic in dedicated hooks.

Is the IntersectionObserver approach performant for pages with many animated elements?

The IntersectionObserver API is highly performant because it runs on the browser's main thread without polling, and the hook's once option automatically disconnects observers after triggering. This prevents memory leaks and ensures smooth performance even when animating dozens of cloned sections on a single page.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →