kindoflew / svelte-parallax

a (small) spring-based parallax component library for Svelte
MIT License
145 stars 5 forks source link
parallax svelte sveltekit

svelte-parallax

A (small) spring-based parallax component library for Svelte.

NOTE: This is at 0.6.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!

BREAKING CHANGE: From v0.6.0 on, the onProgress prop in Parallax receives a number representing scroll progress, instead of an object with several fields. See CHANGELOG for details.


Content


Install

npm i -D svelte-parallax


svelte-parallax

This package is based on react-spring/parallax. The API is pretty similar and it functions (mostly) the same under the hood (See differences between them).


Play with a basic demo here


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> and <StickyLayer> 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, StickyLayer } 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>

  <StickyLayer offset={{ top: 0.5, bottom: 2 }}>
    <p>A description of a horse and a bird.</p>
  </StickyLayer>

  <ParallaxLayer rate={2} offset={2} style="background-color: lightblue;" />
</Parallax>


<Parallax>

Props Type Default
sections number 1
sectionHeight number window.innerHeight
config
{
stiffness?: number;
damping?: number;
precision?: number;
}
{ stiffness: 0.017, damping: 0.26 }
threshold { top: number, bottom: number } { top: 1, bottom: 1 }
onProgress (number) => void undefined
onScroll (number) => void undefined
disabled boolean false

Details


<ParallaxLayer>

Props Type Default
rate number 0.5
offset number 0
span number 1
onProgress (number) => void undefined

Details


<StickyLayer>

Props Type Default
offset { top?: number, bottom?: number } 0
onProgress (number) => void undefined

Details


Using progress in ParallaxLayer and StickyLayer

Both of these components expose progress in two different ways. The first way is through their onProgress prop. The example below is using progress to dynamically change the background-color of Parallax as the ParallaxLayer crosses the viewport:

<script>
  import { Parallax, ParallaxLayer } from 'svelte-parallax';

  const [red, green, blue] = [100, 100, 200];
  let backgroundColor = `rgb(${red}, ${green}, ${blue})`;

  const handleProgress = (progress) => {
    const p = 1 + progress;
    backgroundColor = `rgb(${red * p}, ${green * p}, ${blue * p})`;
  };
</script>

<Parallax sections={3} style="background-color: {backgroundColor}">
  <ParallaxLayer
    offset={1}
    onProgress={handleProgress}
  >
    <div>
      I'm changing the background color!
    </div>
  </ParallaxLayer>
  ...
</Parallax>

The second way is using let:progress. This example is rotating a div inside StickyLayer while it is sticky:

<script>
  import { Parallax, StickyLayer } from 'svelte-parallax';
</script>

<Parallax sections={3}>
  <StickyLayer
    offset={{ top: 1, bottom: 2 }}
    let:progress
  >
    <div style="transform: rotate({progress * 360}deg)">
      I'm spinning!
    </div>
  </StickyLayer>
  ...
</Parallax>

Which one you use is up to you, but I think onProgress probably makes the most sense when the value will be used in something outside of the layer it comes from and let:progress is more useful when you want to use the value inside the layer it comes from.

NOTE: In the Parallax container, progress is only accessible through the onProgress prop. This is mostly because I'm not exactly sure how svelte handles shadowing let variables (they are all named progress to have a unified API), and I'd rather not introduce a feature that could accidentally rely on internal svelte behavior that could change. The general usage of onProgress in Parallax is the same as that of the two layer components.


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)
{
selector?: string;
duration?: number;
easing?: (number) => number;
}
See below


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 (number) => number 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


Differences from react-spring/parallax

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="https://github.com/kindoflew/svelte-parallax/raw/main/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="https://github.com/kindoflew/svelte-parallax/raw/main/horse.jpg"
      alt="a horse"
    />
  </ParallaxLayer>
  <ParallaxLayer rate={-0.4} offset={2}>
    <img 
      src="https://github.com/kindoflew/svelte-parallax/raw/main/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>

Get current section

If you want to get the current section that is being scrolled, you can do something like this (starts when the section is at the top of the viewport; changes when the next section reaches the top of the viewport -- you can adjust the math to suit your specific needs):

<script>
  import { Parallax, ParallaxLayer } from 'svelte-parallax';

  // bind:innerHeight
  let innerHeight;
  // `section` prop passed to `Parallax`
  const sections = 3;

  let section;
  const handleScroll = (scrollTop) => {
    // add 1 so section isn't zero-indexed
    section = Math.floor(scrollTop / innerHeight) + 1;
  };
</script>

<svelte:window bind:innerHeight />

<div style="position: fixed;">
  Currently in section {section}!
</div>

<Parallax sections={sections} onScroll={handleScroll}>
  <!-- your stuff here -->
</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 sandbox app
cd sandbox
npm install
npm run dev # can also be run from root folder once installed

This will run a dev server on localhost:3000. The source lives in src and sandbox is there for live feedback while working.

Things I Probably Need: