戴兜

戴兜的小屋

Coding the world.
github
bilibili
twitter

Automatically hide Header on scroll

image

This is the header of the web version of Juejin. When the scrollbar is scrolled down, the main header will hide, and the secondary header will stick to the top of the page.

image

Style#

First, we simulate these two headers with two divs.

<template>
    <div class="main-header-wrapper">
      <div class="main-header"></div>
    </div>
  <div class="sub-header-wrapper">
    <div class="sub-header"></div>
  </div>
  <div v-for="i of 100" :key="i">Test Scroll</div>
</template>

<style scoped>
  .main-header-wrapper {
    height: 50px;
  }
  
  .main-header {
    height: 50px;
    width: 100%;
    background: #ff0000;
    position: fixed;
    top: 0;
  }
  
  .sub-header-wrapper {
    height: 40px;
  }
  
  .sub-header {
    height: 40px;
    width: 100%;
    background: #00ff00;
    position: fixed;
    top: 50px;
  }
</style>

image

Here, absolute positioning is used to fix the two headers at the top of the page. These two divs are main-header and sub-header. They are wrapped in containers main-header-wrapper and sub-header-wrapper to prevent the headers from covering the main content due to being removed from the document flow.

Simplification#

Let's set aside the detection of scroll direction for now and first achieve this effect: when adding the class name hidden to the two headers, the main header hides, and the secondary header sticks to the top. This can be directly implemented using transform.

To facilitate demonstration, a button has been added to toggle the hidden class name.

    <script setup>
      import { ref } from 'vue';
      
      const isHidden = ref(false);
    </script>
    
    <template>
        <div class="main-header-wrapper">
          <div class="main-header" :class="{'hidden':isHidden}"></div>
        </div>
      <div class="sub-header-wrapper">
        <div class="sub-header" :class="{'hidden':isHidden}"></div>
      </div>
      <div v-for="i of 100" :key="i">Test Scroll</div>
      <button style="position:fixed;bottom: 10px; right: 10px;" @click="isHidden = !isHidden">
        Toggle
      </button>
    </template>
    
    <style>
      html, body {
        margin: 0;
        padding: 0;
      }
    </style>
    
    <style scoped>
      .main-header-wrapper {
        height: 50px;
      }
      
      .main-header {
        height: 50px;
        width: 100%;
        background: #ff0000;
        position: fixed;
        top: 0;
      }
      
      .sub-header-wrapper {
        height: 40px;
      }
      
      .sub-header {
        height: 40px;
        width: 100%;
        background: #00ff00;
        position: fixed;
        top: 50px;
      }
      
      .main-header, .sub-header {
        transition: transform .15s ease-in-out;
        transform: translateY(0px);
      }
      
      .main-header.hidden, .sub-header.hidden {
        transform: translateY(-50px);
      }
    </style>

View Example

Using the selectors .main-header.hidden and .sub-header.hidden to select the header elements with the hidden class name, we set their transform property to translateY(-50px) (which means offset upwards by 50px). We also set a transition for the elements to provide a smooth effect.

Thus, the display and hiding of the headers have been implemented, and the next step is to detect the scroll direction.

Implementation#

Using useScroll and useDebounceFn from vueUse simplifies some of the code.

    <script setup>
      import { ref, watch } from 'vue';
      import { useScroll, useDebounceFn } from '@vueuse/core'
      
      const isHidden = ref(false);
      
      const { directions, isScrolling, arrivedState } = useScroll(document)
      
      const checkHeaderStatus = useDebounceFn(
        (top, bottom, topArrived) => {
          if (topArrived) {
            isHidden.value = false
            return
          }
          if (top) {
            isHidden.value = false
          } else if (bottom) {
            isHidden.value = true
          }
        },
        100
      )
      watch(directions, () => {
        if (isScrolling.value) {
          checkHeaderStatus(directions.top, directions.bottom, arrivedState.top)
        }
      })
    </script>

useScroll can output reactive scroll directions, scroll states (to top/bottom), and whether it is scrolling.

The directions returned by the function are a reactive object, which we listen to. When this variable changes, it indicates that the scroll direction has changed.

To prevent high-frequency triggers (such as repeatedly scrolling up and down) from causing the header to behave erratically, a debounce function is used to limit it. In the checkHeaderStatus function:

    if (top) {
      isHidden.value = false
    } else if (bottom) {
      isHidden.value = true
    }

is key; when scrolling up, the header is shown; when scrolling down, the header is hidden.

You may notice that there is an unclear piece of code above this segment:

    if (topArrived) {
      isHidden.value = false
      return
    }

When the scrollbar reaches the top, the header is directly displayed to avoid misjudgment caused by the rubber band effect in Safari on iOS devices.

So why does the Safari rubber band effect also trigger the scroll event ah ah ah ah ah!!! Even the y value can go negative.

View Example

There is also a problem here; only the scroll to the top is detected, but not the scroll to the bottom. This is because I couldn't think of a good way to detect it.

For the former, it can be achieved by checking if the y value is 0 (or less than 0); for the latter, normally, when scrolling to the bottom, the document height minus the viewport height should be consistent with the y value of the scrollbar. However, this may not be consistent in Safari; in Safari, when the address bar collapses, the above formula holds; but when the address bar is expanded, the two will differ by the height of the address bar. I haven't found a way to determine whether the address bar is expanded.

If anyone knows how to determine whether it has scrolled to the bottom, I would appreciate it if you could share.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.