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