Type-safe events
Zod-validated event schemas, no untyped trackEvent calls.
Server components first
Built for the App Router. Client islands only where needed.
Theme-aware
Every component respects --background, --foreground, and --primary tokens.
Installation
npx shadcn@latest add https://tentui.com/r/reveal-on-scroll.json
Usage
Wrap any block of content. The wrapper holds it at opacity: 0 plus a small offset until the element becomes ~20% visible, then animates to its resting position.
import { RevealOnScroll } from "@/components/reveal-on-scroll";
export function FeatureSection() {
return (
<RevealOnScroll>
<h2>Why tent ui</h2>
<p>Production-ready components with sensible defaults.</p>
</RevealOnScroll>
);
}Patterns
Stagger a list
Compose individual delays for a clean cascade. Keep the delay step small (≤ 100 ms) so the section doesn't feel laggy on slow connections.
{features.map((feature, i) => (
<RevealOnScroll key={feature.title} delay={i * 0.08}>
<FeatureCard {...feature} />
</RevealOnScroll>
))}Direction
Pick the direction the content slides in from. none keeps the fade but skips the translate — useful for elements that already have their own subtle motion.
<RevealOnScroll direction="left">{leftColumn}</RevealOnScroll>
<RevealOnScroll direction="right">{rightColumn}</RevealOnScroll>
<RevealOnScroll direction="none">{calmHero}</RevealOnScroll>Replay on re-enter
Default behaviour is reveal-once. Set repeat for content that should re-animate every time it scrolls back in, e.g. a horizontally-scrolled gallery.
<RevealOnScroll repeat>…</RevealOnScroll>Props
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "up" | "down" | "left" | "right" | "none" | "up" | Direction the content slides in from. |
delay | number | 0 | Delay before the reveal starts, in seconds. |
duration | number | 0.6 | Animation duration in seconds. |
staggerChildren | number | — | Stagger gap, in seconds, applied to direct child motion components if you nest them. |
amount | number | 0.2 | Fraction of the element that must be visible before triggering (0–1). |
repeat | boolean | false | Replay the animation each time the element re-enters the viewport. |
ease | Easing | "easeOut" | Easing curve passed straight to motion. |
className | string | — | Forwarded to the inner motion.div. |
All other motion.div props pass through, so you can override initial, animate, or transition at the call site if you need to.
Reduced motion
useInView itself doesn't read prefers-reduced-motion. If reduced-motion users should skip the animation, gate it at the call site:
"use client";
import { useReducedMotion } from "motion/react";
import { RevealOnScroll } from "@/components/reveal-on-scroll";
export function Section({ children }: { children: React.ReactNode }) {
const reduce = useReducedMotion();
if (reduce) return <div>{children}</div>;
return <RevealOnScroll>{children}</RevealOnScroll>;
}