セレクトボックス 2 — 複数選択
このコンポーネントについて
「複数選択」ができるフォームUIには、ネイティブ <select multiple>・チェックボックスリスト・カスタムドロップダウン など複数のアプローチがあります。このページでは Pattern 1:ネイティブ と Pattern 2:カスタムドロップダウン(クリックで開閉+チェックボックス) の2パターンを収録し、チェックボックスリストとの違いも解説します。
ネイティブ select は HTML1行で動きますが、Ctrl/Shift クリックという操作方法がユーザーに伝わりにくいのが難点です。チェックボックスリストはHTMLのみで実装でき操作も直感的ですが、選択肢が多いと縦スペースを消費します。カスタムドロップダウンはチェックボックスをパネルに格納して折り畳むことで、スペース効率と直感的な操作性を両立します。
- Pattern 1 — ネイティブ —
multiple属性を付けるだけで複数選択に対応。size属性で表示行数を固定し、スクロールで全選択肢を閲覧できる - Pattern 2 — カスタムドロップダウン — トリガーボタンをクリックするとパネルが開き、チェックボックスで選択する。閉じているときは1行のボタンに収まるためスペース効率がよい
- チェックボックスリストとの違い — チェックボックスを画面上に直接並べる「チェックボックスリスト」はHTMLのみで実装できシンプルですが、常時表示されるため選択肢が多い場合に縦スペースを消費します。カスタムドロップダウンはパネルに折り畳むことで同じ操作性を保ちつつスペースを節約します
- 決定ボタンで確定 — どちらのパターンも決定ボタン押下で選択内容を確定し、タグ形式で表示する。選択途中の不完全な状態が反映されない設計
ネイティブ vs チェックボックスリスト vs カスタムドロップダウン — 使い分け
| 観点 | Pattern 1:ネイティブ<select multiple> |
チェックボックスリスト | Pattern 2:カスタムドロップダウン |
|---|---|---|---|
| 実装コスト | HTML の属性1つで動く | HTML のみで実装できる | HTML / CSS / JS すべて必要 |
| 操作の直感性 | Ctrl/Shift クリックを知らないと分かりにくい | チェックボックスなので直感的 | チェックボックスなので直感的 |
| 表示領域 | 常にリスト表示(縦スペースを消費) | 常にリスト表示(縦スペースを消費) | 1行のボタンに収まる(開いたときのみ展開) |
| スタイルの自由度 | ブラウザ依存で限定的 | CSS で完全に自由 | CSS で完全に自由 |
| 向いている場面 | 管理画面・社内ツールなど、操作に慣れたユーザー向け | 選択肢が少ない・常時表示したい場合 | 選択肢が多い・スペースを節約したい場合 |
実装のポイント・注意点
Pattern 1: 選択中の全項目を取得するには select.selectedOptions を使います。これは HTMLCollection のため、Array.from(select.selectedOptions) で配列に変換してから操作します。全選択解除は select.selectedIndex = -1 が確実です(select.value = "" では複数選択を解除できないケースがある)。
Pattern 2: ドロップダウンパネルを「外側クリックで閉じる」には、document に click イベントを登録し、クリック対象がドロップダウン内かどうかを dropdown.contains(e.target) で判定します。アクセシビリティのため、トリガーボタンに aria-expanded を付与し、パネル開閉と連動させてください。
カスタムドロップダウン vs チェックボックスリスト: <input type="checkbox"> を画面上に直接並べる「チェックボックスリスト」はHTMLのみで実装でき、選択肢は常時表示されます。カスタムドロップダウンとの最大の違いは「折り畳み」です。選択肢が5件以下など少ない場合はチェックボックスリストのほうがシンプルで済みます。選択肢が多くフォームの縦スペースを節約したい場合はカスタムドロップダウンが向いています。
チェックボックスのテキスト取得: <label><input type="checkbox"> テキスト</label> の構造では、input.parentElement.textContent.trim() でラベルのテキストを取得できます(input 要素自体は textContent を持たないため、parentElement の textContent はテキスト部分のみになります)。
デモ
Pattern 1 — ネイティブ(select multiple)
Ctrl(Mac: ⌘)を押しながらクリックで複数選択、Shift+クリックで範囲選択できます
選択された技術
Pattern 2 — カスタムドロップダウン
興味のある技術を選んでください
選択された技術
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>複数選択 サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- Pattern 1: ネイティブ(select multiple) -->
<div class="fs2-pattern">
<p class="fs2-pattern-label">Pattern 1 — ネイティブ(select multiple)</p>
<div class="ms-wrapper">
<label class="ms-label" for="ms-tech">興味のある技術を選んでください</label>
<!-- 操作方法をヒントテキストで伝える -->
<p class="ms-hint">Ctrl(Mac: ⌘)を押しながらクリックで複数選択、Shift+クリックで範囲選択できます</p>
<div class="ms-select-row">
<!-- multiple 属性で複数選択を有効化、size で表示行数を固定する -->
<select id="ms-tech" class="ms-select" multiple size="8">
<option value="html">HTML / CSS</option>
<option value="js">JavaScript</option>
<option value="ts">TypeScript</option>
<option value="react">React</option>
<option value="vue">Vue.js</option>
<option value="python">Python</option>
<option value="go">Go</option>
<option value="rust">Rust</option>
<option value="sql">SQL / データベース</option>
<option value="docker">Docker / コンテナ</option>
</select>
<div class="ms-side">
<span class="ms-count-badge" id="ms-count">0件選択中</span>
<!-- 1件以上選択されたとき有効化する -->
<button class="ms-confirm-btn" id="ms-confirm-btn" disabled>決定</button>
</div>
</div>
<div class="ms-result" id="ms-result">
<p class="ms-result-label">選択された技術</p>
<div class="ms-result-body" id="ms-result-body" aria-live="polite">
<span class="ms-empty">まだ確定されていません</span>
</div>
</div>
</div>
</div>
<!-- Pattern 2: カスタムドロップダウン -->
<div class="fs2-pattern">
<p class="fs2-pattern-label">Pattern 2 — カスタムドロップダウン</p>
<div class="cd-wrapper">
<p class="cd-label">興味のある技術を選んでください</p>
<div class="cd-dropdown" id="cd-dropdown">
<!-- aria-expanded でパネルの開閉状態をスクリーンリーダーに伝える -->
<button class="cd-trigger" id="cd-trigger"
aria-haspopup="true" aria-expanded="false">
<span id="cd-trigger-text">選択してください</span>
<span class="cd-arrow" aria-hidden="true">▼</span>
</button>
<div class="cd-panel" id="cd-panel" hidden>
<ul class="cd-list" role="listbox" aria-multiselectable="true" aria-label="技術スタック">
<li><label class="cd-item"><input type="checkbox" value="html"> HTML / CSS</label></li>
<li><label class="cd-item"><input type="checkbox" value="js"> JavaScript</label></li>
<li><label class="cd-item"><input type="checkbox" value="ts"> TypeScript</label></li>
<li><label class="cd-item"><input type="checkbox" value="react"> React</label></li>
<li><label class="cd-item"><input type="checkbox" value="vue"> Vue.js</label></li>
<li><label class="cd-item"><input type="checkbox" value="python"> Python</label></li>
<li><label class="cd-item"><input type="checkbox" value="go"> Go</label></li>
<li><label class="cd-item"><input type="checkbox" value="rust"> Rust</label></li>
<li><label class="cd-item"><input type="checkbox" value="sql"> SQL / データベース</label></li>
<li><label class="cd-item"><input type="checkbox" value="docker"> Docker / コンテナ</label></li>
</ul>
<div class="cd-panel-footer">
<button class="cd-confirm-btn" id="cd-confirm-btn">決定</button>
</div>
</div>
</div>
<div class="cd-result" id="cd-result">
<p class="cd-result-label">選択された技術</p>
<div class="cd-result-body" id="cd-result-body" aria-live="polite">
<span class="cd-empty">まだ確定されていません</span>
</div>
</div>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
/* === 複数選択 サンプル ===
色を変えたいときは :root の変数を書き換えるだけでOKです */
:root {
--color-accent: #2B7FE8;
--color-accent-bg: #EBF3FD;
--color-text: #1A2332;
--color-border: #D0D7E0;
--color-bg: #F4F6F9;
--color-muted: #9AA5B4;
}
/* パターンラベル */
.fs2-pattern { margin-bottom: 32px; }
.fs2-pattern-label {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
color: var(--color-accent);
letter-spacing: 0.04em;
}
/* ========== Pattern 1: ネイティブ ========== */
.ms-wrapper { max-width: 480px; font-family: sans-serif; }
.ms-label {
display: block;
margin-bottom: 6px;
font-size: 14px;
font-weight: 700;
color: var(--color-text);
}
.ms-hint {
margin: 0 0 12px;
font-size: 12px;
color: var(--color-muted);
line-height: 1.5;
}
.ms-select-row { display: flex; gap: 12px; align-items: flex-start; }
.ms-select {
flex: 1;
border: 1.5px solid var(--color-border);
border-radius: 8px;
font-size: 14px;
color: var(--color-text);
background: #fff;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
font-family: sans-serif;
}
.ms-select:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.12);
}
.ms-side { display: flex; flex-direction: column; gap: 8px; min-width: 88px; }
.ms-count-badge {
display: block;
text-align: center;
font-size: 12px;
font-weight: 700;
color: var(--color-accent);
background: var(--color-accent-bg);
border-radius: 20px;
padding: 4px 8px;
white-space: nowrap;
}
.ms-confirm-btn {
padding: 8px 0;
font-size: 14px;
font-weight: 700;
color: #fff;
background: var(--color-accent);
border: none;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, opacity 0.15s;
}
.ms-confirm-btn:hover:not(:disabled) { background: #1a6fd1; }
.ms-confirm-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.ms-result {
margin-top: 16px;
padding: 14px 16px;
border: 1.5px solid var(--color-border);
border-radius: 8px;
background: #fff;
}
.ms-result-label {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.ms-result-body { display: flex; flex-wrap: wrap; gap: 6px; min-height: 28px; }
.ms-tag {
display: inline-block;
padding: 4px 12px;
background: var(--color-accent);
color: #fff;
border-radius: 20px;
font-size: 13px;
}
.ms-empty { font-size: 13px; color: var(--color-muted); }
/* ========== Pattern 2: カスタムドロップダウン ========== */
.cd-wrapper { max-width: 480px; font-family: sans-serif; }
.cd-label {
margin: 0 0 6px;
font-size: 14px;
font-weight: 700;
color: var(--color-text);
}
/* ドロップダウン全体(position: relative で パネルの基点にする) */
.cd-dropdown { position: relative; }
/* トリガーボタン */
.cd-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 9px 12px;
font-size: 14px;
color: var(--color-text);
background: #fff;
border: 1.5px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
font-family: sans-serif;
text-align: left;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.cd-trigger:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.12);
outline: none;
}
#cd-trigger-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.cd-arrow {
font-size: 10px;
color: var(--color-muted);
transition: transform 0.2s;
flex-shrink: 0;
margin-left: 8px;
}
/* パネルが開いているとき矢印を上向きに回転する */
.cd-trigger[aria-expanded="true"] .cd-arrow { transform: rotate(180deg); }
/* ドロップダウンパネル */
.cd-panel {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: #fff;
border: 1.5px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.cd-list {
list-style: none;
margin: 0;
padding: 6px 0;
max-height: 280px;
overflow-y: auto;
}
.cd-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
font-size: 14px;
color: var(--color-text);
cursor: pointer;
transition: background 0.12s;
}
.cd-item:hover { background: var(--color-bg); }
.cd-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--color-accent);
flex-shrink: 0;
cursor: pointer;
}
.cd-panel-footer {
padding: 8px 14px 12px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
}
.cd-confirm-btn {
padding: 7px 20px;
font-size: 14px;
font-weight: 700;
color: #fff;
background: var(--color-accent);
border: none;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s;
}
.cd-confirm-btn:hover { background: #1a6fd1; }
/* 結果表示エリア */
.cd-result {
margin-top: 16px;
padding: 14px 16px;
border: 1.5px solid var(--color-border);
border-radius: 8px;
background: #fff;
}
.cd-result-label {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.cd-result-body { display: flex; flex-wrap: wrap; gap: 6px; min-height: 28px; }
.cd-tag {
display: inline-block;
padding: 4px 12px;
background: var(--color-accent);
color: #fff;
border-radius: 20px;
font-size: 13px;
}
.cd-empty { font-size: 13px; color: var(--color-muted); }
/* スマホ対応 */
@media (max-width: 480px) {
.ms-wrapper, .cd-wrapper { max-width: 100%; }
}
/* ========== Pattern 1: ネイティブ(select multiple) ========== */
var select = document.getElementById('ms-tech');
var countBadge = document.getElementById('ms-count');
var msConfirmBtn = document.getElementById('ms-confirm-btn');
var msResultBody = document.getElementById('ms-result-body');
// 選択が変わるたびに件数バッジと決定ボタンの状態を更新する
select.addEventListener('change', function () {
var count = select.selectedOptions.length;
countBadge.textContent = count + '件選択中';
msConfirmBtn.disabled = count === 0;
});
// 決定ボタン: 選択中の項目をタグ形式で表示する
msConfirmBtn.addEventListener('click', function () {
// selectedOptions は HTMLCollection のため Array.from で配列に変換する
var selected = Array.from(select.selectedOptions);
var html = '';
for (var i = 0; i < selected.length; i++) {
html += '<span class="ms-tag">' + selected[i].text + '</span>';
}
msResultBody.innerHTML = html;
});
/* ========== Pattern 2: カスタムドロップダウン ========== */
var cdDropdown = document.getElementById('cd-dropdown');
var cdTrigger = document.getElementById('cd-trigger');
var cdTriggerText = document.getElementById('cd-trigger-text');
var cdPanel = document.getElementById('cd-panel');
var cdCheckboxes = cdPanel.querySelectorAll('input[type="checkbox"]');
var cdConfirmBtn = document.getElementById('cd-confirm-btn');
var cdResultBody = document.getElementById('cd-result-body');
// トリガークリックでパネルを開閉する
cdTrigger.addEventListener('click', function () {
if (cdPanel.hidden) {
openPanel();
} else {
closePanel();
}
});
function openPanel() {
cdPanel.hidden = false;
cdTrigger.setAttribute('aria-expanded', 'true');
}
function closePanel() {
cdPanel.hidden = true;
cdTrigger.setAttribute('aria-expanded', 'false');
}
// ドロップダウン外をクリックしたらパネルを閉じる
document.addEventListener('click', function (e) {
if (!cdDropdown.contains(e.target)) {
closePanel();
}
});
// チェック変更時にトリガーテキストを選択項目名でリアルタイム更新する
cdCheckboxes.forEach(function (cb) {
cb.addEventListener('change', function () {
var labels = getCheckedLabels();
cdTriggerText.textContent = labels.length > 0 ? labels.join('、') : '選択してください';
});
});
function getCheckedLabels() {
var labels = [];
cdCheckboxes.forEach(function (cb) {
if (cb.checked) labels.push(cb.parentElement.textContent.trim());
});
return labels;
}
// 決定ボタン: 選択中の項目をタグ形式で表示してパネルを閉じる
cdConfirmBtn.addEventListener('click', function () {
var selected = [];
cdCheckboxes.forEach(function (cb) {
if (cb.checked) {
// input.parentElement(label)の textContent でラベルテキストを取得する
selected.push(cb.parentElement.textContent.trim());
}
});
if (selected.length === 0) {
cdResultBody.innerHTML = '<span class="cd-empty">まだ確定されていません</span>';
} else {
var html = '';
for (var i = 0; i < selected.length; i++) {
html += '<span class="cd-tag">' + selected[i] + '</span>';
}
cdResultBody.innerHTML = html;
}
closePanel();
});
/* ========== 共通リセット ========== */
function resetDemo() {
// Pattern 1 リセット
select.selectedIndex = -1;
countBadge.textContent = '0件選択中';
msConfirmBtn.disabled = true;
msResultBody.innerHTML = '<span class="ms-empty">まだ確定されていません</span>';
// Pattern 2 リセット
cdCheckboxes.forEach(function (cb) { cb.checked = false; });
cdTriggerText.textContent = '選択してください';
closePanel();
cdResultBody.innerHTML = '<span class="cd-empty">まだ確定されていません</span>';
}
AI用プロンプト
各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や選択肢の変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
Pattern 1 — ネイティブ(select multiple)
# セレクトボックス(ネイティブ複数選択)作成依頼
## 概要
`<select multiple>` を使って複数の選択肢を選べるフォームUIを作成してください。
選択後に「決定」ボタンを押すと選択内容が確定・表示される設計です。
## 要件
- 10件の選択肢を持つ `<select multiple size="8">` を配置する
- Ctrl(Mac: ⌘)+ クリックで複数選択、Shift + クリックで範囲選択できることをヒントテキストで説明する
- 選択件数をリアルタイムでバッジ表示する(例:「3件選択中」)
- 「決定」ボタン:1件以上選択されたとき有効化、未選択時は disabled にする
- 決定ボタン押下で選択項目をタグ形式(丸角バッジ)で結果エリアに一覧表示する
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
選択肢(10件): HTML / CSS, JavaScript, TypeScript, React, Vue.js, Python, Go, Rust, SQL / データベース, Docker / コンテナ
全選択解除は select.selectedIndex = -1 で行う。
selectedOptions は HTMLCollection のため Array.from で配列変換してから操作する。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。
Pattern 2 — カスタムドロップダウン
# カスタムドロップダウン(複数選択)作成依頼
## 概要
クリックで開閉するカスタムドロップダウンパネルにチェックボックスを並べ、
複数項目を選択して「決定」ボタンで確定するフォームUIを作成してください。
## 要件
- トリガーボタン(未選択時「選択してください」、選択中は選択項目名を「、」区切りで表示)
- ボタンをクリックするとドロップダウンパネルが開き、10件のチェックボックスが並ぶ
- チェックのたびにトリガーボタンのテキストを選択項目名(「、」区切り)でリアルタイム更新する
- パネル内の「決定」ボタン押下で選択を確定し、パネルを閉じ、結果エリアにタグ表示する
- ドロップダウン外をクリックするとパネルが閉じる
- トリガーの矢印(▼)はパネル開閉に合わせて回転する
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
選択肢(10件): HTML / CSS, JavaScript, TypeScript, React, Vue.js, Python, Go, Rust, SQL / データベース, Docker / コンテナ
外側クリック検知:document の click イベントで dropdown.contains(e.target) を判定する。
aria-expanded をトリガーに付与し、パネル開閉と連動させること。
チェックボックスのテキストは input.parentElement.textContent.trim() で取得し、複数選択時は「、」で結合してトリガーに表示する。
トリガーテキストが長くなる場合は text-overflow: ellipsis で省略する(#cd-trigger-text に overflow: hidden; white-space: nowrap; min-width: 0 を指定)。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。