CSSで、fieldsetとlegendを使って入力欄を枠で囲む方法

​​CSS​HTML,Playwright,フォーム

入力欄のまとまりを <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つの枠にまとめる例です。

html
<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> の文字がそのままグループの名前になっていました。

text
- 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 は省いています。

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 に広げると、文字と枠線のあいだが空きます。

2つのフォームを縦に並べた画面。上は「CSS を書かないとき」で、灰色の細い枠の左上に「配送方法」の文字が載り、ラジオボタン3つが横に並んでいる。その下の枠は、左上に「ギフト包装にする」のチェックボックスが載り、中の「のし紙の名前」と「メッセージ」の欄が薄い灰色になっている。下は「枠を水色の角丸にしたとき」で、水色の角丸の枠の上に太字の「配送方法」が載り、ラジオボタン3つが縦に並んでいる。その下の枠は灰色の角丸で、中のラベルと欄が薄く表示されている
同じフォームの、CSS を書かないときと、角丸の枠にしたときの比較。下の枠はどちらも disabled の状態。文字の大きさとギフト包装の欄の並びは、どちらにも同じ CSS をあてている

<fieldset> には display: flex も使えます。MDN の <fieldset> のページには、<fieldset> を display: flex にすると、中身を入れる無名のボックスがフレックス整形コンテキストになる、と書かれています。<legend> は、この無名のボックスには入りません。

検証環境で位置を測ると、ラジオボタンのラベルどうしのあいだは gap のとおり 8px でした。<legend> の下端から最初のラベルまでは 12px で、padding-top の値と同じです。gap の 8px は足されていませんでした。<legend> はフレックスアイテムとして並ばず、枠線の上に残ります。

disabled で、中の入力欄をまとめて無効にする

<fieldset> に disabled を付けると、中の入力欄がすべて無効になります。HTML の仕様では、最初の <legend> の中にある入力欄だけは、無効になりません。

これを使うと、「ギフト包装にする」にチェックを入れたときだけ、包装の欄に入力できるフォームを作れます。チェックボックスを <legend> の中に置きます。

html
<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 を切り替えます。

js
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> が横にスクロールできる状態になりました。

css
fieldset {
  min-inline-size: 0;
}
2つの画面を縦に並べたもの。灰色の点線が幅 320px の箱を表している。上の「そのまま(min-inline-size: min-content)」では、「実行するコマンド」の枠が点線の箱を越えて右の端まで伸び、コマンドの文字も点線の箱の外まで続いている。下の「min-inline-size: 0 を付けたとき」では、枠が点線の箱の中に収まり、コマンドの文字は枠の右端で切れている
点線は幅 320px の箱。min-inline-size: 0 を付けると、fieldset が箱の中に収まる

MDN の min-inline-size のページにあるとおり、min-inline-size は書字方向によって min-width か min-height に対応します。横書きのページでは min-width に当たります。

legend を枠線の上に載せたくないとき

<legend> を枠線の上ではなく、枠の内側に置きたいときは、<legend> に float を付けます。HTML の仕様では、枠線の上に置かれるのは、float が none で、position が absolute でも fixed でもない、最初の <legend> だけです。

css
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 を付けたあとも、グループの名前は「配送方法」のままでした。

枠そのものが要らず、グループとしての意味だけを使いたいときは、既定のスタイルを消します。

css
fieldset {
  border: 0;
  padding: 0;
  margin: 0;
  min-inline-size: 0;
}

legend {
  padding: 0;
}

検証環境では、<legend> は <fieldset> の上端に置かれ、その下に、すき間なく最初のラベルが続きました。見た目はただの見出しと入力欄ですが、<fieldset> と <legend> のまとまりは残ります。

​Posted by 86