Depth layers
ParallaxLayers renders a stack of absolutely-positioned layers that drift at
different rates as the scene scrolls through the viewport, and optionally lean
with the pointer. Each layer declares a depth, and its travel is derived from
that depth — so a rig is described entirely as data, and the content of each
layer comes from a slot.
Scroll progress is smoothed with a physical spring, so a fast flick or a trackpad fling settles rather than snapping to the end position.
Usage
<template>
<MParallaxLayers
class="h-64 rounded-xl border border-default"
:layers="layers"
>
<template #layer="{ layer }">
<div
v-if="layer.id === 'sky'"
class="grid h-full place-items-center bg-gradient-to-b from-#1e293b to-#0f172a"
>
<span class="text-3xl font-bold text-white">Sky · depth 0</span>
</div>
<div
v-else-if="layer.id === 'hills'"
class="grid h-full place-items-center bg-#1e40af/70"
>
<span class="text-2xl font-bold text-white">Hills · depth 0.5</span>
</div>
<div
v-else
class="grid h-full place-items-center bg-#0ea5e9/80"
>
<span class="text-xl font-bold text-white">Foreground · depth 1</span>
</div>
</template>
</MParallaxLayers>
</template>
<script setup lang="ts">
// No type import needed: the layer config is a plain structural type, so a
// literal is enough. Import `ParallaxLayerConfig` only when you want the
// compiler to check the shape.
const layers = [
{ id: 'sky', depth: 0 },
{ id: 'hills', depth: 0.5 },
{ id: 'foreground', depth: 1 },
]
</script>
Sizing
Every layer is absolutely positioned, so the root has no in-flow children to
give it height. A min-h-64 floor is applied so the scene cannot collapse, and
overflow-hidden keeps layers from bleeding outside the box. Pass a class with
your own h-* or min-h-* to size the rig properly — yours wins.
Examples
Depth with blur and opacity
blur and opacity are plain CSS values applied to each layer's own element —
not motion values — so they cost nothing per frame. Combined with a wider
range, a small depth spread is enough to read as real separation.
<template>
<MParallaxLayers
class="h-72 rounded-xl border border-default"
:layers="layers"
:range="140"
>
<template #layer="{ layer }">
<div
v-if="layer.id === 'far'"
class="grid h-full place-items-center bg-#334155"
>
<span class="text-sm font-semibold text-white/70">
far · depth 0.3 · blur 6px · opacity 0.5
</span>
</div>
<div
v-else-if="layer.id === 'mid'"
class="grid h-full place-items-center bg-#0d9488/70"
>
<span class="text-sm font-semibold text-white/90">
mid · depth 0.7 · blur 2px
</span>
</div>
<div
v-else
class="grid h-full place-items-center bg-#f59e0b/80"
>
<span class="text-sm font-semibold text-white">near · depth 1.4</span>
</div>
</template>
</MParallaxLayers>
</template>
<script setup lang="ts">
const layers = [
{ id: 'far', depth: 0.3, blur: 6, opacity: 0.5 },
{ id: 'mid', depth: 0.7, blur: 2 },
{ id: 'near', depth: 1.4 },
]
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
layers | ParallaxLayerConfig[] | - | The layer stack, in back-to-front paint order. Each entry is { id, depth } plus optional blur and opacity. A depth of 0 stays fixed. |
direction | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which axes scroll travel is applied to. Pointer tracking is applied to the
axis not selected here, so a vertical rig still leans with the pointer
without gaining horizontal scroll drift. |
range | number | 80 | Peak travel in pixels at depth 1, before the pointer contribution. Both
directions scale this by range * depth, so a depth of 2 at the default
range travels 160px. |
pointerParallax | boolean | false | Add pointer-driven travel. Gated behind a
(hover: hover) and (pointer: fine) media query, so touch devices never
attach the listener. |
container | HTMLElement | Ref<HTMLElement | null> | - | Drive scroll off this element instead of the window. Use it when the rig sits inside a scrollable panel rather than the document. |
class | string | '' | Classes for the bounding box. See Sizing. |
Slots
| Slot | Props | Description |
|---|
Accessibility
Respects prefers-reduced-motion. When the user has asked for reduced motion,
both scroll and pointer travel are pinned to 0 — layers stay in place but the
scene keeps its full opacity and layout, rather than collapsing to a single
point.
The scroll window is deliberately narrow: progress is tracked from
start 0.95 to end 0.6, so travel is concentrated on the stretch where the
scene is actually on screen. Over a full enter-to-exit range most of the
separation would be spent off-view.
Notes
Layer configs are plain structural types, so a literal array is enough — import
ParallaxLayerConfig only if you want the compiler to check the shape.
Scroll progress is read through useScroll, which in motion-v 2 attempts a
native CSS ViewTimeline first and falls back to JS scroll tracking when the
offset pattern isn't recognised.