input[type="checkbox"]要素のswitch属性を使用したスイッチUIの実装例
広告
input[type="checkbox"]
要素のswitch
属性を使用したスイッチの実装メモです。まだ実務で使用したわけではないのであくまで自由研究としてですが、将来的にスイッチUIはinput[type="checkbox"]
要素にswitch
属性を指定して実装するケースが増えそうなので実装例としてまとめておきます。
switch属性とは
現在のHTMLの仕様を提供しているWHATWGより提案された、スイッチUIを実装するためのinput[type="checkbox"]
要素に新たに追加された属性です。switch
属性を指定することでWeb標準でスイッチUIを作成することが可能となります。
switch
属性は現在のHTMLの仕様にはまだ含まれていないものの、iOS 含む Safari 17.4 より先行的にサポートされています。
switch
属性を使用するメリットとしてはデフォルトのrole
がswitch
に変更されるということです。これにより、switch
属性がサポートされている環境では「スイッチ」と読み上げられ、スイッチUIがトグルされた場合には「オン」「オフ」と現在の状態が読み上げられます。また、従来のinput[type="checkbox"]
と同じくキーボード操作時はスペースキー押下でトグルすることが可能です。
現時点では iOS 含む Safari 17.4 以降でしかサポートされておりませんが、チェックボックスと同じ読み上げをされても差し支えないスイッチUIであればJS不要ということもありプログレッシブ・エンハンスメントの一環で導入しても問題ないでしょう。仮にチェックボックスとして読み上げられるのが望ましくないスイッチUIであれば、button
要素とaria-pressed
属性の組み合わせでトグルボタンとして実装するのがベターだと思います。
スイッチUIをCSSで実装する
switch
属性がサポートされていない環境ではユーザーエージェントで用意されたスイッチUIを使用することができなかったり、デザインの要件によっては自作でスイッチUIを実装したほうが都合が良いケースも考えられるため、スイッチUIをCSSで作成します。この実装はbutton
要素とaria-pressed
属性の組み合わせで作成するトグルボタンにも流用可能です。
以下はスイッチUIの実装例です。
スイッチUIのスタイリングは input 要素に直接適用する
前提としてスイッチUIのスタイリングは input 要素に直接適用するようにします。
過去に投稿したZennの記事のように、かつては自作のラジオボタンやチェックボックスを作成する際は元のinput
要素を非表示にした上で空のspan
要素などを装飾し、隣接セレクタなどで状態管理するのが一般的でした。しかし、現在ではinput
要素を直接スタイリングすることが可能です。
加えて[type="radio"]
および[type="checkbox"]
には::before
::after
疑似要素も適用できるため、実装コストやアクセシビリティチェックの観点からもinput
要素を直接スタイリングするのが望ましいでしょう。
input[type=“checkbox”] のデフォルトのスタイルをリセットする
下準備としてユーザーエージェントで用意されているスイッチおよびチェックボックスのスタイルを無効化するためにappearance
プロパティの値をnone
にします。また、デフォルトのmargin
は不要であり、line-height
も後述するlh
の使用に支障があるためリセットしておくと良いでしょう。
スイッチUIの外枠をスタイリングする
続いてスイッチUIの外枠をスタイリングしていきます。形状は錠剤型にしたいのでborder-radius:calc(infinity * 1px)
で角丸を実装します。
角丸の指定はborder-radius:999px
やborder-radius:100vmax
でも大抵の場合は問題ありませんが、border-radius:calc(infinity * 1px)
であれば確実に角丸を実装することが可能です。
デザインガイドラインで角丸がトークン化されていて、グローバルスコープのCSS変数で定義している場合は角丸用の変数も用意しておくと良いでしょう。長方形であれば錠剤型に、正方形であれば正円となります。
スイッチUIのサイズはデザインの仕様に従いますが、指定がない場合は高さを行の高さと同等にしておくと見た目が綺麗になります。前回の記事で紹介した1lh
を指定すれば現在のline-height
の高さと同等になるため柔軟性が高くなります。1lh
がサポートされていない iOS 16.4 未満を考慮する場合は@supports
機能クエリを使用して適当な値をフォールバックしておくと良いでしょう。
そのままだと横幅と高さの指定が効かないためdisplay:inline-block
などの値を指定する必要がありますが、後述する理由からスイッチUIの場合はdisplay:inline-flex
を指定するようにします。ついでにvertical-align:middle
を指定してラベルのテキストの縦中央に揃えておきます。
ちなみにサンプルおよびこのブログではdisplay
の値に2値構文を使用していますが、論理プロパティ・論理値の指定とは違い2値構文を使用するメリットは「display
の指定が論理的になる」以外特に無いため、display:inline-flex
のように従来の指定をすることを推奨します。
また、縦横比が決まっている場合であってもinput[type="checkbox"]
にaspect-ratio
を指定するとSafariで潰れて表示されてしまうため、inline-size
(width
)とblock-size
(height
) でサイジングすることを強く勧めます。
また、「オン」「オフ」時の背景色も設定します。サンプルではHEX値を直書きしていますが、グローバルスコープのCSS変数で定義されているカラースキームを参照するほうが望ましいです。
スイッチのハンドル部分を作成する
スイッチのハンドル部分はinput
要素の::before
::after
疑似要素を使用して作成します。
switch
属性を付与したUIは::thumb
疑似要素を装飾することでハンドルをカスタマイズできますが、switch
属性がサポートされていることが前提であることに加えてinput
要素の::before
::after
疑似要素でもハンドルは作成できるため、::thumb
疑似要素を使うメリットはあまり感じられませんでした。
ハンドル部分は::after
疑似要素を正円形にして実装します。潰れるのを防止するためflex-shrink:0
も指定しておきます。
サイジングは高さ(block-size
)を100%
にした上でaspect-ratio:1
で縦横比を1:1にするようにしてください。こうすることでスイッチUIの高さが可変になっても正円形を保つことができます。
「オン」「オフ」時のハンドルの移動は::before
疑似要素をflex
で収縮することで実現します。他のスイッチUIの実装記事を見ているとtranslate
等で頑張っている印象ですが、空の疑似要素を収縮させた方が実装コストは少ない印象ですし、スイッチUIの横幅が可変しても動作に影響は起こらなくなります。
あとはinput
要素のpadding
の値を調整すれば簡単にスイッチUIのできあがりです。
強制カラーモードの対応を行う
デバイスによっては文章を読みやすくするために背景色や文字色をコントラストが明白な配色に変更する「強制カラーモード」が備わっています。原則的にテキストやSVGを含む画像であれば強制カラーモード適用下でも表示されるため問題にはなり得ないことが多いですが、疑似要素を使ったアイコンやUIに関しては消失してしまう可能性があります。
また、以下のプロパティは、強制カラーモードでは特別な動作をします。
box-shadow は ‘none’ に強制されます
text-shadow は ‘none’ に強制されます
background-image は URL ベースでない値では ‘none’ に強制されます
color-scheme は ‘light dark’ に強制されます
scrollbar-color は ‘auto’ に強制されます
(MDNforced-colorsより引用)
先程のスイッチUIの実装例に関しても強制カラーモード適用下では無事消失していましたので、forced-colors
メディア特性を使用してスイッチUIを認識できるようにします。
原則的にborder
は強制カラーモード適用下でも消失せず、システムカラーを指定したbackground-color
は認識できる色として表示されるため、forced-colors:active
時はborder
の付与とハンドルの背景色をCanvasText
(ユーザーエージェントで提供されるテキスト色)にすることで強制カラーモード適用下でもスイッチUIを表示することができます。
【おまけ】フォールバックトリックを使って強制カラーモードの対応を行う
実務で導入する価値があるかは皆さんの判断にお任せしますが、このブログではフォールバックトリックを使って強制カラーモードの対応を行っています。
フォールバックトリックってなんやねんって方も多いかとは思いますが(ご存じの方は読み飛ばしてください)、CSSカスタムプロパティの仕様を利用して--is-forced-true: ;
--is-forced-false: initial;
のようにフラグを書き換えることで強制カラーモードが適用されているかどうか否かで値を出し分ける方法です。
2.2. Guaranteed-Invalid Values
The initial value of a custom property is a guaranteed-invalid value. As defined in § 3 Using Cascading Variables: the var() notation, using var() to substitute a custom property with this as its value makes the property referencing it invalid at computed-value time.
This value serializes as the empty string, but actually writing an empty value into a custom property, like —foo: ;, is a valid (empty) value, not the guaranteed-invalid value. If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.
2.2. 保証された無効値 カスタムプロパティの初期値は、保証された無効値です。セクション3「カスケーディング変数の使用法:var() 表記」で定義されているように、この値を持つカスタムプロパティを var() を使用して代入すると、そのプロパティは計算値の時点で無効になります。
この値は空文字列としてシリアライズされますが、実際にカスタムプロパティに空の値を書き込む(例:—foo: ;)ことは有効な(空の)値であり、保証された無効値ではありません。何らかの理由で変数を手動で保証された無効値にリセットしたい場合は、キーワード initial を使用します。
上記仕様によれば--is-forced-false: ;
は強制カラーモード適用時には「有効な値」かつ「空文字」として扱わるため、サンプルコードの--foreground
変数は強制カラーモード適用時には次にような形になります。
そして、CSSカスタムプロパティにinitial
を指定するとフォールバックで指定した値を返すため、var(--is-forced-true)
のフォールバックで指定したcanvastext
が適用されます。
フォールバックトリックを使うメリットとしては「強制カラーモードが適用されているか否か」といった所謂if...else
のような指定をする際にわざわざメディアクエリを呼び出す必要が無いという点でしょうか。また、CSS Variable Autocompleteといった拡張機能を導入してCSS変数の補完を有効にすることでコーディング速度が向上したような気がします。
ただし、ややワザップ感ある方法ですし、恐らくはマイナーな手法であるため実務で使用するかどうかは考えたほうがいいと思います。