Hide header on scroll in Svelte

Krzysztof Kowalczyk
Oct 4 · 4 min read · 99 views
In: Svelte
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.

Defining header in CSS

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.

Reacting to scroll

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

This is based on slightly modified svelte-headroom component.
That in turn is based on Headroom.js library.
Updating...

Share on