🌃 背景
えらい人「スクロールに合わせてヘッダーを出したり引っ込めたりしたいんだけど....」
僕「あぁ、headroom.jsっていうのを使えばいけますね!」
僕(なんか古い?全然メンテされてなさそうなんだが😢)
僕「他のライブラリにしたいけど、ScrollMagicとかはやりたいことに対してオーバースペックすぎるしな....(サンプル読み中)」
...あ?...これくらいなら別に自分で作れるくね?
じゃあ自分で作るか!!
という記事です(?)
🛠️ 環境
- Vue.js v2.7
- Vuex v3.6.2
- tailwind v2.1.4
🏁 結論
こういうコードになりました 🙈
let pastTimestamp, pastScroll;
/**
* スクロール速度測定
* @param {number} scrollHeight
* @return {number}
*/
function trackScrollVelocity(scrollHeight) {
const currentTimestamp = new Date();
if (!pastTimestamp) {
pastTimestamp = currentTimestamp;
}
if (!pastScroll) {
pastScroll = scrollHeight;
}
const scrollAmount = scrollHeight - pastScroll;
const time = currentTimestamp.getTime() - pastTimestamp.getTime();
// 参照用の値を更新
pastTimestamp = currentTimestamp;
pastScroll = scrollHeight;
return scrollAmount / time;
}
/**
* スクロール方向に合わせて追従UIの位置を変更
* @param {number} velocity スクロール速度
* @param {Element | null} element 追従UIのDOM
* @param {string | undefined} addClass 付け外しするクラス(デフォルトは-translate-y-full)
* @param {number | undefined} offset 連動処理判定に入るスクロール距離(デフォルトは96px)
* @return {void}
*/
function translateStickyElement(velocity, element, addClass = '-translate-y-full', offset = 96) {
if (!element || window.innerWidth >= 1366) {
return
}
// 上にスワイプした場合は速度に関わらず上方向にスライド
// 下にスワイプした場合は速度が-1以上またはoffset内に入った瞬間に下方向にスライド
if (velocity > 0 && window.scrollY > offset) {
element.classList.add(addClass);
} else if (-1 > velocity || offset > window.scrollY) {
element.classList.remove(addClass);
}
}
export { trackScrollVelocity, translateStickyElement };
🗣️ 解説
正直スクロール速度を計測する機構さえできてしまえばあとはDOMを良い感じに動かす(今回の例ではtailwindのクラスを付け外しする)だけなので、重要なのはtrackScrollVelocity
になります。
pastTimestamp
(過去のスクロールイベント発生日時)とpastScroll
(過去のスクロールイベント発生時のページ位置)を適宜保存し、次にスクロールイベントが発生した時に比較してスクロール速度を算出してます。
あんまりグローバル変数は書きたくないんですが、この関数以外で使わないので記述を閉じ込めるためにこういう形にしました
<template>
<!--- 省略 --->
<!-- SP用ヘッダーメニュー -->
<div ref="headerNav"></div>
<!--- 省略 --->
</template>
<script>
export default {
mounted() {
document.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
handleScroll() {
const nav = this.$refs.headerNav;
if (!nav) {
return;
}
const velocity = trackScrollVelocity(window.scrollY);
translateStickyElement(velocity, nav);
this.$store.commit('setScrollVelocity', velocity);
},
}
</script>
このように共通レイアウト部分でスクロールイベントを監視して、スクロール速度をvuexで状態管理し、それを基にヘッダー以外の追従UI(ローカルナビなど)をヘッダーに合わせて移動させています。
<template>
<!--- 省略 --->
<!-- ローカルナビゲーション -->
<div ref="localNav"></div>
<!--- 省略 --->
</template>
<script>
import { translateStickyElement } from '@/Utils/scroll';
export default {
data() {
return {
nav: null,
}
},
computed: {
velocity() {
return this.$store.state.scrollVelocity;
},
},
watch: {
velocity(val) {
translateStickyElement(val, this.nav);
},
},
mounted() {
this.nav = this.$refs.localNav;
},
};
</script>
storeに格納されているスクロール速度を監視して、変更があればscroll.jsに同梱しているUI移動用の処理(translateStickyElement
)を使ってヘッダーの動きに追従させています。
なぜ自作したのか
今回採用を考えていたheadroom.jsがあまりにも古かったために代用ライブラリを探していました。
古いライブラリを採用していると、後々の脆弱性対応で痛い目をみる可能性があるからです。というか痛い目をみました😡
そのうえでシンプルな代用ライブラリを探していたのですが、headroom.jsのように「ヘッダーを良い感じにする」だけのライブラリというものが見つけられず...
ただ冷静に考えた結果「やりたいことはシンプルなんだから自分でつくればええやないか」ということに気づいたのでこうなりました。
🥰 最後に
お手製のなんちゃってheadroom.jsを披露するだけの記事でしたが、いかがでしたでしょうか?
もしかしたら拙い部分があるコードになっているかもしれませんが、似たような実装をしている方にとって少しでも参考になれば幸いです!
それでは👋