CSSで、fieldsetとlegendを使って入力欄を枠で囲む方法
入力欄のまとまりを <fieldset> で囲み、最初の子に <legend> を置くと、<legend> の文字が枠線の上に載った枠ができます。CSS を書かなくても枠は出ますが、border と border-radius を書き換えれば、角丸の枠にもできます。<fieldset> に disabled を付けると、中の入力欄をまとめて無効にできます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、getComputedStyle() と getBoundingClientRect() の値、操作の結果を記録しました。Firefox と Safari、スマートフォンのブラウザー、スクリーンリーダーでの読み上げは試していません。
HTML は fieldset の最初の子に legend を置く
配送方法を選ぶラジオボタンを、1つの枠にまとめる例です。
<fieldset id="ship">
<legend>配送方法</legend>
<label><input type="radio" name="ship" value="normal" checked> 通常配送</label>
<label><input type="radio" name="ship" value="express"> お急ぎ便</label>
<label><input type="radio" name="ship" value="store"> 店舗で受け取る</label>
</fieldset>
HTML の仕様では、<fieldset> の中身は「<legend> を1つ(省略してよい)、そのあとにフローコンテンツ(ほかの要素や文字)」と決まっています。まとまりの見出しになるのは、<fieldset> の最初の子の <legend> です。
MDN の <fieldset> のページには、<fieldset> の暗黙の ARIA ロールは group と書かれています。検証環境で Playwright のアクセシビリティツリーを出すと、<legend> の文字がそのままグループの名前になっていました。
- group "配送方法":
- text: 配送方法
- radio "通常配送" [checked]
- text: 通常配送
- radio "お急ぎ便"
- text: お急ぎ便
- radio "店舗で受け取る"
- text: 店舗で受け取る
見た目の枠だけでなく、「この3つのラジオボタンは配送方法の選択肢」というまとまりも、アクセシビリティツリーに入ります。
CSS を書かないときの枠
<fieldset> と <legend> に CSS を書かないページを検証環境の Chromium で開き、計算値を取り出しました。文字の大きさは 16px です。
| プロパティ | <fieldset> の値 |
|---|---|
border |
2px groove |
padding |
上 5.6px、左右 12px、下 10px |
margin |
左右 2px |
min-inline-size |
min-content |
HTML の仕様の表示の節にある、ブラウザーが持つ既定のスタイルと同じ値です(padding-block: 0.35em 0.625em と padding-inline: 0.75em を、文字の大きさ 16px で計算した値)。<legend> には左右 2px の padding があり、この分だけ枠線が文字から離れます。
仕様では、<legend> と重なる部分には枠線を描かない、と決められています。検証環境の画面でも、<legend> は枠の左上に置かれ、<legend> のところだけ枠線が切れて、文字が枠線の上に載ったように見えました(次の節の画像の上半分)。
枠を角丸にして、項目を縦に並べる
既定の枠を、水色の角丸の枠に変えます。CSS は次のとおりです。文字の大きさなど、見た目だけの CSS は省いています。
fieldset {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0 0 16px;
padding: 12px 16px 16px;
border: 2px solid #00c8e8;
border-radius: 8px;
}
legend {
padding: 0 8px;
color: #067a98;
font-weight: bold;
}
fieldset:disabled {
border-color: #9aa5ab;
}
fieldset:disabled > label {
opacity: 0.5;
}
border を書き換えても、<legend> は枠線の上に載ったままです。border-radius で角を丸めても、<legend> のところで枠線が切れます。<legend> の左右の padding を 8px に広げると、文字と枠線のあいだが空きます。

<fieldset> には display: flex も使えます。MDN の <fieldset> のページには、<fieldset> を display: flex にすると、中身を入れる無名のボックスがフレックス整形コンテキストになる、と書かれています。<legend> は、この無名のボックスには入りません。
検証環境で位置を測ると、ラジオボタンのラベルどうしのあいだは gap のとおり 8px でした。<legend> の下端から最初のラベルまでは 12px で、padding-top の値と同じです。gap の 8px は足されていませんでした。<legend> はフレックスアイテムとして並ばず、枠線の上に残ります。
disabled で、中の入力欄をまとめて無効にする
<fieldset> に disabled を付けると、中の入力欄がすべて無効になります。HTML の仕様では、最初の <legend> の中にある入力欄だけは、無効になりません。
これを使うと、「ギフト包装にする」にチェックを入れたときだけ、包装の欄に入力できるフォームを作れます。チェックボックスを <legend> の中に置きます。
<fieldset id="gift" disabled>
<legend>
<label><input type="checkbox" id="use-gift"> ギフト包装にする</label>
</legend>
<label>のし紙の名前 <input name="giftName" value="山田"></label>
<label>メッセージ <textarea name="giftMessage">いつもありがとう</textarea></label>
</fieldset>
チェックボックスが変わったときに、<fieldset> の disabled を切り替えます。
const gift = document.getElementById('gift');
document.getElementById('use-gift').addEventListener('change', (e) => {
gift.disabled = !e.target.checked;
});
検証環境で、ページを開いた直後とチェックを入れた後の状態を記録しました。
| 確かめたこと | 開いた直後 | チェックを入れた後 |
|---|---|---|
<legend> の中のチェックボックス |
有効 | 有効 |
| のし紙の名前とメッセージ | 無効(:disabled に当てはまる) |
有効 |
FormData に入る名前 |
ship だけ |
ship、giftName、giftMessage |
無効のあいだは、のし紙の名前の欄をマウスでクリックしてもフォーカスが入りませんでした。チェックボックスから Tab キーを押すと、無効の2つの欄を飛ばして「注文する」ボタンに移りました。チェックを入れた後は、のし紙の名前に「佐藤」と入力できました。
入力欄そのものには、disabled 属性は付いていません。<fieldset> の disabled だけで、中の欄が :disabled に当てはまります。見た目は fieldset:disabled で切り替えられます。先ほどの CSS では、無効のあいだは枠を灰色にし、fieldset:disabled > label でラベルを半透明(opacity: 0.5)にしています。<legend> の中のラベルは <fieldset> の直接の子ではないので、チェックボックスの文字は半透明になりません。検証環境で計算値を見ると、そのとおりになっていました。
細い画面で枠がはみ出すときは min-inline-size: 0 を付ける
<fieldset> には、既定で min-inline-size: min-content が付いています。中に折り返さない長い行があると、<fieldset> はその幅より細くなりません。
検証環境で、幅 320px の箱の中に <fieldset> を置き、overflow-x: auto を付けた長い1行の <pre> を入れました。<fieldset> の幅は 663.77px になり、箱から右にはみ出しました。<pre> も横にスクロールできる状態にはなりませんでした。同じ <pre> を <div> で囲んだときは、<div> は 320px に収まり、<pre> は横にスクロールできる状態になりました。
<fieldset> に min-inline-size: 0 を付けると、幅は 316px(箱の 320px から左右の margin 2px ずつを引いた幅)に収まり、<pre> が横にスクロールできる状態になりました。
fieldset {
min-inline-size: 0;
}

MDN の min-inline-size のページにあるとおり、min-inline-size は書字方向によって min-width か min-height に対応します。横書きのページでは min-width に当たります。
legend を枠線の上に載せたくないとき
<legend> を枠線の上ではなく、枠の内側に置きたいときは、<legend> に float を付けます。HTML の仕様では、枠線の上に置かれるのは、float が none で、position が absolute でも fixed でもない、最初の <legend> だけです。
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 8px;
}
legend + * {
clear: both;
}
<legend> に float: left と width: 100% を指定し、次の要素に clear: both を付けて、<legend> の下から並ぶようにしています。
検証環境で、border を 2px、padding の上を 12px にした <fieldset> に付けると、<legend> の上端は <fieldset> の上端から 14px の位置になりました。枠線と padding の内側です。アクセシビリティツリーでは、float を付けたあとも、グループの名前は「配送方法」のままでした。
枠そのものが要らず、グループとしての意味だけを使いたいときは、既定のスタイルを消します。
fieldset {
border: 0;
padding: 0;
margin: 0;
min-inline-size: 0;
}
legend {
padding: 0;
}
検証環境では、<legend> は <fieldset> の上端に置かれ、その下に、すき間なく最初のラベルが続きました。見た目はただの見出しと入力欄ですが、<fieldset> と <legend> のまとまりは残ります。





