📝

CSSだけでtextFieldのスタイルを作る(:has()疑似クラス)

2023/02/03に公開約8,500字

初めに

初めまして。記事初投稿です。

最近:has()疑似クラスを知り幅が広がったので、JavaScriptを使わずCSSだけで何か作ってみようと思いました。
題材はMaterial Designtext field

↓完成品
making finished products

:has()疑似クラスの概要

実装の前に今回のケースにおける:has()疑似クラスを簡単に紹介しておきます。

:has() は CSS の擬似クラスで、引数として渡されたセレクターに (指定された要素の :scope の相対で) 該当する要素が一つ以上の要素に一致することを表します。
引用:mdn web docs

一言で言うと引数に渡した子要素を参照できる疑似クラスです。

:has()は色々なことができるので全ては解説しません。今回使用する使い方は子要素に発生したイベントを検知して親のスタイルを変更するという使用例を紹介したいと思います。

従来の親要素へのスタイル適応

これまでもトリガーとなる要素の子のスタイルを変更することはできました。しかし親のスタイルの変更にはJavaScriptでのクラスの付け替えが必要でした。(他に実装方法があったらごめんなさい)

:has()の登場によってJavaScriptを使わなくてもCSSだけで親のスタイルの変更が可能になりました。
ブラウザのサポートも整ってきたので一時期話題になってましたね。

使用例

以下のような親と子があるとします。

HTML
<div class="parent">
    <div class="child">child</div>
</div>

.childをホバーしたときに.parentの色を変えたいです。
通常ならJavaScriptでクラスの付け替えを行いますがCSSのみで行うとこうなります。

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{}内のスタイルを適応

また引数はデフォルトで条件に合う子要素を参照するので>を省略して下記の書き方もできる。

CSS
-.parent:has(> .child:hover) {
    /* .parent`の子である`.child`をホバーしたとき`.parent`に`{}`内のスタイルを適応 */
+.parent:has(.child:hover) {
    background: #f00;
}

text fieldの実装

それでは本題、Material Designのtext fieldのoutlineタイプ(アイコン無し)をJS無しのSCSSで作ってみます。(スタイルだけ)

HTML

なるべくシンプルに以下のHTMLでいきます。

HTML
<div class="text-field">
    <label for="name">name</label>
    <input type="text" id="name">
</div>

デフォルトはこんな感じ
text field default style

スタイルを整える

動きをつける前に見た目を整えます。
一応Material Design>text field(outlineアイコン無し)のサイズにのっとってみます。

SCSS
.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
ここではdppxとします。

スタイル適応後はこんな感じ
text field styled

フォーカス時のスタイル適応

ここから動きをつけていきます。

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);
+        }
+    }
    
    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の色を濃くする
  • inputfocus
    • .text-fieldのボーダーを青にする
    • labelの色を青、配置をずらす、サイズ縮小

text field animation styled

:has()でやっていることを簡単に解説すると、まずHTMLが下記のようになっています。

  1. .text-fieldにスタイルをあてたいので.text-field内に.text-field{&:has(){}}となる。
SCSS
.text-field{
    &:has(){
    }
}

/*css変換後*/

.text-field:has(){
}
  1. 子要素であるinputfocusを検知したいので()の引数にinput:focusを入れる
SCSS
.text-field{
    &:has(input:focus){
    }
}

/*css変換後*/

.text-field:has(input:focus){
}
  1. focus時にラベルも変化させたいので:has(){}内にlabelを指定。あとは変化後のスタイルを記述する
SCSS
.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だけで完結することができます。
bad focus animation

: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=" "で空文字列を指定しておきます。

HTML
 <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に追加します。

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が入力中の時、{}のスタイルを適応
といったところですかね。
making finished products

さらにおまけ

この記事ではわかりやすくに伝えるため簡潔な作りにしていますが、MUIの構造はより複雑な作りになっていたのでクラス名を弄ってHMTLだけ紹介します。

HTML
<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は人に使ってもらいやすくしているのでこのようになってますが、自作するなら下記くらいでいいかなと思います。

HTML
<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

ログインするとコメントできます