Docs

Scroll Reveal

A component that reveals text based on the scroll position. Make sure to scroll inside the preview to see the effect.

requires interactionscroll

Installation

(optional): Update globals.css

This is just for changing the color of the icon once it is revealed. You can skip this step if you don't want to change the color.

.scroll-baby[style*="opacity: 1"] {
  @apply text-yellow-300 dark:text-yellow-500;
}
 
.scroll-file[style*="opacity: 1"] {
  @apply text-blue-300 dark:text-blue-500;
}

Run the following command

It will create a new file scroll-reveal.tsx inside the components/animata/text directory.

mkdir -p components/animata/text && touch components/animata/text/scroll-reveal.tsx

Paste the code

Open the newly created file and paste the following code:

import React, { useRef } from "react";
import { motion, MotionValue, useScroll, useTransform } from "framer-motion";
 
import { cn } from "@/lib/utils";
 
interface ScrollRevealProps
  extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
  children: React.ReactNode;
  className?: string;
}
 
// This function might need updates to support different cases.
const flatten = (children: React.ReactNode): React.ReactNode[] => {
  const result: React.ReactNode[] = [];
 
  React.Children.forEach(children, (child) => {
    if (React.isValidElement(child)) {
      if (child.type === React.Fragment) {
        result.push(...flatten(child.props.children));
      } else if (child.props.children) {
        result.push(React.cloneElement(child, {}));
      } else {
        result.push(child);
      }
    } else {
      const parts = String(child).split(/(\s+)/);
      result.push(
        ...parts.map((part, index) => <React.Fragment key={index}>{part}</React.Fragment>),
      );
    }
  });
 
  return result.flatMap((child) => (Array.isArray(child) ? child : [child]));
};
 
function OpacityChild({
  children,
  index,
  progress,
  total,
}: {
  children: React.ReactNode;
  index: number;
  total: number;
  progress: MotionValue<number>;
}) {
  const opacity = useTransform(progress, [index / total, (index + 1) / total], [0.5, 1]);
 
  let className = "";
  if (React.isValidElement(children)) {
    className = Reflect.get(children, "props")?.className;
  }
 
  return (
    <motion.span style={{ opacity }} className={cn(className, "h-fit")}>
      {children}
    </motion.span>
  );
}
 
export default function ScrollReveal({ children, className, ...props }: ScrollRevealProps) {
  const flat = flatten(children);
  const count = flat.length;
  const containerRef = useRef<HTMLDivElement>(null);
 
  const { scrollYProgress } = useScroll({
    container: containerRef,
  });
 
  return (
    <div
      {...props}
      ref={containerRef}
      className={cn(
        // Adjust the height and spacing according to the need
        "storybook-fix relative h-96 w-full overflow-y-scroll bg-foreground text-background dark:text-zinc-900",
        className,
      )}
    >
      <div className="sticky top-0 flex h-full w-full items-center justify-center">
        <div className="flex h-fit w-full min-w-fit flex-wrap whitespace-break-spaces p-8">
          {flat.map((child, index) => {
            return (
              <OpacityChild
                progress={scrollYProgress}
                index={index}
                total={flat.length}
                key={index}
              >
                {child}
              </OpacityChild>
            );
          })}
        </div>
      </div>
      {Array.from({ length: count }).map((_, index) => (
        // Create really large area to make the scroll effect work
        <div key={index} className="h-32" />
      ))}
    </div>
  );
}

Credits

Built by hari

Inspired by: onassemble