CSSのみでAppleのサイトのような横スクロールするナビゲーションメニューを作る

Appleのサイトなどにあるような、横スクロールタイプのナビゲーションをCSSのみで実装するの巻。

タッチデバイスの台頭以降、今では横スクロールは珍しいものでもなんでもなくなってきました。そして、メニュー項目についても横スクロールで実装しているサイトも多くみかけるようになりました。というわけで、仕事で作る機会があったので作ってみました。

CSSのみで実装できます。JavaScript等は不要です。まずはサンプルとサンプルコードをご覧ください。できる限りシンプルな記述で実装しました。

サンプル

Appleのサイトのような、横スクロールタイプのナビゲーションメニューのサンプルを見る

サンプルコード

忙しい方は、コピペですぐに実装できます。

ナビゲーションの仕様

まずは、今回作成するナビゲーションの仕様についてまとめておきます。

  • iPhoneなどの小画面の時に、メニュー項目を横スクロールさせる
  • 大画面の時はメニュー全体を中央寄せ
  • スクロールバーを表示させない

クセ者は「スクロールバーを表示させない」というもの。こと、某crosoft製のブラウザではスクロールしてなくても常時表示される残念仕様なので、非表示にさせます。

解説

HTMLの解説

まずHTML側から。構造は、メニューオブジェクト ul を2つのブロックでラップします。今回はナビゲーションなので、一番外側のブロックを nav.scroll-nav、それに内包されるブロックを .scroll-nav__view とします。で、.scroll-nav__view の中に、メニュー本体である ul.scroll-nav__list を配置します。

「外側 + スクロールするメニュー の2層でよくね?」とお思いかも知れませんが、「スクロールバーの非表示」を実装するためには、残念ながら3層にする必要がありました。詳しくは後述します。

これら3層の役割は以下のとおりとなります。

nav.scroll-nav
ユーザーに見えるインターフェース部分。このビューに高さを設定して、子ビューが持っているスクロールバーを隠す。
.scroll-nav__view
このビューの中身がスクロールする。つまりこのビューにスクロールバーが表示される。
ul.scroll-nav__list

メニュー本体。スクロールさせるために親ビューの横幅からはみ出す必要がある。また、メニューの幅は数値指定せず、中身なりの幅にしたい。

CSSの解説

次にCSS側。スクロールバーなし横スクロールメニューを実現するためには、2つの大きなポイントがあります。ポイントを絞って解説していきます。

  • メニューの横幅が親要素の横幅からはみ出さなければならないが、サイズ指定はしたくない
  • 設定で消せないスクロールバーを見えないようにしなければならない

というわけで、これを実装していきます。実装は、内側からやっていきましょう。まずはメニュー本体から。

メニューオブジェクト ul.scroll-nav__list のCSS

まず、どうやって親要素からはみ出させるかという問題を解決します。通常の display: block;, display: inline-block;, display: inline な要素では、最大横幅を数値指定しない限り、親要素の横幅を超えることは(スペースのない英文をひたすら羅列などしない限り)基本的にはできません。でも、だからと言ってメニュー全体の幅を測って数値指定するのはご法度。自動的に中身なりの横幅になってもらわなければいけません。そこで、このメニュー要素(ul)を display: table; 、メニューの各項目(li)を display: table-cell; に設定し、テーブル化させます。

これによって、まずメニューの各項目(li)がテーブルセル th td みたいな挙動になり、つまり横並びになります。

また、テーブルセルを内包する要素(つまりテーブル)は、横幅がテーブルの親要素を超えそうな時、内包するテーブルセルの幅をできる限り縮めて表示しますが、それでも親要素の幅に収まりきらない場合は親要素の幅を超えて表示される性質がありますので、これを利用します。さらに、テーブルのもうひとつの特長として、幅指定をしない限りは中身なりの最小横幅になりますので、後述の margin: auto; による中央寄せが可能になります。

li要素を横並びにするのは display: table-cell; じゃなくて display: inline-block; とか display: inline; とかでいいんじゃね?って気もしますが、実際は table-cell じゃないとうまくいきません。親要素がdisplay: table; の場合、inlineinline-block では横並びにならないのです。

また、ul要素も display: table; じゃなくて display: inline;display: inline-block; でもよさげな気がしますが、こちらも table 以外ではうまくいきません。 inline はこの後登場する、スクロールバーを消す際に設定するpaddingが無効になるし、inline-block の場合はmargin: auto; が無効になるので、大画面時に中央寄せにすることができません。もちろん、中央寄せが不要なら大丈夫です。

もうひとつ。メニュー項目の文字列に半角スペースがあったり日本語だったりする場合、テーブルセルが収縮した時に改行されてしまうので、これを解決するために li.scroll-nav__itemwhite-space: nowrap; を設定します。

なお、display: table; ではなく display: flex; (Flexbox)を使ってもできないことはありませんが、小画面時には左寄せ、大画面時には中央寄せ、という実装を考慮するとtableの方がシンプルに実装できるため、あえてFlexboxを使う必要はないと思いました。中央寄せが不要ならFlexboxで実装可です。

これで、ひとまずメニューオブジェクトは一段落。(後で戻ってきますが。)次は外側のビューを設定していきます。

メニューをスクロールさせるビュー .scroll-nav__view のCSS

前述のとおり、この人の役割は、内包する横にめちゃめちゃ長いメニュー本体をスクロール表示させることです。つまり、ここに overflow-x: scroll; を設定します。また、タッチデバイス等でスクロールに慣性を持たせるための -webkit-overflow-scrolling: touch; も、スクロール対象の親であるここに書きます。

これでひとまずスクロールメニューの実装は完成しますが、現状ではスクロールバーが表示されます。ChromeやSafariのように、スクロール中だけちょこっと表示されるなら問題ないのですが、某crosoft製のブラウザでは、ものものしいスクロールバーが常時表示され、残念な見た目になります。というわけで、これを非表示にします。

スクロールバーを非表示にするハック

残念ながら、スクロールバーはCSSの設定では消せません。scroll-bar: hidden; みたいなのがあれば楽なんですが。タッチデバイスのネイティブアプリとは違い、ポインタデバイスでも使われるブラウザはユーザビリティ的に非表示にはできない、ということなのでしょうか。というわけで、セコ技を使って消すしかありません。なのでハックと表現します。

方法としては、スクロールバーが表示される部分を隠して見えなくしてしまおう、ということです。以下解説します。

まず、スクロールバーがどこに表示されるかというと、言うまでもなく「スクロールされる要素(はみ出してる要素)を内包している親要素」に表示されます。今回の例では .scroll-nav__view に表示されます。

で、スクロールバーの表示はブラウザによって異なりますが、いずれもビューの下側に表示されることは共通しています。というわけで、スクロールバーを隠すには、以下のオペレーションで行けそうです。

  1. ビュー(またはメニュー本体)の下側に余分な余白(padding)を作る
  2. スクロールバーは下に表示される = 余分な余白(padding)のエリアにスクロールバーが表示される
  3. ビューの親要素の高さを、本来表示する高さに設定して、スクロールバーが表示される余分な余白(padding)部分を隠す

というわけで、HTMLを3層設計にした理由は、このためです。簡単に解説します。

まず、.scroll-nav__view の下側に padding: 0 0 24px 0; などとしてpaddingをつけます。一括設定にしているのは、ulがもともと持っているpadding-leftのリセットも兼ねて。

これまでは、.scroll-nav__view の高さは中身なりの高さ(= メニューの高さ)だったのが、24pxの余白分の高さが下側に追加された形になります。

最後に仕上げです。.scroll-nav__view の親要素である nav.scroll-nav の高さを、本来のメニューの高さに設定します。今回の場合メニューの高さは48pxになるようにしているので、height: 48px; とします。この時、内包する .scroll-nav__view の高さは48px + 24px(余分な余白)、つまりoverflowしているので、overflow-y: hidden; を書いて余分な余白部分を表示もスクロールもできないようにします。

実際はこの隠した部分にスクロールバーが表示されていますが、これで完全に見えなくすることに成功!というあんばいです。これにて完成!

実際にAppleのサイトはどうなってんの

実際、同じ手法でスクロールバーが隠されていました。隠されたエリアをひっぺがすと、実際ににスクロールバーが出てきます。興味のある方はお試しください。