WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

ドロップダウンメニューの作り方|HTML/CSSで作る多階層ナビ

生徒

グローバルナビにドロップダウンを付けたいんですが、マウスを離すと消えてしまって選べません…。

ペン博士

それは親要素と子メニューの間に隙間があるのが原因だね。構造から順番に作れば、JavaScriptなしでも安定して動くよ。多階層とスマホ対応まで一気に見ていこう。

グローバルナビの項目にカーソルを乗せると、下にメニューが開く。このドロップダウンメニューは、ページ数の多いサイトでは欠かせない導線です。

実装自体は:hoverposition: absoluteの組み合わせで作れます。しかし「マウスが外れて消える」「他の要素の下に隠れる」「スマホで開けない」といったつまずきが起きやすいパーツでもあります。

この記事では基本構造から始めて、消えない工夫、多階層(2階層目)、スマホでの開閉、キーボード操作の対応まで、コピペで動く形で解説します。

  • ドロップダウンの基本構造(HTML/CSS)
  • マウスが外れると消える問題の直し方
  • アニメーションを付けて自然に開く
  • 2階層目(多階層メニュー)の作り方
  • 画面端で切れないようにする位置調整
  • メガメニュー型のレイアウト
  • スマホでの開閉と表示切り替え
  • キーボード操作とアクセシビリティ
  • 開かない・隠れるときの原因と対処
  • コピペで使える完成版

目次

ドロップダウンの基本構造

重ねて開く仕組み

入れ子のリストで作ります。親のliを基準にして、子のulを絶対配置するのが基本形です。

<nav class="gnav">
  <ul class="gnav__list">
    <li><a href="/">ホーム</a></li>
    <li class="has-child">
      <a href="/service/">サービス</a>
      <ul class="sub">
        <li><a href="/service/web/">Web制作</a></li>
        <li><a href="/service/design/">デザイン</a></li>
        <li><a href="/service/ai/">AI活用支援</a></li>
      </ul>
    </li>
    <li><a href="/contact/">お問い合わせ</a></li>
  </ul>
</nav>
.gnav__list {
  display: flex;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 子メニューの位置の基準になる */
.has-child {
  position: relative;
}

.sub {
  position: absolute;
  top: 100%;              /* 親のすぐ下 */
  left: 0;
  min-width: 220px;
  margin: 0;
  padding: 8px 0;
  list-style: none;
  background-color: #fff;
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, .14);
  opacity: 0;
  visibility: hidden;     /* 閉じている状態 */
  transition: opacity .2s ease, visibility .2s;
  z-index: 100;
}

.has-child:hover .sub {
  opacity: 1;
  visibility: visible;
}

閉じた状態をdisplay: noneではなくopacityとvisibilityで作るのがポイントです。displayはアニメーションできないため、フェードイン演出が使えなくなります。

マウスが外れると消える問題の直し方

消える問題の直し方

最も多いトラブルがこれです。親のリンクと子メニューの間に数pxの隙間があると、カーソルがそこを通った瞬間に:hoverが切れて閉じてしまいます。

/* 方法1: 親の余白で隙間を埋める */
.has-child > a {
  display: block;
  padding: 1em 1.2em;   /* 下方向のpaddingが「橋」になる */
}

/* 方法2: 擬似要素で見えない橋を架ける */
.sub::before {
  content: "";
  position: absolute;
  top: -12px;            /* 隙間のぶんだけ上に伸ばす */
  left: 0;
  width: 100%;
  height: 12px;
}

top: 100%で親の直下に置いていても、親にmargin-bottomがあると隙間が生まれます。擬似要素で透明な領域を作っておけば、カーソルが通っても:hoverが維持されます。

もう一つの対策が閉じるまでの遅延です。transition-delayを使うと、カーソルが一瞬外れても即座には閉じません。

.sub {
  transition: opacity .2s ease, visibility .2s;
  transition-delay: .15s;   /* 閉じるのを少し待つ */
}

.has-child:hover .sub {
  transition-delay: 0s;     /* 開くときは即座に */
}

アニメーションを付けて自然に開く

上からすっと降りてくる動きを足すと、階層関係が伝わりやすくなります。移動距離は控えめにします。

.sub {
  transform: translateY(-8px);
  transition: opacity .2s ease, transform .2s ease, visibility .2s;
}

.has-child:hover .sub {
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .sub {
    transition: none;
    transform: none;
  }
}

動く距離は6〜10px、時間は0.2秒前後が目安です。大きく動かすとメニューが暴れて見え、目的の項目を追いにくくなります。

2階層目(多階層メニュー)の作り方

サービスの下にさらにカテゴリがある場合は、子メニューの中にもう一段入れ子を作ります。開く方向を横にするのが定石です。

.sub .has-child2 {
  position: relative;
}

.sub .sub2 {
  position: absolute;
  top: 0;
  left: 100%;             /* 右隣に開く */
  min-width: 200px;
  padding: 8px 0;
  background-color: #fff;
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, .14);
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s, visibility .2s;
}

.sub .has-child2:hover .sub2 {
  opacity: 1;
  visibility: visible;
}

/* 子がある項目には矢印を出す */
.sub .has-child2 > a::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 8px;
  border-top: 2px solid #6b7280;
  border-right: 2px solid #6b7280;
  transform: rotate(45deg);
}

階層は2階層までに留めるのが原則です。3階層以上はマウス操作が難しく、スマホでは事実上使えません。項目が多いなら次のメガメニュー型を検討します。

画面端で切れないようにする位置調整

右端のメニューをleft: 0で開くと、画面外にはみ出して読めなくなります。端の項目だけ基準を反転させます。

/* 右端の項目は右揃えで開く */
.gnav__list > li:last-child .sub,
.sub.-right {
  left: auto;
  right: 0;
}

/* 2階層目が右に入りきらない場合は左へ */
.sub2.-left {
  left: auto;
  right: 100%;
}

固定ヘッダーの中で使う場合は、親要素にoverflow: hiddenが付いていないかも確認します。付いているとメニューが切り取られて表示されません。

メガメニュー型のレイアウト

項目数が多いサイトでは、画面幅いっぱいに広がるメガメニューが有効です。Gridで多段組みにします。

.mega {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;            /* ヘッダー幅いっぱい */
  padding: 28px 40px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px 40px;
  background-color: #fff;
  box-shadow: 0 16px 40px rgba(15, 23, 42, .16);
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s, visibility .2s;
}

.gnav__list > .has-mega {
  position: static;       /* 基準をヘッダー全体にする */
}

.gnav__list > .has-mega:hover .mega {
  opacity: 1;
  visibility: visible;
}

.mega__group-title {
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 2px solid #e5e7eb;
  font-weight: 700;
}

コツは親のliposition: static を指定することです。基準がヘッダー側になるため、画面幅いっぱいに広げられます。

スマホでの開閉と表示切り替え

タッチ端末には:hoverがありません。スマホではアコーディオン型に切り替え、チェックボックスで開閉させます。

<li class="has-child">
  <input type="checkbox" id="acc-service" class="acc-toggle">
  <label for="acc-service" class="acc-label">サービス</label>
  <ul class="sub">
    <li><a href="/service/web/">Web制作</a></li>
    <li><a href="/service/design/">デザイン</a></li>
  </ul>
</li>
.acc-toggle,
.acc-label {
  display: none;              /* PCでは使わない */
}

@media (max-width: 960px) {
  .gnav__list {
    display: block;
  }

  .acc-label {
    display: block;
    padding: 1em;
    font-weight: 700;
    cursor: pointer;
  }

  .sub {
    position: static;         /* 重ねず縦に積む */
    opacity: 1;
    visibility: visible;
    max-height: 0;            /* 閉じた状態 */
    overflow: hidden;
    box-shadow: none;
    transition: max-height .3s ease;
  }

  .acc-toggle:checked ~ .sub {
    max-height: 500px;        /* 開いた状態 */
  }
}

スマホでは重ねる必要がないのでposition: staticに戻し、高さのアニメーションで開閉します。max-heightは中身より少し大きい値を入れておきます。

キーボード操作とアクセシビリティ

キーボード対応の順序

:hoverだけで作ると、キーボード利用者はサブメニューへ到達できません。:focus-withinを必ず併記します。

/* Tabキーで子リンクに入ったときも開いたままにする */
.has-child:hover .sub,
.has-child:focus-within .sub {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.gnav a:focus-visible {
  outline: 3px solid #0ea5e9;
  outline-offset: 2px;
}
  • :focus-withinで、Tabキー移動中も開いた状態を保つ
  • 親項目もリンクにする場合は、押せる場所と開く動作が競合しないか確認する
  • aria-expandedなど状態を伝える属性はJavaScriptで更新する
  • 矢印記号は装飾なので、読み上げには影響しない擬似要素で描く

CSSだけの実装では開閉状態を支援技術に伝えられません。公共性の高いサイトや厳密な要件がある案件では、JavaScriptでaria-expandedを切り替える実装を選んでください。

開かない・隠れるときの原因と対処

開かない時の原因

動かない場合は次の表を上から確認します。ほとんどがこの6つのどれかです。

症状原因対処
開かない親にposition: relativeがない親のliに指定する
他の要素の下に隠れる重なり順が低いz-indexを上げる
途中で切れる親にoverflow: hidden指定を外す
カーソルを移すと消える親子間に隙間paddingか擬似要素で埋める
スマホで開けない:hover依存チェックボックス方式に切替
Tabで選べないfocus対応がない:focus-withinを追加

2番目の重なり順は、固定ヘッダーとの組み合わせで頻発します。ヘッダー自体のz-indexと、サブメニューの値の両方を確認してください。

コピペで使える完成版

PCはホバー、スマホはアコーディオンに切り替わる一式です。ブレイクポイントは960pxにしています。

.gnav__list {
  display: flex;
  gap: 4px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.gnav__list a {
  display: block;
  padding: 1em 1.2em;
  text-decoration: none;
  color: #0f172a;
  font-weight: 700;
}

.has-child {
  position: relative;
}

.sub {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 220px;
  margin: 0;
  padding: 8px 0;
  list-style: none;
  background-color: #fff;
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, .14);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity .2s ease, transform .2s ease, visibility .2s;
  transition-delay: .15s;
  z-index: 100;
}

.sub::before {
  content: "";
  position: absolute;
  top: -12px;
  left: 0;
  width: 100%;
  height: 12px;
}

.has-child:hover .sub,
.has-child:focus-within .sub {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

.sub a {
  padding: 0.7em 1.2em;
  font-weight: 400;
  white-space: nowrap;
}

.sub a:hover {
  background-color: #f1f5f9;
}

@media (max-width: 960px) {
  .gnav__list {
    display: block;
  }

  .sub {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    max-height: 0;
    overflow: hidden;
    box-shadow: none;
    transition: max-height .3s ease;
  }

  .acc-toggle:checked ~ .sub {
    max-height: 500px;
  }
}

ホバー時のデザインバリエーション

開いたメニューの見た目も、サイトのトーンに合わせて選べます。3種類の作り分けを紹介します。

1. 下線が伸びるタイプ

.gnav__list > li > a {
  position: relative;
}

.gnav__list > li > a::after {
  content: "";
  position: absolute;
  left: 1.2em;
  right: 1.2em;
  bottom: 0.6em;
  height: 2px;
  background-color: #0ea5e9;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .25s ease;
}

.gnav__list > li:hover > a::after {
  transform: scaleX(1);
}

transform: scaleX()で伸ばすと、レイアウトを再計算せずに動くので滑らかです。widthを変える書き方より負荷が小さくなります。

2. 背景が塗りつぶされるタイプ

.gnav.-fill .gnav__list > li > a {
  border-radius: 8px;
  transition: background-color .2s, color .2s;
}

.gnav.-fill .gnav__list > li:hover > a {
  background-color: #0ea5e9;
  color: #fff;
}

.gnav.-fill .sub {
  margin-top: 4px;
  border-radius: 8px;
}

管理画面やサービスサイトなど、押せる範囲を明確に見せたい場面に向きます。角丸をヘッダーのトーンと揃えると馴染みます。

3. 区切り線付きのシンプル型

.gnav.-plain .sub li + li {
  border-top: 1px solid #f1f5f9;
}

.gnav.-plain .sub {
  padding: 4px 0;
  border: 1px solid #e5e7eb;
  box-shadow: 0 6px 18px rgba(15, 23, 42, .08);
}

.gnav.-plain .sub a:hover {
  background-color: #f8fafc;
  padding-left: 1.5em;      /* わずかに右へずらして反応を示す */
  transition: padding-left .15s, background-color .15s;
}

影を弱めて線で区切ると、情報量の多いサイトでも落ち着いて見えます。項目数が10を超える場合はこの形が読みやすくなります。

親項目をリンクにするかどうか

「サービス」のような親項目を、リンクにするか単なる開閉トリガーにするかは設計判断です。それぞれの利点を整理します。

方式利点注意点
親もリンクにする一覧ページへ直接行けるスマホでタップが競合する
親は開閉のみ誤タップが起きない一覧ページへの導線が減る
親リンク+子に「一覧を見る」両立できる項目が1つ増える

実務でよく採用されるのは3つ目です。子メニューの先頭に「サービス一覧を見る」を置けば、PCでもスマホでも迷いません。

<ul class="sub">
  <li><a href="/service/"><strong>サービス一覧を見る</strong></a></li>
  <li><a href="/service/web/">Web制作</a></li>
  <li><a href="/service/design/">デザイン</a></li>
</ul>

スマホのアコーディオンでは、親のラベルをタップすると開くだけにして、遷移は子の先頭項目に任せる形が安全です。


まとめ

ドロップダウンメニューは、親のliposition: relative、子のulposition: absolutetop: 100%を指定するのが基本形です。閉じた状態はopacityvisibilityで作るとアニメーションが効きます。マウスが外れて消える問題は、親のpaddingか擬似要素で隙間を埋め、transition-delayで閉じるのを少し待たせれば解決します。スマホでは:hoverが使えないため、チェックボックスによるアコーディオンに切り替え、キーボード対応として:focus-withinを必ず併記してください。

よくある質問(FAQ)

サブメニューが他の要素の下に隠れてしまいます。

重なり順の問題です。サブメニューにz-indexを指定し、親にもpositionが設定されているか確認してください。固定ヘッダー内で使う場合は、ヘッダー自体のz-indexとの関係も見直す必要があります。親要素にoverflow: hiddenがあると切り取られるため、その指定も外します。

カーソルを子メニューへ動かす途中で閉じてしまいます。

親のリンクと子メニューの間に隙間があるためです。親のaに下方向のpaddingを持たせるか、子メニューに::beforeで透明な帯を作って隙間を埋めてください。加えてtransition-delayを0.15秒ほど入れると、一瞬外れても閉じません。

スマホでドロップダウンが開きません。

タッチ端末には:hover状態がないためです。960px以下ではチェックボックスとlabelによる開閉に切り替え、position: staticで縦に積むアコーディオン型にしてください。この記事の完成版はその切り替えを含んでいます。

何階層まで作っていいですか?

2階層までを推奨します。3階層以上はカーソルを保ったまま移動する操作が難しく、スマホでは深すぎて到達されません。項目が多い場合は、画面幅いっぱいに広げるメガメニュー型にして、1階層で見渡せる形にするほうが有効です。

CSSだけの実装でアクセシビリティは十分ですか?

キーボード移動は:focus-withinで対応できますが、開閉状態を示すaria-expandedの更新はCSSではできません。公共サイトなど要件が厳しい案件では、JavaScriptで状態属性を切り替える実装を選んでください。

あわせて読みたい関連記事

CSS・Webデザイン実装の総まとめ

CSSの実装テクニックを「レイアウト・装飾・アニメーション・基礎・トラブル解決」の目的別に探せる総まとめページを用意しています。実装で迷ったときの索引としてどうぞ。

AIスキルで、未来の自分をアップデート!

今なら完全無料でAIを学べる!WithAI

今なら完全無料でAIを学べる!

  • 動画や実践で楽しく学べる:初心者でも安心のカリキュラム
  • スマホ・PCどちらでもOK:好きな時間に学習できる
  • 料金は一切ナシ0円でAIスキルが身につく

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。

会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!

この記事を書いた人

WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次