# Implementing Scroll-Driven Interactions with IntersectionObserver in Cloned Components

> Learn to implement scroll-driven interactions with IntersectionObserver in cloned components using a custom React hook and Tailwind CSS for dynamic animations and highlighting.

- Repository: [JCodesMore/ai-website-cloner-template](https://github.com/JCodesMore/ai-website-cloner-template)
- Tags: how-to-guide
- Published: 2026-07-09

---

**Use the native Intersection Observer API inside a custom React hook to detect when elements enter the viewport, then apply conditional Tailwind classes via the `cn()` utility to create scroll-based navigation highlighting and animations in any cloned component.**

The **ai-website-cloner-template** provides a modern Next.js 16 foundation with TypeScript strict mode, Tailwind v4, and shadcn/ui primitives. When cloning websites that require scroll-driven behaviors—such as sticky navigation highlighting or section-triggered animations—you need a performant, reusable solution that doesn't rely on expensive scroll event listeners. The browser's native **Intersection Observer API** paired with a custom React hook provides exactly that, integrating seamlessly with the template's existing architecture in [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) and `src/components/ui/`.

## Understanding the Architecture

The template separates concerns across distinct layers, making it straightforward to add scroll-driven functionality without modifying core UI primitives.

| Layer | Purpose | Key File |
|-------|---------|----------|
| **Utility helpers** | Tailwind class merging and type-safe utilities | [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) |
| **UI primitives** | Reusable shadcn/ui components (buttons, cards, etc.) | [`src/components/ui/button.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/button.tsx) |
| **Custom hooks** | Encapsulated reusable logic for browser APIs | [`src/hooks/useIntersectionObserver.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/hooks/useIntersectionObserver.ts) |
| **Page components** | Route-level components consuming hooks and UI | [`src/app/page.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/page.tsx) |

By placing Intersection Observer logic in a dedicated hook layer, you keep presentational components pure while enabling any cloned component—tabs, carousels, or section navigators—to react to viewport changes.

## Creating the useIntersectionObserver Hook

Create [`src/hooks/useIntersectionObserver.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/hooks/useIntersectionObserver.ts) to encapsulate the native API with automatic cleanup and TypeScript safety.

```typescript
// src/hooks/useIntersectionObserver.ts
import { useEffect, useState, RefObject } from "react";

type Options = IntersectionObserverInit;

export function useIntersectionObserver<T extends Element>(
  ref: RefObject<T>,
  options?: Options
): { isIntersecting: boolean; entry?: IntersectionObserverEntry } {
  const [state, setState] = useState<{
    isIntersecting: boolean;
    entry?: IntersectionObserverEntry;
  }>({ isIntersecting: false });

  useEffect(() => {
    const node = ref?.current;
    if (!node) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        setState({ isIntersecting: entry.isIntersecting, entry });
      },
      options
    );

    observer.observe(node);
    
    return () => {
      observer.unobserve(node);
      observer.disconnect();
    };
  }, [ref, options?.root, options?.rootMargin, options?.threshold]);

  return state;
}

```

The hook returns an `isIntersecting` boolean and the full `entry` object, giving components access to intersection ratios and bounding rectangles. The cleanup function prevents memory leaks when components unmount—a critical consideration for cloned components that may be dynamically added or removed.

## Implementing Scroll-Driven Navigation Highlighting

Here's how to build a sticky navigation that automatically highlights the active section as users scroll, using the hook in [`src/app/page.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/page.tsx).

```tsx
// src/app/page.tsx
import { cn } from "@/src/lib/utils";
import { useRef } from "react";
import { useIntersectionObserver } from "@/src/hooks/useIntersectionObserver";

export default function HomePage() {
  const sections = [
    { id: "intro", title: "Introduction" },
    { id: "features", title: "Features" },
    { id: "pricing", title: "Pricing" },
  ];

  return (
    <main className="flex flex-col">
      <nav className="sticky top-0 bg-background z-10 flex gap-4 p-4 shadow">
        {sections.map((sec) => (
          <NavItem key={sec.id} targetId={sec.id} label={sec.title} />
        ))}
      </nav>

      {sections.map((sec) => (
        <Section key={sec.id} id={sec.id} title={sec.title} />
      ))}
    </main>
  );
}

function NavItem({ targetId, label }: { targetId: string; label: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const { isIntersecting } = useIntersectionObserver(ref, {
    rootMargin: "-50% 0px -50% 0px",
  });

  return (
    <div ref={ref}>
      <a
        href={`#${targetId}`}
        className={cn(
          "text-sm font-medium transition-colors",
          isIntersecting ? "text-primary" : "text-muted-foreground"
        )}
      >
        {label}
      </a>
    </div>
  );
}

function Section({ id, title }: { id: string; title: string }) {
  return (
    <section id={id} className="min-h-screen py-20">
      <h2 className="text-3xl font-bold">{title}</h2>
      <p className="mt-4 text-base">
        Content for {title} section...
      </p>
    </section>
  );
}

```

The `rootMargin: "-50% 0px -50% 0px"` option triggers the observer when the section is roughly centered in the viewport, creating an intuitive "active" state that feels responsive to the user's reading position.

## Building Reusable Scroll-Triggered UI Components

Extend the pattern to create interactive elements that respond to scroll position. This **ScrollButton** component demonstrates opacity changes based on section visibility.

```tsx
// src/components/ui/ScrollButton.tsx
import { Button } from "./button";
import { useRef } from "react";
import { useIntersectionObserver } from "@/src/hooks/useIntersectionObserver";

export function ScrollButton({ targetId }: { targetId: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const { isIntersecting } = useIntersectionObserver(ref, {
    rootMargin: "-80% 0px -20% 0px",
  });

  const scrollTo = () => {
    document.getElementById(targetId)?.scrollIntoView({ behavior: "smooth" });
  };

  return (
    <div ref={ref}>
      <Button
        onClick={scrollTo}
        className={cn(
          "transition-opacity",
          isIntersecting ? "opacity-100" : "opacity-50"
        )}
      >
        Go to {targetId}
      </Button>
    </div>
  );
}

```

The `cn()` utility from [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) cleanly handles conditional class application without template string concatenation, maintaining type safety and preventing class conflicts.

## Configuring IntersectionObserver Options for Different Scroll Behaviors

Different cloned components require different intersection thresholds. Adjust these options in your hook calls:

| Option | Use Case | Example Value |
|--------|----------|---------------|
| `threshold` | Trigger when specific percentage of element is visible | `0.25` (25% visible) |
| `rootMargin` | Offset the trigger point (positive expands, negative shrinks) | `"-100px 0px"` (100px buffer) |
| `root` | Observe against a specific scroll container instead of viewport | `scrollContainerRef.current` |

For **lazy-loaded images** in cloned galleries, use `threshold: 0.1` with a small `rootMargin` to preload just before visibility. For **scroll spy navigation**, use negative `rootMargin` values to trigger at the section center rather than edge.

## Performance Considerations for Cloned Components

The native Intersection Observer API runs on a separate thread, avoiding the main-thread blocking common with `scroll` event listeners. The template's strict TypeScript configuration ensures your hook implementations remain type-safe:

- **Single observer per element**: The hook creates one observer instance per `useEffect` call, disconnected on unmount
- **Memoized options**: The dependency array includes `options` properties to prevent unnecessary recreations
- **Ref-based observation**: Passing DOM nodes directly avoids React state overhead for the observer itself

When cloning complex websites with many scroll-driven elements, consider using a single **IntersectionObserverContext** to share observer instances across multiple components, reducing memory overhead from many individual observers.

## Summary

- **Create [`src/hooks/useIntersectionObserver.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/hooks/useIntersectionObserver.ts)** to encapsulate the native API with automatic cleanup and TypeScript generics
- **Use `rootMargin` and `threshold` options** to fine-tune when interactions trigger—centered sections need different values than edge-triggered animations
- **Apply conditional classes via `cn()`** from [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) to maintain consistent styling patterns across cloned components
- **Place refs on wrapper elements** rather than interactive elements themselves to avoid disrupting event handling
- **Clean up observers on unmount** via the returned cleanup function to prevent memory leaks in dynamic cloned layouts

## Frequently Asked Questions

### How do I observe multiple sections with a single scroll spy navigation?

Create a separate `NavItem` component for each section, each with its own `useIntersectionObserver` call and ref. This pattern scales to any number of sections without complex state management, though for very large lists (50+ items) consider a single observer with multiple targets and a shared state object.

### Can I use this with server-side rendering in Next.js 16?

Yes—the Intersection Observer API only runs in the browser, so the hook safely returns `isIntersecting: false` during SSR. The `useEffect` hook ensures observer creation only happens client-side, compatible with the template's App Router architecture and `npm run build` output.

### Why does my observer trigger immediately on page load?

The observer evaluates elements against the viewport immediately upon creation. If the element is already visible, `isIntersecting` starts as `true`. To prevent initial animation triggers, add a `useState` flag that ignores the first intersection or use CSS `animation-fill-mode` to handle initial states.

### How do I animate elements as they scroll into view from different directions?

Use the `entry` object returned by the hook, specifically `entry.boundingClientRect` and `entry.rootBounds`, to determine scroll direction. Combine with CSS transforms or animation libraries like Framer Motion, wiring the direction detection to the `isIntersecting` state change.