This article explains how to implement an element that reacts to scrolling the web page. In
Svelte.
In this example the element is a header at the top that vanishes when you scroll down and appears when you scroll up.
You’ll learn:
- how to define a simple Svelte component
- how to react to scrolling with
<svelte:window bind:scrollY={y} />
use:action
element directive to change element’s CSS style
We’ll package it up as a re-usable component VanishingHeader
.
The header is always at the top so we’ll give it a fixed position, anchored to the top and covering the whole width:
div {
position: fixed;
width: 100%;
top: 0;
}
We don’t define height, so it’ll take the height of its content.
Unfortunately, a fixed element is taken out of the layout so it’ll be on top of other content.
To fix that we’ll need a trick:
- we set a fixed height on the header
- we offset the content of the page by the same height with
padding-top
This is how we’ll use it in the page:
<style>
.header-content {
height: 42px;
}
main {
padding-top: 42px;
}
</style>
<VanishingHeader>
<div class="header-content">
Content inside vanishing header
</div>
</VanishingHeader>
<main>
<div>This is a content of the page.</div>
<div>Just a lot of lines to make the page scrollable.</div>
{ #each lines as line }
<div>A line number {line}.</div>
{ /each }
</main>
Showing and hiding the header
We will be a little bit fancy and slide the header in and out by using translateY
transition:
div {
transition: transform 300ms linear;
}
.show {
transform: translateY(0%);
}
.hide {
transform: translateY(-100%);
}
To hide the element, we’ll set hide
class which will transform y
position by -100%
of height. Since the element is at the top, this fully moves it offscreen.
To show it back, we’ll set show
class which restores y
position back to it’s original position 0, which is top of the window.
Changing CSS style with props
By default the duration of the transformation is 300 milli-seconds.
What if we want this to be configurable via props?
Svelte doesn’t have a way to template CSS definition inside <style>
but we can do it with use:action
element directive.
export let duration = "300ms";
function setTransitionDuration(node) {
node.style.transitionDuration = duration;
}
<div use:setTransitionDuration>
<VanishingHeader duration="350ms">
We define a component prop duration
with default value of 300ms
. We over-write it with value of 350ms
.
Element directive use:setTransitionDuration
calls function setTransitionDuration
when element is crated.
The argument to the function is HTML node for that element.
We can modify CSS style there.
We can bind window’s scroll position to a reactive variable:
let y = 0;
<svelte:window bind:scrollY={y} />
Whenever window.scrollY
changes, variable y
is updated.
We use Svelte’s reactivity to call a function when that happens:
$: headerClass = updateClass(y);
To determine direction and speed of scrolling, we remember the last scrollY
position and calculate delta dy
from that:
let y = 0;
let lastY = 0;
function updateClass(y) {
const dy = lastY - y;
lastY = y;
// determine show / hide class
return deriveClass(y, dy);
}
In our case, we want to hide when user scrolls down and show when user scrolls up.
To not be too jerky, we don’t change the state unless scrolling delta is above a configurable threshold:
function deriveClass(y, dy) {
// show if at the top of page
if (y < offset) {
return "show";
}
// don't change the state unless scroll delta
// is above a threshold
if (Math.abs(dy) <= tolerance) {
return headerClass;
}
// if scrolling up, show
if (dy < 0) {
return "show";
}
// if scrolling down, hide
return "hide";
}
Using slot for component children
Some components need to display arbitrary components as their children. In Svelte we achieve that with slots.
In the component:
<div>
<slot />
</div>
In the caller:
<VanishingHeader>
<div class="header-content">
Content inside vanishing header
</div>
</VanishingHeader>
When rendering a component, <slot />
will be replaced with the children of <VanishingHeader>
i.e.:
<div class="header-content">
Content inside vanishing header
</div>
All together
We encapsulate this as VanishingHeader.svelte
component:
<script>
export let duration = "300ms";
export let offset = 0;
export let tolerance = 0;
let headerClass = "show";
let y = 0;
let lastY = 0;
function deriveClass(y, dy) {
if (y < offset) {
return "show";
}
if (Math.abs(dy) <= tolerance) {
return headerClass;
}
if (dy < 0) {
return "show";
}
return "hide";
}
function updateClass(y) {
const dy = lastY - y;
lastY = y;
return deriveClass(y, dy);
}
function setTransitionDuration(node) {
node.style.transitionDuration = duration;
}
$: headerClass = updateClass(y);
</script>
<style>
div {
position: fixed;
width: 100%;
top: 0;
transition: transform 300ms linear;
}
.show {
transform: translateY(0%);
}
.hide {
transform: translateY(-100%);
}
</style>
<svelte:window bind:scrollY={y} />
<div use:setTransitionDuration class={headerClass}>
<slot />
</div>
We can use it from another component:
<script>
import VanishingHeader from "./VanishingHeader.svelte";
let lines = [];
for (let i = 1; i < 256; i++) {
lines.push(i);
}
</script>
<style>
.header-content {
height: 42px;
background-color: lightblue;
display: flex;
align-items: center;
justify-content: center;
}
main {
padding-top: 42px;
}
</style>
<VanishingHeader duration="350ms" offset={50} tolerance={5}>
<div class="header-content">
Content inside vanishing header
</div>
</VanishingHeader>
<main>
<div>This is a content of the page.</div>
<div>Just a lot of lines to make the page scrollable.</div>
{ #each lines as line }
<div>A line number {line}.</div>
{ /each }
</main>
More resources