Scrolly

section icon home home icon

Svelte Scroller Functionality

link icon

Minimal example of how Svelte Scroller works.

Main points

  • Scroller parameters (can be bound to the Scroller with bind or as one-way binding):
    1. index - Index of the currently active (= in viewport and at the correct vertical position, as determined by top, bottom and threshold params) foreground element
    2. offset - Float between 0 and 1 corresponding to the offset of the currently active foreground element. Value is 1 when that element becomes inactive.
    3. progress - Float between 0 and 1 for the progress of the whole scrolly. Becomes 1 when the last foreground element has become inactive.
    4. top - The vertical position that the top of the first foreground element must scroll past before the background becomes fixed, as a proportion of viewport height (0 to 1). In the most common usecase, we want to keep this as 0 and give the background a height of 100vh, and also push the first foreground element to the 'top', i.e. so that the top of the first foreground element and the top of the background element align. Then the background becomes fixed in place as soon as its top aligns with the top of the viewport. In this example, the foreground elements each have a height of 100vh as well. The foreground content is less than 100vh (see the while part with text), but the extra 'padding' of height is what allow for the scrolly effect to happen.
    5. bottom - Once the bottom of the last foreground element passes this point, the background becomes unfixed. Best to just keep it at 1 to avoid the background becoming unfixed and scrolling over other elements down the page unintentionally.
    6. threshold - Once a foreground element crosses this point in the element's total height, it becomes 'active'. E.g. if it's 0.5 that means that when a foreground element is 50% visible in the viewport, it becomes active.

Code highlights


Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

This is the background content. It will stay fixed in place while the foreground scrolls over the top.

Index: 0, i.e. section #0 is currently active

Offset: 0

Progress: 0

Count: 0

This is the zero foreground section.
This is the first foreground section.
This is the second foreground section.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Svelte Scroller with D3 charts

link icon

Svelte Scroller with a D3 graph where the graph updates happen from D3 based on index of steps.

Outline of the problem

  • In the Graph Svelte component, we pass props from the Scroller, namely index, progress and offset. Here we only use index, which tells us the index of the step we're currently on.
  • The goal is to use this index to pass as a prop to D3 graph so that the graph will update based on the index, i.e. the step.
  • The problem is that if it's done the wrong way, the index will be passed in too many times/too often to the graph and it will get confused about when to update. This can interfere with update logic inside of D3, such as transitions. It can also impact performance.
  • First note that any reference to any of the props width, height, index etc. passed in from the scroller needs to happen inside a reactive statement. You can test this with console.log(index), which will produce undefined, as opposed to $: console.log(index), which will console-log the index exactly once each time it updates. By the same logic, any graph props that use any of these values need to be defined reactively. If you want to test this, add a console.log(index) in the Graph code logic, in onMount and in afterUpdate. You will see that onMount will return undefined, while afterUpate will return many many console logs, rather than only when the index value actually changes. Only the reactive statement, $: console.log(index), will log the index as expected - exactly once each time it changes, which in this case is each time the next or previous step enters the correct place in the viewport.
  • Suppose you're updating the props correctly with a reactive declaration
    $: props = ... but then either only calling all your graph-creation, update and drawing logic from onMount or from afterUpdate or from both. This is no good because using onMount alone and then nothing else will give the graph no chance to update its props, so it can't take in the index as a parameter and draw different things depending on the index. Using afterUpdate is also no good because that will keep firing every time anything updates and so it will overfire and pass in the index prop or other props to the graph way too often. This is exactly what we want to avoid. So, in conclusion, onMount will not allow for updates based on props, while afterUpdate will unintentionally create too many updates.

Summary of solution

  • The takeaway is: use reactive statements or declarations if referring to Scrolly variables to ensure enclosed logic updates correctly. More specifically:
  • Define graph props (which depend on the Scrolly variables) reactively with
    $: props = ...
  • Set up the Graph from onMount, passing it the container, (initial) props and (initial) data.
  • Write another reactive declaration that passes in the props to the graph and calls any functions that depend on these props which are relevant to the given step, e.g. $: graph.props(props).draw()
  • Inside of that logic, use conditions based on the index to call different functions that will draw different things. These will now only be called once because the reactive declaration only updates once each time that index updates.

Scroller Logic

  • Each step has a height of 100vh (semi-transparent) but the actual content is less.
  • The background container has height of 100vh (red lines) but the content can have less, in which case you can position it in the middle of the background container by making the background container have display: flex.
  • The scrolly becomes sticky when the top of the background container hits the top of the viewport. This corresponds to top=0. It becomes unstuck when the bottom of the background container hits the top of the viewport. This corresponds to bottom=1.
  • The scroller threshold is 0.5, meaning that for each step, it becomes active when 50% of the step's total height is visible in the viewport. Be careful if you position the content of the steps anywhere other than the start of the step, because then the trigger point will be the same but will look visually different, as the position of the actual content for the step has shifted downwards.
  • If you don't like having the 0th step at the top of the scroller container, the easiest thing to do would be to start with a step that has no content and adjust its height. Here we have step 0 with height: 50vh, which makes the start of step 1 align with the center of the viewport.

Code highlights


This is the first foreground section. Bars will grow to their final position.
This is the second foreground section. Bars will change colour.
This is the third foreground section. Bars will go back to initial state.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Svelte Scroller with GSAP for transitions

link icon

Svelte Scroller with a D3 graph using GSAP to transition on ScrollTrigger-based steps. Running vanilla JS logic, targetting DOM elements with query selectors, from inside of Svelte.

Main points

  • In this approach, we don't make use of the index variable that can be passed in as a prop from Svelte Scroller or write any conditional on index code for the graph state inside of Svelte. Instead, we give each step's DOM element (here a div) a unique id and use this to target the step entirely from the vanilla JS-based GSAP ScrollTrigger code. The graph state for each step is still controlled from D3.
  • In the graph-drawing logic, we define all the different steps as separate functions, e.g. stepOneLogic(), and then call all of them as callbacks to the onToggle method of the ScrollTrigger when the correct step is hit. We call all of them from the same method gsapAnimate(). Note that each step becomes active and displays the graph state for that step when the top of the step hits the center of the viewport on scrolling down, but when the bottom of the step enters the viewport when scrolling back up.
  • In the Svelte code, we wrap the graph-drawing logic inside a reactive declaration, which also checks that the DOM element into which we are injecting the graph exists. We could also have done that in an onMount (which has to check that the width and height variables exist). However, then it wouldn't be responsive, as it only draws the graph once and doesn't update when the width and height update. The reactive declaration approach does that.

Code highlights


This is the first foreground section. Bars will grow to their final position.
This is the second foreground section. Bars will change colour.
This is the third foreground section. Bars will get random length.