Sticky Reveal
A sticky text-and-visual reveal: the text on the left flows with the scroll while the visual on the right stays pinned and cross-fades between states.
npx shadcn@latest add https://webberui.com/r/sticky-reveal.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<StickyReveal />
Installation
npx shadcn@latest add https://webberui.com/r/sticky-reveal.jsonOr, once registries are configured in components.json, install it as @webberui/sticky-reveal.
Usage
Declare each paired step with StickyRevealItem: media holds the sticky visual on the right, children holds the text on the left:
import { StickyReveal, StickyRevealItem } from "@/components/ui/sticky-reveal";
<StickyReveal>
<StickyRevealItem media={<img src="/design.jpg" alt="Design" className="h-full w-full object-cover" />}>
<h3 className="text-2xl font-semibold">Design</h3>
<p className="mt-2 text-neutral-600">Start from semantic tokens, keep one rhythm across the whole site.</p>
</StickyRevealItem>
<StickyRevealItem media={<img src="/ship.jpg" alt="Ship" className="h-full w-full object-cover" />}>
<h3 className="text-2xl font-semibold">Ship</h3>
<p className="mt-2 text-neutral-600">One-command install, reduced-motion built in.</p>
</StickyRevealItem>
</StickyReveal>If the content lives inside a nested scroll container, pass that container's ref through container (it can be omitted for page-level scrolling):
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[80vh] overflow-y-auto">
<StickyReveal container={scrollerRef} stickyTop={16}>
{/* ...StickyRevealItem */}
</StickyReveal>
</div>Props
StickyReveal
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A set of StickyRevealItem elements |
mediaSide | "left" | "right" | "right" | Which side the visual sits on (text left / image right, or the reverse) |
stickyTop | number | 24 | Offset of the sticky visual frame from the top of the container (px) |
activeIndex | number | — | controlled mode: the active step index is set from the outside |
onActiveChange | (index: number) => void | — | Fires when the active step changes |
container | RefObject<HTMLElement | null> | — | Ref of the nested scroll container; when omitted, the window is used |
showProgress | boolean | true | Show clickable step progress dots |
stepClassName | string | "min-h-[70vh]" | Class for each text step container (it stretches out the scroll distance) |
mediaClassName | string | — | Class for the sticky visual frame (override the aspect ratio, corner radius, and so on) |
className | string | — | Class for the root container |
StickyRevealItem
| Prop | Type | Default | Description |
|---|---|---|---|
media | ReactNode | — | The paired visual content (an image or graphic) |
children | ReactNode | — | The text content that flows with the scroll |
className | string | — | Appended to the class of that text step container |
How it works
- An
IntersectionObserverwatches each text step and picks the active one using a thin band across the middle of the scroll container, so exactly one step is active at any moment. - All
mediaelements are stacked inside the same sticky frame: the active one fades in while the inactive ones fade out with a slight scale and blur, producing the cross-fade reveal. - controlled and uncontrolled modes: when
activeIndexis omitted, the component manages it internally; when you pass it, you are in charge, and scrolling still reports back throughonActiveChange. StickyRevealItemis declarative only — the actual layout is rendered byStickyReveal, which reads its props, so stickiness and cross-fading stay centrally managed.- Use
useStickyReveal()anywhere in the subtree to readactive/total/isStaticand build a custom progress indicator.
Accessibility
- Respects
prefers-reduced-motion: the cross-fade transition and blur are disabled along with the progress-dot transition, and clicking to jump becomes an instant jump; the layout (stickiness) is unchanged. - Inactive visual layers are marked
aria-hiddenso assistive technology does not read them twice. - The progress dots are native
<button>elements, focusable and activatable by keyboard; the active one carriesaria-current, and clicking smoothly scrolls back to that step. - An
aria-live="polite"region announces "step N of M" as it changes.
Horizontal Scroll Section
A pinned section that turns vertical scrolling into horizontal movement, with built-in spring feel, a progress bar, and edge fading.
Image Sequence Scrub
The product-page effect that scrubs a set of frames with scroll progress — the view stays pinned while the imagery plays frame by frame.