CSS:fixedの要素をスクロールバーなしでスクロールさせる

サイトのメニューをオーバーレイで出す場合には、その要素に position:fixed を指定する場合が多いと思いますが、要素の長さがデバイスの表示高よりも長い場合にスクロールさせ、かつ非タッチデバイスの場合にスクロールバーを非表示にし、背景を固定しスクロールバー非表示による要素のズレを解消する方法です。



メニューをオーバーレイでアニメーション

まず、メニューをオーバーレイで上部からアニメーションでスライドさせてメニューをスクロール可能にする方法です。


<div class="content">
  <p>テキストコンテンツ</p>
  <!-- 確認のためにはブラウザの表示高以上のテキストコンテンツが必要 -->
</div>

<div class="btn" id="btn">
    MENU
</div>

<ul class="menu">
    <li>MENU1</li>
    <li>MENU2</li>
    <li>MENU3</li>
    <li>MENU4</li>
    <li>MENU5</li>
    <!-- 確認のためにはブラウザの表示高以上のメニューの高さが必要 -->
</ul>


まずこういう htmlを想定して考えてみます。テキストコンテンツはスクロールしなければ全文が読めない想定です。また、考え方だけのサンプルですの見た目などは考慮していません。

サンプルの完成形が codepenに置いてあります。


See the Pen hide scrollbar but keep functionality by ausnichts (@ausnichts) on CodePen.


アニメーションの発火

メニューをドロップダウンさせる方法はいろいろありますが、今回は一番手短な javascriptのクリックイベントを使っています。


// メニューボタンにクリックイベントを設定する
document.getElementById('btn').addEventListener('click', function(){
  document.body.classList.toggle('active')
});


メニューボタンをクリックしますと、body要素に class='active' を追加します。


メニューをドロップダウンさせる

メニューボタンをブラウザの右上に固定し、ボタンの左側にドロップダウンさせるためにボタンの横幅を指定しておきます。


.btn {
  cursor: pointer;
  position: fixed;
  top: 0;
  right: 0;
  width: 80px;
  /* 以下は見やすくするため */
  background: red;
  text-align: center;
  padding: 20px 0;
  color: white;
}

.menu {
  position: fixed;
  top: 0;
  right: 80px;
  height: 0;
  overflow-y: auto;
  transition: height 500ms;
  /* 以下は見やすくするため */
  width: 100px;
  background: blue;
}
.menu li {
  /* 以下は見やすくするため */
  color: white;
  text-align: center;
  list-style: none;
}

.active .menu {
  height: 100%;
}


通常はメニューを height:0 にしておき、メニューボタンをクリックしますと body要素に class='active' が追加され、メニューの高さがブラウザの下まで伸びてきます。


なお、アニメーションに heightを使う場合は autoを指定してもアニメーションしませんので数値、または %や vhを指定する必要があります。javascriptを使えば正確にメニューの高さを計算できますが、今回のようにメニューをスクロールさせるのであれば、数値で指定すればいいと思います。また、max-heightに想定する高さよりも大きな数値をしておく方法もあります。ただ、いずれにしてもスクロールさせるのであればあまり意味がありません。


メニューのスクロールバーを非表示にする

これまでの設定ではメニューにスクロールバーが表示され見た目がよくありません。スクロールはできるけれどもスクロールバーを非表示にする方法です。


以下のサイトにその方法の記載があります。


/* メニューのスクロールバーを非表示 */
.menu {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.menu::-webkit-scrollbar {
    display: none;
}


詳細はリンク先をご覧ください。


背景のスクロールを止める

この設定では、メニューはスクロールするものの背景となっているテキストコンテンツもスクロールしてしまいます。

背景のテキストコンテンツがスクロールしないようにするには次のようにします。


/* 背景のスクロールを止める */
.active {
  overflow: hidden;
}


body要素に overflow:hidden を指定します。スクロールバーを消してスクロールできなくしてしまうということです。

ただし、iPhoneではこの指定ではスクロールを止められないと思います。最近の記事ではありませんが、iPhoneでスクロールを止める方法を下の記事で書いています。日常的に iPhoneを使っているわけではありませんので最近は検証していません。


www.imuza.com


スクロールバー非表示による要素のズレを解消する

以上の方法ですと、背景のスクロールは止まりましたが、スクロールバーを非表示にすることで body要素がスクロール幅分ひろがって要素全体がずれますので違和感があります。


CSSで処理する方法はないかと探しましたがいまのところ見つかりません。ですので、javascriptでスクロール幅を取得し、メニュー表示の際にその幅分のマージンや位置で調整する方法を取ってみます。なお、スクロールバーの幅は各ブラウザで異なっていますので決め打ちは出来ません。また、CSSで calc(100vw - 100%) としてスクロール幅を取得してみましたが、メニューを表示した瞬間にスクロールバーは消えてしまいますのでスクロールバーの値は 0 となってしまいます。


// スクロールバーの幅を取得し CSSカスタムプロパティーに定義する
const scrollbarWidth = (window.innerWidth - document.documentElement.clientWidth) + 'px';
document.documentElement.style.setProperty('--scrollbar-width', scrollbarWidth);


/* 背景のスクロールバー非表示による要素のズレを補正 */
.active .content {
    margin-right: var(--scrollbar-width);
}
.active .menu {
  right: calc(var(--scrollbar-width) + 80px);
}
.active .btn {
  right: var(--scrollbar-width);
}


あらかじめ javascriptでスクロールバー幅を取得し、CSSカスタムプロパティに設定しておきます。CSSで各要素のズレを補正します


codepen サンプル

以上の設定を codepenで確認できます。


See the Pen hide scrollbar but keep functionality by ausnichts (@ausnichts) on CodePen.