CSSでトグルスイッチを作るには
チェックボックスを視覚的に隠したうえで、:checked と隣接兄弟セレクタを使ってオン・オフが切り替わるトグルスイッチの見た目を作る。
JavaScript は使わない。
実行確認済みは、ブラウザで実際に描画して見た目を確認したものである。
公開:
<label class="switch">
<input type="checkbox" class="switch__input" checked />
<span class="switch__track"></span>
<span class="switch__text">通知を受け取る</span>
</label>
<label class="switch">
<input type="checkbox" class="switch__input" />
<span class="switch__track"></span>
<span class="switch__text">位置情報を使う</span>
</label>.switch {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
color: #27272a;
font-family: system-ui;
font-size: 14px;
/* label で包むと、文字をクリックしても切り替わる */
cursor: pointer;
}
/* input は display: none にせず、視覚的にだけ隠す。
none にするとフォーカスもキーボード操作も失われる */
.switch__input {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.switch__track {
position: relative;
flex: none;
width: 46px;
height: 26px;
border-radius: 999px;
background: #a1a1aa;
transition: background-color 0.2s ease;
}
/* つまみ。track の中を左右に動かす */
.switch__track::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease;
}
/* :checked を隣接兄弟セレクタで受け取り、見た目だけを差し替える */
.switch__input:checked + .switch__track {
background: #10b981;
}
.switch__input:checked + .switch__track::after {
transform: translateX(20px);
}
/* 隠した input のフォーカスリングは見えないので、track に出し直す */
.switch__input:focus-visible + .switch__track {
outline: 2px solid #0f766e;
outline-offset: 3px;
}チェックボックスを視覚的に隠し、隣に置いた要素へ :checked の状態を伝えてスイッチの見た目を作る。
input を display: none で消さないのは、消すとフォーカスもキーボード操作も失われ、支援技術からチェックボックスとして扱われなくなるためである。
つまずき
切り替わらないときは、まず input と track が兄弟になっているかを確認するとよい。
隣接兄弟セレクタ + は「直後の兄弟」しか選べないため、input を div で包んだり、input と track の間に別の要素を挟んだりすると :checked の状態が届かなくなる。
構造を変えたい場合は、共通の親に :has(:checked) を書く方法もある。
input を消さずに隠す
display: none や visibility: hidden で消したチェックボックスは、タブキーで到達できず、スクリーンリーダーからも読まれない。
見た目だけを消して要素は生かすために、上の例では 1px の領域に押し込んで clip-path で切り取っている。
opacity: 0 のまま track の上に重ねる書き方でも同じ目的を果たせる。
いずれの場合も、操作しているのは本物のチェックボックスなので、フォームに入れればそのまま値が送信され、required や disabled もそのまま効く。
フォーカスの見え方
input を隠すとブラウザ既定のフォーカスリングも一緒に見えなくなるので、:focus-visible を使って track 側に出し直す。
:focus ではなく :focus-visible を選ぶのは、マウスでクリックしたときには枠を出さず、キーボードで移動したときだけ出すためである。
アニメーションは transition で track の背景色とつまみの transform に掛けている。
prefers-reduced-motion を尊重するなら、この 2 つの transition を無効にする指定を足すとよい。