



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




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









生徒タブで内容を切り替えるUIを作りたいんですが、JavaScriptを書かないと無理ですか?
ペン博士いや、ラジオボタンの:checkedを使えばCSSだけで作れるよ。仕組みはシンプルだし、コピペで動く形にして渡すね。キーボード操作の配慮まで一緒に見ていこう。
料金プランの比較、商品の詳細と仕様、FAQのカテゴリ分け。タブUIは狭い場所に複数の情報を収めるときの定番パーツです。
JavaScriptで実装するのが一般的ですが、CSSの:checked疑似クラスと兄弟セレクタを組み合わせれば、スクリプトなしでも切り替えは実現できます。読み込むファイルが減り、表示も速くなります。
この記事では、ラジオボタン方式の基本構造から、見た目の作り込み、縦型タブ、スマホでの横スクロール対応、キーボード操作の配慮まで順番に解説します。最後にコピペ用の完成版を置いています。

まず選択肢を整理します。実装方法によって、できることと手間が変わります。
| 方法 | 仕組み | 向いている場面 |
|---|---|---|
| ラジオボタン方式 | input:checked+兄弟セレクタ | 静的サイト・表示切り替えだけ |
| details/summary | HTML標準の開閉要素 | アコーディオン型・モバイル主体 |
| 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"を足すと支援技術に構造が伝わるなお、厳密なタブUIのアクセシビリティ要件(aria-selectedの動的更新など)を満たすには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をパネルより前に置くという順序も必須条件です。
ここまでの内容をまとめた一式です。タブを増やす場合は、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; }
}同じ仕組みのまま、タブバーの見た目だけ差し替えられます。サイトのトーンに合うものを選んでください。
.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;
}背景色を使わないので、記事本文の中に置いても浮きません。選択中は文字色を濃くし、下線で位置を示します。
.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個のときに収まりよく見えます。
.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で幅を均等にすると整列します。
見た目が整っても、情報設計を誤ると読まれない内容が生まれます。導入前に次の3点を確認してください。
検索エンジンについては、タブ内のテキストも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に切り替える、という判断で十分です。
input・label・パネルを同じ数だけ追加し、:checkedのセレクタにも行を足します。#tab4:checked ~ .tabs__panels .tabs__panel:nth-child(4)のように、番号を対応させるのがポイントです。数が多い場合はSassのループで生成すると管理が楽になります。
display: noneの要素はフォーカスを受け取れないため、キーボードだけで操作する人がタブを切り替えられなくなります。position: absoluteとopacity: 0で視覚的にだけ隠せば、操作性を保てます。
チェックボックスは複数同時にオンにできるため、パネルが同時に開きます。1つだけ表示したいタブUIでは必ずラジオボタンを使ってください。逆に、複数同時に開くアコーディオンではチェックボックスやdetails要素が適しています。
ラジオ方式ではできません。:target疑似クラスを使う方法ならURLのハッシュで指定できますが、ページ内リンクとして扱われるためスクロール位置が動きます。URL連動が必須ならJavaScriptでの実装を選んでください。
CSSだけでは通信を発生させられないため、遅延読み込みはできません。すべての内容が最初のHTMLに含まれます。中身が非常に大きい場合や、外部APIから取得する場合はJavaScriptでの実装が適しています。
CSSの実装テクニックを「レイアウト・装飾・アニメーション・基礎・トラブル解決」の目的別に探せる総まとめページを用意しています。実装で迷ったときの索引としてどうぞ。

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