svelte-parallax
A (small) spring-based parallax component library for Svelte.
NOTE: This is at 0.4.x and I'm still working on stuff. Something could break and while I'm not trying to remove anything from the API it's still a possibility (I'll post a deprecation notice first instead of outright yanking something). If anything is weird, open an issue and let me know!
DEPRECATED: From v0.3.0 on, onEnter
and onExit
are being replaced with threshold
. See Parallax and CHANGELOG for details.
Content
Install
npm i -D svelte-parallax
svelte-parallax
This package is based on react-spring/parallax. The API is very similar and it functions (mostly) the same under the hood (See differences between them).
The <Parallax>
component is a container whose height will be the number of sections
you choose multiplied by the sectionHeight
prop (defaults to window.innerHeight
, which should be good for most use cases). <ParallaxLayer>
components contain anything you want to be affected and are nested inside <Parallax>
. A simple set-up may look like this:
<script>
import { Parallax, ParallaxLayer } from 'svelte-parallax';
</script>
<Parallax sections={3} config={{stiffness: 0.2, damping: 0.3}}>
<ParallaxLayer rate={0} span={3} style={"background-color: orange;"} />
<ParallaxLayer rate={-0.5} offset={1}>
<img src='horse.jpg' alt='a horse'>
</ParallaxLayer>
<ParallaxLayer rate={1} offset={1.75}>
<img src='bird.jpg' alt='a bird'>
</ParallaxLayer>
<ParallaxLayer rate={2} offset={2} style={"background-color: lightblue;"} />
</Parallax>
<Parallax>
Props | Type | Default |
---|---|---|
sections |
number | 1 |
sectionHeight |
number | window.innerHeight |
config |
object | { stiffness: 0.017, damping: 0.26 } |
threshold |
object | { top: 1, bottom: 1 } |
onProgress |
function | undefined |
disabled |
boolean | false |
Details
-
sections
: How many sections the container has. -
sectionHeight
: The height of an individual section. Defaults towindow.innerHeight
, using Svelte'sbind:innerHeight
. -
config
: Optional Svelte spring store configuration, if you want more control over parallax physics. -
threshold
: Adds a threshold above/belowParallax
that is equal to the height of the viewport multiplied by the value, each one should be a number between0
and1
.threshold.top = 1
: the effect will be active whenever the top of the container is at or above the top of the viewport,threshold.top = 0
: effect will be active whenever the top of the container is at or above the bottom of the viewport.threshold.bottom
is similar, but on the other end --1
: active when bottom of container is at or below the bottom of the viewport,0
: active when bottom is at or below the top of the viewport. -
onProgress
: Takes a function that recieves aprogress
object (See below). -
disabled
: Whether or not the effect is disabled (for a11y, etc. see Prefers-reduced-motion). Whendisabled = true
, layers will stay at their target positions. -
$$restProps
: You can add any other props you need (includingstyle
orclass
) and they will be passed to the underlyingdiv
container. If you're adding styles, messing withheight
,position
, oroverflow
might break stuff, but you do you.
onProgress
Property | Type |
---|---|
parallaxProgress |
float |
section |
number |
sectionProgress |
float |
-
parallaxProgress
: Represents the progress of the entireParallax
container scrolled, represented as a float between0
and1
. Starts at0
when the top of theParallax
container is at the top of the viewport, ends at1
when the bottom of theParallax
container is at the bottom of the viewport. -
section
: the number of the current section -
sectionProgress
: Represents the scroll progress of the currentsection
, represented as a float between0
and1
. Starts at0
when the top of thesection
is at the top of the viewport, ends at1
when the bottom of thesection
is at the top of the viewport (NOTE: not the same behavior asparallaxProgress
).
Example Usage:
<script>
import { Parallax, ParallaxLayer } from 'svelte-parallax';
let rotate;
let percentScrolled;
const handleProgress = (progress) => {
const { parallaxProgress, section, sectionProgress } = progress;
if (section === 2) {
rotate = sectionProgress * 360;
}
percentScrolled = Math.floor(parallaxProgress * 100);
};
</script>
<h1 style="position: fixed;">
{perecentScrolled}% scrolled so far!
</h1>
<Parallax sections={3} onProgress={handleProgress}>
<ParallaxLayer offset={1} style={`transform: rotate(${rotate})`} />
...
</Parallax>
NOTE: parallaxProgress
will be 0
whenever the top of the container is at or below the top of the viewport. It will be 1
when the bottom of the container is at or above the bottom of the viewport.
<ParallaxLayer>
Props | Type | Default |
---|---|---|
rate |
number | 0.5 |
offset |
number | 0 |
span |
number | 1 |
Details
-
rate
: Rate the layer will scroll relative toscrollY
. Can be positive or negative: positive will translate the layer up and negative, down.0
will scroll normally. -
offset
: Offset from the top of the container when the layer's section completely fills the viewport. Can be a float (0.5
will place the layer halfway down the first section when the first section takes up the whole viewport). -
span
: How many innerHeight-sized sections the layer will span. -
$$restProps
: You can add any other props you need (includingstyle
orclass
) and they will be passed to the underlyingdiv
container.
scrollTo
Rather than have a click listener on an entire <ParallaxLayer>
(which I think is bad for a11y because a page sized <div>
probably shouldn't be a button), <Parallax>
exports a scrollTo
function for click-to-scroll so you can use semantic HTML. It takes a little set-up because of this, but I believe the benefits outweigh a little boilerplate.
scrollTo
uses a fork of svelte-scrollto to animate scrolling, instead of relying on the native browser implementation. Because of this, you can have smooth, custom scrolling regardless of browser support for scroll-behavior
.
Parameters | Type | Description |
---|---|---|
section |
number | The section to scroll to |
config (optional) |
object | Scroll animation config options |
config
object:
Key | Type | Description | Default |
---|---|---|---|
selector |
string | CSS selector of element to focus on after scroll | "" |
duration |
number | Duration of scroll in milliseconds | 500 |
easing |
function | Easing function (import from 'svelte/easing') | quadInOut |
Example setup:
<script>
import { Parallax, ParallaxLayer } from 'svelte-parallax';
// for bind:this. name can be anything
let parallax;
</script>
<!-- bind to component instance -->
<Parallax sections={2} bind:this={parallax}>
<ParallaxLayer>
<!-- function is a method on component instance -->
<button
class='horse-btn'
on:click={() => parallax.scrollTo(2, { selector: '.top-btn', duration: 2000 })}
>
Scroll to horse
</button>
</ParallaxLayer>
<ParallaxLayer offset={1}>
<img src='horse.jpg' alt='a horse'>
<button
class="top-btn"
on:click={() => parallax.scrollTo(1, { selector: '.horse-btn', duration: 1000 })}
>
Scroll to top
</button>
</ParallaxLayer>
</Parallax>
If you really need to use something besides buttons for scrollTo
make sure to address tabindex
, focus-style, and keyup
/keydown
events (More best practices at MDN - ARIA: button role).
Tips
-
rate
: Therate
prop will affect the initial position of<ParallaxLayer>
because of the way the motion formulas work. A suggested workflow would be intially settingdisabled=true
on<Parallax>
and placing content where you want it to end up. After that, removedisabled
and then tweak rate and style until the motion is how you'd like it. -
z-index
:<ParallaxLayer>
s are absolutely positioned so they are organized on the z-axis in the order they are written in the HTML. If you don't want to mess with z-index (and who does?) make sure to place all content that should always be visible/clickable towards the bottom of<Parallax>
. y-axis order is unaffected by this because that is decided byoffset
. -
scrollY
: svelte-parallax uses Svelte'sbind:scrollY
in its motion functions, so it will not work if placed inside a scrollable element (like adiv
withoverflow: scroll
). I don't have plans to change this right now, but if enough people ask for it, who knows? -
optimization: We aren't adding
will-change: transform
or doing thetransform: translate3d(0,0,0)
hack anymore -- both rules can get in the way of other styles and can lead to performance problems of their own (plus, addingwill-change
to everything isn't recommended anyways). However, you can add these to any components that you think need it usingstyle
orclass
props.
Differences from react-spring/parallax
-
Some of the prop names are changed for no reason other than that I like them more. If you are coming from react,
span = factor
,sections = pages
,rate = speed
. -
react-spring/parallax is a scrollable container, svelte-parallax is not (you are scrolling the actual page). This means that svelte-parallax can be anywhere on the page and also that the only scrollbar will be the browser's.
-
react-spring/parallax has a
horizontal
prop on the container component, svelte-parallax does not. This is mostly because of the point mentioned above — this is not a scrollable container, so you'd have to scroll the actual browser window horizontally, which is gross to me. -
react-spring/parallax has a
sticky
prop onParallaxLayer
(that I implemented!), svelte-parallax does not. I'm working on bringing it here too, but it's tricky because CSS hates fun.
All that being said, I'd like to thank anyone and everyone who made react-spring/parallax, without whom this package would not exist.
Side-by-side example:
Recipes
Prefers-reduced-motion
Parallax effects can be jarring for those sensitive to too much visual motion. Browsers expose information about whether or not your user prefers reduced motion. You can use something like this to dynamically disable the effect for those users:
<script>
import { Parallax, ParallaxLayer } from 'svelte-parallax';
let prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
</script>
<Parallax disabled={prefersReducedMotion}>
<!-- your stuff here -->
</Parallax>
NOTE: For SvelteKit/Sapper with SSR you'd have to do that in onMount
or behind an if (process.browser)
or if (typeof window !== 'undefined')
check.
Squarespace-style
For simple, no-frills parallax effects, you can set stiffness
and damping
to 1
which will cancel out the spring effect, and then set threshold
properties to 0
so the effect will be enabled whenever the container is in the viewport.
<Parallax
config={{ stiffness: 1, damping: 1 }}
threshold={{ top: 0, bottom: 0 }}
sections={1}
>
<ParallaxLayer rate={-0.4}>
<img
src="horse.jpg"
alt="a horse"
/>
</ParallaxLayer>
</Parallax>
You could even have multiple parallaxing layers with static divs in between like this:
<Parallax
config={{ stiffness: 1, damping: 1 }}
threshold={{ top: 0, bottom: 0 }}
sections={3}
>
<ParallaxLayer rate={-0.4}>
<img
src="horse.jpg"
alt="a horse"
/>
</ParallaxLayer>
<ParallaxLayer rate={-0.4} offset={2}>
<img
src="bird.jpg"
alt="a bird"
/>
</ParallaxLayer>
<!-- Rate is 0, offset is between the two parallaxing layers above -->
<ParallaxLayer rate={0} offset={1} style={"background-color:lightblue;"} />
</Parallax>
Contributing
Contributions are welcome! I'm keeping everything in JavaScript for now and I've tried to comment a lot to make jumping in easier. There really isn't a whole lot to the JavaScript parts so that helps too.
To work locally:
git clone git@github.com:kindoflew/svelte-parallax
cd svelte-parallax
npm install
# if you want to use the demo app
cd demo
npm install
npm run dev # can also be run from root folder once installed
This will run a dev server on localhost:5000. The source lives in src
and demo
is there for live feedback while working.
Things I Probably Need:
- Optimzations: Didn't want to optimize in advance (YAGNI and Svelte takes care of a lot of it already), but I did notice that on mobile any
<ParallaxLayer>
that has only abackground-image
(no nested content) and arate
other than0
will flicker until it stops moving. Only tested on an iPhone7 and iPhone8 so far. Also, note thatwill-change: transform
has had no effect. Don't know much about rendering optimizations, so I'm open to any suggestions!