MENU CLOSE

レスポンシブなドロップダウンメニューをつくる

Dropdown Menu
Dropdown Menu

ドロップダウンメニューって?

メニューをクリックしたりマウスを上に載せたりすることでそのメニューの下に出てくるようなメニューのことです。

ドロップダウンメニューの例

ドロップダウンメニューの実装方法はググると結構出てきます。
ただjQueryプラグインとかをこのためだけに読み込ませたりするのはなんか違う気がするし、「CSSだけでできる!」とかいうのもありますが、実際は動いてもデザインが思い通りにならなかったり、タブレットで全然使い物にならなかったり、なんてことがありますね。

僕なんかは特にそうですが、ほぼ「コーディング専業」の人は依頼されたデザインに合わせて自由自在に変えられるように、このくらいのUIはササっと書ける知識を持っておく必要があります。

今回はそういう時に使えるメモです。
言うて自分もけっこう忘れるので。w

まずはデザインをhtml・cssで実装

基本的にはデザイナーから依頼されたデザインを完全に再現するという体で進めます。 そのためCSSでデザインを忠実に再現することが前提となります。

今回はデザイナーなしで自分で作りますがご容赦ください。

html

<nav class="nav" id="js-nav">
  <ul class="nav-inner">
    <li class="nav-item">
      <a href="<?php the_permalink()?>">TOP</a>
    </li>
    <!-- /.nav-item -->
    <li class="nav-item">
      <a href="<?php the_permalink()?>">ABOUT</a>
    </li>
    <!-- /.nav-item -->
    <li class="nav-item nav-dropdown js-dropdown">
      <a href="<?php the_permalink()?>">CATEGORY</a>
      <button type="button" class="nav-toggle js-toggle"></button>
      <ul class="dropdown">
        <li class="dropdown-item has-child js-has-child">
          <a href="<?php the_permalink()?>">CATEGORY A</a>
          <button type="button" class="nav-toggle js-toggle"></button>
          <ul class="dropdown-child">
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">A-ITEM 1</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">A-ITEM 2</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">A-ITEM 2</a>
            </li>
            <!-- /.dropdown-child-item -->
          </ul>
          <!-- /.dropdown-child -->
        </li>
        <!-- /.dropdown-item -->
        <li class="dropdown-item has-child js-has-child">
          <a href="<?php the_permalink()?>">CATEGORY B</a>
          <button type="button" class="nav-toggle js-toggle"></button>
          <ul class="dropdown-child">
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 1</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 2</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 3</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 4</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 5</a>
            </li>
            <!-- /.dropdown-child-item -->
            <li class="dropdown-child-item">
              <a href="<?php the_permalink()?>">B-ITEM 6</a>
            </li>
            <!-- /.dropdown-child-item -->
          </ul>
          <!-- /.dropdown-child -->
        </li>
        <!-- /.dropdown-item -->
        <li class="dropdown-item">
          <a href="<?php the_permalink()?>">CATEGORY C</a>
        </li>
        <!-- /.dropdown-item -->
      </ul>
      <!-- /.dropdown -->
    </li>
    <!-- /.nav-item -->
    <li class="nav-item">
      <a href="<?php the_permalink()?>">CONTACT</a>
    </li>
    <!-- /.nav-item -->
  </ul>
  <!-- /.nav-inner -->
</nav>
<!-- /.nav -->

このコードはWordPressでデモ用に適当に作ったのでリンクが全てthe_permalink()になっちゃってますが、今回は動きだけの紹介ですので気にしないでください。笑

css

CSSはリセットされている前提で進めます。

//ブレイクポイントを変数で設定
$bp_tb_v: 768px; //タブレット縦
$bp_tb_h: 960px; //タブレット横・PC

.nav{
  @include min-screen($bp_tb_v){
    display: flex;
    justify-content: center;
  }
}

.nav-inner{
  background: #eee;

  @include min-screen($bp_tb_v){
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    width: 100%;
    max-width: 1080px;
    margin-right: auto;
    margin-left: auto;
    padding:9px;
    background: #eee;
    border-radius: 9px;
    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
  }

}

.nav-item{
  position: relative;
  @include min-screen($bp_tb_v){
    width: 25%;
  }

  &.is-show > a{
    @include min-screen($bp_tb_v){
      background: #ddd;
    }
  }

  + .nav-item{
    border-top:1px solid #fff;
    @include min-screen($bp_tb_v){
      border-top: none;
    }
  }

  &.is-show,
  &:hover {
    .dropdown{
      @include min-screen($bp_tb_v){
        opacity: 1;
        visibility: visible;
      }

      &::before{
        @include min-screen($bp_tb_v){
          transform:none;
        }
      }
    }
  }

  > a{
    padding: 15px;
    font-size: 18px;
    @include min-screen($bp_tb_v){
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 21px 30px;
      font-size: 15px;
      border-radius: 9px;
    }
    @include min-screen($bp_tb_h) {
      min-width:180px;
      font-size: 18px;
    }

    &:hover{
      @include min-screen($bp_tb_v){
        background: #ddd;
      }
    }
  }

}

.nav-dropdown{
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  @include min-screen($bp_tb_v){
    display: block;
  }

  a{
    flex-grow: 1;
  }
}

.nav-toggle{
  position: relative;
  width: 49px;
  min-height:49px;
  background: $blue;

  @include min-screen($bp_tb_v){
    display: none;
  }

  &::before,
  &::after{
    content:"";
    position: absolute;
    top:0;
    right:0;
    bottom:0;
    left:0;
    display: block;
    margin:auto;
    background: #fff;
    transition:.2s ease;
  }

  &::before{
    width:15px;
    height:1px;
  }

  &::after{
    width:1px;
    height:15px;

  }

  &.is-open{
    &::before{
      opacity: 0;
      transform:rotate(90deg);
    }
    &::after{
      transform:rotate(90deg);
    }
  }
}


.dropdown{
  display: none;
  width: 100%;
  background: rgba(#fff,.5);
  border-top:1px solid #fff;
  @include min-screen($bp_tb_v){
    opacity: 0;
    visibility: hidden;
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 10;
    display: block;
    width: 100%;
    padding: 30px 0 0;
    background: none;
    border-top: none;
    border-radius:9px;
    transition:.2s ease;
  }

  &::before{
    @include min-screen($bp_tb_v){
      content:"";
      position: absolute;
      top:-9px;
      right:0;
      left:0;
      z-index: 10;
      display: block;
      width: 1px;
      height: 30px;
      margin:auto;
      background: #000;
      transform-origin: top center;
      transform:scaleY(0);
      transition:.2s ease-out;
    }
  }
}

.dropdown-item{
  @include min-screen($bp_tb_v){
    position: relative;
  }

  + .dropdown-item {
    border-top: 1px solid #fff;
    @include min-screen($bp_tb_v){
      border-top: none;
    }
  }

  &.has-child{
    display: flex;
    flex-wrap: wrap;

    @include min-screen($bp_tb_v){
      position: relative;
      display: block;
    }

    &::after{
      @include min-screen($bp_tb_v){
        opacity: 0;
        visibility: hidden;
        content:"";
        position: absolute;
        top:0;
        right:-22px;
        z-index: 10;
        bottom:0;
        display: block;
        width: 30px;
        height: 1px;
        margin:auto;
        background: #000;
        transform-origin: left center;
        transform: scaleX(.5);
        transition:.2s ease-out;
      }
    }
  }

  &.is-show,
  &:hover{
    &::after{
      @include min-screen($bp_tb_v){
        opacity: 1;
        visibility: visible;
        transform: none;
      }
    }
    .dropdown-child{
      @include min-screen($bp_tb_v){
        opacity: 1;
        visibility: visible;
      }
    }
  }

  &.is-show {
    > a{
      @include min-screen($bp_tb_v){
        color:#fff;
        background: $blue;
      }
    }
  }

  a{
    padding:15px;
    font-size: 15px;
    @include min-screen($bp_tb_v){
      padding:18px 30px;
      text-align: center;
      border-radius:9px;
    }

    &:hover{
      @include min-screen($bp_tb_v){
        color:#fff;
        background: $blue;
      }
    }
  }
}

.dropdown-child{
  display: none;
  width: 100%;
  border-top:1px solid #fff;
  @include min-screen($bp_tb_v){
    opacity: 0;
    visibility: hidden;
    position: absolute;
    top:0;
    left:100%;
    display: block;
    padding-left:15px;
    background: none;
    border-top: none;
    transition:.2s ease;
  }
}

.dropdown-child-item{
  background: rgba(#fff,.3);
  @include min-screen($bp_tb_v){
    background: none;
  }

  + .dropdown-child-item{
    border-top: 1px solid #fff;
    @include min-screen($bp_tb_v){
      border-top: none;
    }
  }

  a{
    display: inline-block;
    font-size: 12px;
    @include min-screen($bp_tb_v){
      display: block;
      font-size: 15px;
    }
  }
}

全部書いてしまいましたが、
第一段階としてPCではCSSの:hoverで動くようにだけしておきます。
JSで追加するクラスのCSSに関してはその都度書いていきます。

基本的にこういった様々なサイトでよく使うようなUIを作る時はCSSのみで解決できる方が好ましいと思っています。
JavaScript全盛の時代ではありますが、Vueなどのフレームワークを使っても結局JSで操作してCSSやCSSの付いたクラスを付け替えることが大半なので:hover:focusといった擬似クラスが使える場合はそちらを優先して使った方が効率的に思います。
そのためこういった小規模UIを自作で作るときは先にCSSだけで動かすことを最初に考えます。

しかしレスポンシブがスタンダードになった最近では、スマホやタブレットといったマウスのないタッチ操作デバイスの対応は違ってきます。 スマホやタブレットでは逆にJSを使わないと制御できない部分が多くなります。
このドロップダウンメニューもCSSのみではスマホやタブレットでうまく機能しないのでJavaScriptでのカスタマイズが必要です。

ともあれひとまず最初の段階でCSSで見た目を作り込んでおくと次のJS実装がやりやすいと思います。

JavaScriptでタップ操作に対応する

スマホ・タブレット・PCで違う動作をするのでちょっとややこしくなりますが、分けて考えていきます。

サンプルはjQuery(ES6+をBabel7でトランスパイル)で書きます。

スマホ

スマホではタップして開くアコーディオンメニューにします。
横の「+」ボタンを押すと下のカテゴリを開いたり閉じたりします。

const $toggleBtns = $('.js-toggle');
$.each($toggleBtns, (i, el) => {
  el.addEventListener('touchstart', (e) => {
    e.preventDefault();
    e.stopPropagation();
    const $self = $(e.currentTarget);
    $self.toggleClass('is-open');
    $self.next().not(':animated').slideToggle(200);
  }, { passive: false });
});

.js-toggleが「+」ボタンです。
タップにはtouchstartイベントを使ってタッチした瞬間に発火させます。
addEventListenerを使っていますが、これで引数に{ passive: false }を付けていないとpreventDefaultを使った時に怒られます。jQueryのonでは{ passive: false }は付けられないので、リンクの動作をJSで制御する時などはaddEventListenerを使いましょう。

タップするとその直後の要素をnext()で取得し、slideToggle()を使ってメニューを開閉します。連打バグ防止のために.not(':animated')でアニメーションしていない時のみ動作するようにします。

「+」ボタンはCSSでタブレットの大きさになるとdisplay:none;で消えるように設定しているので、ここでは「スマホだけ動作するような判定処理」はしません。

タブレット

タブレットではPCと同じデザインになりますが、動作をマウスからタップに置き換えます。
1度目のタップでメニューを開いてメニューを開いた状態で同じリンクをタップするとリンクに飛ばす仕様です。また、メニューが開いた状態でメニュー以外をタップするとメニューを閉じます。

ここはスマホでもPCでも動作して欲しくない処理なので、「タブレットのみ」で動作するようにデバイス判定します。

デバイス判定

//デバイス判定
device(){
  const device = {
    isSp     : false,
    isTablet : false,
    isPC     : false
  };
  const ua = navigator.userAgent;
  //スマホ
  if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 || (ua.indexOf('Android') > 0) && (ua.indexOf('Mobile') > 0) || ua.indexOf('Windows Phone') > 0){
    device.isSp = true;
  }
  //タブレット
  else if(ua.indexOf('iPad') > 0 || ua.indexOf('Android') > 0){
    device.isTablet = true;
  }
  //PC
  else{
    device.isPC = true;
  }
  return device;
}

親カテゴリのドロップダウン

デバイス判定の関数を使って、次に親カテゴリの動作。

const dropdown = document.querySelector('.js-dropdown');
dropdown.addEventListener('touchstart', (e) => {
  const device = device();
  if (!device.isTablet) {
    return;
  }
  const $dropdown = $(e.currentTarget);
  if($dropdown.hasClass('is-show')){
    return;
  }
  else {
    e.preventDefault();
    e.stopPropagation();
    $dropdown.addClass('is-show');
  }
}, false);

タブレットもタップするのでtouchstartでイベントを登録します。
最初にデバイス判定してタブレット以外の場合はreturnで終了させます。
タブレットの時のみ、.is-showクラスがない時につけるようにします。
この時、.is-showクラスがある場合はそのままreturnで終了し、ない場合はリンクの機能を止めてドロップダウンを開きたいのでpreventDefault()を使います。stopPropagation()はイベントの伝播を止めたい場合に使います。今回はなくても動きますが一応書いています。

子カテゴリのドロップダウン

//子カテゴリ
const $childDropdown = $('.js-has-child');
$.each($childDropdown, (i,el) => {
  el.addEventListener('touchstart', (e) => {
    const device = device();
    if (!device.isTablet) {
      return;
    }
    const $dropdown = $(e.currentTarget);
    if($dropdown.hasClass('is-show')){
      return;
    }
    else {
      e.preventDefault();
      e.stopPropagation();
      $childDropdown.removeClass('is-show');
      $dropdown.addClass('is-show');
    }
  }, false);
});

子カテゴリも親カテゴリと挙動は同じです。
ドロップダウンさせたいカテゴリに.js-has-childというクラスを付けておいて、それに対して動作させます。 
親カテゴリと違う点は、

  • 複数要素に対してイベントを発火させる
  • 1つのカテゴリが表示されたらそれ以外のカテゴリはすべて非表示にする

という2点です。

エリア外をタップすると閉じる

document.addEventListener('touchstart', (e) => {
  const device = device();
  if (!device.isTablet) {
    return;
  }
  if(!$('.js-dropdown').hasClass('is-show')){
    return;
  }
  if(!$(e.target).closest('.js-dropdown').length) {
    e.preventDefault();
    e.stopPropagation();
    $('.js-dropdown').removeClass('is-show');
    $('.js-has-child').removeClass('is-show');
  }
}, { passive: false });

documenttouchstartイベントを追加します。
これでどこをタップしてもイベントが発火する形になります。
タブレット以外の時やドロップダウンが開いていない時はすぐに終了。
タップした要素(e.target)の先祖に.js-dropdownが含まれていない場合にすべてのドロップダウンを閉じます。
その際にやはりpreventDefault()でリンクを機能させないようにしておきます。

これでタブレットで「1度目のタップでメニューを開いてメニューを開いた状態で同じリンクをタップするとリンクに飛ばす、またメニューが開いた状態でメニュー以外をタップするとメニューを閉じる」というちょっと複雑な条件をクリアできました。

JS全体

Classにまとめたもの

class Main {
  constructor(){
    this.bind()
  }

  //デバイス判定
  device(){
    const device = {
      isSp     : false,
      isTablet : false,
      isPC     : false
    };
    const ua = navigator.userAgent;
    //スマホ
    if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 || (ua.indexOf('Android') > 0) && (ua.indexOf('Mobile') > 0) || ua.indexOf('Windows Phone') > 0){
      device.isSp = true;
    }
    //タブレット
    else if(ua.indexOf('iPad') > 0 || ua.indexOf('Android') > 0){
      device.isTablet = true;
    }
    //PC
    else{
      device.isPC = true;
    }
    return device;
  }

  bind(){
    //カテゴリ
    const dropdown = document.querySelector('.js-dropdown');
    dropdown.addEventListener('touchstart', (e) => {
      const device = this.device();
      if (!device.isTablet) {
        return;
      }
      const $dropdown = $(e.currentTarget);
      if($dropdown.hasClass('is-show')){
        return;
      }
      else {
        e.preventDefault();
        e.stopPropagation();
        $dropdown.addClass('is-show');
      }
    }, false);

    //子カテゴリ
    const $childDropdown = $('.js-has-child');
    $.each($childDropdown, (i,el) => {
      el.addEventListener('touchstart', (e) => {
        const device = this.device();
        if (!device.isTablet) {
          return;
        }
        const $dropdown = $(e.currentTarget);
        if($dropdown.hasClass('is-show')){
          return;
        }
        else {
          e.preventDefault();
          e.stopPropagation();
          $childDropdown.removeClass('is-show');
          $dropdown.addClass('is-show');
        }
      }, false);
    });

    document.addEventListener('touchstart', (e) => {
      const device = this.device();
      if (!device.isTablet) {
        return;
      }
      if(!$('.js-dropdown').hasClass('is-show')){
        return;
      }
      if(!$(e.target).closest('.js-dropdown').length) {
        e.preventDefault();
        e.stopPropagation();
        $('.js-dropdown').removeClass('is-show');
        $('.js-has-child').removeClass('is-show');
      }
    }, { passive: false });

    const $toggleBtns = $('.js-toggle');
    $.each($toggleBtns, (i, el) => {
      el.addEventListener('touchstart', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $self = $(e.currentTarget);
        $self.toggleClass('is-open');
        $self.next().not(':animated').slideToggle(200);
      }, { passive: false });
    });

  }


}

new Main();

レスポンシブなドロップダウンメニューのDEMO

レスポンシブなドロップダウンメニューのDEMO

どのデバイスでも動作するドロップダウンメニューができました。
デザインに関してはイケてないので参考にしないでくださいw

おわりに

最近よく作っているBASEデザインマーケットで販売しているテーマで、「タブレットのタップ操作がうまくいかない」といったご指摘を頂いて何度か修正しました。 その時に行ったこととほとんど同じ処理なのですが、しばらくすると忘れそうなのでここに書き残しておくことにしました。

ただこんなUI、Vueとかで作った方が早そうな気がしますね。
最近はjQueryは嫌いって人たちがかなり多いと思うので、また時間がある時にVueやネイティブバージョンも作ってみます。

記事一覧

RELATED

Flatpickr
inoue
inoue
TIPS

Flatpickrで日時入力をカレンダー表示にする

BASE DESIGN THEME
inoue
inoue
TIPS

BASEテーマカスタマイズ【実践編】見出しの文言を変更できるようにする

BASE DESIGN THEME
inoue
inoue
TIPS

BASEテーマカスタマイズ【デザインオプション編】テーマに新しい機能を加える方法

BASE DESIGN THEME
inoue
inoue
TIPS

BASEテーマカスタマイズ【準備編】カスタマイズに必要な前提知識について

BACKGROUND VIDEO
inoue
inoue
TIPS

動画をWebページの背景に埋め込む時のテクニック

Google Apps Script
inoue
inoue
TIPS

【GAS】スプレッドシートからメールを送信する方法

NEW POSTS

Sass @import → @use
inoue
inoue
TRY

【Sass】@importを@useに置き換えてみる《FLOCSS対応》

Stripe Payment Links
inoue
inoue
TRY

コーディング一切不要のStripe Payment Linksで決済機能を試してみる

BASE Partners
inoue
inoue
COLUMN

【BASE Partners】有料テーマの無償提供特典について

BASE Partners
inoue
inoue
COLUMN

BASEオフィシャルパートナーに認定されました

BASE DESIGN THEME Q &A よくある質問
inoue
inoue
COLUMN

【BASEデザインテーマ】よくある質問まとめ

Drawer Menu
inoue
inoue
TIPS

CSSと簡単なJSでできるドロワーメニューの実装方法

ブログ記事一覧
  • HOME
  • TIPS
  • レスポンシブなドロップダウンメニューをつくる