-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add reveal text on scroll component (#187)
- Loading branch information
Showing
5 changed files
with
210 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Baby, File } from "lucide-react"; | ||
|
||
import ScrollReveal from "@/animata/text/scroll-reveal"; | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
|
||
const meta = { | ||
title: "Text/Scroll Reveal", | ||
component: ScrollReveal, | ||
parameters: { | ||
layout: "centered", | ||
}, | ||
tags: ["autodocs"], | ||
argTypes: {}, | ||
} satisfies Meta<typeof ScrollReveal>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Primary: Story = { | ||
args: { | ||
className: "md:text-2xl", | ||
children: ( | ||
<> | ||
This component reveals its children{" "} | ||
<Baby className="scroll-baby size-5 transition-all duration-75 ease-in-out md:size-8" /> as | ||
you scroll down the page{" "} | ||
<File className="scroll-file size-5 transition-all duration-75 ease-in-out md:size-8" /> | ||
. | ||
<div className="my-4 w-full" /> | ||
It uses a sticky container with a fixed height and a large space at the bottom. Finally, it | ||
calculates the scroll position and applies an opacity effect to each child based on its | ||
position. | ||
<div className="mt-4 w-full">Node with children.</div> | ||
</> | ||
), | ||
}, | ||
}; | ||
|
||
export const TextOnly: Story = { | ||
args: { | ||
className: "md:text-3xl text-blue-200 dark:text-blue-800", | ||
children: | ||
"It uses a sticky container with a fixed height and a large space at the bottom. Finally, it calculates the scroll position and applies an opacity effect to each child based on its position.", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--- | ||
title: Scroll Reveal | ||
description: A component that reveals text based on the scroll position. Make sure to scroll inside the preview to see the effect. | ||
author: harimanok_ | ||
labels: ["requires interaction", "scroll"] | ||
--- | ||
|
||
<ComponentPreview name="text-scroll-reveal--docs" /> | ||
|
||
## Installation | ||
|
||
<Steps> | ||
|
||
<Step>(optional): Update globals.css</Step> | ||
|
||
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. | ||
|
||
```css | ||
.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; | ||
} | ||
``` | ||
|
||
<Step>Run the following command</Step> | ||
|
||
It will create a new file `scroll-reveal.tsx` inside the `components/animata/text` directory. | ||
|
||
```bash | ||
mkdir -p components/animata/text && touch components/animata/text/scroll-reveal.tsx | ||
``` | ||
|
||
<Step>Paste the code</Step>{" "} | ||
|
||
Open the newly created file and paste the following code: | ||
|
||
```jsx file=<rootDir>/animata/text/scroll-reveal.tsx | ||
|
||
``` | ||
|
||
</Steps> | ||
|
||
## Credits | ||
|
||
Built by [hari](https://github.com/hari) | ||
|
||
Inspired by: [onassemble](https://onassemble.com/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters