Reveal
Wrap a section, a card or a row and it fades — and rises, or grows — into
place the first time the reader gets to it. Content that is already on screen
when the page loads plays the same entrance straight away, so the component
reads the same whether it is the hero or the fourth section down. It plays
once; repeat is there for the cases where it should not.
It is a wrapper and nothing else: no scroll listener, no animation library,
no bundle. One IntersectionObserver per element decides when, and the
entrance itself is two states and one CSS transition.
Import
import { Reveal } from "@forte-ui/react";Content is never gated behind the entrance
Nothing is hidden until the component has mounted in a browser and an observer is watching. A page whose JavaScript fails, or has not arrived yet, shows the content rather than an empty column; a crawler reads the markup it was served; a screen reader, which never depended on any of this, reads it either way. Printing is covered too — paper does not scroll, so anything still waiting for its entrance prints at rest rather than as a blank page.
Examples
Effects
Seven entrances. fade moves nothing; the four directional ones fade while
travelling --forte-reveal-travel; scale grows into place without
fading, which is the one to reach for over text that should stay readable the
whole way in; fade-scale does both.
fade-start and fade-end are named for the reading direction rather than
for a side — fade-start arrives from the left of this page and from the
right of an Arabic one. Flip the frame to RTL and watch the pair swap.
Staggering a list
There is no group component and no orchestration: delay is a per-element
wait, so a mapped list staggers with delay={i * 70}. Each row still starts
its own clock when it reaches the screen, which is what keeps a long list
from dealing out its last rows to an empty viewport.
Install
pnpm add @forte-ui/react
Import the styles
Once, in the root layout
Pick a seed
Every color derives from it
Ship
No runtime theming layer
The rows above are <li>s, not wrappers around <li>s. render replaces
the element the component draws, so the entrance is applied to the list item
itself — the same prop every Base UI-backed component in the library takes,
and the answer wherever a <div> would be invalid or would break a grid:
<Reveal render={<li />}>…</Reveal>
<Reveal render={<section />}>…</Reveal>
<Reveal render={<Card.Root />}>…</Reveal>Playing again
Off by default, because the honest reading of a second entrance is "this is
new", and content the reader has already scrolled past is not. Turn repeat
on for a demo of the effect itself, or for a panel whose contents genuinely
change while it is away.
The reset is a cut rather than a reverse, and it happens only once the content is entirely off screen, where there is nothing to watch. It is also skipped while focus is inside, so a keyboard user is never left standing on something that has faded out underneath them.
How much has to be on screen
amount is the fraction of the content that has to be visible before it
counts, from 0 — any sliver, the default — to 1, all of it. The default
starts the entrance as the top edge crosses into view, which is what makes it
read as the content arriving with the scroll.
<Reveal amount={0.6}>…</Reveal>Anything too big to fit on screen is exempt and plays as soon as it appears: a section two screens tall can never be 60% visible, and a component that waited for it would leave that section invisible on a phone and look fine on the desk it was written at.
Theming
Reduced motion
Nothing travels and nothing scales — the geometry tokens behind the knobs are
already 0px and 1 there, and the two gates in the component's CSS carry
the same collapse over to a distance or a size you set by hand. What is left
is the fade, on a duration the token has already shortened: reduced, not
removed, so the content still reads as arriving rather than as having always
been there.
API
| Prop | Type | Default | Description |
|---|---|---|---|
amount | number | 0 | How much of the content has to be on screen before it counts, from 0 — any sliver of it — to 1, all of it. The default starts the entrance as the top edge crosses into view, which is what makes it read as the content arriving with the scroll rather than as something that happened before the reader got there. Anything too big to fit on screen is exempt and starts as soon as it appears, since it could never satisfy the fraction. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
delay | number | 0 | Milliseconds to wait after the content is on screen before it starts. Sets `--forte-reveal-delay`, and exists as a prop because the reason to reach for it is nearly always a stagger: `delay={i * 60}` over a mapped list. Keep the total under a few hundred milliseconds — a list whose last row arrives a second late reads as a page still loading. |
effect | RevealEffect | fade-up | Which entrance plays. - `fade` — opacity alone. The only one that moves nothing, so the only one that cannot widen a page (see the note on the inline effects). - `fade-up` / `fade-down` — fades while rising from below, or settling from above. - `fade-start` / `fade-end` — the same along the inline axis, so `fade-start` arrives from the left in LTR and from the right in RTL. Named for the reading direction rather than for a side because that is what makes them survive `dir="rtl"` unchanged. - `scale` — grows into place with no fade, for something that should stay legible the whole way in. - `fade-scale` — both: the "pop" used for a card or a modal-like panel. The distance and the starting size are knobs, not values baked into the effect — `--forte-reveal-travel` and `--forte-reveal-scale`. |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<div>` with another element or component — `render={<li />}` inside a list, `render={<section />}` for a landmark, `render={<Card.Root />}` to animate the card itself rather than a wrapper around it. The entrance is applied to whatever element comes back, so nothing about the layout changes. | |
repeat | boolean | false | Play the entrance again every time the content comes back on screen, instead of once and never again. Off by default, because the honest reading of a second entrance is "this content is new", and content the reader has already scrolled past is not. Turn it on for a demo of the effect itself, or for a panel whose contents genuinely change while it is away. The reset is a cut, not a reverse: it happens once the content is entirely off screen, where there is nothing to see. It is also skipped while focus is inside, so a keyboard user cannot be left on an element that has faded out underneath them. |
Theming
| Property | Controls | Default |
|---|---|---|
--forte-reveal-travel | How far the content travels before it settles, for the four directional effects. | var(--forte-travel-lg) |
--forte-reveal-scale | The size the content starts at, for scale and fade-scale. Below 1 it grows into place; above 1 it settles down into it. | var(--forte-scale-enter) |
--forte-reveal-duration | How long the entrance takes. | var(--forte-duration-spring-gentle) |
--forte-reveal-ease | The curve it runs on. | var(--forte-ease-spring-gentle) |
--forte-reveal-delay | How long to wait once the content is on screen. Set by the delay prop, which is how a mapped list staggers. | 0s |