CSSで、入力欄のラベルを上に浮かせる方法
入力欄の中にラベルを重ねておき、フォーカスしたときと文字が入っているときに、ラベルを欄の上のほうへ小さく動かす方法です。JavaScript は使いません。placeholder に空白を1文字だけ入れておき、:placeholder-shown で「欄が空かどうか」を見分けるのがポイントです。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、ラベルの位置や色を測りました。Firefox と Safari、スマートフォンの実機、ブラウザの自動入力(オートフィル)、スクリーンリーダーでの読み上げは試していません。

HTML は、入力欄のあとにラベルを書く
<div class="field">
<input id="email" type="email" name="email" placeholder=" ">
<label for="email">メールアドレス</label>
</div>
ラベルを入力欄のあとに書くのは、CSS の次兄弟結合子(+)で、入力欄の状態からラベルを選ぶためです。+ は、同じ親の中で直後にある要素にだけ一致します。ラベルを前に書きたいときの書き方は、後の節で紹介します。
placeholder=" " は、空白を1文字入れた placeholder です。画面には何も見えませんが、これがないと次の節の CSS が働きません。
CSS で、空でないときにラベルを上へ動かす
.field {
position: relative;
}
.field input {
box-sizing: border-box;
width: 100%;
padding: 22px 12px 6px;
font: inherit;
font-size: 16px;
border: 1px solid #7a868c;
border-radius: 6px;
}
.field input:focus {
outline: 2px solid #067a98;
outline-offset: -1px;
border-color: #067a98;
}
.field label {
position: absolute;
left: 13px;
top: 15px;
font-size: 16px;
line-height: 1.2;
color: #5b6870;
pointer-events: none;
transform-origin: left top;
transition: transform 0.15s ease, color 0.15s ease;
}
/* フォーカスしたとき、または空でないとき */
.field input:focus + label,
.field input:not(:placeholder-shown) + label {
transform: translateY(-9px) scale(0.75);
color: #067a98;
}
@media (prefers-reduced-motion: reduce) {
.field label {
transition: none;
}
}
:placeholder-shown は、placeholder の文字列が表示されている <input> や <textarea> に一致する擬似クラスです。placeholder が表示されるのは欄が空のときなので、:not(:placeholder-shown) と書くと「何か入っている欄」を選べます。これに :focus を並べ、どちらかに当てはまればラベルを動かします。
ラベルは position: absolute で欄の中に重ね、浮かせるときは transform で上へ9px動かして75%に縮めます。transform-origin: left top にしているので、縮めても左端の位置は変わりません(測ると、どの状態でも入力欄の左端から13pxでした)。入力欄の上の余白(padding-top: 22px)を広めにして、文字が入る場所を下に寄せ、浮かせたラベルとぶつからないようにしています。
幅420pxの画面で、入力欄の上端からの位置を測りました。
| 状態 | ラベルの上端 | ラベルの下端 | ラベルの高さ |
|---|---|---|---|
| 空のとき | 15px | 34.19px | 19.19px |
| フォーカスしたとき(まだ空) | 6px | 20.39px | 14.39px |
| 入力したあと | 6px | 20.39px | 14.39px |
| Tab キーで欄から外したあと(値あり) | 6px | 20.39px | 14.39px |
| 値を消して欄から外したあと | 15px | 34.19px | 19.19px |
入力した文字が始まる高さ(枠と上の余白の下)は23pxです。浮かせたラベルの下端は20.39pxなので、入力した文字とは重なりません。

JavaScript で value を書き換えたときも、ラベルは上に動きました(input イベントは起こしていません)。form.reset() で空に戻すと、ラベルも欄の中に戻りました。
placeholder を書かないと、空でもラベルが上に行ったままになる
placeholder 属性を書かずに同じ CSS を当てると、空の欄でもラベルは上に浮いたままでした。測ると、ラベルの上端は6pxで、:placeholder-shown には一致していませんでした。placeholder がなければ表示される文字列もないので、:not(:placeholder-shown) がいつも当てはまります。
空白1文字にしているのは、:placeholder-shown を働かせながら、画面には何も表示しないためです。
placeholder 属性の説明には、placeholder を <label> の代わりに使ってはいけないとあります。スクリーンリーダーは、既定では placeholder の内容を読み上げず、ラベルの内容を読み上げるためです。この記事の形は、見た目は欄の中に文字があるだけですが、中身は本物の <label> です。Playwright でアクセシビリティツリーを見ると、入力欄の名前は「メールアドレス」になっていて、getByLabel('メールアドレス') でこの入力欄が見つかりました。
ラベルに pointer-events: none を付けておく
ラベルは入力欄の上に重なっているので、pointer-events を指定しなければ、ラベルの文字の上をクリックしたときにクリックを受けるのはラベルです。<label for> の働きで、ラベルをクリックしても入力欄にフォーカスが移ります。実際に、pointer-events: none のあるなしのどちらでも、ラベルの文字の上をクリックすると入力欄にフォーカスが移りました。
違いが出たのは、マウスカーソルとダブルクリックです。
| ラベルの文字の上で | pointer-events: none あり |
なし |
|---|---|---|
| いちばん手前の要素 | input |
label |
| マウスカーソル | text(文字入力の形) |
default(矢印) |
| クリックしたあとのフォーカス | 入力欄 | 入力欄 |
| ダブルクリックしたあとのフォーカス | 入力欄 | 外れる(body) |
| ダブルクリックで選ばれた文字 | なし | ラベルの一部 |

pointer-events: none を付けると、ラベルはポインターイベントの対象になりません。ラベルの文字の上でも、マウスの操作はその下の入力欄が受けます。付けないと、ダブルクリックでラベルの文字が選ばれ、フォーカスも入力欄から外れ、ラベルも欄の中のままでした。
ラベルを前に書きたいときは :has() を使う
HTML の都合でラベルを入力欄より前に書くときは、+ では選べません。:has() を使うと、親の .field から「中の入力欄がフォーカスされているか、空でないか」を見て、ラベルを選べます。
<div class="field">
<label for="has-in">ラベルが先</label>
<input id="has-in" type="text" name="has" placeholder=" ">
</div>
.field:has(input:focus, input:not(:placeholder-shown)) label {
transform: translateY(-9px) scale(0.75);
color: #067a98;
}
ラベルは position: absolute で重ねているので、HTML の順番を変えても見た目の位置は同じです。この形でも、空のときは上端15px、フォーカスしたときと、入力して欄から外したあとは上端6pxで、ラベルをあとに書いた形と同じ位置になりました。
動きを減らす設定では、すぐに切り替える
ラベルは transition で0.15秒かけて動かしています。端末で動きを減らす設定を有効にしている人には、prefers-reduced-motion の reduce が当てはまります。最初の CSS の最後で、そのときは transition: none にしています。
Playwright で動きを減らす設定にして試すと、transition-duration は 0s になり、フォーカスして50ミリ秒後には、ラベルはもう浮かせた位置(matrix(0.75, 0, 0, 0.75, 0, -9))にありました。設定しない場合は、50ミリ秒後のラベルはまだ動いている途中で、75%まで縮みきっていませんでした。
色の選び方
浮かせたラベルは、16pxを75%に縮めた12pxの文字になります。小さい文字は、白い背景とのコントラスト比が4.5:1以上ほしいので、色を濃い水色の #067a98 にしました。WCAG のコントラスト比の式で計算すると4.95です。欄の中にあるときの灰色 #5b6870 は5.74でした。
入力欄の枠も、どこに欄があるかを示すものなので、非テキストのコントラストの3:1以上を目安にしました。枠の #7a868c は3.74です。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べました。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
:placeholder-shown |
47 | 51 | 9 |
:not() |
1 | 1 | 3.1 |
+(次兄弟結合子) |
1 | 1 | 1 |
:has() |
105 | 121 | 15.4 |
transition |
26 | 16 | 9 |
transform |
36 | 16 | 9 |
pointer-events |
1 | 1.5 | 4 |
prefers-reduced-motion |
74 | 63 | 10.1 |
ラベルを入力欄のあとに書く形では、動きを減らす設定(prefers-reduced-motion)を除くと、:placeholder-shown がいちばん新しく、Safari は9からです。ラベルを前に書いて :has() を使う形は、Firefox が121からになります。





