Skip to content

Commit

Permalink
feat: add reveal text on scroll component (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
hari authored Jul 31, 2024
1 parent 9ef9862 commit d54b60a
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 1 deletion.
45 changes: 45 additions & 0 deletions animata/text/scroll-reveal.stories.tsx
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.",
},
};
102 changes: 102 additions & 0 deletions animata/text/scroll-reveal.tsx
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>
);
}
6 changes: 5 additions & 1 deletion components/mdx-base-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export const baseComponents = {
/>
),
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
<a className={cn("font-medium underline underline-offset-4", className)} {...props} />
<a
className={cn("font-medium underline underline-offset-4", className)}
{...props}
target={Reflect.get(props, "href")?.toString().startsWith("http") ? "_blank" : undefined}
/>
),
p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className={cn("leading-7 [&:not(:first-child)]:mt-6", className)} {...props} />
Expand Down
50 changes: 50 additions & 0 deletions content/docs/text/scroll-reveal.mdx
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/)
8 changes: 8 additions & 0 deletions styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,11 @@
initial-value: 0deg;
inherits: false;
}

.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;
}

0 comments on commit d54b60a

Please sign in to comment.