要素が自動配置されるトグルスイッチを作る(CSSのみ・計算不要)

Featured image of the post

目次

まえがき

このブログはこちらの記事の方法によってダークモードを追加しています。

記事の通りにやるだけで簡単にダークモードが導入できたので、astro-notion-blogを始めるまでHTMLもCSSも未経験だった僕には本当にありがたい記事でした。

Image in a image block
Image in a image block
サイドバーに設置したモード切替用トグルスイッチ

最近少しずつHTMLとCSSの扱い方が解ってきたので、トグルスイッチをよくある太陽と月のマーク付きのものにしたいと思うようになりました。改造や参考のためにトグルスイッチのCSSについて調べてみたところ、どれも円の間隔や座標を計算して入力するタイプのものばかりで、縦と横の長さとボタンの尺度だけ入れたら勝手に辻褄の合ったトグルスイッチになるCSSを作ったら便利そうだと思ったので作ってみることにしました。

参考

Marcus氏によって作成された、CSSのみで動作するシンプルなトグルスイッチです。これはボタン部分をクリックするとニュッと伸びるという弾性的なアニメーション効果があるんですが、全てCSSのみで作られています。CSSだけで非常にシンプルであることから、このトグルスイッチを基にして作ることにしました。

ボタン位置が自動で決まるトグルスイッチ

比較のために一部順番を入れ替えています。また、行を揃えるために色の指定も削除していますが動作には関係ありません。

input[type=checkbox]{
	height: 0;
	width: 0;
	visibility: hidden;
}

label {
  display: block;
  position: relative;
	cursor: pointer;
	text-indent: -9999px;
  border-radius: 100px;
  background: grey;
	width: 200px;
	height: 100px;
}

input:checked + label {
	background: #bada55;
}

label:after {
	content: '';
	position: absolute;
	top: 5px;
	left: 5px;
	width: 90px;
	height: 90px;
	border-radius: 90px;
  background: #fff;
	transition: 0.3s;
}


input:checked + label:after {
	left: calc(100% - 5px);
	transform: translateX(-100%);
}

label:active:after {
	width: 130px;
}
Marcus氏のトグルスイッチ
input[type="checkbox"] {
  height: 0;
  width: 0;
  visibility: hidden;
}

label {
  display: block;
  position: relative;
  cursor: pointer;
  text-indent: -9999px;
  border-radius: 9999px;
  background: grey;
  width: 200px; /*幅の指定*/
  height: 100px; /*高さの指定*/
}

input:checked + label {
  background: #bada55;
}

label:after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 9999px;
  background: white;
  transition: 0.3s; /*ボタンの移動時間*/
  scale: 90%; /*ボタンの縮尺指定*/
}

input:checked + label:after {
  left: 100%;
  translate: -100%;
}

label:active:after {
  aspect-ratio: 1.4 / 1;
}
要素自動配置トグルスイッチ

設定箇所はlabelwidth: 200px;height: 100px;の部分です。トグルボタン(円のところ)のサイズを変更する場合はlabel:afterscale: 90%;を変更します。

色はどこを変更しても動作に影響ありませんが、トグルボタンにborderを設定するときにはbox-sizing: border-box;が必須です。後述しますが、position: absolute;top: 0;left: 0;でトグルボタンの位置合わせを行っているため、scale以外の方法を用いて要素のサイズが変わるとズレます。

border-radius: 9999px;については、長丸もしくは円を作る時に、ある程度の大きさでも対応できるようにしています。9999vh9999em でもいいです。99999999px でもいいです。とりあえず、border-radiusの対象となる要素の幅と高さより常に大きな固定値を入れます。

デモ

Image in a image block
サイズ変更のデモ

トグルスイッチのサイズを指定すると、トグルボタンが自動で配置されるので楽です。

これはどれもそうなんですが、トグルスイッチ自体が横長じゃないと変な見た目になります。高さに対して幅が1.5倍以上あればまともな見た目かなと思います。

Image in a image block

これはトグルボタンの尺度変更の様子です。トグルスイッチの円弧とズレがないか見えるように枠線を付けて半透明にしてあります。

90%~120%くらいがちょうどいいです。

CSSにおける角丸の難しさ

角丸がある図形は尺度を変更するだけでは美しく重なりません。単純に尺度を変更すると角丸を校正する円弧の中心がズレてしまうからです。トグルスイッチは長穴状の形ですが、4隅に角丸があると考えれば同じです。

Image in a image block
個別の静止画像
Image in a image block
Image in a image block
Image in a image block

これは、長方形の段階で尺度を変更してから角丸を付けても、角丸を付けた後に尺度を変更しても同じです。どちらもズレます。

Image in a image block

これを解決するためには、中心が一致するように縦方向と横方向の尺度を分けて考える必要があります。これは高さを基準にしていますが、幅の尺度とは合っていません。内側に行けば行くほど細長くなっています。

Image in a image block

これがつまりどういうことなのかというと、尺度の変更ではなく全周に対して等しい量のオフセットを付けることで図形を作らなければ、角丸が綺麗に合わないということです。

Image in a image block

CSSでは、outline-offsetborder-radiusborderの組み合わせでこのようなデザインを簡単に作ることができますが、デザインではなく親要素のサイズに対する子要素のサイズとして相対的に定めるのは非常に難しいと思います。

純粋にCSSのみで、自分で計算して数値を入れるのではなく、外側のサイズを指定するだけで自動的に子要素のサイズが定まるようなものは見つけられませんでしたし、思いつきませんでした。

相対的な数値となるrememでトグルスイッチを作り、フォントサイズから一括でサイズ変更というのはできますが、アスペクト比の変更には対応できないのであまり意味がありません。

外側の縦横サイズと付けたい隙間から計算してボタンの大きさや座標を設定するのはめんどくさくて嫌だと思ったので、これを自動的に解決できる方法を考えました。

原理

中心点の一致

先程の角丸の難しさというのは、内側の要素が単純な円でないことに由来します。

そこで、長方形の中に高さを一辺とした正方形をaspect-ratio: 1 / 1;で作り、この正方形から円(4隅に最大角丸)を作ることで、長方形由来のトグルスイッチの角丸と、正方形由来のトグルボタンの角丸の中心が常に一致するようにしました。

Image in a image block
個別の静止画像
Image in a image block
Image in a image block
Image in a image block
Image in a image block
Image in a image block

正方形を長方形の縁に重ねると、辺の長さが一致する(=直径が一致する)円ということになり、円と角丸は必ず重なります。2つの円弧が重なるということは、中心点も一致します。

このようにして親要素の高さから正方形を作り、position: absolute;top: 0;left: 0;で常に正方形が長方形の左端に重なるように固定します。これにより、常に中心点が一致するトグルボタンの円が完成します。その後、scale: 〇〇%;を用いてこの円を拡大・縮小することによって、中心点を一致させたままトグルボタンのサイズを変更できます。

正方形の要素の大きさが変わり、正方形と長方形の高さが同じでないとposition: absolute;で中心点が一致しないため、トグルボタンにborderを設定するときにはbox-sizing: border-box;が必須となっています。

scale: 〇〇%;の拡大・縮小は要素自身の中心点が基準となるため、中心点はズレません。

スイッチON時の座標移動

トグルスイッチがONになったときのトグルボタン座標移動は、left: 100%;translate: -100%;を用いています。scale: 〇〇%;は中心基準のサイズ変更であるため中心点の座標のズレが発生しません。そのため、Marcus氏のトグルスイッチの座標移動から隙間に対する数値指定を取り除いたものとなっています。

また、translaterotatescaleプロパティはtransformから独立したため、上書きされることもなく、非常にシンプルに記述できるようになりました。

(この要件を満たすコードを完成させて、これを書いているときに知りました。別のネタとして、上書きのせいで2箇所に同じscaleの割合を書かなければいけない点を改善した入れ子追加バージョンを作っていたのですが、完全に無駄になりました。)

Image in a image block
個別の静止画像
Image in a image block
Image in a image block
Image in a image block

親要素内の左端から右端へ、親要素の中央を基準に線対称となる位置に移動をしたような動作となります。left: 100%;は親要素の幅、translate: -100%;では自身の要素幅を参照するため、親要素の大きさに関わらず、常に反対側の端へと中心点が一致したまま移動が可能です。

クリック時の変形

これはオマケ機能ですが、Marcus氏のトグルスイッチでの記述を書き換えて、アスペクト比の変更をしても見た目が変にならないようにしました。

scaleの影響によってほんの少しだけ歪んでいるのですが、実用ではほとんど気にならないと思います。

Image in a image block

元々は絶対値でクリック時のトグルボタン横幅を指定してありましたが、これをアスペクト比指定に変更してあります。aspect-ratio: 1.4 / 1;の場合、クリック時にトグルボタンが1.4倍横長になります。

最初はwidth: 60%;としていたんですが、これでは親要素の幅を参照するため、非常に横長なトグルスイッチにした際に変な見た目になっていました。あとボタンの変形が一瞬ですね。なんでこれだけtransition: 0.3s;が掛かっていないのか良く分かりませんでした。

Image in a image block

aspect-ratioに変更したら自然な変形だと思います。

Image in a image block

あとがき

擬似要素という未知の概念と対峙することになったので、トグルスイッチ弄るだけで1日かかりました。それでも、汎用性が高くて使い勝手の良いトグルスイッチが作れたと思います。

次はSVGと組み合わせて、ダークモード用トグルスイッチを作ってブログに配置したいです。