



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒グローバルナビにドロップダウンを付けたいんですが、マウスを離すと消えてしまって選べません…。
ペン博士それは親要素と子メニューの間に隙間があるのが原因だね。構造から順番に作れば、JavaScriptなしでも安定して動くよ。多階層とスマホ対応まで一気に見ていこう。
グローバルナビの項目にカーソルを乗せると、下にメニューが開く。このドロップダウンメニューは、ページ数の多いサイトでは欠かせない導線です。
実装自体は:hoverとposition: absoluteの組み合わせで作れます。しかし「マウスが外れて消える」「他の要素の下に隠れる」「スマホで開けない」といったつまずきが起きやすいパーツでもあります。
この記事では基本構造から始めて、消えない工夫、多階層(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秒前後が目安です。大きく動かすとメニューが暴れて見え、目的の項目を追いにくくなります。
サービスの下にさらにカテゴリがある場合は、子メニューの中にもう一段入れ子を作ります。開く方向を横にするのが定石です。
.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;
}コツは親のliにposition: 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種類の作り分けを紹介します。
.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を変える書き方より負荷が小さくなります。
.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;
}管理画面やサービスサイトなど、押せる範囲を明確に見せたい場面に向きます。角丸をヘッダーのトーンと揃えると馴染みます。
.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>スマホのアコーディオンでは、親のラベルをタップすると開くだけにして、遷移は子の先頭項目に任せる形が安全です。
ドロップダウンメニューは、親のliにposition: relative、子のulにposition: absoluteとtop: 100%を指定するのが基本形です。閉じた状態はopacityとvisibilityで作るとアニメーションが効きます。マウスが外れて消える問題は、親のpaddingか擬似要素で隙間を埋め、transition-delayで閉じるのを少し待たせれば解決します。スマホでは:hoverが使えないため、チェックボックスによるアコーディオンに切り替え、キーボード対応として:focus-withinを必ず併記してください。
重なり順の問題です。サブメニューにz-indexを指定し、親にもpositionが設定されているか確認してください。固定ヘッダー内で使う場合は、ヘッダー自体のz-indexとの関係も見直す必要があります。親要素にoverflow: hiddenがあると切り取られるため、その指定も外します。
親のリンクと子メニューの間に隙間があるためです。親のaに下方向のpaddingを持たせるか、子メニューに::beforeで透明な帯を作って隙間を埋めてください。加えてtransition-delayを0.15秒ほど入れると、一瞬外れても閉じません。
タッチ端末には:hover状態がないためです。960px以下ではチェックボックスとlabelによる開閉に切り替え、position: staticで縦に積むアコーディオン型にしてください。この記事の完成版はその切り替えを含んでいます。
2階層までを推奨します。3階層以上はカーソルを保ったまま移動する操作が難しく、スマホでは深すぎて到達されません。項目が多い場合は、画面幅いっぱいに広げるメガメニュー型にして、1階層で見渡せる形にするほうが有効です。
キーボード移動は:focus-withinで対応できますが、開閉状態を示すaria-expandedの更新はCSSではできません。公共サイトなど要件が厳しい案件では、JavaScriptで状態属性を切り替える実装を選んでください。
CSSの実装テクニックを「レイアウト・装飾・アニメーション・基礎・トラブル解決」の目的別に探せる総まとめページを用意しています。実装で迷ったときの索引としてどうぞ。

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。
会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!
WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。
公式サイト より
今すぐ
無料カウンセリング
を予約!