Reveal
gr8monk3ys/revealFreeComponent
IntersectionObserver scroll reveal; reduced-motion safe, works without JS.
Install it
npx shadcn@latest add https://ui.lscaturchio.xyz/r/reveal.jsonSource
1"use client";23import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";45import { cn } from "@/lib/utils";67type RevealProps = {8 children: ReactNode;9 className?: string;10 /** Y-offset applied before reveal (in pixels). */11 y?: number;12 /** Delay before the transition starts (in ms). */13 delayMs?: number;14 /** Observe once then disconnect. */15 once?: boolean;16 /** IntersectionObserver rootMargin. */17 margin?: string;18 /** IntersectionObserver threshold. */19 threshold?: number | number[];20};2122/**23 * Scroll-reveal wrapper. Server-renders as visible ("in") so content is never24 * hidden without JavaScript; after mount, elements still below the viewport25 * are set to "out" and revealed by an IntersectionObserver as they scroll in.26 * Reduced-motion users are never hidden (the observer is skipped entirely,27 * and the CSS `.reveal` reduced-motion block forces visibility regardless).28 */29export function Reveal({30 children,31 className,32 y = 14,33 delayMs = 0,34 once = true,35 margin = "0px 0px -10% 0px",36 threshold = 0.15,37}: RevealProps) {38 const ref = useRef<HTMLDivElement>(null);39 const [state, setState] = useState<"in" | "out">("in");4041 useEffect(() => {42 const el = ref.current;43 if (!el || typeof IntersectionObserver === "undefined") return;44 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;4546 // Only hide elements that start below the viewport — above-the-fold47 // content keeps its mount animation and never flashes out.48 if (el.getBoundingClientRect().top <= window.innerHeight) return;49 setState("out");5051 const observer = new IntersectionObserver(52 (entries) => {53 for (const entry of entries) {54 if (entry.isIntersecting) {55 setState("in");56 if (once) observer.disconnect();57 } else if (!once) {58 setState("out");59 }60 }61 },62 { rootMargin: margin, threshold },63 );64 observer.observe(el);65 return () => observer.disconnect();66 }, [once, margin, threshold]);6768 const style = {69 "--reveal-y": `${y}px`,70 "--reveal-delay": `${delayMs}ms`,71 } as CSSProperties;7273 return (74 <div75 ref={ref}76 data-reveal-state={state}77 className={cn("reveal", className)}78 style={style}79 >80 {children}81 </div>82 );83}
What it pulls in
Other registry items
Files it writes
More from gr8monk3ys
All 69 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordionaccordion | gr8monk3ys | Components | Free | 2 deps | |
| ActiveNavLinkactive-nav-link | gr8monk3ys | Components | Free | no deps · 2 files | |
| alertalert | gr8monk3ys | Components | Free | 1 dep | |
| alert-dialogalert-dialog | gr8monk3ys | Components | Free | 1 dep | |
| aspect-ratioaspect-ratio | gr8monk3ys | Components | Free | 1 dep | |
| attachmentattachment | gr8monk3ys | Components | Free | 2 deps | |
| Avataravatar | gr8monk3ys | Components | Free | 1 dep | |
| Badgebadge | gr8monk3ys | Components | Free | 1 dep |
