💡

Toggle Switchの実装で:checkedセレクタの有用性を知る

2021/11/21に公開

モチベーション

今更ですがToggle SwitchをReactで作ってみたという話です. :checked疑似クラス・セレクタが便利という話です.

チェックポイント

✅ inputとlabel要素の関係性
✅ :checked疑似クラス・セレクタの使い方を知っている
✅ 要素を横に均等に並べらえれる
✅ 兄弟コンビネータ~と+の使い分けを知っている

Toggle Switchを作る

完成品は以下のような感じになる. 誰でも一度ぐらいは見たことがあるからあまり説明は必要ないと思います.

Reactコンポーネントは以下のように定義した. よくある状態をトグルさせるロジックがあるだけのシンプルな要素です.

import React, { useState } from 'react';
import './switch.css';

interface SwitchProps {
    /**
     * Switch state
     */
    active?: boolean;
}

export const Switch: React.VFC<SwitchProps> = ({
    active = false,
    ...props
}: SwitchProps): JSX.Element => {
    const [toggled, setToggled] = useState(active);
    const handleToggle = () => setToggled(!toggled);

    return (
<label className="switch">
    <input type="checkbox" checked={toggled} onChange={handleToggle} />
    <div className="moving-area">
        <div className="switch-on"><span>🌜</span></div>
        <div className="switch-off"><span>🌞</span></div>
    </div>
    <span className="switch-button"></span>
</label>
    );
}

ベース

まずベースが必要になる. ここではlabelとinputを使って基本的な構造を作る. inputをラベルで覆っておくことでfor属性を使わずにlabelとinputの関係を暗黙に指定できる.

Alternatively, you can nest the <input> directly inside the <label>, in which case the for and id attributes are not needed because the association is implicit: [1]

そのためlabelで囲った領域をクリックすると勝手にinputへの処理だと判断してくれる. inputを使う特徴として

  • イベントの処理がしやすい
  • CSSで状態を管理できる

というのがある. CSSには:checkedという疑似クラス・セレクタがあり, これを使うとinput要素がチェックされた状態の時だけスタイルを適用するということができコンポーネント側で不必要に状態を弄らなくても良くなる.

またlabelは, それ以下の子要素に座標軸を与える役割もする. position: relativeを指定することでlabelの左上が(0, 0)としてそれ以下の要素の位置が計算されるようになり, ボタンの移動をlabelの範囲内で計算しやすくなる.

/* Container */
label.switch {
  cursor: pointer;
  position: relative;
  display: inline-block;
  width: 50px;
  height: 25px;
}

/* Hide default checkbox */
label.switch input[type='checkbox'] {
  display: none;
}

便利な点もあるが少しこの層でいろんなことをやりすぎな気もする. inputを隠して仕込むなら普通にdiv要素をコンテナとしてイベント・ハンドラをつけた方が良い気もする. この場合はコンポーネントでいろいろしなくてはいけなくなるので全部JS側でコントロールできるCSS-in-JSでスタイルを採用する適用する場合に向ているのかもしれない.

表示領域

次はベースの上にアイコンを横方向に並べる. アイコンは排他的に隠れるようにした. 疑似クラス・セレクタの:checkedで何も考えずにアイコンを隠せるのはとても便利です.

/* Switch button moving area */
.switch .moving-area {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: white;
  transition: background-color 0.2s ease;

  display: flex;
  justify-content: space-evenly;
}

/* Hide one of each buttons exclusively*/
.switch input[type='checkbox']:not(:checked) + .moving-area .switch-on {
  visibility: hidden;
}

.switch input[type='checkbox']:checked .switch-off {
  visibility: hidden;
}

ボタン

最後にスライド移動するボタンを作れば静的な構造をは完成する.

/* Switch botton shape */
.switch .switch-button {
  position: absolute;
  left: 2px;
  top: 2px;
  width: 21px;
  height: 21px;
  background-color: black;
  transition: transform 0.3s ease;
}	

移動

inputを採用したことでボタンの移動もCSSで簡単に記述できる. 上述の:checkedセレクタを使い有効な時だけスタイルを適用してやることができる.

/* Transition when switching on */
.switch input[type='checkbox']:checked ~ .switch-button {
  transform: translateX(25px);
  background-color: #6699cc;
}

.switch input[type='checkbox']:checked + .moving-area {
  background-color: #336699;
}

感想

実装してみてCSSのいい勉強になったと思いました. しかしこの要素は実際使っていて分かりにくいかなとも思いました. 今どちらの状態なのかが分かりにくいというのが最大の理由です. スライドするボタンは不要でボタンを二つに並べてどっちかを押す方が分かりやすいかなと思いました.

Reference

脚注
  1. <label>: The Input Label element ↩︎

Discussion