WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

CSSだけでタブ切り替えUIを作る方法|JavaScript不要のコピペ実装

生徒

タブで内容を切り替えるUIを作りたいんですが、JavaScriptを書かないと無理ですか?

ペン博士

いや、ラジオボタンの:checkedを使えばCSSだけで作れるよ。仕組みはシンプルだし、コピペで動く形にして渡すね。キーボード操作の配慮まで一緒に見ていこう。

料金プランの比較、商品の詳細と仕様、FAQのカテゴリ分け。タブUIは狭い場所に複数の情報を収めるときの定番パーツです。

JavaScriptで実装するのが一般的ですが、CSSの:checked疑似クラスと兄弟セレクタを組み合わせれば、スクリプトなしでも切り替えは実現できます。読み込むファイルが減り、表示も速くなります。

この記事では、ラジオボタン方式の基本構造から、見た目の作り込み、縦型タブ、スマホでの横スクロール対応、キーボード操作の配慮まで順番に解説します。最後にコピペ用の完成版を置いています。

  • タブUIを作る3つの方法と選び方
  • ラジオボタン方式の基本構造(HTML/CSS)
  • タブバーとアクティブ状態のデザイン
  • 切り替えにアニメーションを付ける
  • 縦型(サイド配置)タブの作り方
  • スマホでの横スクロールタブ対応
  • キーボード操作とアクセシビリティ
  • JavaScript実装との使い分け
  • 切り替わらないときの原因と対処
  • コピペで使える完成版タブUI

目次

タブUIを作る3つの方法

タブUIを作る3つの方法

まず選択肢を整理します。実装方法によって、できることと手間が変わります。

方法仕組み向いている場面
ラジオボタン方式input:checked+兄弟セレクタ静的サイト・表示切り替えだけ
details/summaryHTML標準の開閉要素アコーディオン型・モバイル主体
JavaScriptクラス付け外し動的な内容・URL連動・多機能

この記事の主役はラジオボタン方式です。依存ゼロで軽いのが最大の利点で、コンテンツが固定のページなら十分に実用的です。

一方、タブの内容をあとから読み込む、URLに現在のタブを残す、といった要件があるならJavaScriptを選びます。判断基準は記事後半の比較で詳しく扱います。

ラジオボタン方式の基本構造

切り替わる仕組み

仕組みは単純です。同じnameを持つラジオボタンを置き、選ばれたものに対応するパネルだけを表示します。

<div class="tabs">
  <!-- 入力(画面には出さない) -->
  <input type="radio" id="tab1" name="tabs" checked>
  <input type="radio" id="tab2" name="tabs">
  <input type="radio" id="tab3" name="tabs">

  <!-- タブの見出し -->
  <div class="tabs__nav">
    <label for="tab1">サービス概要</label>
    <label for="tab2">料金プラン</label>
    <label for="tab3">よくある質問</label>
  </div>

  <!-- 中身 -->
  <div class="tabs__panels">
    <div class="tabs__panel">サービス概要の内容が入ります。</div>
    <div class="tabs__panel">料金プランの内容が入ります。</div>
    <div class="tabs__panel">よくある質問の内容が入ります。</div>
  </div>
</div>
/* ラジオ本体は隠す(display:noneは使わない) */
.tabs > input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.tabs__panel {
  display: none;
}

/* 選ばれた順番に対応するパネルだけ表示する */
#tab1:checked ~ .tabs__panels .tabs__panel:nth-child(1),
#tab2:checked ~ .tabs__panels .tabs__panel:nth-child(2),
#tab3:checked ~ .tabs__panels .tabs__panel:nth-child(3) {
  display: block;
}

重要なのは、ラジオをdisplay:noneで消さないことです。完全に消すとキーボードのフォーカスが当たらなくなり、Tabキーで操作できないタブになってしまいます。位置をずらして透明にする方法なら、操作性を保ったまま隠せます。

タブバーとアクティブ状態のデザイン

骨組みができたら見た目を作ります。今どのタブを見ているかが一目で分かることが最優先です。

.tabs__nav {
  display: flex;
  gap: 4px;
  border-bottom: 2px solid #e5e7eb;
}

.tabs__nav label {
  padding: 0.8em 1.4em;
  border-radius: 8px 8px 0 0;
  background-color: #f3f4f6;
  color: #6b7280;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .2s, color .2s;
  user-select: none;
}

.tabs__nav label:hover {
  background-color: #e5e7eb;
}

/* 選択中のタブを強調する */
#tab1:checked ~ .tabs__nav label[for="tab1"],
#tab2:checked ~ .tabs__nav label[for="tab2"],
#tab3:checked ~ .tabs__nav label[for="tab3"] {
  background-color: #0ea5e9;
  color: #fff;
  box-shadow: 0 2px 0 #0ea5e9;
}

.tabs__panels {
  padding: 1.5em;
  border: 2px solid #e5e7eb;
  border-top: none;
  border-radius: 0 0 10px 10px;
}

色を変えるだけでなく、下線や影でタブバーとパネルをつなげると、面として一体に見えます。非選択のタブは彩度を落として、視線が選択中に集まるようにします。

切り替えにアニメーションを付ける

displayの切り替えはアニメーションできません。ふわっと出したい場合は、表示と同時にキーフレームを走らせます。

@keyframes tab-fade {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

#tab1:checked ~ .tabs__panels .tabs__panel:nth-child(1),
#tab2:checked ~ .tabs__panels .tabs__panel:nth-child(2),
#tab3:checked ~ .tabs__panels .tabs__panel:nth-child(3) {
  display: block;
  animation: tab-fade .25s ease-out;
}

/* 動きを抑える設定の利用者には出さない */
@media (prefers-reduced-motion: reduce) {
  .tabs__panel {
    animation: none !important;
  }
}

動きは0.2〜0.3秒が目安です。長いと切り替えが重く感じられ、短すぎると気づかれません。移動距離も数pxで十分です。

縦型(サイド配置)タブの作り方

項目数が多い場合や、管理画面のような設定ページでは縦並びが読みやすくなります。Gridで配置を変えるだけです。

.tabs.-vertical {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 0 1.5rem;
  align-items: start;
}

.tabs.-vertical .tabs__nav {
  display: block;
  border-bottom: none;
  border-right: 2px solid #e5e7eb;
}

.tabs.-vertical .tabs__nav label {
  display: block;
  border-radius: 8px 0 0 8px;
  margin-bottom: 4px;
  text-align: left;
}

.tabs.-vertical .tabs__panels {
  border: none;
  padding: 0;
}

@media (max-width: 640px) {
  .tabs.-vertical {
    grid-template-columns: 1fr; /* スマホでは横並びに戻す */
  }
  .tabs.-vertical .tabs__nav {
    display: flex;
    border-right: none;
    border-bottom: 2px solid #e5e7eb;
  }
}

縦型は項目名が長くても折り返さずに置けるのが利点です。スマホでは幅が足りないので、横並びに戻す切り替えを忘れずに入れます。

スマホでの横スクロールタブ

タブが4つ以上ある場合、スマホでは横に収まりません。折り返すより、横スクロールにしたほうが操作しやすくなります。

.tabs__nav {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
}

.tabs__nav::-webkit-scrollbar {
  display: none;
}

.tabs__nav label {
  flex: 0 0 auto;        /* 縮ませず横に並べる */
  scroll-snap-align: start;
  white-space: nowrap;
}

flex: 0 0 autoタブを縮ませないのがポイントです。これが無いと、文字が潰れて読めない細いタブが並びます。スクロールバーは隠しても、端で見切れる形にしておけば「まだ続く」と伝わります。

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

操作性を落とさない配慮

ラジオ方式の利点は、キーボード操作が最初から効くことです。ラジオグループは矢印キーで移動できるため、追加のスクリプトなしでタブ移動が成立します。

/* フォーカス位置を必ず見せる */
.tabs > input[type="radio"]:focus-visible + .tabs__nav label,
.tabs__nav label:focus-visible {
  outline: 3px solid #0ea5e9;
  outline-offset: 2px;
}
<!-- 見出しとして読み上げさせたい場合はラベルにテキストを持たせる -->
<div class="tabs__nav" role="tablist">
  <label for="tab1" role="tab">サービス概要</label>
  <label for="tab2" role="tab">料金プラン</label>
</div>
  • ラジオをdisplay: noneで消さない(フォーカスが失われる)
  • 選択中の状態は色だけでなく、太字や下線でも示す
  • role="tablist"を足すと支援技術に構造が伝わる
  • タブ名は「詳細1」ではなく内容が分かる語にする

なお、厳密なタブUIのアクセシビリティ要件(aria-selectedの動的更新など)を満たすにはJavaScriptが必要です。要件が厳しい案件では次のセクションの比較を参考にしてください。

JavaScript実装との使い分け

CSSだけで作れるとはいえ、万能ではありません。判断基準をまとめます。

観点CSSのみ(ラジオ方式)JavaScript
読み込み追加ファイルなしスクリプトが必要
URL連動できないできる(履歴も残せる)
内容の遅延読み込みできないできる
ARIA属性の更新静的な指定のみ状態に応じて更新できる
実装量少ない多い
向く用途固定内容の切り替えアプリ的なUI

目安として、中身が固定でページ内完結ならCSS、URL共有や動的読み込みが要るならJavaScriptです。両者は排他ではなく、CSSで組んだうえで必要な部分だけスクリプトを足す形も有効です。

切り替わらないときの原因と対処

切り替わらない時の原因

動かない場合、原因はほぼ次の5つに絞られます。上から順に確認してください。

症状原因対処
何も切り替わらないnameが違う全ラジオで同じnameにする
複数同時に開くcheckboxを使っているradioに変更する
labelを押しても反応しないforとidが不一致対応を合わせる
セレクタが効かない兄弟関係が崩れているinputとパネルを同じ親に置く
Tabキーで移動できないdisplay:noneで隠したopacity+位置ずらしに変更

特に多いのが4つ目です。~同じ親を持つ後続の兄弟にしか効きません。inputをパネルより前に置くという順序も必須条件です。

コピペで使える完成版タブUI

ここまでの内容をまとめた一式です。タブを増やす場合は、input・label・panelを同じ数だけ足し、セレクタに行を追加してください。

<div class="tabs">
  <input type="radio" id="tab1" name="tabs" checked>
  <input type="radio" id="tab2" name="tabs">
  <input type="radio" id="tab3" name="tabs">

  <div class="tabs__nav" role="tablist">
    <label for="tab1" role="tab">サービス概要</label>
    <label for="tab2" role="tab">料金プラン</label>
    <label for="tab3" role="tab">よくある質問</label>
  </div>

  <div class="tabs__panels">
    <div class="tabs__panel">
      <h3>サービス概要</h3>
      <p>ここに1つ目の内容が入ります。</p>
    </div>
    <div class="tabs__panel">
      <h3>料金プラン</h3>
      <p>ここに2つ目の内容が入ります。</p>
    </div>
    <div class="tabs__panel">
      <h3>よくある質問</h3>
      <p>ここに3つ目の内容が入ります。</p>
    </div>
  </div>
</div>
.tabs {
  max-width: 800px;
  margin-inline: auto;
}

.tabs > input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.tabs__nav {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  border-bottom: 2px solid #e5e7eb;
}

.tabs__nav label {
  flex: 0 0 auto;
  padding: 0.8em 1.4em;
  border-radius: 8px 8px 0 0;
  background-color: #f3f4f6;
  color: #6b7280;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .2s, color .2s;
}

.tabs__panels {
  padding: 1.5em;
  border: 2px solid #e5e7eb;
  border-top: none;
  border-radius: 0 0 10px 10px;
}

.tabs__panel {
  display: none;
}

#tab1:checked ~ .tabs__nav label[for="tab1"],
#tab2:checked ~ .tabs__nav label[for="tab2"],
#tab3:checked ~ .tabs__nav label[for="tab3"] {
  background-color: #0ea5e9;
  color: #fff;
}

#tab1:checked ~ .tabs__panels .tabs__panel:nth-child(1),
#tab2:checked ~ .tabs__panels .tabs__panel:nth-child(2),
#tab3:checked ~ .tabs__panels .tabs__panel:nth-child(3) {
  display: block;
  animation: tab-fade .25s ease-out;
}

@keyframes tab-fade {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .tabs__panel { animation: none !important; }
}

デザインバリエーション3種

同じ仕組みのまま、タブバーの見た目だけ差し替えられます。サイトのトーンに合うものを選んでください。

1. 下線タイプ(記事メディア向け)

.tabs.-line .tabs__nav label {
  background: transparent;
  border-radius: 0;
  border-bottom: 3px solid transparent;
  color: #6b7280;
  padding: 0.7em 1.2em;
}

#tab1:checked ~ .tabs__nav label[for="tab1"],
#tab2:checked ~ .tabs__nav label[for="tab2"],
#tab3:checked ~ .tabs__nav label[for="tab3"] {
  background: transparent;
  color: #0f172a;
  border-bottom-color: #0ea5e9;
}

背景色を使わないので、記事本文の中に置いても浮きません。選択中は文字色を濃くし、下線で位置を示します。

2. ピル型(アプリ風)

.tabs.-pill .tabs__nav {
  gap: 6px;
  padding: 6px;
  border: none;
  border-radius: 999px;
  background-color: #f3f4f6;
}

.tabs.-pill .tabs__nav label {
  border-radius: 999px;
  padding: 0.6em 1.4em;
  background: transparent;
}

.tabs.-pill input:checked + .tabs__nav label {
  background-color: #fff;
  color: #0f172a;
  box-shadow: 0 1px 3px rgba(15, 23, 42, .15);
}

角丸の帯の中で、選択中だけ白く浮かせる形です。項目数が2〜3個のときに収まりよく見えます。

3. カード型(サービス比較向け)

.tabs.-card .tabs__nav {
  gap: 8px;
  border-bottom: none;
}

.tabs.-card .tabs__nav label {
  flex: 1;
  text-align: center;
  border: 2px solid #e5e7eb;
  border-radius: 12px;
  background-color: #fff;
  padding: 1em 0.8em;
  line-height: 1.5;
}

.tabs.-card input:checked + .tabs__nav label {
  border-color: #0ea5e9;
  background-color: #f0f9ff;
  color: #0369a1;
}

タブ自体に説明文を入れられるので、料金プランの比較のように「選ばせる」場面に向きます。flex: 1で幅を均等にすると整列します。

タブUIを使うときの設計上の注意

見た目が整っても、情報設計を誤ると読まれない内容が生まれます。導入前に次の3点を確認してください。

  • 重要な情報を隠さない:初期表示のタブ以外は読まれにくい。価格や結論は最初のタブか本文に置く
  • タブ数は5つまで:それ以上はカテゴリ設計を見直すか、ページを分ける
  • 印刷・検索を意識する:非表示のパネルはページ内検索で見つからない

検索エンジンについては、タブ内のテキストもHTMLに存在すればインデックスされます。ただし利用者にとって見つけにくいことに変わりはないため、SEO目的で情報をタブへ押し込むのは逆効果です。

/* 印刷時はすべてのパネルを開いて出す */
@media print {
  .tabs__panel {
    display: block !important;
    animation: none !important;
    break-inside: avoid;
  }
  .tabs__nav {
    display: none;
  }
}

印刷用の指定を入れておくと、資料として出力したときに全内容が出ます。数行で済むので入れておくと親切です。


まとめ

CSSのタブUIは、同じnameのラジオボタンと兄弟セレクタ~の組み合わせで作れます。ラジオはdisplay: noneではなく透明にして隠すことで、キーボード操作を保ったまま見た目を整えられます。切り替えのアニメーションは@keyframesで0.25秒程度、スマホではタブバーを横スクロールにしてflex: 0 0 autoで潰れを防ぎます。URL連動や遅延読み込みが必要なときだけJavaScriptに切り替える、という判断で十分です。

よくある質問(FAQ)

タブを4つ以上に増やすにはどうすればいいですか?

input・label・パネルを同じ数だけ追加し、:checkedのセレクタにも行を足します。#tab4:checked ~ .tabs__panels .tabs__panel:nth-child(4)のように、番号を対応させるのがポイントです。数が多い場合はSassのループで生成すると管理が楽になります。

ラジオボタンを display: none で隠してはいけないのはなぜですか?

display: noneの要素はフォーカスを受け取れないため、キーボードだけで操作する人がタブを切り替えられなくなります。position: absoluteopacity: 0で視覚的にだけ隠せば、操作性を保てます。

チェックボックスで作ると何が変わりますか?

チェックボックスは複数同時にオンにできるため、パネルが同時に開きます。1つだけ表示したいタブUIでは必ずラジオボタンを使ってください。逆に、複数同時に開くアコーディオンではチェックボックスやdetails要素が適しています。

URLでタブを指定して共有できますか?

ラジオ方式ではできません。:target疑似クラスを使う方法ならURLのハッシュで指定できますが、ページ内リンクとして扱われるためスクロール位置が動きます。URL連動が必須ならJavaScriptでの実装を選んでください。

タブの中身をあとから読み込むことはできますか?

CSSだけでは通信を発生させられないため、遅延読み込みはできません。すべての内容が最初のHTMLに含まれます。中身が非常に大きい場合や、外部APIから取得する場合は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
    - ウィズフリ -

    実案件サポート

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

    詳細はこちら

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

目次