Parallax
<quiet-parallax>
Drifts a background layer as the element travels through the viewport, creating a scroll-driven parallax effect. Use a parallax for hero banners, full-bleed section breaks, and landing pages where the background should add a sense of depth as the reader scrolls.
Slot content into the component and it drifts as the element scrolls through the viewport. The effect is built entirely on CSS scroll-driven , so it runs off the main thread with no scroll listeners and nothing to wire up. Scroll the page to watch it move.
<quiet-parallax id="parallax__overview"> <img slot="background" style="--offset: 0.18;" src="/assets/images/parallax/overview.svg" alt="Stylized sunset over layered hills" /> <img slot="background" style="--offset: 0.32;" src="/assets/images/parallax/layer-ridge-3.svg" alt="" /> <div class="hero"> <strong>Parallax</strong> <span>Scroll to watch the scene drift</span> </div> </quiet-parallax> <style> #parallax__overview { height: 360px; border-radius: var(--quiet-border-radius-md); /* Stack the layers so the near hill drifts faster than the scene behind it */ img { position: absolute; inset: 0; } .hero { display: grid; justify-items: center; gap: 0.5rem; text-align: center; color: white; } strong { font-size: clamp(2rem, 6vw, 3.5rem); font-weight: 600; } span { font-size: 0.8rem; letter-spacing: 0.15em; text-transform: uppercase; opacity: 0.85; } } </style>
Scroll-driven animations are supported in Chrome, Edge, and Safari, but Firefox keeps them behind a . Unsupportive browsers and users who prefer reduced motion will see the image, but not the effect.
Examples Jump to heading
Providing content Jump to heading
Slot an image, video, or any other visual element into the background slot. It's sized to fill
the box for you, so a bare element works with no wrapper.
The parallax element has no intrinsic size of its own, so it ships with a small min-height to
keep a bare one visible. Set a height or min-height to size it to your layout,
since the drift needs room to travel.
The background slot is scaled and clipped, so reserve it for content that can fill the box and
take some cropping. Anything that must stay crisp or sit still belongs in the default slot instead. See the
Overlaying Content example for details.
<quiet-parallax id="parallax__background"> <img slot="background" src="https://images.unsplash.com/photo-1536500152107-01ab1422f932?q=80&w=2070&auto=format&fit=crop" alt="Calico kitten lying down against a warm yellow background" /> </quiet-parallax> <style> #parallax__background { height: 320px; border-radius: var(--quiet-border-radius-md); } </style>
Adjusting the intensity Jump to heading
Set the --offset custom property to control how far the background drifts, as a fraction of its
own size, so 0.2 is 20%. The background scales up to cover the extra travel, so any value stays
gap-free.
<quiet-parallax id="parallax__subtle" style="--offset: 0.06;"> <img slot="background" src="/assets/images/parallax/dunes-cool.svg" alt="Stylized teal dunes at dusk" /> </quiet-parallax> <quiet-parallax id="parallax__dramatic" style="--offset: 0.38;"> <img slot="background" src="/assets/images/parallax/dunes-warm.svg" alt="Stylized magenta dunes at dusk" /> </quiet-parallax> <style> #parallax__subtle, #parallax__dramatic { height: 280px; border-radius: var(--quiet-border-radius-md); } </style>
Overlaying content Jump to heading
Put anything that must stay crisp, like a heading, a paragraph, or a button, in the default slot. It sits still and sharp above the drifting background, centered by default, and stays interactive so links and buttons work as usual.
Stays crisp
The background drifts. This text doesn't.
<quiet-parallax id="parallax__overlay" style="--offset: 0.24;"> <img slot="background" src="/assets/images/parallax/overlay.svg" alt="Stylized purple dusk landscape" /> <div class="hero"> <h3>Stays crisp</h3> <p>The background drifts. This text doesn't.</p> <quiet-button variant="primary" pill>Get started</quiet-button> </div> </quiet-parallax> <style> #parallax__overlay { height: 360px; border-radius: var(--quiet-border-radius-md); .hero { display: grid; justify-items: center; gap: 1rem; padding: 2rem; text-align: center; color: white; } h3 { margin: 0; font-size: clamp(1.75rem, 5vw, 2.75rem); } p { margin: 0; color: rgb(255 255 255 / 0.85); } } </style>
Reversing the drift Jump to heading
Set --direction to -1 to flip which way the background moves relative to the
scroll.
<quiet-parallax id="parallax__reverse" style="--offset: 0.26; --direction: -1;"> <img slot="background" src="/assets/images/parallax/reverse.svg" alt="Stylized green hills beneath a pale sun" /> </quiet-parallax> <style> #parallax__reverse { height: 320px; border-radius: var(--quiet-border-radius-md); } </style>
Adjusting the range Jump to heading
--offset sets how far the background drifts, but --range sets when it
drifts. The travel covers the same distance either way, just spread over a longer or shorter stretch of the
element's scroll through the viewport.
By default, cover spreads the drift across the element's entire scroll through the viewport, so
the background keeps moving the whole time it's on screen. The other values pack it into a shorter window:
cover(default): the whole time the element overlaps the viewport.contain: only while it's fully on screen, resting as it enters and leaves.entry: only as it scrolls into view, then it rests.exit: rests until it starts to leave, then drifts out.
Any
value works, so you can also pass explicit lengths or percentages. Pick a value below and scroll to compare
each one.
<div id="parallax__range"> <quiet-parallax style="--offset: 0.28;"> <img slot="background" src="/assets/images/parallax/overlay.svg" alt="Stylized purple dusk landscape" /> </quiet-parallax> <quiet-radio label="Range" name="range" value="cover" class="quiet-vh-label"> <quiet-radio-item value="cover">cover</quiet-radio-item> <quiet-radio-item value="contain">contain</quiet-radio-item> <quiet-radio-item value="entry">entry</quiet-radio-item> <quiet-radio-item value="exit">exit</quiet-radio-item> </quiet-radio> </div> <script type="module"> const container = document.getElementById('parallax__range'); const parallax = container.querySelector('quiet-parallax'); const range = container.querySelector('quiet-radio'); range.addEventListener('quiet-change', () => { parallax.style.setProperty('--range', range.value); }); </script> <style> #parallax__range { display: flex; flex-direction: column; gap: 1.5rem; quiet-parallax { height: 280px; border-radius: var(--quiet-border-radius-md); } } </style>
Creating depth with layers Jump to heading
Each element in the background slot can set its own --offset, since the value
inherits. It's a unitless fraction of the layer's own size, so 0.04 drifts only slightly while
0.34 moves much farther. Give distant layers a small offset and nearer ones a larger one, and
the gap in their travel reads as depth. A lone background uses the default of 0.2.
<quiet-parallax id="parallax__layers"> <img slot="background" style="--offset: 0.04;" src="/assets/images/parallax/layer-sky.svg" alt="" /> <img slot="background" style="--offset: 0.07;" src="/assets/images/parallax/layer-sun.svg" alt="" /> <img slot="background" style="--offset: 0.12;" src="/assets/images/parallax/layer-ridge-1.svg" alt="" /> <img slot="background" style="--offset: 0.2;" src="/assets/images/parallax/layer-ridge-2.svg" alt="" /> <img slot="background" style="--offset: 0.34;" src="/assets/images/parallax/layer-ridge-3.svg" alt="" /> </quiet-parallax> <style> #parallax__layers { height: 360px; border-radius: var(--quiet-border-radius-md); /* Stack the layers so they share the box instead of running top to bottom */ img { position: absolute; inset: 0; } } </style>
Scrolling horizontally Jump to heading
Inside a horizontal scroller, set orientation="horizontal" to make the parallax
effect drift sideways.
<div class="row" id="parallax__horizontal"> <quiet-parallax orientation="horizontal" style="--offset: 0.28;"> <img slot="background" src="/assets/images/parallax/horizontal-1.svg" alt="Stylized coastal scene at dusk" /> </quiet-parallax> <quiet-parallax orientation="horizontal" style="--offset: 0.28;"> <img slot="background" src="/assets/images/parallax/horizontal-2.svg" alt="Stylized magenta scene at dusk" /> </quiet-parallax> <quiet-parallax orientation="horizontal" style="--offset: 0.28;"> <img slot="background" src="/assets/images/parallax/horizontal-3.svg" alt="Stylized green hills beneath a pale sun" /> </quiet-parallax> </div> <style> #parallax__horizontal { display: flex; gap: 1rem; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity; quiet-parallax { flex: 0 0 76%; height: 300px; border-radius: var(--quiet-border-radius-md); scroll-snap-align: center; } } </style>
For a diagonal drift, set both --x and --y. The
orientation attribute simply presets this pair (vertical is
--x: 0; --y: 1, horizontal is --x: 1; --y: 0), but you can override
them via CSS.
Drifting text Jump to heading
The background doesn't have to be a picture. Drop large display type into the background slot
and the words drift too. Keep the offset subtle so the text remains legible.
<quiet-parallax id="parallax__text" style="--offset: 0.12;"> <div slot="background"> <strong>words in motion</strong> <span>the background layer, set adrift</span> </div> </quiet-parallax> <style> #parallax__text { height: 320px; border-radius: var(--quiet-border-radius-md); background: linear-gradient(135deg, var(--quiet-primary-900), var(--quiet-primary-950)); [slot='background'] { display: grid; place-content: center; padding: 2rem; text-align: center; } strong { font-size: clamp(2.4rem, 8vw, 4.5rem); font-weight: 600; line-height: 1.05; /* Tint the drifting type with the primary scale instead of a flat color */ background: linear-gradient(180deg, var(--quiet-primary-100), var(--quiet-primary-300)); background-clip: text; -webkit-background-clip: text; color: transparent; } span { margin-block-start: 1rem; font-size: 0.8rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--quiet-primary-300); } } </style>
Syncing to scroll progress Jump to heading
The element exposes a read-only --parallax-progress custom property that climbs from
0 to 1 as the box travels through the viewport, riding the same timeline as the
drift. Because it's a CSS variable, you can drive your own effects from it inside the overlay without using
JavaScript.
Here a heading drifts upward and a bar fills as you scroll past. This effect pairs nicely with
--range: contain so the progress remains visible from start to end.
<quiet-parallax id="parallax__progress" style="--offset: 0.26; --range: contain;"> <img slot="background" src="/assets/images/parallax/progress.svg" alt="Stylized purple landscape at dusk" /> <div class="hero"> <strong>Scroll-linked</strong> <div class="bar"><span></span></div> </div> </quiet-parallax> <style> #parallax__progress { height: 360px; border-radius: var(--quiet-border-radius-md); .hero { display: grid; gap: 1.25rem; justify-items: center; color: white; /* a no-op at progress 0, so it sits normally where the effect isn't supported */ translate: 0 calc((1 - var(--parallax-progress)) * 0.75rem); } strong { font-size: clamp(1.75rem, 5vw, 2.75rem); font-weight: 600; } .bar { width: min(60%, 240px); height: 6px; border-radius: 999px; background: rgb(255 255 255 / 0.25); overflow: hidden; } .bar span { display: block; height: 100%; border-radius: inherit; background: white; transform-origin: left; /* fill from empty to full as progress goes 0 → 1 — composited, no JS, no reflow */ transform: scaleX(var(--parallax-progress)); } } /* The bar only means something while progress can move, so hide it where it can't. */ @supports not (animation-timeline: view()) { #parallax__progress .bar { display: none; } } </style>
--parallax-progress is a read-only custom property. It's set to 0 in browsers
without scroll-driven animation support and for users who prefer reduced motion.
Using as a page section Jump to heading
A parallax also works as a full-bleed section in the middle of a page. Place one between blocks of regular content and it drifts as the reader scrolls past. The example below uses a browser frame to stand in for the page, so scroll inside it to watch the band travel through.
Above the band
Ordinary content scrolls normally. Keep reading and the band below drifts as it passes.
The band sits in the regular flow, so nothing is pinned and nothing is wired up.
Below the band
More content continues the flow. The band only moves while it overlaps the viewport.
Keep scrolling and it settles back to rest as it leaves.
<quiet-browser-frame label="example.com" flush style="max-height: 420px;"> <quiet-icon slot="icon" name="cat"></quiet-icon> <div id="parallax__section"> <div class="prose"> <h3>Above the band</h3> <p>Ordinary content scrolls normally. Keep reading and the band below drifts as it passes.</p> <p>The band sits in the regular flow, so nothing is pinned and nothing is wired up.</p> </div> <quiet-parallax style="--offset: 0.3;"> <img slot="background" src="https://images.unsplash.com/photo-1532597306305-c91ad16429f0?q=80&w=2070&auto=format&fit=crop" alt="Green-eyed brown tabby cat looking up at the camera on a tiled floor" /> <div class="hero"><strong>Full-bleed band</strong></div> </quiet-parallax> <div class="prose"> <h3>Below the band</h3> <p>More content continues the flow. The band only moves while it overlaps the viewport.</p> <p>Keep scrolling and it settles back to rest as it leaves.</p> </div> </div> </quiet-browser-frame> <style> #parallax__section { .prose { max-width: 60ch; margin-inline: auto; padding: 3rem 2rem; p { color: var(--quiet-text-muted); } } quiet-parallax { height: 300px; .hero { color: white; font-size: clamp(1.75rem, 5vw, 2.75rem); font-weight: 600; text-shadow: 0 1px 12px rgb(0 0 0 / 0.5); /* keep the overlay legible over the photo */ } } } </style>
API Jump to heading
Importing Jump to heading
The autoloader is the recommended way to import components but, if you prefer to do it manually, the following code snippets will be helpful.
To manually import <quiet-parallax> from the CDN, use the following code.
import 'https://cdn.quietui.org/v5.3.1/components/parallax/parallax.js';
To manually import <quiet-parallax> from a self-hosted distribution, use the
following code. Remember to replace /path/to/quiet with the appropriate local path.
import '/path/to/quiet/components/parallax/parallax.js';
Slots Jump to heading
Parallax supports the following slots. Learn more about using slots
| Name | Description |
|---|---|
background
|
The background layer that drifts as the element scrolls through the viewport. It's sized to fill the
box automatically, so you can drop in a bare image, video, or SVG with no wrapper. Each child rides
the same timeline, so giving each its own --offset produces a layered sense of depth.
Use fill-the-box content or large display type. It's scaled up and clipped, so it's not meant for
content that must stay pixel-crisp.
|
| (default) | Static overlay content (headings, captions, calls to action) that sits above the background and does not drift. This is the home for anything interactive or pixel-crisp. |
Properties Jump to heading
Parallax has the following properties that can be set with corresponding attributes. In many cases, the attribute's name is the same as the property's name. If an attribute is different, it will be displayed after the property. Learn more about attributes and properties
| Property | Description | Reflects | Type | Default |
|---|---|---|---|---|
orientation
|
The axis the background drifts along. Sets sensible defaults for --timeline-axis,
--x, and --y so a horizontal scroller works without wiring those up by
hand. Override any of those custom properties to fine-tune or to produce a diagonal drift.
|
|
'vertical' |
'horizontal'
|
'vertical'
|
CSS custom properties Jump to heading
Parallax supports the following CSS custom properties. You can style them like any other CSS property. Learn more about CSS custom properties
| Name | Description | Default |
|---|---|---|
--offset |
Intensity. Travel as a unitless fraction of the element's own size, each way, so 0.2 is
20%. The scale-to-cover derives from this, so any value stays gap-free.
|
0.2
|
--direction |
Set -1 to reverse the drift. |
1
|
--x |
Horizontal travel multiplier. 0 off, 1 full. Set by
orientation; override it (alongside --y) for diagonal travel.
|
0
|
--y |
Vertical travel multiplier. The default x:0, y:1 is the classic top-to-bottom drift.
Set by orientation; override it (alongside --x) for diagonal travel.
|
1
|
--timeline-axis |
Which scroll direction the timeline reads. block for a vertical scroller,
inline for a horizontal one. Set by orientation; override only for unusual
scroller setups.
|
block
|
--range |
How much of the element's trip across the viewport the drift covers. cover runs the
whole time the box overlaps the viewport; contain only while it's fully inside;
entry/exit confine it to the moments it's coming in or leaving. Accepts
any CSS animation-range value.
|
cover
|
--parallax-progress |
Read-only. A unitless 0–1 value that tracks the element's progress through the viewport on the same
timeline as the drift, inherited onto both slots and the overlay. Consume it from your own CSS (e.g.
inside a calc()) to sync effects to the scroll. Stays 0 where scroll-driven when
animations are unavailable, and remains at 0 with reduced motion.
|
0
|
CSS parts Jump to heading
Parallax exposes internal elements that can be styled with CSS using the selectors shown below. Learn more about CSS parts
| Name | Description | CSS selector |
|---|---|---|
overlay |
The container that wraps the static overlay (default slot) content. Centered over the background by default; restyle this part to reposition it. |
::part(overlay)
|