CSSだけでtextFieldのスタイルを作る(:has()疑似クラス)
初めに
初めまして。記事初投稿です。
最近:has()
疑似クラスを知り幅が広がったので、JavaScriptを使わずCSSだけで何か作ってみようと思いました。
題材はMaterial Designのtext field。
↓完成品
:has()
疑似クラスの概要
実装の前に今回のケースにおける:has()
疑似クラスを簡単に紹介しておきます。
:has() は CSS の擬似クラスで、引数として渡されたセレクターに (指定された要素の :scope の相対で) 該当する要素が一つ以上の要素に一致することを表します。
引用:mdn web docs
一言で言うと引数に渡した子要素を参照できる疑似クラスです。
:has()
は色々なことができるので全ては解説しません。今回使用する使い方は子要素に発生したイベントを検知して親のスタイルを変更するという使用例を紹介したいと思います。
従来の親要素へのスタイル適応
これまでもトリガーとなる要素の子のスタイルを変更することはできました。しかし親のスタイルの変更にはJavaScriptでのクラスの付け替えが必要でした。(他に実装方法があったらごめんなさい)
:has()
の登場によってJavaScriptを使わなくてもCSSだけで親のスタイルの変更が可能になりました。
ブラウザのサポートも整ってきたので一時期話題になってましたね。
使用例
以下のような親と子があるとします。
<div class="parent">
<div class="child">child</div>
</div>
.child
をホバーしたときに.parent
の色を変えたいです。
通常ならJavaScriptでクラスの付け替えを行いますがCSSのみで行うとこうなります。
.parent:has(> .child:hover) {
background: #f00;
}
これで.child
ホバー時に.parent
の背景色を変更できます。
要素 | 意味 |
---|---|
> |
直接の子要素を参照 |
.child:hover |
「.child をホバーした時」の条件 |
> .child:hover |
直接の子である.child をホバーした時 |
:has(){} |
スタイルをあてる要素:has(子の条件){スタイル} となる。 |
ここでいう:has()
の特徴
-
()
内はスタイルをあてたい要素の子、孫の条件(直接の子、ホバー時、n番目の子要素など)を参照する -
()
の引数に要素だけを渡した場合、条件は「要素があるかどうか」となる。 -
{}
内のスタイルは:has
の前の要素に適応される。
つまり.parent:has(> .child:hover){}
を日本語に訳すとこうなる。
.parent
の直接の子である.child
をホバーしたとき.parent
に{}
内のスタイルを適応
また引数はデフォルトで条件に合う子要素を参照するので>
を省略して下記の書き方もできる。
-.parent:has(> .child:hover) {
/* .parent`の子である`.child`をホバーしたとき`.parent`に`{}`内のスタイルを適応 */
+.parent:has(.child:hover) {
background: #f00;
}
text fieldの実装
それでは本題、Material Designのtext fieldのoutlineタイプ(アイコン無し)をJS無しのSCSSで作ってみます。(スタイルだけ)
HTML
なるべくシンプルに以下のHTMLでいきます。
<div class="text-field">
<label for="name">name</label>
<input type="text" id="name">
</div>
デフォルトはこんな感じ
スタイルを整える
動きをつける前に見た目を整えます。
一応Material Design>text field(outlineアイコン無し)のサイズにのっとってみます。
.text-field{
display: flex;
align-items: center;
/* height56pxにするためborder幅を除く */
height: calc(56px - 2px);
width: 25ch;
border: 1px solid rgba(0, 0, 0, 0.23);
border-radius: 4px;
padding: 0 16px;
position: relative;
label{
position: absolute;
padding: 0 4px;
color: rgba(0, 0, 0, 0.6);
background: #fff;
}
input{
/* ここから */
border: none;
outline: none;
/* ここまでデフォルトスタイル削除 */
height: 1.4em;
width: 100%;
}
}
サイズの要件
レイアウト属性 | 値 |
---|---|
対象サイズ | 56dp |
左右のpadding | 16dp |
ラベルの配置 | 垂直方向に中央揃え |
ラベルのpadding | 4dp |
引用: Material Design3
ここではdp
→ px
とします。
スタイル適応後はこんな感じ
フォーカス時のスタイル適応
ここから動きをつけていきます。
.text-field{
display: flex;
align-items: center;
height: calc(56px - 2px);
width: 25ch;
border: 1px solid rgba(0, 0, 0, 0.23);
border-radius: 4px;
padding: 0 16px;
position: relative;
+ &:hover{
+ border: 1px solid #000;
+ }
+ &:has(input:focus){
+ border: 2px solid #1976d2;
+ label {
+ color: #1976d2;
+ transform: translate(0, -28px) scale(.75);
+ }
+ }
label{
position: absolute;
padding: 0 4px;
color: rgba(0, 0, 0, 0.6);
background: #fff;
+ transition: .2s;
}
input{
border: none;
outline: none;
height: 1.4em;
width: 100%;
}
}
追加した動きはこんな感じ
-
.text-field
ホバー時-
border
の色を濃くする
-
-
input
のfocus
時-
.text-field
のボーダーを青にする -
label
の色を青、配置をずらす、サイズ縮小
-
:has()
でやっていることを簡単に解説すると、まずHTMLが下記のようになっています。
-
.text-field
にスタイルをあてたいので.text-field
内に.text-field{&:has(){}}
となる。
.text-field{
&:has(){
}
}
/*css変換後*/
.text-field:has(){
}
- 子要素である
input
のfocus
を検知したいので()
の引数にinput:focus
を入れる
.text-field{
&:has(input:focus){
}
}
/*css変換後*/
.text-field:has(input:focus){
}
- focus時にラベルも変化させたいので
:has(){}
内にlabelを指定。あとは変化後のスタイルを記述する
.text-field{
/*.text-fieldのスタイルの記述*/
&:has(input:focus){
/*focus時の.text-fieldのスタイルの記述*/
label {
/*focus時のlabelのスタイルの記述*/
}
}
}
/*css変換後*/
.text-field:has(input:focus){
}
.text-field:has(input:focus) label{
}
ひとまずこれで完成。
おまけ(CSSで空文字判定)
現状、文字入力後に他の部分をクリックすると変化前のスタイルに戻るという問題があります。:placeholder-shown
疑似クラスを使ってから判定もCSSだけで完結することができます。
:placeholder-shown
の概要
今回のケースにおける:placeholder-shown
疑似クラスを簡単に紹介しておきます。
:placeholder-shown は CSS の擬似クラスで、プレイスホルダー文字列が表示されている <input> または <textarea> 要素を表します。
引用:mdn web docs
:placeholder-shown
はHTMLのplaceholder
属性が表示されているか否かを判定することができます。
placeholder
の表示、非表示は以下の挙動をします。
input の状態 |
placeholder の状態 |
---|---|
空 | 表示 |
入力中 | 非表示 |
つまり実装の流れを簡潔に言うと、
:placeholder-shownで入力判定を行い、スタイルを変える。
だけです。
実装
それではやっていきます。
HTMLの準備
:placeholder-shown
で判定をするにはplaceholder
属性を指定する必要があります。不要なテキストは表示したくないので先ほどのHTMLに、placeholder=" "
で空文字列を指定しておきます。
<div class="text-field">
<label for="name">name</label>
- <input type="text" id="name">
+ <input type="text" id="name" placeholder=" ">
</div>
スタイルの適応
次にCSSの指定です。
:placeholder-shown{}
の中は空の場合(placeholder表示中)のスタイルとなります。
input:placeholder-shown{
/* inputが空の場合のスタイル */
}
なので:not()
疑似クラスで反転する必要があります。
すると入力中のスタイル指定になります。
:not(input:placeholder-shown){
/* inputが入力中のスタイル */
}
後は先ほどのscssに追加します。
.text-field{
display: flex;
align-items: center;
height: calc(56px - 2px);
width: 25ch;
border: 1px solid rgba(0, 0, 0, 0.23);
border-radius: 4px;
padding: 0 16px;
position: relative;
&:hover{
border: 1px solid #000;
}
&:has(input:focus){
border: 2px solid #1976d2;
label {
color: #1976d2;
transform: translate(0, -28px) scale(.75);
}
}
+ &:not(:has(input:placeholder-shown)){
+ border: 2px solid #1976d2;
+ label {
+ color: #1976d2;
+ transform: translate(0, -28px) scale(.75);
+ }
+ }
label{
position: absolute;
padding: 0 4px;
color: rgba(0, 0, 0, 0.6);
background: #fff;
transition: .2s;
}
input{
border: none;
outline: none;
height: 1.4em;
width: 100%;
}
}
&:not(:has(input:placeholder-shown)){}
を日本語訳すると
.text-field
の子要素であるinput
が入力中の時、{}
のスタイルを適応
といったところですかね。
さらにおまけ
この記事ではわかりやすくに伝えるため簡潔な作りにしていますが、MUIの構造はより複雑な作りになっていたのでクラス名を弄ってHMTLだけ紹介します。
<div class="text-field">
<label></label>
<div class="text-field__input-container">
<input/>
<fieldset>
<legend>
<span></span>
</legend>
</fieldset>
</div>
</div>
軽く見ただけですが、それぞれの役割は下記です。(多分)
要素 | 役割 |
---|---|
.text-field |
textFieldのコンテナ |
label |
ラベル |
input |
入力欄 |
fieldset |
アウトライン |
legend |
fieldset のタイトル位置にspan を持ってくる |
span |
focus 時のlabel 位置のアウトラインを消す |
もちろんSEOへの適切なマークアップも含んでいると思います。
MUIは人に使ってもらいやすくしているのでこのようになってますが、自作するなら下記くらいでいいかなと思います。
<div class="text-field">
<label></label>
<div class="text-field__outline">
<input/>
</div>
</div>
要素 | 役割 |
---|---|
.text-field |
textFieldのコンテナ |
label |
ラベル |
.text-field__outline |
アウトライン |
input |
入力欄 |
まとめ
Reactなんかを使っているとinput
周りのコンポーネントはstateや関数が多く、煩雑になりがちなのでアニメーションをCSSで完結できるのは読みやすくていいですね。
最後まで読んで頂きありがとうございました。
Discussion