CSSだけで横メニューをつくる。但し、ボックスからはみ出たリストはボタンにして、そのボタンを押すと縦のリストメニューが出る

  • 163
    Like
  • 2
    Comment

うちのデザイナーが無茶言うんだが...。できちゃったかもしれないシリーズ

本日の課題はこれです。

cssだけで、横並びのメニューを作る。但し、ボックスからはみ出たリストはボタンにして、そのボタンを押すと縦のリストメニューが出る

自分でも、何を言っているのか分からなくなってきましたが、
ようはchromeのブックマークバーとかにあるこんなUIですね

Chrome Bookmark Bar

※ 正式名称分かる人教えてください。タイトル長すぎる。

うん、わかる。簡単にできそうだよね。

デザイナー: こんなの瞬殺でしょ?

いやいや、できないから

cssだけで、横並びのメニューを作る

⇒ これは出来る。(こっちは瞬殺です)

問題は条件の方

但し、ボックスからはみ出たリストはボタンにして、そのボタンを押すと縦のリストメニューが出る

⇒ これムリゲーじゃね?

overflow: listmenuとかあればいいんだが、そんなものはない。

なんとなく、元々の要素も、はみ出た要素もリスト要素なので、:checked疑似要素と組み合わせれば、出来るような、出来ないような....。

で、作りました。

結局、作っとるやん!

デモ

デモサイト(Codepen)

通常時

通常時

幅が狭まってリストが隠れると、右端に「≫」ボタンが出現

ボタン出現

右端の「≫」ボタンをクリックすると隠された要素が縦のリストで表示される

隠されたリストを表示する

一応、最新のブラウザ(IE11,firefox,chrome)に対応しています。

これ作った奴マジ、頭沸いているとしか思えない。

あ、俺でした。

コード

HTML

html
  <label for="menuOn">
    <input id="menuOn" type="checkbox">
    <menu>
      <ul>
        <li><a href="#menu1">概要</a>
        <li><a href="#menu2">逆L字をつくる</a>
        <li><a href="#menu3">ボタン切替</a>
        <li><a href="#menu4">リスト幅可変</a>
        <li><a href="#menu5">ボタン表示</a>
        <li><a href="#menu6">動作環境</a>
        <li><a href="#menu7">仕様</a>
        <li><a href="#menu8">免責事項</a>
      </ul>
    </menu>
    <div class="spacer"></div>
    <div class="overlay"></div>
  </label>

CSS

css
/* ========================================================== */
/*   ここから下がメニューの設定                                */
/* ========================================================== */

/* メニューのON/OFFを保存する為のチェックボックス 非表示 */
#menuOn{
  display : none
}

/* 隠しメニューを表示したとき用のスペーサー */
/* なのでデフォルトでは非表示               */
#menuOn + menu + div.spacer{
  display : none;
}

/* 隠しメニューを表示時のメニューの設定 */
/* absolute指定を行い、高さも与える     */
#menuOn:checked + menu{
  height    : calc(100% - 96px); /* 96pxはヘッダの高さ可変だと厳しい */
  position  : absolute;
  max-width : 960px;/*コンテナと同じ数値を指定しておく必要あり*/
  z-index   : 20;
  background: transparent;
}

/* スペーサー                                           */
/* 隠しメニューが表示されているときにメニュー全体が     */
/* absoluteになり高さが失われるので、                   */
/* スペーサーで高さを補います                           */
#menuOn:checked + menu + div.spacer{
  display   : block;
  height    : 40px; /*メニューの高さに合わせる*/
}

/* オーバーレイ                                         */
/* 隠しメニューが表示されているときに                   */
/* LightBoxのように画面全体を覆うブロックを表示する     */
/* これによってメニュー部分を除いて                     */
/* 画面全体がチェックボックスへのクリックになるので     */
/* メニュー以外の部分をクリックするとメニューが閉じます */
#menuOn:checked + menu + div.spacer + div.overlay{
  position : fixed;
  top      :  0;
  bottom   :  0;
  left     :  0;
  right    :  0;
  z-index  : 10;
  /*background : rgba(0,0,0,0.5);*/
}

menu{
  height      : 40px; /* メニューの高さすべてこれに合わせる*/
  overflow    : hidden;
  font-size   : 20px;
  line-height : 20px;
  width       : 100%;
  min-width   : 200px;
  background  : #33aa33;  /* リスト(li)要素の背景色と合わせる*/
}

/* メニューの高さ分のスペースを空ける */
menu::before {
  display : block;
  content : '';
  float   : left;
  width   : 0;
  height  : 40px; /*メニューの高さに合わせる*/
}

ul{
  height : 320px;/*リスト項目(8)*メニューの高さ(40px)=320px以上*/
  width  : calc(100% + 200px);/*隠しリストの幅を足す*/
}

/* 逆L字を作る為の左下のブロック*/
ul::before {
    display : block;
    content : '';
    float   : left;
    clear   : both;
    width   : calc(100% - 200px);/*隠しリストの幅を引く*/
    height  : 100%;
}

li:first-child{
  padding-left : 200px;/*隠しリストの幅*/
  max-width    : 600px;/*隠しリストの幅*3 */
}

li{
  display       : inline-block;
  background    : #33aa33;  /* メニュー要素の背景色と合わせる*/
  min-width     :  305px;/*隠しリストの幅/2+α*/
  max-width     :  400px;/*隠しリストの幅*2 */
  margin-left   : -200px;/*-隠しリストの幅*/
  white-space   : nowrap;
  text-overflow : ellipsis;
}

li a{
  display       : block;
  padding       :  10px;
  padding-right : 210px;/*隠しリストの幅+10px */
}

/* ========================================================== */
/*   ここから下ははみ出たリストを表示する為のボタンの制御     */
/* ========================================================== */

li:last-child{
  position : relative;
}

/* メニューのボックスの幅が狭まって、リストが隠れた場合に*/
/* 隠れたリストが見れるボタンを作る */
li:last-child::after{
  position    : absolute;
  z-index     : 30;
  display     : block;
  /* メニューの項目数以上必要                     */
  /* \bb は終わりギュメ                           */
  /*   RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */
  /* \a  は改行コード                             */
  content     : '\bb\a\bb\a\bb\a\bb\a\bb\a\bb\a\bb\a\bb\a';
  bottom      : 40px;/* メニューの高さに合わせる*/
  right       :  0;
  width       : 125px;
  color       : white;
  white-space : pre;
  line-height : 40px;
  background  : #229922;
  padding-left: 10px;
}

/* リストを見るボタンが押されている場合は消す */
#menuOn:checked + menu li:last-child::after{
  display : none;
}

/* 隠しリストが表示されているときは常に表示 */
#menuOn:checked + menu::after {
  position     : absolute;
  z-index      : 30;
  display      : block;
  content      : '\bb';
  line-height  : 40px;
  width        : 20px;
  padding-left : 10px;
  color        : white;
  right        :  0;
  top          :  0;
  background   : #229922;
}

解説

さて、htmlとcssのコードはわりとすっきり書けたのですが、解説ないと意味不明だと思われるので、基本的な考え方を解説して行きます。

1. 逆L字を作る

まず、隠れたリストも表示された状態である、逆L字を作ります。

これは、

右下または左下においた画像に文字列を回り込ませるHTML/CSS

こちらの画像を右下に置いた時の回り込みを応用し、画像の部分を:before,:afterなどの疑似要素を使って実現しています。

これで大枠が出来ました!

2. 次にこれをボタンを押したら逆L字状態にしたい

htmlとcssだけでボタンを作りたいという場合は、定番のテクニック、チェックボックス使って、:checked疑似要素と隣接セレクタのコンボですね。

これで、はみ出た要素をボタン化することが出来ました。

3. 横メニューのリストは文字数によって可変・ボタンを押した時の縦メニューは固定幅

横メニューのリストとボタンを押した時の縦メニューの幅が等幅だったらここで終わりだったんだが...。

マジ言ってんの?
いや、確かにchromeのブックマークツールバーはそうだけどさ
overflowの状態が分かる疑似要素:overflowみたいなのがないので、途中で固定幅には出来ない。

んじゃ、最初からリスト要素の右側を幅大目に取っておけばいいんじゃないのという戦略でネガティブマージン使ってリスト要素を重ねる。

尚、この戦略はメニューが透明だと要素が重なっているのが見えてしまうので採れない
サンプルでは隠されたメニューの固定幅は200pxに設定している。

ちなみに、横並びのメニューもすべて同じ固定幅にする場合は、こんなにややこしくならない。数値の設定も直観的です。ネガティブマージンを使うと空間がねじ曲がっている感じ、background-colorをrgbaで設定しても、実体と表示部がずれているので、非常にデバックしにくい

4. 最後に隠された要素がある場合だけ表示される「≫」ボタン

メニューのリンク以外のすべての領域をクリックするとメニューの開閉となるので、機能上はほぼ完成している。しかし、隠された要素がある場合にボタンが表示されないと、要素が隠れているかどうか分からない。ですが、前述のとおり、疑似要素:overflowみたいなのがないので、コンテンツがはみ出ているかどうか知るすべはない。(もちろんJavaScriptならできますともJavaScriptならね...。)

なので、通常時は横リスト、隠された要素は縦リストという、縦、横の差異を利用して実現しています。

具体的には最後のli要素に要素1つの高さ分上に疑似要素を作りそこに要素の個数分「≫」記号が改行で入って縦に並んでいます。しかし、overflow:hiddenで要素1個分の高さしか設定されていないので1つしか「≫」記号は表示されません。最後のli要素に要素1つの高さ分上に疑似要素が設定されているので、1つも要素が折り返されていない場合は「≫」記号が表示されないと言うわけですね。

ただ、この方法は、隠しメニューが開いていると縦に「≫」記号があるのが見えてしまうので、隠しメニューが開いている時はこの方法は非表示にし、隠しメニューが開いている時は、メニュー(ul)要素のafterで「≫」記号を常時表示しています。こちらも、もちろん、隠しメニューが開いている時は非表示にしています。

つまり、「≫」記号の表示ロジックは、隠しメニューが開いている時と閉じている時の2パターンで別々の設定がされているということですね。

ちなみに、テキストの記号ではなくて画像を使いたい場合は、疑似要素に、リスト項目数分の高さを設定し、background-repeat-yを使えばいいかと思います。

動作環境

基本的に最新ブラウザであれば対応していますが、わりと古い技術を使っているので、IEの古い奴とかじゃなければ動くかもしれない。IE10のエミュレーションモードでは動作。IE9のエミューレーションモードは無理だった。Operaは未確認。

  • 動作確認済みブラウザ
    Windows 7 Chrome 56.0.2924.87 (64-bit)
    Firefox 51.0.1 (32 ビット)
    IE11 11.0.38
    macOS Sierra(10.12.2) Chrome 56.0.2924.87 (64-bit)
    Firefox 52.0.1 (64 ビット)
    Safari 10.0.2
    iOS 9.3.5 mobile safari
    Android 5.0.0(ZenFone2) chrome 54.0.2840.68
    ASUS Browser(標準ブラウザ) 2.1.2.71_160715

仕様

  • 隠されたリストメニューの固定幅(200px)はネガティブマージンやcss calcの調整で魔境と化しているので触らない方がいいかもしれない
  • IE11でメニューの文字に()があるとnowrapが効かずにバグる
  • リストの幅が可変ではあるが、隠しリストにfloatを使っている都合上制約が発生する。隠しリストの固定幅の半分(100px)より大きく、隠しリストの固定幅(200px)以下である必要がある。それぞれmin-widthとmax-widthで調整しているが、ネガティブマージンの調整分加算されている
  • メニュー右端のリストのホバー領域が大きい。すまん、これどうにもならなかった。ul:afterでもう1個余計にスタックすればいける?

免責事項

基本的に実用上は問題ないレベルに仕上がっているとは思っていますが、ご利用は自己責任でお願いします。

多分、実際にサービスに投入されることはないと思います
とはいえ、たまには無茶な要求にチャレンジして腕を磨くのもいいですね。

そんな私はインフラエンジニアです。

え?

えっ??