入力欄を「編集させたくない」ときに使う属性が readonly と disabled です。どちらを付けても画面上は入力できなくなるため、なんとなく好きなほうを書いてしまいがちですが、この 2 つはフォームを送信したときの結果がまったく違います。この記事では、値が送信されるか、フォーカスやコピーができるか、required によるチェックの対象になるか、そして使える要素の違いまでを整理し、どちらを選べばよいか判断できるようにします。
目次
2 つの属性が表している意味の違い
readonly は「読み取り専用」という名前のとおり、値は生きているが、利用者には書き換えさせないという状態を表します。ログイン中のユーザーの会員 ID や、前の画面で確定した金額のように、「その値でフォームを送りたいけれど、勝手に変えられては困る」ものに使います。
いっぽう disabled は「無効」で、その入力欄を今は使わせないという状態を表します。条件を満たすまで触らせたくない項目や、選択肢としては表示しておきたいが今回は選べない項目に使います。無効になった入力欄はフォームの一員として扱われなくなるため、値もサーバーに送られません。
書き方はどちらも同じで、値を持たない真偽値属性として書きます。属性名を書くだけで有効になります。
<!-- 値は送りたいが、書き換えさせたくない -->
<input type="text" name="member" value="A-12345" readonly>
<!-- 今回は使わせない(値も送られない) -->
<input type="text" name="coupon" value="SUMMER2026" disabled>
ちなみに disabled="false" と書いても無効化は解除されません。真偽値属性は属性が書かれているかどうかだけで判断されるため、有効に戻したいときは属性そのものを削除します。readonly も同じです。
readonly と disabled の違いを項目ごとに比べる
実際にどこがどう違うのかを、フォームで問題になりやすい項目に絞ってまとめると次のようになります。
| 項目 | readonly | disabled |
|---|---|---|
| 値がサーバーに送信される | 送信される | 送信されない |
| キーボードでの編集 | できない | できない |
| フォーカス・Tab キーでの移動 | できる | できない |
| 値の選択とコピー | できる | 多くのブラウザーでできない |
required などの検証対象 | 対象外 | 対象外 |
| 一致する擬似クラス | :read-only | :disabled と :read-only |
| 既定の見た目 | 通常とほぼ同じ | 灰色に薄く表示される |
| JavaScript のプロパティ | el.readOnly | el.disabled |
最初の行がいちばん重要な違いです。readonly の入力欄は、見た目こそ編集できなくなっているものの、フォームの一員としてはそのまま扱われるので値が送信されます。disabled の入力欄は送信対象から外れるため、サーバー側ではその項目がそもそも存在しなかったかのように受け取ることになります。
意外と見落とされるのが検証の行です。required や pattern を付けていても、その要素に readonly または disabled が付いていると制約の検証そのものが行われません。「必須にしてあるのに空のまま送信できてしまう」という不具合の原因がこれであることは珍しくありません。
送信される値がどう変わるか確かめる
言葉で読むよりも動かしたほうが早いので、3 つの入力欄を並べたフォームで確かめてみましょう。下のデモの「送信される値を確認する」ボタンを押すと、FormData を使ってサーバーに送られる内容を書き出します。readonly の会員 ID は一覧に現れますが、disabled のクーポンコードは値が入っているのに現れません。入力欄をクリックしたりタブキーで移動したりして、フォーカスの当たり方の違いも見てみてください。
<form id="demo-form">
<p>
<label for="name">お名前</label>
<input type="text" id="name" name="name" value="山田太郎">
</p>
<p>
<label for="member">会員ID(readonly)</label>
<input type="text" id="member" name="member" value="A-12345" readonly>
</p>
<p>
<label for="coupon">クーポンコード(disabled)</label>
<input type="text" id="coupon" name="coupon" value="SUMMER2026" disabled>
</p>
<button type="submit">送信される値を確認する</button>
</form>
<pre id="result">ボタンを押すと、サーバーに送られる値が表示されます。</pre>
body {
font-family: sans-serif;
line-height: 1.8;
margin: 16px;
}
label {
display: block;
font-size: 13px;
font-weight: bold;
}
input {
width: 100%;
box-sizing: border-box;
padding: 6px 8px;
border: 1px solid #999;
border-radius: 4px;
font-size: 14px;
}
/* readonly の入力欄。disabled も :read-only に一致するので除外しておく */
input:read-only:not(:disabled) {
background: #f2f6ff;
border-color: #7aa7ff;
}
/* disabled の入力欄 */
input:disabled {
background: #eee;
color: #888;
border-style: dashed;
}
button {
margin-top: 8px;
padding: 8px 16px;
font-size: 14px;
}
#result {
margin-top: 16px;
padding: 12px;
background: #222;
color: #fff;
border-radius: 4px;
font-size: 13px;
white-space: pre-wrap;
}
const form = document.getElementById('demo-form');
const result = document.getElementById('result');
form.addEventListener('submit', (event) => {
// デモなので実際の送信は止める
event.preventDefault();
// FormData には「送信対象になる入力欄」だけが入る
const data = new FormData(form);
const lines = [];
for (const [name, value] of data.entries()) {
lines.push(name + ' = ' + value);
}
result.textContent = '送信される値\n' + lines.join('\n');
});
CSS タブでは :read-only と :disabled という 2 つの擬似クラスを使って、それぞれの状態に色を付けています。ここで input:read-only:not(:disabled) と書いているのには理由があり、あとの「スタイルを当てるときの落とし穴」で説明します。
使える要素が違う(select やチェックボックスに readonly は効かない)
もうひとつの大きな違いが、そもそも属性が使える要素の範囲です。disabled はフォーム部品のほぼ全体で使えますが、readonly は文字を打ち込む種類の入力欄にしか効きません。
| 要素・型 | readonly | disabled |
|---|---|---|
<input>(text・email・password・number・date など) | 効く | 効く |
<textarea> | 効く | 効く |
<input type="checkbox"> / <input type="radio"> | 効かない | 効く |
<input type="file"> / color / range | 効かない | 効く |
<select> | 効かない | 効く |
<option> / <optgroup> | 効かない | 効く |
<button> / <input type="submit"> | 効かない | 効く |
<fieldset> | 効かない | 効く(中身をまとめて無効化) |
「効かない」というのは、書いてもブラウザーが単に無視するという意味です。エラーにはならないので、<select readonly> と書いて動作を確かめないまま公開してしまい、あとから普通に選択できると気付く、というのがよくある事故です。
select を変更させずに値も送りたいとき
<select> を disabled にすると、当然ながら選択された値は送信されません。値は送りたいけれど変更はさせたくない、という場合は、選択肢を 1 つだけにしてしまうのが手軽な方法です。選べる先がないので実質的に固定できます。
<!-- 選択肢が 1 つだけなので変えようがない -->
<select name="plan">
<option value="standard" selected>スタンダード</option>
</select>
選択肢の一覧は見せたうえで、そのうちの一部だけを選べなくしたい場合は、<option> 側に disabled を付けます。項目としては表示されますが選択できません。
<select name="plan">
<option value="free">フリー</option>
<option value="standard" selected>スタンダード</option>
<!-- 表示はするが選ばせない -->
<option value="enterprise" disabled>エンタープライズ(お問い合わせください)</option>
</select>
チェックボックス・ラジオボタンを固定したいとき
チェックボックスやラジオボタンにも readonly は効かないため、確認画面などで状態を固定したい場合は disabled を使うことになります。ただし前述のとおり値が送信されなくなるので、チェック状態をサーバーに伝える必要があるなら、同じ内容を <input type="hidden"> で別途送ります。
<!-- 画面には「チェック済みで変更不可」として見せる -->
<label>
<input type="checkbox" checked disabled> メールマガジンを購読する
</label>
<!-- 実際に送信されるのはこちら -->
<input type="hidden" name="newsletter" value="1">
チェックボックスの name を消して hidden のほうにだけ name を付けている点に注意してください。両方に同じ name を付けても、disabled のほうは送信されないので実害はありませんが、意図を明確にするために送信しない部品からは name を外しておくと読みやすくなります。
fieldset の disabled でまとめて無効にする
disabled ならではの便利な使い方が <fieldset> との組み合わせです。<fieldset> に disabled を付けると、その中にあるフォーム部品がまとめて無効になります。入力欄ひとつひとつに属性を付けて回る必要がありません。
下のデモは「別の住所に配送する」にチェックを入れたときだけ配送先を入力させる、というよくある作りです。チェックボックスを切り替えると、テキスト入力・セレクトボックス・ボタンがいっせいに有効/無効に変わります。
<form>
<p>
<label><input type="checkbox" id="use-other"> 別の住所に配送する</label>
</p>
<!-- fieldset に disabled を付けると、中の入力欄がまとめて無効になる -->
<fieldset id="shipping" disabled>
<legend>配送先</legend>
<p>
<label for="zip">郵便番号</label>
<input type="text" id="zip" name="zip" value="100-0001">
</p>
<p>
<label for="pref">都道府県</label>
<select id="pref" name="pref">
<option>東京都</option>
<option>大阪府</option>
</select>
</p>
<p>
<button type="button">住所を検索</button>
</p>
</fieldset>
</form>
body {
font-family: sans-serif;
line-height: 1.8;
margin: 16px;
}
fieldset {
border: 1px solid #999;
border-radius: 4px;
padding: 8px 16px;
}
legend {
font-weight: bold;
padding: 0 4px;
}
label {
display: block;
font-size: 13px;
}
input[type="text"],
select {
padding: 6px 8px;
border: 1px solid #999;
border-radius: 4px;
font-size: 14px;
}
/* fieldset 自身が無効なときの見た目をまとめて変えられる */
fieldset:disabled {
background: #f5f5f5;
border-style: dashed;
}
fieldset:disabled legend {
color: #888;
}
const toggle = document.getElementById('use-other');
const shipping = document.getElementById('shipping');
toggle.addEventListener('change', () => {
// disabled はプロパティで切り替える(属性の付け外しをしなくてよい)
shipping.disabled = !toggle.checked;
});
この方法なら、あとから <fieldset> の中に入力欄を追加しても、JavaScript を書き足す必要がありません。無効化の対象が「この枠の中」という形で HTML に表れているので、後から読む人にも意図が伝わります。
最初の legend の中だけは無効にならない
ひとつ知っておきたい仕様として、<fieldset disabled> でも最初の <legend> の中にあるフォーム部品は無効になりません。これは「グループ全体を有効にするかどうかのスイッチを見出し部分に置く」という使い方ができるように決められたものです。
<fieldset disabled>
<legend>
<!-- 最初の legend の中なので、これだけは操作できる -->
<label><input type="checkbox" name="use-other"> 配送先を指定する</label>
</legend>
<!-- ここから下はまとめて無効 -->
<input type="text" name="zip">
</fieldset>
JavaScript から切り替える
状態を動的に変えたい場合は、属性を付け外しするよりもプロパティに真偽値を代入するほうが簡単です。disabled はそのまま disabled プロパティですが、readonly のプロパティ名は readOnly と途中の O が大文字になる点に注意してください。ここを小文字で書いてしまい、動かないと悩むのはよくあるつまずきです。
const input = document.getElementById('member');
// 編集不可にする(値は送信される)
input.readOnly = true;
// 編集できるように戻す
input.readOnly = false;
const coupon = document.getElementById('coupon');
// 無効にする(値は送信されなくなる)
coupon.disabled = true;
// 現在の状態は読み取ることもできる
console.log(coupon.disabled); // true
setAttribute() と removeAttribute() でも同じことができますが、解除するときに setAttribute('disabled', 'false') と書いてしまう間違いが起きやすいので、プロパティを使うほうが安全です。真偽値属性は値の中身を見ないため、'false' を設定しても無効のままです。
// これは解除にならない(無効のまま)
coupon.setAttribute('disabled', 'false');
// 属性で操作するなら、削除する
coupon.removeAttribute('disabled');
なお readonly はあくまで利用者の入力を防ぐだけで、JavaScript から input.value = '…' のように値を書き換えることは問題なくできます。計算結果を表示する欄などに向いています。
disabled にしたせいで値が届かないとき
実装中にいちばん多いトラブルが、「画面には値が表示されているのに、サーバー側で受け取れない」というものです。ほとんどの場合、原因は編集させないつもりで disabled を付けたことにあります。
値を送りたいなら readonly に置き換える
対象が <input type="text"> や <textarea> であれば、いちばん素直な解決は disabled を readonly に書き換えることです。編集できない見た目は保ったまま、値は送信されるようになります。「編集させたくないだけなのか、その項目自体を無かったことにしたいのか」を考えて選び直してください。
readonly が使えない部品は hidden で補う
セレクトボックスやチェックボックスのように readonly が効かない部品では、先ほどのチェックボックスの例と同じように、<input type="hidden"> で同じ値を並べて送ります。表示のための部品と、送信のための部品を分けて考える、という発想です。
<!-- 表示用(送信されない) -->
<select disabled>
<option selected>スタンダード</option>
</select>
<!-- 送信用 -->
<input type="hidden" name="plan" value="standard">
どちらもサーバー側の検証の代わりにはならない
忘れてはいけないのが、readonly も disabled もあくまでブラウザー上での制限にすぎないという点です。開発者ツールを開いて属性を消せば誰でも編集できますし、送信内容そのものを書き換えることもできます。金額や権限のように改ざんされて困る値は、必ずサーバー側で「送られてきた値が正しいか」を検証してください。hidden で送る値も同じで、隠れているだけで中身は誰にでも見えます。
スタイルを当てるときの落とし穴
状態に応じて見た目を変えるための擬似クラスは用意されていますが、両者の関係が直感と少しずれているため、思ったところに色が付かないことがあります。
:read-only は disabled の入力欄にも一致する
:read-only は「readonly が付いている要素」を選ぶ擬似クラスだと思われがちですが、実際には利用者が編集できない要素を選びます。disabled の入力欄も編集できないので、こちらにも一致してしまいます。デモの CSS で input:read-only:not(:disabled) と書いたのはこのためで、こうしないと readonly 用に用意した配色が disabled の欄にも当たってしまいます。
/* readonly の欄だけを狙う */
input:read-only:not(:disabled) {
background: #f2f6ff;
}
/* disabled の欄 */
input:disabled {
background: #eee;
}
さらに紛らわしいのが、<p> や <div> のような編集できない普通の要素も :read-only に一致するという点です。:read-only { … } のように要素を絞らずに書くとページ全体に当たってしまうので、input や textarea を必ず前に付けてください。
操作できない理由が伝わるようにする
disabled の要素はフォーカスを受け取らないため、キーボードやスクリーンリーダーで順に読み進めている利用者にはその項目の存在自体が伝わらないことがあります。ボタンを無効にして「なぜ押せないのか分からない」という状態は起きやすいので、条件を満たしていない理由を近くのテキストで示しておくと親切です。
読み飛ばされては困る場面では、disabled の代わりに aria-disabled="true" を付けて「無効であることを伝えつつフォーカスは当たる」状態にする方法もあります。ただし aria-disabled は伝えるだけで、実際の操作は止めません。押されたときの処理を JavaScript 側で無視する実装が別途必要になるので、まずは素直に disabled を使い、必要になったときに検討するとよいでしょう。
まとめ
readonly と disabled は、どちらも入力欄を編集できなくしますが、選ぶ基準はその値をサーバーに送りたいかどうかです。値を送りたいなら readonly、その項目自体を無効にしたいなら disabled を使います。readonly はフォーカスやコピーができ、disabled はフォーカスも受け取らず灰色に表示されます。どちらも required などの検証対象から外れるので、必須チェックが働かなくなる点には注意してください。使える範囲も違い、readonly が効くのは文字を入力する <input> と <textarea> だけで、<select> やチェックボックス、ボタンには効きません。逆に disabled は <fieldset> にも付けられ、中のフォーム部品をまとめて無効にできます(最初の <legend> の中を除く)。JavaScript から切り替えるときは el.disabled と el.readOnly というプロパティを使い、disabled にしたせいで値が消えてしまう場面では <input type="hidden"> で補います。そして、どちらの属性もブラウザー上の制限でしかないため、大事な値は必ずサーバー側で検証する、という原則を忘れないようにしてください。