連動プルダウン(Cascading Select)— 親の選択で子の選択肢が切り替わる2段連動

フォーム入力 中級

このコンポーネントについて

親のセレクトボックスで選んだ値に応じて、子のセレクトボックスの選択肢が切り替わる「連動プルダウン(cascading select)」の実装例です。都道府県を選ぶと市区町村の候補が絞り込まれる住所入力や、大分類を選ぶと中分類が切り替わる商品カテゴリ選択など、業務アプリで最も頻出する連動UIを扱います。このページでは Pattern 1:都道府県 → 市区町村Pattern 2:商品カテゴリ(大分類 → 中分類) の2パターンで、連動の実装とコピペができます。親を変えたときに子の選択が前のまま残る典型バグを起こさない、状態管理の正解を動くデモで確認できます。

  • 親子2段の連動 — 親セレクトの change で子セレクトの選択肢を作り直す。親に子がぶら下がったネスト構造のデータから、対応する子だけを展開する
  • 子の初期 disabled 制御 — 親が未選択のうちは子を disabled+プレースホルダにし、親を選んだタイミングで有効化する。誤操作を防ぐ定番パターン
  • 選択リセットの徹底 — 親を変更・クリアしたとき、子の option を全削除して作り直すことで、前に選んでいた市区町村が残らないようにする
  • createElement による安全な再構築 — 選択肢の生成は createElement + appendChild で行い、innerHTML に文字列を結合しない(XSS対策)
  • 2つの実務題材 — 住所(都道府県→市区町村)と商品カテゴリ(大分類→中分類)で、住所以外への転用イメージも示す

実装のポイント・注意点

連動プルダウンで最もよくあるバグが「親を変えたのに子の選択が前のまま残る」というものです。これを防ぐ最短の方法は、親の change イベントで子の option をすべて削除してから作り直すことです。こうすると選択肢の更新と選択値のリセットが1つの処理で同時に達成でき、child.value = '' のような後始末を別途書く必要がありません。

子セレクトは初期状態で disabled 属性を付けておき、親が選ばれたときに JavaScript で child.disabled = false に切り替えます。親が未選択に戻された場合は、子を再び disabled にしてプレースホルダ(「(都道府県を選択)」など)だけを残すと、操作の順序が自然になりミスを防げます。データは「親の中に子の配列がネストした」構造で持つと、親の key から対応する子配列をそのまま取り出せて実装がシンプルになります。

全国の市区町村のように選択肢が膨大な場合は、データをHTMLに埋め込まず、静的JSONを fetch で読み込む(このデモの方式)か、親を選ぶたびにAPIから子の候補を取得する構成にします。件数が数十件で固定的なら、JS内のオブジェクトに直接持たせるのが最も手軽です。このデモは全国データを同梱せず、代表的な6都道府県のみを収録しています。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

Pattern 1 — 都道府県 → 市区町村

Pattern 2 — 商品カテゴリ(大分類 → 中分類)

サンプルソース

4つのファイルを同じフォルダに保存し、簡易サーバーで index.html を開くと動作確認できます。
ファイル構成:index.html / style.css / script.js / data/data.jsondata フォルダを作って中に配置)
保存時の文字コードは UTF-8 を指定してください。fetch()file:// では動作しないため、VS Code の Live Server 等で開いてください。

<!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: 都道府県 → 市区町村 -->
<div class="fs4-pattern">
  <p class="fs4-pattern-label">Pattern 1 — 都道府県 → 市区町村</p>
  <div class="fs4-row">
    <!-- 都道府県はJSONから動的に生成する -->
    <select id="fs4-pref" class="fs4-select">
      <option value="">選択してください</option>
    </select>
    <!-- 市区町村は親を選ぶまで disabled -->
    <select id="fs4-city" class="fs4-select" disabled>
      <option value="">(都道府県を選択)</option>
    </select>
    <button class="fs4-submit-btn" id="fs4-addr-submit">決定</button>
  </div>
  <div class="fs4-result" id="fs4-addr-result" aria-live="polite"></div>
</div>

<!-- Pattern 2: 商品カテゴリ(大分類 → 中分類) -->
<div class="fs4-pattern">
  <p class="fs4-pattern-label">Pattern 2 — 商品カテゴリ(大分類 → 中分類)</p>
  <div class="fs4-row">
    <!-- 大分類はJSONから動的に生成する -->
    <select id="fs4-cat" class="fs4-select">
      <option value="">選択してください</option>
    </select>
    <!-- 中分類は親を選ぶまで disabled -->
    <select id="fs4-subcat" class="fs4-select" disabled>
      <option value="">(大分類を選択)</option>
    </select>
    <button class="fs4-submit-btn" id="fs4-cat-submit">決定</button>
  </div>
  <div class="fs4-result" id="fs4-cat-result" aria-live="polite"></div>
</div>

<!-- リセットボタン -->
<div class="demo-controls">
  <button class="reset-btn" onclick="resetDemo()">リセット</button>
</div>

<script src="./script.js"></script>
</body>
</html>
/* === 連動プルダウン サンプル ===
   :root の変数を書き換えると色を一括変更できます */
:root {
  --color-accent: #2B7FE8;
  --color-text:   #1A2332;
  --color-border: #D0D7E0;
  --color-muted:  #9AA5B4;
  --color-bg:     #F4F6F9;
  --color-danger: #E65100;
}
* { box-sizing: border-box; }
body { font-family: sans-serif; color: var(--color-text); padding: 24px; max-width: 600px; }

/* パターン共通 */
.fs4-pattern { max-width: 520px; margin-bottom: 32px; }
.fs4-pattern:last-of-type { margin-bottom: 0; }
.fs4-pattern-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: 0.04em;
  margin: 0 0 12px;
}

/* セレクトの並び */
.fs4-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.fs4-select {
  min-width: 160px;
  height: 40px;
  padding: 0 10px;
  font-size: 14px;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  outline: none;
  background: #fff;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.fs4-select:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.12);
}
.fs4-select:disabled {
  background: var(--color-bg);
  color: var(--color-muted);
  cursor: not-allowed;
}

/* 決定ボタン */
.fs4-submit-btn {
  padding: 8px 20px;
  font-size: 14px;
  font-weight: 700;
  color: #fff;
  background: var(--color-accent);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}
.fs4-submit-btn:hover { background: #1a6fd1; }

/* 結果エリア */
.fs4-result { margin-top: 14px; }
.fs4-result.has-value {
  padding: 12px 16px;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  background: #fff;
  font-size: 14px;
  font-weight: 700;
}
.fs4-result.has-error { font-size: 13px; color: var(--color-danger); padding: 4px 0; }

/* リセットボタン */
.demo-controls { margin-top: 12px; }
.reset-btn {
  padding: 6px 16px;
  font-size: 13px;
  color: #5A6A7A;
  background: #fff;
  border: 1.5px solid var(--color-border);
  border-radius: 6px;
  cursor: pointer;
}
.reset-btn:hover { background: var(--color-bg); border-color: var(--color-muted); }

/* スマホ対応 */
@media (max-width: 480px) {
  .fs4-select { min-width: 0; flex: 1 1 100%; }
  .fs4-submit-btn { width: 100%; }
}
var jsonData = null;

// ページ読み込み時にJSONを取得し、両パターンの親セレクトを初期化する
// ※ fetch() は file:// では動作しないため、簡易サーバーで開いてください
(function () {
  fetch('./data/data.json')
    .then(function (res) { return res.json(); })
    .then(function (data) {
      jsonData = data;
      buildParent(document.getElementById('fs4-pref'), data.prefectures);
      buildParent(document.getElementById('fs4-cat'), data.categories);
    })
    .catch(function (err) {
      console.error('data.json の読み込みに失敗しました。data/ フォルダを確認してください。', err);
    });
})();

// 親セレクトの option をJSONから生成する(先頭の「選択してください」は残す)
function buildParent(selectEl, items) {
  while (selectEl.options.length > 1) {
    selectEl.remove(1);
  }
  items.forEach(function (item) {
    var opt = document.createElement('option');
    opt.value = item.key;
    opt.textContent = item.name;
    selectEl.appendChild(opt);
  });
}

// 子セレクトを親の選択に合わせて作り直す
// option を全削除してから作り直すため、前に選んでいた子の値は自動的に破棄される
function updateChild(parent, child, dataKey, childrenKey, placeholder) {
  child.innerHTML = '';

  // 親が未選択なら、プレースホルダのみを入れて disabled に戻す
  if (!parent.value) {
    child.appendChild(makeOption('', placeholder));
    child.disabled = true;
    return;
  }

  // 親が選択済みなら、先頭「選択してください」+対応する子を並べて有効化する
  child.appendChild(makeOption('', '選択してください'));
  var parentItem = jsonData[dataKey].find(function (it) { return it.key === parent.value; });
  var children = parentItem ? parentItem[childrenKey] : [];
  children.forEach(function (c) {
    child.appendChild(makeOption(c.key, c.name));
  });
  child.disabled = false;
}

// option 要素を作るヘルパー
function makeOption(value, text) {
  var opt = document.createElement('option');
  opt.value = value;
  opt.textContent = text;
  return opt;
}

// 連動をセットアップする(親の change で子を作り直す)
function setupCascade(parentId, childId, dataKey, childrenKey, placeholder) {
  var parent = document.getElementById(parentId);
  var child = document.getElementById(childId);
  parent.addEventListener('change', function () {
    updateChild(parent, child, dataKey, childrenKey, placeholder);
  });
}

setupCascade('fs4-pref', 'fs4-city', 'prefectures', 'cities', '(都道府県を選択)');
setupCascade('fs4-cat', 'fs4-subcat', 'categories', 'children', '(大分類を選択)');

// 決定ボタン:親・子の選択値を検証して結果を表示する
function submitCascade(parentId, childId, resultId, childErrMsg) {
  var parent = document.getElementById(parentId);
  var child = document.getElementById(childId);
  var resultEl = document.getElementById(resultId);

  if (!parent.value) { showError(resultEl, '選択してください'); return; }
  if (!child.value) { showError(resultEl, childErrMsg); return; }

  var parentText = parent.options[parent.selectedIndex].text;
  var childText = child.options[child.selectedIndex].text;
  showValue(resultEl, parentText, childText);
}

document.getElementById('fs4-addr-submit').addEventListener('click', function () {
  submitCascade('fs4-pref', 'fs4-city', 'fs4-addr-result', '市区町村を選択してください');
});
document.getElementById('fs4-cat-submit').addEventListener('click', function () {
  submitCascade('fs4-cat', 'fs4-subcat', 'fs4-cat-result', '中分類を選択してください');
});

// 結果表示(textContent を使うため innerHTML への文字列結合は不要)
function showValue(el, parentText, childText) {
  el.className = 'fs4-result has-value';
  el.textContent = parentText + ' / ' + childText;
}
function showError(el, msg) {
  el.className = 'fs4-result has-error';
  el.textContent = msg;
}
function clearResult(id) {
  var el = document.getElementById(id);
  el.className = 'fs4-result';
  el.textContent = '';
}

// リセット:親を未選択に戻し、子を空+disabled の初期状態に戻す
function resetDemo() {
  var pref = document.getElementById('fs4-pref');
  var cat = document.getElementById('fs4-cat');
  pref.selectedIndex = 0;
  cat.selectedIndex = 0;
  updateChild(pref, document.getElementById('fs4-city'), 'prefectures', 'cities', '(都道府県を選択)');
  updateChild(cat, document.getElementById('fs4-subcat'), 'categories', 'children', '(大分類を選択)');
  clearResult('fs4-addr-result');
  clearResult('fs4-cat-result');
}

data フォルダを作成し、data.json として保存してください。親(都道府県・大分類)の中に子(市区町村・中分類)の配列がネストした構造です。

{
  "prefectures": [
    { "key": "hokkaido", "name": "北海道", "cities": [
      { "key": "sapporo",   "name": "札幌市" },
      { "key": "asahikawa", "name": "旭川市" },
      { "key": "hakodate",  "name": "函館市" }
    ]},
    { "key": "tokyo", "name": "東京都", "cities": [
      { "key": "chiyoda",  "name": "千代田区" },
      { "key": "shinjuku", "name": "新宿区" },
      { "key": "setagaya", "name": "世田谷区" },
      { "key": "hachioji", "name": "八王子市" }
    ]},
    { "key": "kanagawa", "name": "神奈川県", "cities": [
      { "key": "yokohama",   "name": "横浜市" },
      { "key": "kawasaki",   "name": "川崎市" },
      { "key": "sagamihara", "name": "相模原市" }
    ]},
    { "key": "aichi", "name": "愛知県", "cities": [
      { "key": "nagoya",  "name": "名古屋市" },
      { "key": "toyota",  "name": "豊田市" },
      { "key": "okazaki", "name": "岡崎市" }
    ]},
    { "key": "osaka", "name": "大阪府", "cities": [
      { "key": "osaka",        "name": "大阪市" },
      { "key": "sakai",        "name": "堺市" },
      { "key": "higashiosaka", "name": "東大阪市" }
    ]},
    { "key": "fukuoka", "name": "福岡県", "cities": [
      { "key": "fukuoka",    "name": "福岡市" },
      { "key": "kitakyushu", "name": "北九州市" },
      { "key": "kurume",     "name": "久留米市" }
    ]}
  ],
  "categories": [
    { "key": "kaden", "name": "家電", "children": [
      { "key": "fridge", "name": "冷蔵庫" },
      { "key": "washer", "name": "洗濯機" },
      { "key": "aircon", "name": "エアコン" }
    ]},
    { "key": "pc", "name": "パソコン・周辺機器", "children": [
      { "key": "laptop",   "name": "ノートPC" },
      { "key": "monitor",  "name": "モニター" },
      { "key": "keyboard", "name": "キーボード" }
    ]},
    { "key": "furniture", "name": "家具", "children": [
      { "key": "desk",    "name": "デスク" },
      { "key": "chair",   "name": "チェア" },
      { "key": "storage", "name": "収納" }
    ]}
  ]
}

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。選択肢のデータや連動の項目など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — 都道府県 → 市区町村

# 連動プルダウン(都道府県 → 市区町村)作成依頼

## 概要
親のセレクトボックス(都道府県)を選ぶと、子のセレクトボックス(市区町村)の選択肢が切り替わる2段の連動プルダウン(cascading select)を実装してください。

## 要件
- ページ読み込み時に fetch でJSONファイルを取得し、親(都道府県)の選択肢を動的生成する
- 子(市区町村)セレクトは初期状態で disabled にし、「(都道府県を選択)」のプレースホルダのみ表示する
- 親を選択したら、対応する市区町村で子を作り直し、子の disabled を解除する。子の先頭は「選択してください」
- 親を別の値に変更したときは、前に選んでいた子の選択を必ずリセットする(前の市区町村が残らないようにする)
- 親を「選択してください」に戻したら、子を空にして再び disabled に戻す
- 「決定」ボタンで選択中の都道府県・市区町村を「都道府県 / 市区町村」の形式で表示する。子が未選択なら警告を出す
- リセットボタンで親を未選択・子を disabled の初期状態に戻す

## JSONデータ仕様
ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)
親に子がネストした構造にする:

{
  "prefectures": [
    { "key": "tokyo", "name": "東京都", "cities": [
      { "key": "chiyoda", "name": "千代田区" },
      { "key": "shinjuku", "name": "新宿区" }
    ]}
  ]
}

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
子の選択肢生成は createElement + appendChild で行い、innerHTML に文字列を結合しないでください。
親の change イベントで子の option を全削除してから再生成することで、前の選択が残らないようにします。
親が未選択のときは子にプレースホルダのみを入れて disabled に戻します。
fetch() は file:// では動作しないため、簡易サーバー(Live Server 等)が必要な旨をコメントに記載してください。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
JSONファイル(data/data.json)も合わせて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — 商品カテゴリ(大分類 → 中分類)

# 連動プルダウン(商品カテゴリ 大分類 → 中分類)作成依頼

## 概要
親のセレクトボックス(大分類)を選ぶと、子のセレクトボックス(中分類)の選択肢が切り替わる2段の連動プルダウンを実装してください。都道府県→市区町村と同じ仕組みを、商品カテゴリの題材で使います。

## 要件
- ページ読み込み時に fetch でJSONファイルを取得し、親(大分類)の選択肢を動的生成する
- 子(中分類)セレクトは初期状態で disabled にし、「(大分類を選択)」のプレースホルダのみ表示する
- 親を選択したら、対応する中分類で子を作り直し、子の disabled を解除する。子の先頭は「選択してください」
- 親を別の値に変更したときは、前に選んでいた子の選択を必ずリセットする
- 親を「選択してください」に戻したら、子を空にして再び disabled に戻す
- 「決定」ボタンで選択中の大分類・中分類を「大分類 / 中分類」の形式で表示する。子が未選択なら警告を出す
- リセットボタンで初期状態に戻す

## JSONデータ仕様
ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)
親に子がネストした構造にする:

{
  "categories": [
    { "key": "kaden", "name": "家電", "children": [
      { "key": "fridge", "name": "冷蔵庫" },
      { "key": "washer", "name": "洗濯機" }
    ]}
  ]
}

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
子の選択肢生成は createElement + appendChild で行い、innerHTML に文字列を結合しないでください。
親の change イベントで子の option を全削除してから再生成することで、前の選択が残らないようにします。
親が未選択のときは子にプレースホルダのみを入れて disabled に戻します。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
JSONファイル(data/data.json)も合わせて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。