Scroll Reveal — <mv-reveal>
Reveal on scroll: children fade, blur, slide or zoom in with a stagger, once or every time; a pure-CSS variant is driven by a view timeline.
| Category | Scroll |
|---|---|
| Type | Web Component (<mv-reveal>) |
| Status | stable |
| Keywords | reveal, blur-fade, fade-in, stagger, in-view, view-timeline, scroll-driven |
When to use
- Cards, feature items or images should fade, blur, slide or zoom in with a stagger as they enter the viewport
- A reversible reveal tied to scroll position is wanted with pure CSS and no script
- Content blocks on a marketing page should replay their entrance each time they come back into view
Avoid when
- A headline should be split into letters, words or lines that animate in → use Text Reveal instead
- Paragraph words should light up progressively in step with the scroll → use Scroll Text instead
- Every block of a dense app screen or long form would animate; staggered entrances slow down task-focused UIs
Install
node scripts/add.mjs scroll-reveal --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["scroll-reveal"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, core/observe.js, components/scroll-reveal/scroll-reveal.js, components/scroll-reveal/scroll-reveal.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="mv-reveal-demo" class="mv-scroll-area" data-orientation="vertical" data-autohide tabindex="0" aria-label="Scroll reveal demo">
<style>
#mv-reveal-demo { width: min(100%, 46rem); height: 400px; margin-inline: auto; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); }
#mv-reveal-demo .in { padding: 2rem 1.75rem 7rem; display: grid; gap: 3.5rem; }
#mv-reveal-demo .eyebrow { margin: 0 0 .5rem; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; color: var(--mv-accent-fg); font-weight: 600; }
#mv-reveal-demo h2 { margin: 0; font: 700 clamp(1.6rem, 3.6vw, 2.3rem)/1.1 var(--mv-font-display); letter-spacing: -.03em; }
#mv-reveal-demo .lead { margin: .75rem 0 0; color: var(--mv-fg-muted); max-width: 34rem; line-height: 1.55; }
#mv-reveal-demo .hint { display: inline-flex; align-items: center; gap: .4rem; margin-top: 1.25rem; font-size: .8rem; color: var(--mv-fg-subtle); }
#mv-reveal-demo .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .75rem; }
#mv-reveal-demo .tile { padding: 1.1rem 1.2rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); }
#mv-reveal-demo .tile h3 { margin: .8rem 0 .3rem; font-size: 1rem; }
#mv-reveal-demo .tile p { margin: 0; font-size: .875rem; color: var(--mv-fg-muted); line-height: 1.5; }
#mv-reveal-demo .ico { display: grid; place-items: center; width: 2.25rem; height: 2.25rem; border-radius: var(--mv-radius-md); background: var(--mv-accent-subtle); color: var(--mv-accent-fg); }
#mv-reveal-demo blockquote { margin: 0; padding: 1.5rem 1.75rem; border-radius: var(--mv-radius-xl); background: linear-gradient(135deg, var(--mv-accent-subtle), transparent 70%), var(--mv-bg-subtle); border: 1px solid var(--mv-border); font-size: 1.15rem; line-height: 1.5; letter-spacing: -.01em; }
#mv-reveal-demo figcaption { margin-top: .75rem; font-size: .85rem; color: var(--mv-fg-muted); }
#mv-reveal-demo .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; }
#mv-reveal-demo .stat { padding: 1rem; border-radius: var(--mv-radius-lg); background: var(--mv-bg-muted); text-align: center; }
#mv-reveal-demo .stat b { display: block; font-size: 1.6rem; letter-spacing: -.03em; }
#mv-reveal-demo .stat span { font-size: .78rem; color: var(--mv-fg-muted); }
#mv-reveal-demo .code { font: 500 .72rem var(--mv-font-mono); color: var(--mv-fg-subtle); margin: 0 0 .6rem; }
#mv-reveal-demo ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
#mv-reveal-demo li { display: flex; gap: .75rem; align-items: center; padding: .8rem 1rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); font-size: .9rem; }
#mv-reveal-demo li i { flex: none; width: .5rem; height: .5rem; border-radius: 50%; background: var(--mv-accent); }
@media (max-width: 36rem) { #mv-reveal-demo .stats { grid-template-columns: repeat(2, 1fr); } }
</style>
<div class="in">
<mv-reveal effect="blur" stagger="120">
<p class="eyebrow">What’s new · September 2026</p>
<h2>Everything you need to ship faster.</h2>
<p class="lead">Each block appears the moment it enters the viewport — even inside a scrolling container like this one.</p>
<span class="hint"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>Scroll down</span>
</mv-reveal>
<mv-reveal effect="fade-up" stagger="90" class="grid">
<article class="tile">
<span class="ico"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>
<h3>Zero dependencies</h3>
<p>Native Web Components, 3 KB on average, no framework required.</p>
</article>
<article class="tile">
<span class="ico"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
<h3>Native animations</h3>
<p>CSS scroll timelines, with an automatic JavaScript fallback.</p>
</article>
<article class="tile">
<span class="ico"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>
<h3>Accessible</h3>
<p>Respects reduced motion: content stays readable, without effects.</p>
</article>
</mv-reveal>
<mv-reveal effect="slide-left">
<figure style="margin:0">
<blockquote>“We replaced three animation libraries with one. The site is faster and the design team is thrilled.”</blockquote>
<figcaption>Chloe Russell — Frontend lead, Northwind Studio</figcaption>
</figure>
</mv-reveal>
<div>
<p class="code"><mv-reveal effect="scale" repeat> — replays every time it scrolls into view</p>
<mv-reveal effect="scale" stagger="70" repeat class="stats">
<div class="stat"><b>98</b><span>Lighthouse score</span></div>
<div class="stat"><b>0</b><span>Dependencies</span></div>
<div class="stat"><b>3 KB</b><span>Average size</span></div>
<div class="stat"><b>AA</b><span>WCAG contrast</span></div>
</mv-reveal>
</div>
<div>
<p class="code">.mv-reveal-group data-effect="slide-right" — pure CSS, follows the scroll</p>
<ul class="mv-reveal-group" data-effect="slide-right">
<li><i></i>Native view() timelines, no JavaScript</li>
<li><i></i>Reversible effect: scroll back up to see it rewind</li>
<li><i></i>Content is simply visible in older browsers</li>
</ul>
</div>
</div>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
effect | fade-up | fade-down | fade | blur | scale | slide-left | slide-right | fade-up | Entrance preset. slide-left comes in from the left, slide-right from the right. |
stagger | number (ms) | 90 | Delay between two direct children. |
duration | number (ms) | 700 | Reveal duration of each child. |
delay | number (ms) | 0 | Delay before the first child. |
distance | CSS length | 1.5rem | Starting offset distance. |
repeat | boolean | false | Hides the children again when the block leaves, so it replays every time (otherwise it plays once). |
threshold | number 0–1 | 0.2 | Visible fraction of the block that triggers the reveal. |
margin | rootMargin | 0px 0px -8% 0px | Margin of the trigger area. |
scroller | selector | Scrolling ancestor used as the root. Defaults to the viewport, clipped by every scrolling ancestor — so it works inside any container. | |
data-effect | same as effect | Preset for the .mv-reveal / .mv-reveal-group CSS classes. |
Methods
| Name | Description |
|---|---|
reveal() | Shows all children immediately. |
Events
| Name | Description |
|---|---|
mv-reveal | Block revealed or hidden. detail: { visible }. |
CSS classes
| Name | Description |
|---|---|
mv-reveal | Pure CSS: the element appears in step with the scroll (animation-timeline: view()), reversible. Without support: simply visible. |
mv-reveal-group | Pure CSS: each child appears, staggered via sibling-index() when available. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-reveal-distance | 1.5rem | Offset distance. |
--mv-reveal-blur | 12px | Starting blur of the blur effect. |
--mv-reveal-easing | var(--mv-ease-out) | Easing of <mv-reveal>. |
--mv-reveal-stagger / -duration / -delay | CSS equivalents of the attributes. | |
--mv-reveal-i | Child index, written by the JS (read-only). |
Accessibility
Content stays in the DOM and readable by screen readers at all times; only its visual rendering is animated. Reduced motion: everything is shown immediately, without offset or blur. Before the JS runs (SSR), content is visible. Print: everything is visible.