Building a Headroom-Style Header in Svelte
Tags: svelte, javascript, webdev, css
Published: 2019-10-02 19:51:13

Let us build a **headroom-style header** in [Svelte](https://svelte.dev)! Our objective in this blog post is to create a header that slides up (and out-of-view) when the user scrolls down, and re-appears when they scroll up (no matter how far down the page they are).

This is a technique used to **save room on the screen** while sparing the user from having to scroll all the way back up the page to get to the header and navigation.

We won't use the popular [headroom.js](https://wicky.nillia.ms/headroom.js) but roll up a our own simple solution while honing our Svelte skills along the way. Are you ready?

# The Layout

We'll begin with a component that has a `fixed` header as if it was already "pinned". Let's give our header a `height` and `background-color` so we can actually see it. Our Svelte component sees the light of day:

```html
<style>
 header {
    background-color: darkgrey;
    height: 80px;
    position: fixed;
    width: 100%;
  }
  main {
    min-height: 150vh;
    padding-top: 80px;
  }
</style>

<header />
<main>Lorem ipsum</main>
```

You can see that we're giving our `main` tag a `padding-top` equal to the `height` of the header otherwise the header (being `fixed`) would cover the top of `main`. We're also giving `main` some `min-height` so we can be sure that we're able to scroll up and down and test our component manually.

As it stands, we've created a fixed header that stays put as you scroll down. [Not great, not terrible](https://www.youtube.com/watch?v=Mg5HOnq7zD0). Here is our **starting point** in a code sandbox:

{% codesandbox svelte-header-1-eexq8 module=/App.svelte %}

# The Plan: Pin or Unpin

In order to hide or show the `header`, we shall target it with a conditional class so that we can joyfully control its CSS. One class will serve to **pin** the header by setting the `top` property to `0`, and the other will bravely **unpin** it by setting `top` to `-80px`, which will hide it out of view (based on its own height of 80px). 

Let's add a **transition** on `header` while we're dealing with the CSS so any change will occur over 0.3 second instead of being instantaneous and jarring and, quite frankly, unusable. I dutifully propose this extra bit of CSS:


```css
 header {
    /* ... existing properties */
    transition: all 0.3s linear;
  }
 .pin {
    top: 0;
  }
  .unpin {
    top: -80px;
  }

```
It will be up to us to **add and remove the appropriate class in response to the user actively scrolling**. Fingers crossed, everybody.

# Using Svelte State

Let's create some state to hold the value of a `headerClass` that we can then refer to in the HTML. Well, **state is simply a JavaScript assignment** in Svelte! Let's give our header a starting class of `pin`.

```html
<script>
  let headerClass = 'pin';
</script>

<header class={headerClass} />

```

Gotta love it. A simple re-assignment like `headerClass = "whatever"` will update our view. We'll do that in just a moment. But let's get our bearings and take stock of our *entire* component as it stands:


```html
<script>
  let headerClass = 'pin';
</script>

<style>
 header {
    background-color: darkgrey;
    height: 80px;
    position: fixed;
    width: 100%;
    transition: all 0.3s linear;
  }
  main {
    height: 150vh;
    padding-top: 80px;
  }
 .pin {
    top: 0;
  }
  .unpin {
    top: -80px;
  }
</style>

<header class={headerClass} />
<main>Lorem ipsum</main>
```

Our code is taking shape but everything is the same visually: still a boring old fixed header. Clearly, we have to react in some way to the user actively scrolling (and eventually update `headerClass`)!

# Scrolling Detection

How do we detect vertical scrolling in the first place?

Well... there *is* a [scroll event](https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event) listener on `window` and we can read the vertical scroll position at any time from `window.scrollY`. So we *could* wire up something like this:

```javascript
// meh
window.addEventListener('scroll', function() {
  scroll_position = window.scrollY;
  // figure out class name
}

```

We would have to do this when the component mounts and remember to remove the listener when the component is destroyed. Certainly, it's a possibility.

However, we can do less typing in Svelte: we can use the [`<svelte:window>`](https://svelte.dev/docs#svelte_window) element and even bind to the `window.scrollY` position so it's available to us as it's changing. In code, it looks like this:

```html
<script>
   let y;
</script>

<svelte:window bind:scrollY={y}/>

<span>{ y }</span>
```

The above code is a valid component. The value of `y` in the `span` will change as you scroll up and down the page ([try it in a sandbox](https://codesandbox.io/embed/svelte-scrolly-binding-fh5il)). Furthermore, we don't have to worry about removing the listener when using `svelte:window`, nor worry about checking if `window` even exists (shall the code be ran server-side). Well, that's pretty cool!

# Reactive Declarations

So we have our scroll position `y` over time. From this stream of data, we can derive our class name. But how shall we even store a new value every time `y` changes? Svelte offers **reactive declarations** with the `$:` syntax. Check out this introductory example:

```html
<script>
  let count = 1;
  $: double = count * 2;
  count = 2;
</script>

<span>
  { double }
</span>
```

The `span` will hold a value of 4 as soon as we've re-assigned `count` to `2`. 

In our case, we want `headerClass` to be reactive to the `y` position. We'll move our logic in a function of its own, much like this:

```html
<script>
   let y = 0;
   let headerClass = 'pin'
   function changeClass(y) {
      // do stuff
   }
   $: headerClass = changeClass(y);
</script>
```

In short, we can update the `class` of the `header` whenever the scroll position `y` changes. Well, it seems we are getting closer to our objective!

# What Class Name?

So we must focus on this newly introduced `changeClass` function which is in fact **the last bit of implementation**. It should return a string,'"pin"' or '"unpin"', and then our CSS can swing (actually, slide) into action. 

### Base Case

If the scroll direction doesn't change, for example if the user was scrolling down and is still scrolling down, we don't need to do anything at all but return the class name as it was. Let's make that our default case:

```javascript
   let headerClass = 'pin';
   function changeClass(y) {
      let result = headerClass;
      // todo: change result as needed
      return result;
   }
``` 

So that's our base case taken care of. But the function should return **'pin'** if the user starts scrolling up, and **'unpin'** if they start scrolling down. We're jumping a bit ahead of ourselves because right now we don't even know which way the user is scrolling; we only have a stream of `y` positions, so let's figure that out. 

### Scroll Direction

We need to compare the last `y` position to the one we're currently holding to know the distance that was scrolled in pixels. So we need to store some `lastY` at the end of each scroll cycle, then the next scroll event can use it.


```javascript
   let headerClass = 'pin';
   let lastY = 0;

   function changeClass(y) {
      let result = headerClass;
      // do stuff, then
      // just before returning the result:
      lastY = y; 
      return result;
   }
```

Now we have a `lastY` to work with so let's get our scroll direction with it. If `lastY - y` is positive the user is scrolling down, else they're scrolling up.

```javascript
   let headerClass = 'pin';
   let y = 0;
   let lastY = 0;

   function changeClass(y) {
      let result = headerClass;
      // new:
      const scrolledPxs = lastY - y;
      const scrollDirection = scrolledPxs < 0 ? "down" : "up"
      // todo: did the direction change?
      lastY = y;
      return result;
   }
```

To determine if the scrolling direction changed, we can compare it to the last scroll direction, much like we did for `lastY` in fact. We'll initialize it to `"up"` so we can trigger our effect (hiding the header) on the initial scroll down.

```javascript
   let headerClass = 'pin';
   let y = 0;
   let lastY = 0;
   let lastDirection = 'up'; // new

   function changeClass(y) {
      let result = headerClass
      const scrollPxs = lastY - y;
      const scrollDirection = scrolledPxs < 0 ? "down" : "up"
      // new:
      const changedDirection = scrollDirection !== lastDirection;
      // todo: change result if the direction has changed
      lastDirection = scrollDirection;
      lastY = y;
      return result;
   }
```

### The Right Class

If my calculations are correct, there is only one step left: to re-assign `result` when the scrolling has actually changed direction, which we now know.

```javascript
   let headerClass = 'pin';
   let y = 0;
   let lastY = 0;
   let lastDirection = 'up';

   function changeClass(y) {
      let result = headerClass
      const scrollPxs = lastY - y;
      const scrollDirection = scrolledPxs < 0 ? "down" : "up"
      const changedDirection = scrollDirection !== lastDirection;
      if(changedDirection) { // new
        result = scrollDirection === 'down' ? 'pin' : 'unpin';
        lastDirection = scrollDirection;
      }
      lastY = y
      return result;
   }
```

And that does trick! Thanks to our conditional class on `header` and our CSS, we find ourselves with a headroom-style header!


# The Whole Thing

Let's see the whole Svelte component, shall we? Let's treat ourselves to a [CSS Variable](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) so we don't have that hard-coded `80px` header height in multiple places.


```html
<script>
  let headerClass = "pin";
  let y = 0;
  let lastY = 0;
  let lastDirection = "up";

  function changeClass(y) {
    let result = headerClass;
    const scrolledPxs = lastY - y;
    const scrollDirection = scrolledPxs < 0 ? "down" : "up";
    const changedDirection = scrollDirection !== lastDirection;
    if (changedDirection) {
      result = scrollDirection === "down" ? "unpin" : "pin";
      lastDirection = scrollDirection;
    }
    lastY = y;
    return result;
  }

  $: headerClass = changeClass(y);
</script>

<svelte:window bind:scrollY={y}/>

<style>
  :root {
    --header-height: 80px;
  }
  header {
    background-color: darkgrey;
    height: var(--header-height);
    position: fixed;
    width: 100%;
    transition: all 0.3s linear;
  }
  main {
    height: 150vh;
    padding-top: var(--header-height);
  }
  .pin {
    top: 0;
  }
  .unpin {
    top: calc(var(--header-height) * -1);
  }
</style>

<header class={headerClass} />
<main>Lorem ipsum</main>


```

Here is a sandbox with this code for your enjoyment:

{% codesandbox svelte-header-blog-post-8vv15 module=/App.svelte %}

Thanks for reading and happy coding! Please feel free to leave a comment or connect with me on [twitter](https://twitter.com/collardeau/).