# Reveal

> Content-agnostic entrance animation triggered by scroll, mount, or a manual switch, with six directional/scale presets and an arbitrary-length JS-computed stagger for direct children.

- Package: `fancy-ui-react` (React 18/19 + Tailwind CSS v4)
- Category: Effects
- Tags: animation, reveal, scroll, stagger, entrance, micro-interaction
- Types: `RevealProps`
- Docs: https://fancy-ui.rama.app/docs/components/reveal
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/react/src/components/reveal

## Import

```tsx
import { Reveal } from 'fancy-ui-react';
```

Setup (once per project): `npm install fancy-ui-react`, then put these lines in your main CSS file (the stylesheet import goes right after `@import "tailwindcss";`):

```css
@import "tailwindcss";
@import "fancy-ui-react/tailwind.css";
```

and import the component styles once in the app entry: `import "fancy-ui-react/styles.css";`. Full guide: https://fancy-ui.rama.app/llms.txt

The names below are the ones to write in React; when one differs from the Svelte name, the Svelte name follows in parentheses. `class` becomes `className`, a callback takes the name the React port declares (`onclick` becomes `onClick`), snippets become `ReactNode` props (render functions when they take arguments) and the default snippet is `children`. Divergences: https://github.com/RamaHerbin/fancy-ui/tree/main/react#divergences-from-the-svelte-api

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `preset` | `"fade" \| "fade-up" \| "fade-down" \| "fade-left" \| "fade-right" \| "scale"` | `"fade-up"` | Which directional/scale look to animate with |
| `trigger` | `"view" \| "mount" \| "manual"` | `"view"` | What starts the reveal — viewport, next frame after mount, or the active prop |
| `active` | `boolean` | `false` | Read only when trigger="manual" — true reveals, false re-arms |
| `once` | `boolean` | `true` | Disconnects the observer after the first reveal; false re-arms on leaving the viewport (trigger="view" only) |
| `threshold` | `number` | `0.1` | IntersectionObserver threshold (trigger="view" only) |
| `rootMargin` | `string` | `"0px 0px -10% 0px"` | IntersectionObserver rootMargin (trigger="view" only) |
| `duration` | `number` | `600` | Entrance duration in ms |
| `delay` | `number` | `0` | Delay before the entrance starts, in ms |
| `easing` | `string` | `"cubic-bezier(0.16, 1, 0.3, 1)"` | CSS easing for the entrance |
| `distance` | `number` | `16` | Travel distance in px for the four directional presets — ignored by scale |
| `stagger` | `number` | `0` | Milliseconds per stagger step; 0 animates the root, any positive value animates direct element children instead |
| `from` | `"first" \| "last" \| "center" \| number` | `"first"` | Where the stagger counts distance from — only meaningful when stagger is greater than 0 |
| `initial` | `"hidden" \| "visible"` | `"hidden"` | Server-rendered starting state: "hidden" paints the content hidden until it reveals (no flash); "visible" paints it visible and hides it on mount, accepting a flash in exchange for no-JS and non-hydrated safety |
| `as` | `keyof HTMLElementTagNameMap` | `"div"` | The element tag Reveal renders as, via <svelte:element> |
| `onReveal` | `() => void` | — | Fires once per reveal, the moment the state reaches visible — every re-reveal when once is false |
| `className` (Svelte `class`) | `string` | — | Additional CSS classes, merged onto the root |
| `ref` | `HTMLElement \| null` | `null` | Bindable reference to the root element |

## Node props

| Prop | Description |
| --- | --- |
| `children` | Content to reveal |

## Example

```tsx
import { useState } from "react";
import { Button, Reveal } from "fancy-ui-react";

const features = [
	{ title: "Fast setup", body: "Drop it around any content, no config required." },
	{
		title: "Accessible by default",
		body: "Keyboard focus and reduced motion are both handled for you.",
	},
	{ title: "Themeable", body: "Every timing and distance is a CSS variable." },
];

export function RevealExample() {
	// Changing the key remounts Reveal, which plays the entrance again.
	const [replayKey, setReplayKey] = useState(0);

	return (
		<div className="flex w-full flex-col gap-4 p-6">
			<div className="flex justify-end">
				<Button variant="outline" size="sm" onClick={() => setReplayKey((key) => key + 1)}>
					Replay
				</Button>
			</div>

			<Reveal
				key={replayKey}
				trigger="mount"
				stagger={80}
				className="grid grid-cols-1 gap-4 sm:grid-cols-3"
			>
				{features.map((feature) => (
					<div key={feature.title} className="bg-card border-border rounded-lg border p-4">
						<h3 className="text-foreground text-sm font-semibold">{feature.title}</h3>
						<p className="text-muted-foreground mt-1 text-sm">{feature.body}</p>
					</div>
				))}
			</Reveal>
		</div>
	);
}
```
