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.
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>
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>
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.
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.