Skip to content

Parallax

A page-scroll-driven band whose layers drift as it passes through the viewport, in CSS alone. Motion stops entirely for visitors who ask for less, and browsers without scroll-driven animation show the same still composition.

Import

import { Parallax, ParallaxLayer } from "@hyzer-labs/ui"

Demo

A band with no in-flow content has no height. Give it one with style, a class, or real content, the way every demo below does with min-height. Most of the demos below sit in a bounded, scrollable box so they fit on this page. Scroll inside that box to see the layers move.

That box is part of these docs, not part of how the component works. On an ordinary page, the page's own scroll drives the same thing, with no box around it. The band below shows it: it sits in normal page flow with nothing wrapping it, so keep scrolling this page and its layer drifts right along with it. It has no Example frame either, because that bordered box would become a scroll container of its own. That is the mistake the console warning on this page describes, a wrapper that quietly becomes the scroll container.

<!-- no ScrollStage, no wrapper — this band sits in normal page flow -->
<Parallax as="section" style="min-height: 16rem">
	<ParallaxLayer y="6rem">…</ParallaxLayer>
</Parallax>

This band lives in the page's own scroll, with no bounded box and no wrapper around it.

A slow background layer behind foreground copy. The layer is a plain ParallaxLayer holding an Image. The copy is an ordinary Container child, so it sets the band's height and sits above the layer by default (layers default to z-index: -1). Make the art itself taller than the band, the way a real hero background is. The layer's own bleed already covers its travel, so a generously tall source image keeps every edge out of sight while it drifts.

Copy sits above the drifting background

The background drifts as you scroll; the copy stays put.

<Parallax as="section" style="min-height: 60vh">
	<ParallaxLayer y="8rem">
		<Image src="/photos/ridge.jpg" alt="" fit="cover" style="width: 100%; height: 100%" />
	</ParallaxLayer>
	<Container>
		<h2>Copy sits above the drifting background</h2>
	</Container>
</Parallax>

Props

NameTypeDefaultNote
asstring'div'Rendered via <svelte:element>. 'section' is the common choice for a decorative band.
axis'y' | 'x''y'Which scroller axis drives the drift. 'y' (default) is the page's vertical scroll. 'x' tracks the band's horizontal crossing. Set it when the band is a panel inside a HorizontalScroll, or its layers will sit still.
childrenSnippet—Layers and foreground content, in normal flow.
classstring—Merged after the hz-parallax class.

Anything not listed above is forwarded as an attribute to the root element (or the native control in form components). So id, data-*, aria-*, and event handlers just work.

ParallaxLayer

NameTypeDefaultNote
xstring | number0Total horizontal travel across the band's pass through the viewport. A number is px; a string is a CSS length used verbatim. Negative drifts the other way.
ystring | number0Total vertical travel. Same rules as x.
znumber-1z-index inside the band's own stacking context. The default sits the layer behind foreground content; 1 puts it in front.
childrenSnippet—The layer's art. Always non-interactive; see Accessibility.
classstring—Merged after hz-parallax-layer.

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-parallax

Data attributes

HookValuesStyles
data-axis'y' | 'x'Which axis of the nearest scroller drives the drift. Stamped for both values. Default y — the page's vertical scroll. x tracks the band's horizontal crossing, for a band used as a panel inside a HorizontalScroll.

Custom properties

HookValuesStyles
--hz-parallax-x<length> — default 0Total horizontal travel of one layer across the band's pass through the viewport. Set it in your own CSS (and omit the x prop) to change travel per breakpoint. Negative drifts the other way.
--hz-parallax-y<length> — default 0Total vertical travel. Same rules as --hz-parallax-x.
--hz-parallax-z<number> — default -1The layer's z-index inside the band. The default sits it behind the band's content; 1 puts it in front.
--hz-parallax-range<animation-range> — default coverWhich part of the band's pass through the viewport the drift is spread over. cover is the whole pass; entry, exit, and contain narrow it.

Part classes

HookValuesStyles
.hz-parallax-layerchild elementOne drifting layer: absolutely positioned, sized to the band plus its own travel so the edges stay covered, decorative (aria-hidden) and click-through by default.

Accessibility

Every layer is aria-hidden and pointer-events: none by default. Layers are decorative, so put buttons and links in a plain, non-layer child of the band instead.

prefers-reduced-motion: reduce removes the drift entirely, with no opt-out. Scroll-triggered parallax is exactly the motion WCAG 2.3.3 asks you to let people turn off, and this component treats none of it as essential. A browser without scroll-driven animation support shows the same still composition, with no polyfill and no console noise. Nothing moves until the visitor scrolls, so there is no automatic motion to satisfy 2.2.2 either. axis="x" follows the same reduced-motion rule: a horizontal band goes just as still.

The component reorders nothing and adds no role, tabindex, or live region. Reading and focus order come from as and your own content. It also contributes no color, so text sitting over a moving layer still has to meet contrast on its own.