GODRICH

CSS トグルスイッチ 9選 — 一行でコピペOK

css トグルスイッチは、チェックボックス一つで実際にオン・オフができ、トラックとノブの位置・色・アイコンだけを変えて見た目を作るコントロールです。ここでは9種類を集め、どれもtransformとopacityを軸にしたコードで組みました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つは飾りの派手さではなく、トラックの上で何が変わるかという軸で並べています。ノブの位置だけが動く基本形(01・02)から、トラックの中身自体が変わるもの(03・04)、押し込みや塗りつぶしのような演出が加わるもの(05〜07)を経て、最後は構造が特殊だったり触れなくなったりする2つ(08・09)で締めくくりました。グリッドの各カードには.is-demoという自動ループ用のクラスが付いていて、実際に押すとpointerdownイベントでこのクラスが外れ、以降は本物のチェックボックスとして:checkedだけで動くようになります。

01基本スライド

チェックボックスにcheckedが付くと丸いノブがトラック右端まで滑り、下に重ねた2枚の背景レイヤーがopacityで入れ替わって、背景色も一緒に切り替わります。設定画面のオプションスイッチや、フォームのチェックボックスをそのまま置き換えたい場所に向いています。

translateX:checked0.25초
.switch__bg--off { background: rgba(255, 255, 255, .18); opacity: 1; }
.switch__bg--on  { background: $color; opacity: 0; }
.switch__knob { transform: translateX(0); transition: transform $duration $easing; }

.switch__input:checked ~ .switch__track .switch__bg--off { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__bg--on  { opacity: 1; }
.switch__input:checked ~ .switch__track .switch__knob    { transform: translateX(24px); }

02iOS風トグル

丸いトラックと白いノブの組み合わせで、ノブが着地する瞬間だけ38%地点で一度オーバーシュートしてから止まるスプリング系のイージングを使っています。iOSの設定アプリのように、指先の反応感が伝わってほしい画面に向いています。

cubic-bezier 스프링그림자0.3초
@keyframes knob-loop {
  0%, 15%   { transform: translateX(0); }
  38%       { transform: translateX(28px); }   // スプリングのオーバーシュート
  40%, 60%  { transform: translateX(24px); }
  85%, 100% { transform: translateX(0); }
}

03ON/OFFラベル内蔵

トラックの両端にON・OFFの文字をあらかじめ固定しておき、ノブが移動する側の文字だけをz-indexで覆い隠します。管理画面のステータス表示のように、今の値が色だけでなく文字でも分かってほしいスイッチに向いています。

z-indexoverflow:hidden0.25초
.switch__text--on  { left: 10px; }
.switch__text--off { right: 10px; }
.switch__knob {
  position: absolute; top: 3px; left: 3px; z-index: 1;
  transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob {
  transform: translateX(44px);
}

04アイコン切替

ノブが横に移動する間に、内側の×アイコンとチェックアイコンがopacityでクロスフェードして入れ替わります。権限の許可・拒否のように、結果をアイコン一つで言い切りたいスイッチに向いています。

opacity 크로스페이드SVG 아이콘0.2초
.switch__knob { transform: translateX(0); transition: transform $duration $easing; }
.switch__icon--x     { opacity: 1; }
.switch__icon--check { opacity: 0; }

.switch__input:checked ~ .switch__track .switch__knob        { transform: translateX(26px); }
.switch__input:checked ~ .switch__track .switch__icon--x     { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__icon--check { opacity: 1; }

05押し込みスクイッシュ

CSSカスタムプロパティの--sx--syを使い、押している間だけノブが横に潰れて楕円になり、離すと元の円に跳ね返ります。タッチ操作中心のモバイルフォームで、押した手応えを画面側から返したいときに使います。

scaleX:active0.15초
.switch__knob {
  --x: 0px; --sx: 1; --sy: 1;
  transform: translateX(var(--x)) scaleX(var(--sx)) scaleY(var(--sy));
  transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob { --x: 24px; }
.switch:active .switch__knob { --sx: 1.35; --sy: .8; }

06背景色の塗りつぶし

トラックの色は一度に切り替わらず、ノブが通った位置を起点にclip-pathの円がだんだん広がって塗りつぶします。プレミアムプランへの切り替えのように、変化そのものを見せ場にしたいカードに向いています。

clip-path:circle()번짐0.4초
.switch__fill {
  background: $color;
  clip-path: circle(0% at 18% 50%);
  transition: clip-path $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__fill {
  clip-path: circle(75% at 82% 50%);
}

07昼夜アイコントグル

ノブが乗るトラック自体が空になっていて、背景グラデーションが昼の色から夜の色へ移り、小さな星がopacityで浮かび上がります。アイコン一つよりも大きな見せ場にしたいテーマ切り替えスイッチに向いています。

background-position별 opacity0.5초
.switch__track {
  background-image: linear-gradient(90deg, $stage-yellow 0%, $stage-yellow 50%, $color 50%, $color 100%);
  background-size: 200% 100%;
  background-position: 0% 0%;
  transition: background-position $duration $easing;
}
.switch__input:checked ~ .switch__track { background-position: 100% 0%; }
.switch__input:checked ~ .switch__track .switch__star { opacity: 1; }

08サイズバリエーション

1つのSCSSミックスインがトラック幅・高さ・ノブの移動距離をまとめて計算し、小・中・大のスイッチを同じtranslateXアニメーションのまま作り分けます。デザインシステムの部品のように、サイズごとに数値を再計算したくないときに向いています。

@mixinem 단위0.25초
@mixin switch-size($track-w, $track-h, $knob) {
  --travel: #{$track-w - $knob - 6px};
  .switch__track { width: $track-w; height: $track-h; }
  .switch__knob  { width: $knob; height: $knob; }
}
.switch--sm { @include switch-size(36px, 20px, 14px); }
.switch--md { @include switch-size(48px, 26px, 20px); }
.switch--lg { @include switch-size(60px, 32px, 26px); }

09無効状態のスタイル

disabled属性が付くとトラックとノブがfilter: grayscaleopacityのトランジションで薄くなり、そのままの位置で固まって反応しなくなります。権限のない設定項目のように、触れられないという事実そのものを伝えたい場所に向いています。

filter:grayscaleopacitycursor:not-allowed
.switch__input:disabled ~ .switch__track { filter: grayscale(1); opacity: .45; }
.switch__input:disabled ~ .switch__label { opacity: .55; }

@keyframes fade-loop {
  0%, 20%  { filter: grayscale(0); opacity: 1; }
  50%, 70% { filter: grayscale(1); opacity: .45; }
  100%     { filter: grayscale(0); opacity: 1; }
}

どこで崩れるか — 落とし穴

9つのうち半分以上(01・02・04・05・08)は、background-colorそのものをアニメーションさせず、2枚の背景レイヤーを重ねてopacityだけで入れ替えています。色そのものを動かすと途中の値がブラウザごとに違う経路で補間され、意図しない濁った色を一瞬通過しますが、重ねたレイヤーを透明度だけで切り替えれば各レイヤーの色はにじみません。MDNのCSS・JSアニメーション性能の解説でも、レイアウトの再計算が要らないtransformopacityを優先するよう勧めています。06のclip-pathと07のbackground-positionはこの原則の例外に見えますが、どちらもリペイントだけで完結しレイアウトの再計算を要求しない点は同じです。caniuse.comでclip-pathを調べると、主要ブラウザはすでに前提として対応しています。

落とし穴とまでは言えませんが、もう一つ気づいた点があります。03のON・OFFの文字は、ノブの直径よりトラックの幅が十分広くないと、文字が半分だけ隠れる中途半端な瞬間が生まれてしまいます。ここに載せたトラック幅と移動距離をそのまま収めた完成版ファイルはa2ejkfw2という文字列を入れないと開けず、下のカードから取り出せます。

アクセシビリティ

9つとも<input type="checkbox">role="switch"を載せた本物のコントロールなので、Tabで移動できSpaceで値を切り替えられます。:focus-visibleにはリングを描き、changeイベントのたびにaria-checkedを明示的にそろえて、古いスクリーンリーダーでも状態を取りこぼさないようにしています。

<label class="switch">
  <input type="checkbox" class="switch__input" role="switch" aria-checked="false">
  <span class="switch__track"><span class="switch__knob"></span></span>
</label>
.switch__input:focus-visible ~ .switch__track {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}
document.querySelectorAll(".switch").forEach(function (s) {
  var input = s.querySelector(".switch__input");
  var sync = function () { input.setAttribute("aria-checked", String(input.checked)); };
  input.addEventListener("pointerdown", function () { s.classList.remove("is-demo"); });
  input.addEventListener("change", sync);
  sync();
});

prefers-reduced-motion: reduceの画面では、9つとも切り替えのトランジションだけが消え、オン・オフの状態そのものはそのまま見えます。09だけはそもそも触れない状態なので、この設定の有無にかかわらず常に薄い見た目のままです。対応状況はMDNのprefers-reduced-motionの解説で確認できます。

チェックボックス型のコントロールをもっと見たい方はCSSアニメーション一覧を、godrichstoryがどんなサイトかは紹介ページを先に読んでみてください。

FAQ

トグルスイッチとチェックボックスは何が違いますか?

マークアップはどちらも同じ<input type="checkbox">ですが、role="switch"を載せるとスクリーンリーダーが「チェックボックス」ではなく「スイッチ」として読み上げます。通知のオンオフのようにすぐ反映される設定にはスイッチ、保存ボタンを押して初めて反映される項目にはチェックボックスの方が信号として正確です。

09はdisabledなのに、なぜ動いて見えるのですか?

実際のコントロールは常に無効な状態のまま固定されていて、画面で繰り返し再生されている薄れる動きはdisabledが付いた瞬間の見た目を伝えるためのデモ専用ループです。人がクリックしてオン・オフを切り替える操作は、この状態ではそもそも成立しません。

Reactではクラスの付け外しの代わりに何を使いますか?

zip内のreact/フォルダの各コンポーネントはcheckeduseStateで持ち、色や速さを決める変数はpropsで受け取ってCSSカスタムプロパティに渡します。SCSSモジュール側はvar()を読むだけなので、値をいくつか変えるだけで違う色・速さのスイッチをすぐに作れます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。