Parallax
Parallax classes and the parallax config ranges.
On this page
Parallax moves media a little slower than the page as it scrolls, inside a frame that crops it. Put parallax on the media and parallax-wrapper on its frame; ParallaxListener (or initParallax() in the vanilla JS bundle) writes the transforms. The Image and Video components set up both classes with their parallax prop.
Quick referenceLink to this section
| Class | Styles |
|---|---|
parallax | & { aspect-ratio: inherit; width: 100%; height: 100%; transform: translateY(-8.33%) scale(1.1666); object-fit: cover; }@media (prefers-reduced-motion: reduce) { & { transform: none; } } |
parallax-wrapper | pointer-events: none;position: relative;overflow: hidden; |
| Modifier | Effect |
|---|---|
parallax--big | Uses translateRangeBig instead of translateRange. Goes on the media or on its wrapper. |
parallax--noscale | Keeps the media at its natural size (scale 1). For media that no wrapper crops, such as a free-floating decoration. |
Basic usageLink to this section
Scroll inside the preview: the image travels against the page within its frame. The demo scrolls inside its own frame because the engine follows the scroll position of the document it runs in.
import { Image } from '@systhemaui/next'
export default function Demo() {
return (
// The frame stays 420px tall; the absolutely positioned page inside it is
// taller, so the demo document scrolls and the listener has something to follow.
<div className="h-[420px] w-full">
<div className="absolute inset-x-0 top-0 flex flex-col gap-10 px-8 pt-24 pb-64">
<p className="text-h4 color-heading">Scroll inside this preview</p>
<Image
src="/demo-assets/mountains.webp"
alt="Pastel mountain ridges"
width={1920}
height={1080}
aspectRatio="21/9"
parallax
disableAnimation
className="rounded-lg"
/>
<p className="max-w-[60ch] color-body">
The image is scaled up slightly and shifted as its frame crosses the viewport, so its
edges never show.
</p>
<Image
src="/demo-assets/coast.webp"
alt="A coastline at low tide"
width={1800}
height={1200}
aspectRatio="16/9"
parallax
disableAnimation
className="rounded-lg"
/>
</div>
</div>
)
}With parallax, Image renders the image inside a figure.parallax-wrapper that carries the aspect ratio, and puts className and the animation classes on that wrapper. Pass parallaxNoWrapper to render the bare image when you provide the frame yourself.
In markup, the frame needs a size (here from an aspect ratio) and the media fills it:
<figure class="parallax-wrapper aspect-21/9">
<img class="parallax" src="/images/mountains.webp" alt="Pastel mountain ridges" />
</figure>.parallax makes the media fill the wrapper with object-fit: cover and gives it a starting transform, so it is already in place before the first scroll event. .parallax-wrapper clips it and ignores pointer events.
How the motion is computedLink to this section
While the element crosses the viewport, the engine maps its progress from entering at the bottom (0%) to leaving at the top (100%) onto the translate range, and writes translateY(<t>%) scale(<scale>) as an inline style. Before the element enters it sits at the start of the range, after it leaves at the end. Updates are batched to one per animation frame.
Reduced motionLink to this section
Under prefers-reduced-motion: reduce the engine writes no transform at all, and the stylesheet's own reduced-motion rule sets transform: none, so the media simply fills its wrapper. Both engines (ParallaxListener and the vanilla initParallax()) follow the preference live: turning it on mid-visit clears the inline transforms, turning it off starts the motion again, without a reload.
Responsive and state variantsLink to this section
The parallax classes have no breakpoint variants: the engine reads the class, not the media query. To have parallax on large screens only, render two images, or toggle the parallax prop from your own media-query state.
CustomizingLink to this section
ConfigurationLink to this section
The ranges and the scale live in systhema.config:
import type { SysthemaConfig } from '@systhemaui/core'
const config: SysthemaConfig = {
parallax: {
translateRange: { from: -16, to: 16 },
translateRangeBig: { from: -32, to: 32 },
scale: 1.1666,
},
}
export default configThese are the defaults. The two knobs are tied: a translate of t percent needs scale >= 1 + |t| / 50, or the media falls short of its wrapper at the extremes. Systhema takes the scale as configured and clamps both ranges to what it covers, so the shipped 1.1666 allows about 8.3% of travel either way, for parallax--big as much as for the default. Raise scale to open up the ranges: scale: 1.32 covers ±16%, scale: 1.64 covers ±32%.
parallax--noscale uses scale 1 with the clamped default range, so inside a parallax-wrapper it uncovers the wrapper's edges as it moves. Use it on media that isn't cropped.