---
title: Reveal
description: Fades and rises its children the first time they reach the viewport. Never un-reveals.
source: https://adamjurek.com/components/reveal
---

## Home


Reveal fades and rises its children into view the instant they first cross
into the viewport, then leaves them revealed. Reach for it to make a
section's content arrive rather than simply appear, and stack several with
staggered `delay` values so a group resolves top to bottom.

<!-- ::start:showcase demo="reveal" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

The server and the first client render both emit the hidden state, so
hydration matches - an `IntersectionObserver` only flips it to shown once it
can actually measure the viewport. Deciding visibility from scroll position
during render would differ between server and browser and produce a mismatch
on every reload that starts part-way down the page. It never un-reveals,
either: content fading back out as you scroll up would read as a bug, not as
motion.

## What it does not do

It fires once and stops watching - there is no re-observing if the element
leaves the viewport again, so `Reveal` is not a fit for parallax or anything
that needs to keep tracking scroll position after the first entrance.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/reveal.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/reveal.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | motion · Scroll effects |
| Files | `reveal.tsx` |
| Dependencies | None |
| Tags | scroll, intersection, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Reveal } from "@sushindustries/ui";

export function Intro() {
	return (
		<Reveal>
			<h2>Every visible element is a component.</h2>
		</Reveal>
	);
}
```

## What you should see

Nothing, until the element's scroll position brings it within about 10% of
the bottom of the viewport - then it fades and rises into place, once.
Scrolling back up doesn't hide it again; a `Reveal` that already fired stays
shown.

## If nothing happens

If content never appears at all, check that `Reveal` is actually below the
fold on first load - an element already in view when the page mounts reveals
almost immediately, since the observer fires on the first frame it can
measure. If it appears instantly with no motion, `prefers-reduced-motion` is
probably set, which is the component working as intended, not a bug.


## Guides


## Composing it

It renders a plain `<div>` around `children` - one extra box in the DOM,
which matters if the parent is a CSS grid or flex context that counts direct
children. Nest more than one `Reveal` with staggered `delay` values, the way
`Section` does with its heading and body, to make a group resolve top-down
rather than as one block.

## Motion and reduced motion

`prefers-reduced-motion: reduce` skips the fade entirely - the check runs
once on mount, and if it matches, `Reveal` sets itself to shown immediately
instead of waiting on the intersection observer. Content is never left
hidden waiting for motion that will not happen.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - |  |
| `delay?` | `number` | `0` | Stagger within a section, in milliseconds. |

<!-- /generated:api -->

## Notes

There's no prop to re-trigger a `Reveal` or make it un-reveal - once `shown`
flips to true the observer disconnects for good. A `Reveal` mounted already
past the trigger point (deep in a route that renders scrolled) reveals on its
first measurable frame rather than waiting on a scroll event that may never
come.

`delay` only staggers the CSS transition; it doesn't delay when the
intersection observer fires. Two `Reveal`s at the same scroll position both
trigger together and differ only in when their own transition starts.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="reveal" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Reveal } from "@sushindustries/ui";

export function FeatureList() {
	return (
		<ul className="flex flex-col gap-4">
			{["Fast", "Typed", "Composable"].map((label, index) => (
				<Reveal key={label} delay={index * 80}>
					<li>{label}</li>
				</Reveal>
			))}
		</ul>
	);
}
```

## What this example is not

A recipe for every list. Stagger a handful of items - three or four - the way
this example does. Past that the last one lands noticeably late, and the
delay reads as the page being slow rather than as an effect.
