「個人・法人で入力項目が変わる」「支払い方法によってフォームが切り替わる」——ラジオボタンで選択肢を切り替えるUIは、不要な入力項目を隠すことでフォームをすっきり見せ、UXを大きく向上させます。
この記事では、ラジオボタンの選択に連動してフォームのセクションを動的に切り替える実装を、基本からアニメーション・バリデーション・アクセシビリティまで体系的に解説します。
ラジオボタンの選択値を取得する
分岐フォームを作る前に、ラジオボタンの値を JavaScript で取得する方法を確認しておきます。
<fieldset> <legend>お客様の種別を選択してください</legend> <label><input type="radio" name="userType" value="personal" checked> 個人</label> <label><input type="radio" name="userType" value="corporate"> 法人</label> </fieldset>
// ① querySelector で checked なものを取得
const checked = document.querySelector('input[name="userType"]:checked');
console.log(checked?.value); // 'personal' など
// ② change イベントで取得(推奨:変更を即座に検知)
document.querySelectorAll('input[name="userType"]').forEach((radio) => {
radio.addEventListener('change', (e) => {
console.log(e.target.value); // 選択されたラジオの value
});
});
// ③ RadioNodeList(form 要素経由)
const form = document.getElementById('myForm');
console.log(form.elements['userType'].value); // 選択中の value を直接取得
change イベントは、選択が変わったときにのみ発火します(選択中のラジオを再クリックしても発火しない)。フォーム分岐の制御には change イベントが最適です。基本実装:ラジオボタンでセクションを切り替える
個人/法人で異なる入力項目を出し分ける、最もよく使われるパターンです。
<form id="regForm">
<fieldset>
<legend>お客様の種別</legend>
<label><input type="radio" name="userType" value="personal" checked> 個人</label>
<label><input type="radio" name="userType" value="corporate"> 法人</label>
</fieldset>
<!-- 個人用セクション -->
<div id="section-personal" class="form-section">
<div class="field-group">
<label for="fullName">お名前 <span class="required">*</span></label>
<input type="text" id="fullName" name="fullName" autocomplete="name">
</div>
<div class="field-group">
<label for="birthdate">生年月日</label>
<input type="date" id="birthdate" name="birthdate">
</div>
</div>
<!-- 法人用セクション -->
<div id="section-corporate" class="form-section" hidden>
<div class="field-group">
<label for="companyName">会社名 <span class="required">*</span></label>
<input type="text" id="companyName" name="companyName" autocomplete="organization">
</div>
<div class="field-group">
<label for="department">部署名</label>
<input type="text" id="department" name="department" autocomplete="organization-title">
</div>
</div>
<button type="submit">登録する</button>
</form>
const sections = {
personal: document.getElementById('section-personal'),
corporate: document.getElementById('section-corporate'),
};
function switchSection(selectedValue) {
Object.entries(sections).forEach(([key, el]) => {
el.hidden = key !== selectedValue;
});
}
// ラジオボタン変更時に切り替え
document.querySelectorAll('input[name="userType"]').forEach((radio) => {
radio.addEventListener('change', (e) => switchSection(e.target.value));
});
// ページ読み込み時:現在の選択に合わせて初期化
const initial = document.querySelector('input[name="userType"]:checked');
if (initial) switchSection(initial.value);
hidden 属性(el.hidden = true)は HTML 標準の非表示属性で、display: none と同等の効果があります。スクリーンリーダーにも非表示として伝わります。visibility: hidden はレイアウト上のスペースが残るため、フォームのセクション切り替えには使いません。CSSトランジションでなめらかに切り替える
hidden 属性の切り替えだけでは瞬時に切り替わります。max-height トランジションを使うとアコーディオン風のなめらかな表示/非表示が実現できます。
.form-section {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.3s ease, opacity 0.3s ease;
}
.form-section.is-visible {
max-height: 600px; /* コンテンツの最大高さより大きい値を設定 */
opacity: 1;
}
function switchSection(selectedValue) {
Object.entries(sections).forEach(([key, el]) => {
if (key === selectedValue) {
el.hidden = false; // まず DOM に表示
// 1フレーム待ってからクラスを付与(トランジションを確実に発火させる)
requestAnimationFrame(() => el.classList.add('is-visible'));
} else {
el.classList.remove('is-visible');
// トランジション完了後に hidden にする
el.addEventListener(
'transitionend',
() => { el.hidden = true; },
{ once: true }
);
}
});
}
max-height に設定する値がコンテンツの実際の高さより小さいと、内容が途中で切れます。コンテンツが動的に変わる場合は el.scrollHeight を使って正確な高さを取得する方法もあります。固定値を使う場合は余裕を持った値(600px〜1000px)を設定してください。data属性を使った汎用設計(HTML に依存しない)
セクションが増えてもJavaScriptを変更しなくてよい、保守しやすい設計です。data-show-when 属性でどの選択値のときに表示するかを HTML 側で指定します。
<fieldset> <legend>支払い方法</legend> <label><input type="radio" name="payment" value="credit" checked> クレジットカード</label> <label><input type="radio" name="payment" value="bank"> 銀行振込</label> <label><input type="radio" name="payment" value="convenience"> コンビニ払い</label> </fieldset> <!-- data-show-when に表示したい value を指定(スペース区切りで複数可) --> <div class="pay-section" data-show-when="credit"> <label for="cardNumber">カード番号</label> <input type="text" id="cardNumber" name="cardNumber" placeholder="1234-5678-9012-3456"> </div> <div class="pay-section" data-show-when="bank"> <p>振込先:〇〇銀行 〇〇支店 普通 1234567(カブシキガイシャ〇〇)</p> <p>※ご入金確認後に発送いたします。</p> </div> <div class="pay-section" data-show-when="convenience"> <label for="convenienceCode">払込番号</label> <input type="text" id="convenienceCode" name="convenienceCode" placeholder="注文後にメールでお知らせします"> </div>
function bindRadioBranch(radioName) {
const radios = document.querySelectorAll(`input[name='${radioName}']`);
// data-show-when 属性でこのラジオグループに属するセクションを特定
const sections = document.querySelectorAll('[data-show-when]');
function update(value) {
sections.forEach((el) => {
const showWhen = el.dataset.showWhen.split(' '); // スペース区切りで複数値対応
el.hidden = !showWhen.includes(value);
});
}
radios.forEach((radio) => {
radio.addEventListener('change', (e) => update(e.target.value));
});
// 初期化
const checked = document.querySelector(`input[name='${radioName}']:checked`);
if (checked) update(checked.value);
}
// 使い方:ラジオグループの name を渡すだけ
bindRadioBranch('payment');
bindRadioBranch('userType');
data-show-when="credit bank" のように書くと、クレジットカードまたは銀行振込のどちらか選択時に表示する、というOR条件が実現できます。表示中のセクションだけバリデーションする
非表示のセクションの必須フィールドまでバリデーションすると、ユーザーには見えない項目でエラーが出ます。hidden 状態のフィールドはバリデーション対象から外すのがポイントです。
document.getElementById('regForm').addEventListener('submit', (e) => {
e.preventDefault();
// hidden でないセクション内の required フィールドだけ検証
const visibleRequiredFields = Array.from(
document.querySelectorAll('.form-section:not([hidden]) [required]')
);
let hasError = false;
visibleRequiredFields.forEach((field) => {
if (field.value.trim() === '') {
field.setAttribute('aria-invalid', 'true');
const errorEl = document.getElementById(field.getAttribute('aria-describedby'));
if (errorEl) errorEl.textContent = '必須項目です';
hasError = true;
} else {
field.setAttribute('aria-invalid', 'false');
const errorEl = document.getElementById(field.getAttribute('aria-describedby'));
if (errorEl) errorEl.textContent = '';
}
});
if (hasError) {
document.querySelector('.form-section:not([hidden]) [aria-invalid="true"]')?.focus();
return;
}
console.log('送信OK');
// fetch('/api/register', { method: 'POST', body: new FormData(e.target) });
});
hidden 属性で非表示にしているだけでは、そのフィールドの値はフォーム送信時に含まれます。送信したくない場合は disabled 属性を追加するか、送信前に値をクリアしてください。切り替え時に非表示セクションのフィールドを disabled = true にし、表示時に disabled = false にするのがシンプルな実装です。非表示セクションを disabled にして送信から除外する
function switchSectionWithDisable(selectedValue) {
Object.entries(sections).forEach(([key, el]) => {
const isVisible = key === selectedValue;
el.hidden = !isVisible;
// 非表示セクション内の全フォーム要素を disabled/enabled で切り替え
el.querySelectorAll('input, select, textarea').forEach((field) => {
field.disabled = !isVisible;
});
});
}
disabled 属性が付いたフォーム要素は、FormData にも form.submit() にも含まれません。「非表示 = 送信除外」にしたい場合は hidden と disabled をセットで使うのが確実です。アクセシビリティ対応
| 対応項目 | 実装方法 | 効果 |
|---|---|---|
| ラジオグループのラベル | <fieldset> + <legend> |
グループ全体の目的をスクリーンリーダーが読み上げる |
| 非表示セクションをスクリーンリーダーから隠す | hidden 属性(aria-hidden="true" より確実) |
タブ移動・読み上げ対象から除外される |
| 表示切り替えの通知 | aria-live="polite" でステータスを告知 |
選択変更時に何が起きたかスクリーンリーダーが伝える |
| キーボード操作 | ラジオボタンは矢印キーで選択切り替え可能(ブラウザ標準) | 追加実装不要 |
<!-- フォームの外に aria-live 領域を用意 --> <div id="form-status" aria-live="polite" class="sr-only"></div>
/* 視覚的に隠しつつスクリーンリーダーには伝える */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
function switchSection(selectedValue) {
Object.entries(sections).forEach(([key, el]) => {
el.hidden = key !== selectedValue;
});
// スクリーンリーダーに切り替えを通知
const labels = {
personal: '個人のお客様用フォームが表示されました',
corporate: '法人のお客様用フォームが表示されました',
};
const status = document.getElementById('form-status');
if (status) status.textContent = labels[selectedValue] ?? '';
}
よくある質問
switchSection() を一度呼び出してください。また、HTML 側でデフォルトで非表示にしたいセクションに hidden 属性を書いておくと、JavaScript 読み込み前の一瞬の表示も防げます。location.hash = selectedValue で URL のハッシュを更新できます。ページ読み込み時に location.hash を読み取って初期セクションを決める処理も追加すると、ブラウザの戻る/進むで選択状態が復元されます。useState で管理し、onChange で更新します。セクションの表示は {value === "personal" && <PersonalSection />} のような条件レンダリングで実現します。Vue では v-model でラジオの値を双方向バインディングし、v-show または v-if でセクションを切り替えます。aria-invalid をリセットし、エラーメッセージをクリアすると、再表示時にエラーが残らずスッキリします。change イベントを設定し、選択が変わったときに下位の選択状態をリセットして関連セクションを非表示にします。状態が複雑になる場合は、現在の選択値をオブジェクトで一元管理し、状態から表示を再計算する関数を作ると保守しやすくなります。まとめ
ラジオボタンで分岐するフォームの実装ポイントをまとめます。
- セクションの表示切り替えは
el.hidden = true/falseが最もシンプルで、スクリーンリーダーにも正しく伝わる data-show-when属性でHTML側に表示条件を書くと、JavaScript を変えずにセクションを増やせる- 非表示セクションのフィールドは
disabled = trueにすることでフォーム送信から除外できる - バリデーションは
.form-section:not([hidden]) [required]セレクタで表示中のみ対象にする aria-live+fieldset/legendでスクリーンリーダー対応を忘れない
チェックボックスでボタンを活性化する方法は【JavaScript】チェックボックスの状態でボタンを活性化・非活性化する方法、フォームバリデーション全体は【JavaScript】フォームバリデーション完全ガイドもあわせてご覧ください。