Parallax <mv-parallax>
Profundidad por capas al hacer scroll sin configuración: marca cualquier descendiente con data-speed y se desplaza a su propio ritmo mientras la sección cruza su contenedor de scroll, movido por una view timeline nativa fuera del hilo principal, con un respaldo JS ligero que solo actúa en lo visible.
| Categoría | Desplazamiento |
|---|---|
| Tipo | Web Component (<mv-parallax>) |
| Estado | estable |
| Keywords | safe-rewrite, parallax, layers, hero, depth, gallery, view-timeline, scroll-driven |
When to use
- A hero or gallery section should gain depth by moving background and foreground layers at different speeds
- Decorative shapes, words or images should drift horizontally or vertically as a section crosses the viewport
- Parallax is needed inside a scrollable panel, not only on the page itself
Avoid when
- Elements should simply fade or slide in once when they appear → use Scroll Reveal instead
- The pointer, not the scroll, should drive layer depth on a card → use Tilt Card instead
- Many large layered sections stack up on one page; strong motion there can cause discomfort
Instalación
node scripts/add.mjs parallax --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["parallax"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/parallax/parallax.js, components/parallax/parallax.css.
Uso
Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="mv-parallax-demo" class="mv-scroll-area" data-orientation="vertical" tabindex="0" aria-label="Parallax demo, scroll to explore">
<style>
#mv-parallax-demo { width: min(100%, 52rem); height: 440px; margin-inline: auto; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); }
#mv-parallax-demo .copy { padding: 2.5rem 1.5rem; text-align: center; }
#mv-parallax-demo .copy p { margin: .5rem auto 0; max-width: 30rem; color: var(--mv-fg-muted); font-size: .92rem; line-height: 1.55; }
#mv-parallax-demo .kicker { margin: 0; font-size: .72rem; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--mv-accent-fg); }
#mv-parallax-demo h2 { margin: .4rem 0 0; font: 700 1.6rem/1.15 var(--mv-font-display); letter-spacing: -.03em; }
/* 1 · Layered scene */
#mv-parallax-demo .scene { height: 440px; margin: 0 1rem; border-radius: var(--mv-radius-xl); color: oklch(.98 .01 80); background: linear-gradient(180deg, oklch(.2 .05 275) 0%, oklch(.34 .09 300) 45%, oklch(.66 .14 40) 78%, oklch(.8 .12 65) 100%); }
#mv-parallax-demo .fill { position: absolute; inset-inline: 0; pointer-events: none; }
#mv-parallax-demo .fill svg { display: block; width: 100%; height: 100%; }
#mv-parallax-demo .sky { top: 0; height: 70%; background-image: radial-gradient(2px 2px at 8% 30%, #fffc, transparent), radial-gradient(1.5px 1.5px at 19% 62%, #fff9, transparent), radial-gradient(2px 2px at 33% 18%, #fffb, transparent), radial-gradient(1.5px 1.5px at 47% 44%, #fff8, transparent), radial-gradient(2px 2px at 61% 26%, #fffa, transparent), radial-gradient(1.5px 1.5px at 74% 58%, #fff8, transparent), radial-gradient(2px 2px at 86% 16%, #fffc, transparent), radial-gradient(1.5px 1.5px at 94% 40%, #fff9, transparent); }
#mv-parallax-demo .moon { position: absolute; top: 16%; left: 16%; width: 4.5rem; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(circle at 36% 34%, oklch(.99 .02 90), oklch(.9 .05 85) 60%, oklch(.82 .06 80)); box-shadow: 0 0 60px 12px oklch(.95 .06 85 / .35); }
#mv-parallax-demo .heading { position: absolute; inset: 22% 1rem auto; text-align: center; text-shadow: 0 8px 32px oklch(.15 .05 290 / .5); }
#mv-parallax-demo .heading p { margin: 0; font-size: .72rem; letter-spacing: .32em; text-transform: uppercase; opacity: .85; }
#mv-parallax-demo .heading h3 { margin: .35rem 0 0; font: 800 clamp(2.2rem, 7vw, 4rem)/.95 var(--mv-font-display); letter-spacing: -.045em; }
#mv-parallax-demo .far { top: 52%; bottom: -20%; color: oklch(.5 .09 330); }
#mv-parallax-demo .near { top: 64%; bottom: -20%; color: oklch(.36 .08 310); }
#mv-parallax-demo .ground { top: 80%; bottom: -1px; color: oklch(.2 .04 290); }
#mv-parallax-demo .chip { position: absolute; right: 1.25rem; bottom: 1.25rem; padding: .45rem .85rem; border-radius: var(--mv-radius-full); background: oklch(1 0 0 / .14); border: 1px solid oklch(1 0 0 / .25); backdrop-filter: blur(8px); font-size: .75rem; font-weight: 500; }
/* 2 · Horizontal drift */
#mv-parallax-demo .drift { display: grid; gap: .25rem; padding: 2.5rem 0; }
#mv-parallax-demo .drift p { margin: 0; white-space: nowrap; text-align: center; font: 800 clamp(2rem, 6vw, 3.4rem)/1.05 var(--mv-font-display); letter-spacing: -.04em; }
#mv-parallax-demo .drift p + p { color: transparent; -webkit-text-stroke: 1px var(--mv-fg-muted); }
/* 3 · Staggered columns */
#mv-parallax-demo .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; padding: 2rem 1rem 3rem; }
#mv-parallax-demo .stack { display: grid; gap: .75rem; align-content: start; }
#mv-parallax-demo .stack:nth-child(2) { margin-top: 2.5rem; }
#mv-parallax-demo figure { margin: 0; height: 140px; display: flex; align-items: flex-end; padding: .75rem; border-radius: var(--mv-radius-lg); color: white; font-size: .8rem; font-weight: 600; text-shadow: 0 1px 8px #0007; }
</style>
<div class="copy">
<p class="kicker">Field notes</p>
<h2>A night in the Atacama</h2>
<p>Scroll the panel: every layer of the scene moves at its own pace.</p>
</div>
<mv-parallax class="scene">
<div class="fill sky" data-speed="-0.4" aria-hidden="true"></div>
<div class="moon" data-speed="-0.34" aria-hidden="true"></div>
<div class="heading" data-speed="-0.26">
<p>San Pedro de Atacama</p>
<h3>Valley of the Moon</h3>
</div>
<div class="fill far" data-speed="-0.16" aria-hidden="true"><svg viewBox="0 0 1200 300" preserveAspectRatio="none" fill="currentColor"><path d="M0 300V120C120 70 220 60 330 95S560 40 690 70 930 30 1040 80 1160 90 1200 70V300Z"/></svg></div>
<div class="fill near" data-speed="-0.07" aria-hidden="true"><svg viewBox="0 0 1200 300" preserveAspectRatio="none" fill="currentColor"><path d="M0 300V110C150 60 260 120 400 90S640 30 780 85 1010 120 1200 60V300Z"/></svg></div>
<div class="fill ground" aria-hidden="true"><svg viewBox="0 0 1200 120" preserveAspectRatio="none" fill="currentColor"><path d="M0 120V50C180 20 330 60 520 40S860 10 1020 38 1150 44 1200 30V120Z"/></svg></div>
<span class="chip" data-speed="0.12">7,900 ft · Clear skies</span>
</mv-parallax>
<mv-parallax class="drift">
<p data-speed-x="0.35">Salt flats · Geysers · Lagoons</p>
<p data-speed-x="-0.35" aria-hidden="true">Salt flats · Geysers · Lagoons</p>
</mv-parallax>
<mv-parallax class="grid" style="--mv-parallax-overflow: visible">
<div class="stack" data-speed="0.1">
<figure style="background: linear-gradient(160deg, oklch(.8 .1 60), oklch(.5 .14 35))">El Tatio Geysers</figure>
<figure style="background: linear-gradient(160deg, oklch(.82 .08 210), oklch(.5 .1 240))">Laguna Cejar</figure>
<figure style="background: linear-gradient(160deg, oklch(.78 .1 20), oklch(.45 .13 10))">Rainbow Valley</figure>
</div>
<div class="stack" data-speed="-0.1">
<figure style="background: linear-gradient(160deg, oklch(.86 .04 250), oklch(.52 .08 270))">Salar de Atacama</figure>
<figure style="background: linear-gradient(160deg, oklch(.76 .12 300), oklch(.42 .14 285))">Stargazing at 2 a.m.</figure>
<figure style="background: linear-gradient(160deg, oklch(.84 .08 95), oklch(.55 .11 70))">Death Valley Dunes</figure>
</div>
<div class="stack" data-speed="0.1">
<figure style="background: linear-gradient(160deg, oklch(.8 .09 180), oklch(.46 .1 200))">Miscanti Lagoon</figure>
<figure style="background: linear-gradient(160deg, oklch(.8 .1 45), oklch(.5 .13 25))">Pukará de Quitor</figure>
<figure style="background: linear-gradient(160deg, oklch(.78 .08 140), oklch(.45 .09 160))">Jere Valley</figure>
</div>
</mv-parallax>
<div class="copy">
<p class="kicker">End of the road</p>
<p>With reduced motion turned on, every layer stays exactly where it was placed.</p>
</div>
</div>API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
data-speed | number | 0 | On a descendant: relative vertical speed. Negative = slower than the scroll (background, e.g. -0.3), positive = faster (foreground). Every layer sits at its authored position when the section is centered. |
data-speed-x | number | 0 | On a descendant: horizontal drift proportional to the scroll. |
scroller | selector | window | Reference scroll container (forces the JS path). Default: the nearest scroll container, or the page. | |
fallback | boolean | Forces the JavaScript fallback (tests, browsers without animation-timeline). |
Content structure
| Name | Description |
|---|---|
[data-speed] / [data-speed-x] | Any non-inline descendant (block, image, SVG). A layer runs its own translate animation: wrap it if it needs another animation or translate. Make background layers taller than the section: the maximum offset is |speed| × (section height + scrollport height) / 2. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-parallax-intensity | 1 | Global speed multiplier. |
--mv-parallax-overflow | clip | Section overflow (visible to let layers spill out). |
Accessibility
Purely visual: DOM order, focus order and content never change. Mark decorative layers aria-hidden. Reduced motion (OS setting or data-motion="reduce"): no animation at all, layers stay at their authored position. No scroll hijacking and no wheel or touch listeners: the native path runs on a view timeline; the fallback uses one passive scroll listener, one frame at a time, only while the section is near the viewport.