Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Created December 14, 2017 15:35
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rich-Harris/351ec2fc4fcf375d2c8e9090620a8bed to your computer and use it in GitHub Desktop.
Save Rich-Harris/351ec2fc4fcf375d2c8e9090620a8bed to your computer and use it in GitHub Desktop.
svelte-kit-scroller

Am planning to open source this when I get some breathing room, but a gist will do for now.

Usage:

<Scroller top=0.2 bottom=0.8 threshold=0.5 bind:index bind:offset bind:progress parallax>
  <div slot='background'>
    <!-- fixed, or parallaxing (if the parallax option is set) background -->
  </div>

  <div slot='foreground'>
    <!-- the foreground scrolls normally. As it does, the values of
         `index`, `offset` and `progress` update -->
  
    <section>
      <!-- typically each section will occupy a decent chunk of vertical space -->
      <p>this is the first section</p>
    </section>

    <section>
      <p>this is the second section</p>
    </section>
  
    <section>
      <p>this is the third section</p>
    </section>
  </div>
</Scroller>

The options are as follows:

  • top — the vertical position at which the background becomes fixed (i.e. with top=0.2, the background will scroll up normally with the rest of the page content until the top edge hits window.innerHeight * 0.2, at which point it becomes fixed
  • bottom — the vertical position at which it becomes unfixed
  • threshold — the vertical position at which each foreground section causes the index to change. At threshold=0.5 (the default), the index changes from 0 to 1 when the top of the second section passes the middle of the page
  • query — the selector used for identifying foreground sections. Default to section but could be e.g. query=li

We can listen to the state of the scroller with these bindings:

  • index — the currently active section. Starts at 0 (maybe it should be -1 before the first section crosses the threshold?)
  • offset — how much of the active section we've scrolled through (i.e. in a window that's 1000px high, if the top edge of the section is at 400px, and bottom edge is at 800px, and threshold=0.5 aka 500px, offset will be 0.25 because 500px is 0.25 of the way between 400px and 800px
  • progress — the total progress, between 0 and 1, made between the foreground's top edge hitting top and the bottom edge hitting bottom
<:Window bind:innerHeight=wh on:scroll='handleScroll()' on:resize='handleResize()'/>
<div ref:outer>
<div class='background-container' :style>
<div ref:background>
<slot name='background'></slot>
</div>
</div>
<div ref:foreground>
<slot name='foreground'></slot>
</div>
</div>
<style>
ref:outer {
position: relative;
}
ref:background {
position: relative;
width: 100%;
}
ref:foreground::after {
content: ' ';
display: block;
clear: both;
}
.background-container {
position: absolute;
width: 100%;
/* height: 100%; */
/* in theory this helps prevent jumping */
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
</style>
<script>
export default {
data() {
return {
top: 0.1,
bottom: 0.9,
threshold: 0.5,
index: 0,
offset: 0,
query: 'section'
};
},
computed: {
top_px: (top, wh) => Math.round(+top * wh),
bottom_px: (bottom, wh) => Math.round(+bottom * wh),
threshold_px: (threshold, wh) => Math.round(+threshold * wh),
style(fixed, offset_top, bottom_px, top_px, left_px, width, height) {
return `
position: ${fixed ? 'fixed' : 'absolute'};
top: ${offset_top}px;
left; ${fixed ? left_px : 0}px;
width: ${width}px;
`;
}
},
oncreate() {
this.sections = this.refs.foreground.querySelectorAll(this.get('query'));
this.handleResize();
},
methods: {
handleScroll() {
const { top_px, bottom_px, threshold_px, parallax } = this.get();
// determine fix state
const foreground = this.refs.foreground.getBoundingClientRect();
const background = this.refs.background.getBoundingClientRect();
const foreground_height = foreground.bottom - foreground.top;
const background_height = background.bottom - background.top;
const available_space = bottom_px - top_px;
const p = (top_px - foreground.top) / (foreground_height - available_space);
const { sections } = this;
let offset_top;
let fixed;
let index;
let offset;
if (p <= 0) {
offset_top = 0;
fixed = false;
offset = 0;
index = 0;
} else if (p >= 1) {
offset_top = foreground.top >= top_px ? 0 : (foreground_height - background_height);
fixed = false;
offset = 0;
index = sections.length;
} else {
offset_top = parallax ?
Math.round(top_px - p * (background_height - available_space)) :
top_px;
fixed = true;
for (index = 0; index < this.sections.length; index += 1) {
const section = this.sections[index];
const { top } = section.getBoundingClientRect();
const next = this.sections[index + 1];
const bottom = next ? next.getBoundingClientRect().top : this.refs.foreground.getBoundingClientRect().bottom;
if (bottom >= threshold_px) {
offset = (threshold_px - top) / (bottom - top);
break;
}
}
}
this.set({
progress: p,
index,
offset,
offset_top,
fixed
});
},
handleResize() {
const { left, right } = this.refs.outer.getBoundingClientRect();
const bg = this.refs.background.getBoundingClientRect();
const fg = this.refs.foreground.getBoundingClientRect();
this.set({
width: right - left,
foreground_height: fg.bottom - fg.top,
background_height: bg.bottom - bg.top,
left
});
}
}
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment