作ったもの
スクロールで出たり消えたりするコレ、なんて呼ばれてるんでしょうかね。
ちょうどこれが使いたくて探してみたものの名称が分からないのでうまく検索できず、いくつか見つかったやつも出てるか消えてるかの0、1の状態のものしかなかったので自分で作ってみることにしました。
用意するもの
- React
- 使い慣れたエディタ
お好みで
- TypeScript
- TailwindCSS
作り方
実装イメージ
- stickyな要素が元の場所にいるとき、ネガティブな値を指定しても表示位置が変わることはない
- stickyな要素が張り付いているとき、ネガティブな値を指定すると指定した位置に表示される
事前に試してみた結果上記のことが確認できたので、下にスクロールすれば(消す場合)現在の位置から移動した分だけマイナスしていき、上にスクロールすれば(出す場合)現在の位置から移動した分だけプラスしていけば消したり出したりできそうです。
また、stickyな要素はいつも通り実装し、その要素を display:contents
な要素でラップする感じで実装すればわりと機能だけに集中して作れそうな気がします。
実装
"use client";
import { useEffect, useRef } from "react";
export type HideStickyProps = {
children: React.ReactNode;
placement: "top" | "bottom";
max?: number;
min?: number;
};
export function HideSticky({
children,
placement,
max = 0,
min,
}: HideStickyProps) {
const el = useRef<HTMLDivElement>(null);
const currentOffset = useRef(max);
const currentScrollTop = useRef(0);
useEffect(() => {
const sticky = el.current?.firstElementChild as HTMLElement;
let { height } = sticky.getBoundingClientRect();
currentScrollTop.current = document.documentElement.scrollTop;
// サイズ変更を反映する
const observer = new ResizeObserver(() => {
height = sticky.getBoundingClientRect().height;
if (min == null) {
handleScroll();
}
});
observer.observe(sticky);
function handleScroll() {
const before = currentScrollTop.current;
currentScrollTop.current = document.documentElement.scrollTop;
const scroll = before - currentScrollTop.current;
currentOffset.current = Math.min(
max,
Math.max(min ?? -height, currentOffset.current + scroll)
);
sticky.style[placement] = `${currentOffset.current}px`;
}
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
observer.disconnect();
};
}, [placement, max, min]);
return (
<div ref={el} className="contents">
{children}
</div>
);
}
解説
display:contents
なラッパー要素から直下のstickyな要素を取得します。
現在のスクロール位置を保存しておいて、あとはスクロールイベントが発火するたびにスクロールした分だけ現在要素の位置から動かしていくだけです。
ただ、そのままだとどこまでも消えていってしまうので表示の最大、最小位置を設定しておきます。
この実装では最小位置を要素の高さ分としているので(普通は気にする必要ないけど)要素の高さの変更を ResizeObserver
を使って監視しています。
(いつのまにか現れたobserver3兄弟本当に便利)
使い方
いつも通り実装したstickyな要素を今回作ったコンポーネントでラップします。
<HideSticky placement="top">
<header className="sticky top-0">
...
</header>
</HideSticky>
まとめ
なんとなく作ってみましたが、自分の持っているデバイスでは案外いい感じに動いているようです。
iPhoneは持っていないのでiPadのSafariで確認したところやや引っかかる感じがしたものの、機能をとるか完璧な見た目をとるかを選べるくらいにはなっているのではないでしょうか。
(もっと古い端末だとどうかは分かりません)
ちなみにiPadのSafariのときだけCSSのTransitionで出てるor消えてるの0,1モードを入れてみたけど対して変わらなかったので消してしまいました。
元々JSで実装していた機能がどんどんブラウザのAPIとして実装されていっているので、アドレスバー等で既に良く使われているこの機能も早く実装されるといいなと思います。
それにしてもなんていう名前なんでしょうね、これ。