とあるサイトを作っているときに、topに戻るボタンなどの要素を、右下のあたりに position: fixed; で固定配置しているときに、下方向にスクロールしていって footer の上端まできたら、その要素を footer の上端でとめたいと思いました。
position: stickey; を使えば簡単に実装できそうですが、JavaScriptでやってみたいと思います。
動き方を考えてみる
DOMの順番を考えると「topに戻るボタン」はfooter内の最後のほうにあるか、footerの直前、mainの最後あたりかと思います。今回はtabキーでの操作で最後に選択できるようにしたいと思ったので、footerの最後のほうで考えることにしました。
マークアップはこんな感じにしておきます。
<body id="top">
<header></header>
<main></main>
<footer id="footer">
<div class="footer_content">
footerのコンテンツ
</div>
<p class="footer_copyright"><small>©コピーライトが入ります。コピーライトが入ります。</small></p>
<p><a href="#top" id="pagetop">トップへもどる</a></p>
</footer>
</body>
CSSはこんな感じにしておきます。
main {
height: 100vh;
background-color: #fff;
}
#footer {
height: 500px;
background-color: #ddd;
position: relative;
}
#pagetop {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4E1C80;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
position: absolute;
top: -25px;
right: 1rem;
z-index: 5;
}
最終的に止めたい位置を先に設定しておきたいので、footerにid=”footer”をつけて「topに戻るボタン」の要素にid=”pagetop”をつけて、(footerが親、要素が子)footerにposition: relative;、要素にposition: absolute;を設定して位置を確認しておきます。要素はfooterよりも要素の半分程度上に異動させておきたいのでtop: -25px;を設定しています。

次に、ファーストビューやスクロールする前は右下に固定しておきたいので、「topに戻るボタン」の要素にclass=”fixed”をつけて、このクラスがついているときだけのCSSを書きます。
<body id="top">
<header></header>
<main></main>
<footer id="footer">
<div class="footer_content">
footerのコンテンツ
</div>
<p class="footer_copyright"><small>©コピーライトが入ります。コピーライトが入ります。</small></p>
<p><a href="#top" id="pagetop" class="fixed">トップへもどる</a></p> <!-- class="fixed"を追記 -->
</footer>
</body>
main {
height: 100vh;
background-color: #fff;
}
#footer {
height: 500px;
background-color: #ddd;
position: relative;
}
#pagetop {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4E1C80;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
position: absolute;
top: -25px;
right: 1rem;
z-index: 5;
}
/* この下を追記 */
#pagetop.fixed {
position: fixed;
top: unset;
bottom: 1rem;
right: 1rem;
}
CSSではtop: -25px;を打ち消すためにunsetを設定しています。これで要素にclass=”fixed”がついた時のみ右下に固定されるようになります。

(多少のズレが出る場合もありますので、適宜リセットするCSSを追加してみてください。)
やりたいことは、「要素にクラスがついている状態で下にスクロールしていくとfooter上端のあたりでクラスがはずれて、そこに要素がおさまるということがしたい」ということになります。
JavaScriptを使ってクラスの付け外しをする
スクロールしたときに、もし画面内にfooterの上端が現れたらクラスをはずして、そうじゃなかったらクラスをつける。というのをプログラムで制御します。
最初に
- footer
- ボタンの要素
をそれぞれ読み込んでいきます。
var fixedElm = document.getElementById('pagetop');
var targetElm = document.getElementById('footer');
読み込まれているか確認してみます。
console.log(fixedElm);

取得できてますね。
console.log(targetElm);

こちらも大丈夫です。
で、まずは要素の高さを調べます。
console.log(fixedElm.clientHeight);

CSSで指定したとおり50pxです。
次にfooterです。スクロールしたとき見えてくる上の部分が欲しいのでgetBoundingClientRect()を使います。
console.log(targetElm.getBoundingClientRect());

heightにはCSSで指定した高さがありますね。
欲しいのは画面の上からfooterの上端なので、ここにあるtopの値を使います。
これをスクロールイベントの中に入れてみましょう。
document.addEventListener('scroll', function(e){
console.log(targetElm.getBoundingClientRect().top);
});

下にスクロールすると値が減っています。

footerがブラウザの表示画面より上にいくとマイナスになります。
ということは、表示されている画面と、footerの上端、要素を組み合わせると目的が達成できそうな気がします。
まずは表示されている画面と、footerの上端との動き方を見てみます。
document.addEventListener('scroll', function(e){
console.log(targetElm.getBoundingClientRect().top + ' / ' + window.innerHeight);
});

footerが画面の上端を超えると値がマイナスになります。
要素は右下に位置していて高さは50pxで下の余白が1remです。footerに留まるときは半分ほど上の位置にいます。
画面の高さ、要素の高さの半分、1rem、と、画面の高さ – 要素の高さの半分 – 1remを表示してみます。
console.log(window.innerHeight + ' / ' + fixedElm.clientHeight / 2 + ' / ' + (1 * parseFloat(getComputedStyle(document.documentElement).fontSize)) + ' / ' + (window.innerHeight - fixedElm.clientHeight / 2 - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize)));

で、footerの上端が、画面の高さ – 要素の高さの半分 – 1remより大きければfixedというクラスがつくという条件ができそうです。
document.addEventListener('scroll', function(e){
console.log(targetElm.getBoundingClientRect().top + ' / ' + window.innerHeight + ' / ' + ((targetElm.getBoundingClientRect().top > window.innerHeight - fixedElm.clientHeight / 2 - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize))));
});

では、これを条件にしたif文を組んでみます。
document.addEventListener('scroll', function(e){
if (targetElm.getBoundingClientRect().top > window.innerHeight - fixedclientHeight - 1 * parseFloat(getComputedStyle(document.documentEle).fontSize) ) {
if (!fixedElm.classList.contains('fixed')) {
fixedElm.classList.add('fixed');
}
} else {
if (fixedElm.classList.contains('fixed')) {
fixedElm.classList.remove('fixed');
}
}
});
条件にあわせてfixedというクラスがついたりはずれたりするようにしています。
せっかくなので関数化してみましょう。
var pagetopFixedOut = function() {
if (targetElm.getBoundingClientRect().top > window.innerHeight - fixedElclientHeight - 1 * parseFloat(getComputedStyle(document.documentElementfontSize) ) {
if (!fixedElm.classList.contains('fixed')) {
fixedElm.classList.add('fixed');
}
} else {
if (fixedElm.classList.contains('fixed')) {
fixedElm.classList.remove('fixed');
}
}
}
document.addEventListener('scroll', function(e){ pagetopFixedOut(); });
pagetopFixedOut();
全体の完成形はこちらになります。
var fixedElm = document.getElementById('pagetop');
var targetElm = document.getElementById('footer');
var pagetopFixedOut = function() {
if (targetElm.getBoundingClientRect().top > window.innerHeight - fixedElclientHeight - 1 * parseFloat(getComputedStyle(document.documentElementfontSize) ) {
if (!fixedElm.classList.contains('fixed')) {
fixedElm.classList.add('fixed');
}
} else {
if (fixedElm.classList.contains('fixed')) {
fixedElm.classList.remove('fixed');
}
}
}
document.addEventListener('scroll', function(e){ pagetopFixedOut(); });
pagetopFixedOut();
See the Pen pagetop_fixed_sample_01 by ulla_mikage (@ulla_mikage) on CodePen.
SPサイズのときにfooterが長くなって「topに戻るボタン」が押せなくなってしまう場合
footerにたくさんの情報があって、PCサイズでは横並び、SPサイズでは縦並びになって、footerの上端に「topに戻るボタン」を止めてしまうと、スクロールでボタンも上にスクロールしてボタンが押せなくなって困ってしまうことがあるかもしれません。
これはCSSでメディアクエリを使って、SPサイズとPCサイズで動作を分ければよいと思います。
main {
height: 100vh;
background-color: #fff;
}
#footer {
height: 1000px; /* 分かりやすく500pxたら1000pxに変更 */
background-color: #ddd;
position: relative;
}
#pagetop {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4E1C80;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
position: fixed; /* absoluteからfixedに変更 */
bottom: 1rem; /* topをbottomに変更 */
right: 1rem;
z-index: 5;
}
/* #pagetop.fixedはメディアクエリ(ここでは仮に500px以上)で動作するように変更 */
@media screen and (min-width: 500px) {
#pagetop {
position: absolute;
top: -25px;
bottom: unset; /* bottomをリセット */
right: 1rem;
}
#pagetop.fixed {
position: fixed;
top: unset;
bottom: 1rem;
right: 1rem;
}
}
See the Pen pagetop_fixed_sample_02 by ulla_mikage (@ulla_mikage) on CodePen.
SPサイズでボタン押せるようになったけど、ボタンがコピーライトに重なってコピーライトが読めなくなったよ?
コピーライトのCSSを追記してボタンが重なってしまう状況を作ってみます。
.footer_copyright {
position: absolute;
bottom: 0;
background-color: #a7a7a7;
width: 100%;
text-align: center;
padding: 1rem 0;
}
SPサイズで一番下までスクロールするとコピーライトの上にボタンが重なって、コピーライトの文字が読めなくしてみました。
See the Pen pagetop_fixed_sample_03 by ulla_mikage (@ulla_mikage) on CodePen.
CSSだけでなんとかしてみる
ボタンの位置をコピーライトの高さより上に上げるとなんとかなりそうなので、#pagetopのbottomを1remから6remに変えてみます。
main {
height: 100vh;
background-color: #fff;
}
#footer {
height: 1000px;
background-color: #ddd;
position: relative;
}
#pagetop {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4E1C80;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
position: fixed;
bottom: 6rem; /* bottomを1remから6remに変更*/
right: 1rem;
z-index: 5;
}
@media screen and (min-width: 500px) {
#pagetop {
position: absolute;
top: -25px;
bottom: unset;
right: 1rem;
}
#pagetop.fixed {
position: fixed;
top: unset;
bottom: 1rem;
right: 1rem;
}
}
.footer_copyright {
position: absolute;
bottom: 0;
background-color: #a7a7a7;
width: 100%;
text-align: center;
padding: 1rem 0;
}
See the Pen pagetop_fixed_sample_04 by ulla_mikage (@ulla_mikage) on CodePen.
ちょっと上過ぎるかもしれませんが、重なりは回避できました。
でも、ファーストビューでもこの位置ですし、PCサイズでは1remでSPサイズでは6remと差があることに違和感があります。
もうちょっと工夫してみる
PCサイズのときと同じように、コピーライトの位置に近づいたらある程度の位置で止まるということができれば、もう少し動きが自然になるように思います。なのでJavaScriptのイベントをもう一つ増やしてみます。
まずはコピーライトの部分をJavaScriptに追加で読み込みます。
var targetCopyElm = document.querySelector('.footer_copyright');
HTMLをclassで書いたのでquerySelectorにしていますが、idに書き換えてgetElementByIdに揃えると見た目が綺麗かもしれません。
次にスクロールイベントの関数の中に上で書いたif分の条件をヒントにこんな条件を書いてみました。
var pagetopFixedOut = function() {
console.log(targetCopyElm.getBoundingClientRect().top + ' / ' + (window.innerHeight - fixedElm.clientHeight / 2 - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize)) + ' / ' + ((targetCopyElm.getBoundingClientRect().top > window.innerHeight - fixedElm.clientHeight / 2 - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize))));
}
document.addEventListener('scroll', function(e){ pagetopFixedOut(); });
pagetopFixedOut();

いけそうな気がします。
スクロールイベントの中にif文を書いてみましょう。
var pagetopFixedOut = function() {
if (targetCopyElm.getBoundingClientRect().top > window.innerHeight - fixedElm.clientHeight - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize) ) {
if (!fixedElm.classList.contains('fixed_sp')) {
fixedElm.classList.add('fixed_sp');
}
} else {
if (fixedElm.classList.contains('fixed_sp')) {
fixedElm.classList.remove('fixed_sp');
}
}
}
document.addEventListener('scroll', function(e){ pagetopFixedOut(); });
pagetopFixedOut();
条件にあわせてfixed_spというクラスがついたりはずれたりするようにしています。
場合によっては上手くいかないこともあるかもしれません。その場合はwindow.innerHeightに50とか100とか足してみるといいかもしれません。
PCサイズのときのものと合わせた全体の完成形はこちらになります。
var fixedElm = document.getElementById('pagetop');
var targetElm = document.getElementById('footer');
var targetCopyElm = document.querySelector('.footer_copyright');
var pagetopFixedOut = function() {
if (targetElm.getBoundingClientRect().top > window.innerHeight - fixedElm.clientHeight - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize) ) {
if (!fixedElm.classList.contains('fixed')) {
fixedElm.classList.add('fixed');
}
} else {
if (fixedElm.classList.contains('fixed')) {
fixedElm.classList.remove('fixed');
}
}
if (targetCopyElm.getBoundingClientRect().top > window.innerHeight - fixedElm.clientHeight - 1 * parseFloat(getComputedStyle(document.documentElement).fontSize) ) {
if (!fixedElm.classList.contains('fixed_sp')) {
fixedElm.classList.add('fixed_sp');
}
} else {
if (fixedElm.classList.contains('fixed_sp')) {
fixedElm.classList.remove('fixed_sp');
}
}
}
document.addEventListener('scroll', function(e){ pagetopFixedOut(); });
pagetopFixedOut();
これで、ファーストビューではfixedとfixed_spというクラスが両方ついていて、スクロールしていくと画面にfooterが見えてきてボタンの要素とfooterの上端のところでfixedクラスがはずれてfixed_spだけ残ります。さらにスクロールして画面にコピーライトの要素の上端が見えてきて、ボタンの要素とコピーライトの上端のところでfixed_spクラスがはずれてクラスがついていない状態にできます。
あとはCSSで調整していきます。
main {
height: 100vh;
background-color: #fff;
}
#footer {
height: 1000px;
background-color: #ddd;
position: relative;
}
/* #pagetopにクラスがついていないとき */
#pagetop {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #4E1C80;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
position: fixed; /* fixedからabsoluteに変更 */
bottom: 6rem;
right: 1rem;
z-index: 5;
}
/* #pagetopに.fixed_spがついているとき */
#pagetop.fixed_sp {
position: fixed; /* absoluteからfixedに変更 */
bottom: 1rem; /* bottomを6remから1rem変更 */
}
/* #pagetop.fixedはメディアクエリで500px以上で動作するように変更 */
@media screen and (min-width: 500px) {
#pagetop.fixed_sp {
position: absolute; /* fixedからabsoluteに変更 */
}
#pagetop {
position: absolute;
top: -25px;
bottom: unset;
right: 1rem;
}
#pagetop.fixed {
position: fixed;
top: unset;
bottom: 1rem;
right: 1rem;
}
}
.footer_copyright {
position: absolute;
bottom: 0;
background-color: #a7a7a7;
width: 100%;
text-align: center;
padding: 1rem 0;
}
See the Pen pagetop_fixed_sample_05 by ulla_mikage (@ulla_mikage) on CodePen.
こんな感じでできました!
おわりに
JavaScriptではクラスのつけはずしをするだけなので、条件さえ見つけてしまえばあとはCSSでなんとかできました。なかなか難しかったです。