Browse the library
Components / Layout
Reveal
FreeScroll reveal wrapper with fade, rise, blur and scale presets plus a RevealGroup for staggered children.
View as Markdownmotion
components/facade/reveal.tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import type { Variants } from "motion/react";
import { cn } from "@/lib/utils";
export type RevealPreset = "fade" | "rise" | "blur" | "scale";
const presets: Record<RevealPreset, Variants> = {
fade: {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
rise: {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0 },
},
blur: {
hidden: { opacity: 0, y: 12, filter: "blur(12px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
},
scale: {
hidden: { opacity: 0, scale: 0.94, y: 8 },
visible: { opacity: 1, scale: 1, y: 0 },
},
};
const reduced: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
const ease = [0.22, 1, 0.36, 1] as const;
interface RevealContextValue {
preset: RevealPreset;
duration: number;
}
const RevealGroupContext = React.createContext<RevealContextValue | null>(null);
export interface RevealProps {
children?: React.ReactNode;
/** Animation preset. Inherits from a parent RevealGroup, otherwise "rise". */
preset?: RevealPreset;
/** Delay in seconds (standalone use only). Defaults to 0. */
delay?: number;
/** Duration in seconds. Defaults to 0.7. */
duration?: number;
/** Animate only the first time it enters view. Defaults to true. */
once?: boolean;
/** Fraction of the element that must be visible. Defaults to 0.25. */
amount?: number;
className?: string;
}
/**
* Animates its children into view. Inside a RevealGroup it becomes a
* staggered child and follows the group's timing.
*/
export function Reveal({
children,
preset,
delay = 0,
duration,
once = true,
amount = 0.25,
className,
}: RevealProps) {
const reduce = useReducedMotion();
const group = React.useContext(RevealGroupContext);
const p = preset ?? group?.preset ?? "rise";
const d = duration ?? group?.duration ?? 0.7;
const variants = reduce ? reduced : presets[p];
if (group) {
return (
<motion.div variants={variants} transition={{ duration: d, ease }} className={className}>
{children}
</motion.div>
);
}
return (
<motion.div
variants={variants}
initial="hidden"
whileInView="visible"
viewport={{ once, amount }}
transition={{ duration: d, delay, ease }}
className={className}
>
{children}
</motion.div>
);
}
export interface RevealGroupProps {
children?: React.ReactNode;
/** Preset for every Reveal inside. Defaults to "rise". */
preset?: RevealPreset;
/** Seconds between each child. Defaults to 0.08. */
stagger?: number;
/** Delay before the first child. Defaults to 0. */
delay?: number;
/** Duration for each child. Defaults to 0.7. */
duration?: number;
once?: boolean;
amount?: number;
/**
* Wrap every direct child in a Reveal automatically. Defaults to true.
* Set false when you place Reveal elements yourself (for nested layouts).
*/
wrapChildren?: boolean;
/** Class applied to the auto-created child wrappers. */
itemClassName?: string;
className?: string;
}
export function RevealGroup({
children,
preset = "rise",
stagger = 0.08,
delay = 0,
duration = 0.7,
once = true,
amount = 0.2,
wrapChildren = true,
itemClassName,
className,
}: RevealGroupProps) {
const reduce = useReducedMotion();
const ctx = React.useMemo(() => ({ preset, duration }), [preset, duration]);
return (
<RevealGroupContext.Provider value={ctx}>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once, amount }}
variants={{
hidden: {},
visible: { transition: { staggerChildren: reduce ? 0 : stagger, delayChildren: delay } },
}}
className={className}
>
{wrapChildren
? React.Children.map(children, (child) =>
child === null || child === undefined || typeof child === "boolean" ? null : (
<Reveal className={itemClassName}>{child}</Reveal>
),
)
: children}
</motion.div>
</RevealGroupContext.Provider>
);
}
export default Reveal;app/page.tsx
import { Bath, BedDouble, MapPin } from "lucide-react";
import { Reveal, RevealGroup } from "@/components/facade/reveal";
const listings = [
{ street: "1847 Alder Lane", city: "Portland, OR", price: "$689,000", beds: 3, baths: 2, hue: "from-brand/30 to-accent" },
{ street: "92 Larkspur Court", city: "Lake Oswego, OR", price: "$1,140,000", beds: 4, baths: 3, hue: "from-success/25 to-muted" },
{ street: "530 NW Everett St #4B", city: "Portland, OR", price: "$449,500", beds: 2, baths: 2, hue: "from-warning/30 to-accent" },
];
export default function Demo() {
return (
<section className="w-full max-w-4xl">
<Reveal preset="blur">
<p className="font-mono text-xs uppercase tracking-[0.18em] text-brand">New this week</p>
<h3 className="mt-2 font-display text-2xl font-semibold tracking-tight sm:text-3xl">Fresh listings in the Portland area</h3>
</Reveal>
<RevealGroup preset="scale" stagger={0.1} delay={0.15} className="mt-6 grid gap-4 sm:grid-cols-3">
{listings.map((l) => (
<article
key={l.street}
className="h-full overflow-hidden rounded-2xl border border-border bg-card text-card-foreground shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]"
>
<div className={`relative h-28 bg-linear-to-br ${l.hue}`}>
<div className="absolute inset-x-6 bottom-0 h-14 rounded-t-lg bg-foreground/80" />
<div className="absolute bottom-0 left-10 h-8 w-6 rounded-t-sm bg-brand/80" />
<div className="absolute bottom-5 right-10 h-5 w-10 rounded-sm bg-background/30" />
</div>
<div className="p-4">
<p className="font-display text-lg font-semibold tracking-tight">{l.price}</p>
<p className="mt-0.5 text-sm">{l.street}</p>
<p className="mt-0.5 inline-flex items-center gap-1 text-xs text-muted-foreground">
<MapPin className="size-3" aria-hidden />
{l.city}
</p>
<div className="mt-3 flex gap-4 border-t border-border pt-3 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<BedDouble className="size-3.5" aria-hidden />
{l.beds} beds
</span>
<span className="inline-flex items-center gap-1.5">
<Bath className="size-3.5" aria-hidden />
{l.baths} baths
</span>
</div>
</div>
</article>
))}
</RevealGroup>
</section>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/reveal.jsonUsage
import { Reveal } from "@/components/facade/reveal";
export default function Page() {
return <Reveal />;
}Props
RevealProps is exported, so you can extend or wrap it. Every prop has a default; <Reveal /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| children | React.ReactNode | |
| preset | RevealPreset | Animation preset. Inherits from a parent RevealGroup, otherwise "rise". |
| delay | number | Delay in seconds (standalone use only). Defaults to 0. |
| duration | number | Duration in seconds. Defaults to 0.7. |
| once | boolean | Animate only the first time it enters view. Defaults to true. |
| amount | number | Fraction of the element that must be visible. Defaults to 0.25. |
| className | string |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade reveal to this page and adapt the copy to our business.