GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

CSS スイッチボタン 9選 — レバー・ダイヤル・イジェクト、本物の感触

css スイッチボタンとは、画面上のオン・オフをレバー・ロッカー・ダイヤルの本物の感触で見せるボタンのことです。9つともライブラリなしのSCSSと短いJSだけで組んであり、押せばその場で傾き、回り、飛び出し、スクリーンリーダーにも状態が伝わります。

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

9つをこの順に並べた基準は、手の動きです。最初の2つ(01・02)は一度押してオン・オフを切り替える電源で、css on off スイッチ として探されるいちばん素朴な形がここにあります。続く2つ(03・04)は値を連続的に決める調節器、真ん中の2つ(05・06)は押す・引くという大きな動作で状態を変える部品、後ろの2つ(07・08)は板とハンドルを倒して選ぶ・落とす物で、最後の09は取り出す動作で締めくくります。舞台の色は墨・ペーパー・イエロー・オレンジの4種を順に回し、オレンジの舞台に載る文字はすべて墨で打ちました。押した瞬間の動きを足したいならCSS ボタン アニメーション 9選、押したあとの見せ方が気になるならcss ボタン 押したら変わる 9選が隣に並び、同じスイッチでももっと薄く平らな作りが欲しいときはCSS トグルスイッチ 9選が近道です。

01レバートグルスイッチ

金属レバーが支点を軸に左右へ傾いてオン・オフを切り替えるスイッチで、オーディオラックの電源パネルや機器設定画面のモード切替に似合います。

レバートグルスイッチ
新しいタブで開く

01 · レバートグルスイッチ — 計測 累積面積 4.119% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="switch"aria-checkedcubic-bezier(.2,1.4,.4,1)linear-gradient

支点の上で回るレバーアーム

レバーアーム(.tl__armWrap)は transform-origin: 50% 100% で根元のドームの中心を軸に据え、rotate(-24deg) と rotate(24deg) の間だけを往復し、点灯するとLEDの点とONの文字、状態を表す語がそろって変わります。

.tl__armWrap {
  position: absolute; left: calc(50% - 5px); bottom: 15px; width: 10px; height: 80px;
  border-radius: $r-pill; transform-origin: 50% 100%; transform: rotate(-24deg);
  transition: transform $duration $easing;
  background: linear-gradient(90deg,
    rgba($stage-ink, .55), rgba($subject-cream, .82) 42%,
    rgba($subject-cream, .95) 50%, rgba($subject-cream, .70) 62%, rgba($stage-ink, .50));
  box-shadow: 0 4px 8px rgba($stage-ink, .55);
}

オーバーシュートで生きるスプリングの手応え

指を離した瞬間の弾みは cubic-bezier(.2, 1.4, .4, 1) のオーバーシュート1回で作られ、プレビューでも38%で28度を行き過ぎてから48%で24度へ戻る手応えがそのまま入っています(動いたフレーム18/23)。

@keyframes tlSwing {
  0%   { transform: rotate(-24deg); }
  8%   { transform: rotate(-24deg); }
  38%  { transform: rotate(28deg); }
  44%  { transform: rotate(23deg); }
  48%  { transform: rotate(24deg); }
  68%  { transform: rotate(24deg); }
  88%  { transform: rotate(-28deg); }
  94%  { transform: rotate(-23deg); }
  97%  { transform: rotate(-24deg); }
  100% { transform: rotate(-24deg); }
}

02ロッカー電源スイッチ

上面にI、下面にOを持つ2つのキャップでできた実物どおりの電源スイッチのモックアップで、機器の裏面スイッチやワイヤレスマウスの設定カードに合わせた手触りです。

ロッカー電源スイッチ
新しいタブで開く

02 · ロッカー電源スイッチ — 計測 累積面積 3.633% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="switch"aria-checkedtranslateYinset

押した側が沈み反対側が浮く構造

2つのキャップの初期値は translateY(-3px) と translateY(3px) で、オンのときにこの2値がちょうど入れ替わり、影のほうも内側に2枚という同じ構成のまま互いに反転します。

.rs__capTop {
  border-radius: $r-sm $r-sm 0 0;
  background: linear-gradient(180deg, $color, rgba($subject-ink, .10)), $color;
  transform: translateY(-3px);
  box-shadow: inset 0 -3px 6px rgba($subject-ink, .10), inset 0 2px 0 rgba($stage-paper, .85);
}
.rs__capBottom {
  border-radius: 0 0 $r-sm $r-sm;
  background: linear-gradient(180deg, rgba($subject-ink, .05), rgba($subject-ink, .17)), $color;
  transform: translateY(3px);
  box-shadow: inset 0 4px 8px rgba($subject-ink, .32), inset 0 -1px 0 rgba($stage-paper, .40);
}

8フレームだけ動く短い呼吸

プレビューの時間表は12〜18%の押し込みと58〜64%の戻しにだけ動きを寄せ、残りの区間はホールドの時間なので、この面は動いたフレームが8/23と9つの中でいちばん静かです。

@keyframes rsPressTop {
  0%   { transform: translateY(-3px); box-shadow: inset 0 -3px 6px rgba($subject-ink, .10), inset 0 2px 0 rgba($stage-paper, .85); }
  12%  { transform: translateY(4px);  box-shadow: inset 0 5px 9px rgba($subject-ink, .38), inset 0 -1px 0 rgba($stage-paper, .35); }
  18%  { transform: translateY(3px);  box-shadow: inset 0 4px 8px rgba($subject-ink, .32), inset 0 -1px 0 rgba($stage-paper, .40); }
  46%  { transform: translateY(3px);  box-shadow: inset 0 4px 8px rgba($subject-ink, .32), inset 0 -1px 0 rgba($stage-paper, .40); }
  58%  { transform: translateY(-4px); box-shadow: inset 0 -4px 7px rgba($subject-ink, .08), inset 0 3px 0 rgba($stage-paper, .9); }
  64%  { transform: translateY(-3px); box-shadow: inset 0 -3px 6px rgba($subject-ink, .10), inset 0 2px 0 rgba($stage-paper, .85); }
  100% { transform: translateY(-3px); box-shadow: inset 0 -3px 6px rgba($subject-ink, .10), inset 0 2px 0 rgba($stage-paper, .85); }
}

03回転ダイヤルノブ

ノブをドラッグするか矢印キーで回して明るさを0から100まで選ぶ調節器で、role="slider" が最小・最大・現在値をまとめて読み上げます。

回転ダイヤルノブ
新しいタブで開く

03 · 回転ダイヤルノブ — 計測 累積面積 13.382% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="slider"aria-valuenowpointermoveconic-gradient

conic-gradient で描く目盛りリングと進行アーク

目盛りは repeating-conic-gradient が27度間隔で打って可動幅の両端まで11本並び、進行アークは conic-gradient が --fill(0〜1)の分だけ270度の扇を埋めますが、--fill は @property で数値として登録してあるので値が数値のまま補間されます。

.dk__ticks {
  position: absolute; inset: 0; border-radius: 50%;
  background: repeating-conic-gradient(from 224deg,
    rgba($subject-ink, .85) 0 1.6deg, rgba($subject-ink, 0) 1.6deg 27deg);
}
.dk__arc {
  position: absolute; inset: 7px; border-radius: 50%;
  background: conic-gradient(from 225deg,
    rgba($subject-ink, .82) 0 calc(var(--fill, 0) * 270deg),
    rgba($subject-ink, .10) calc(var(--fill, 0) * 270deg) 270deg,
    rgba($subject-ink, 0) 270deg 360deg);
}

atan2 の引数を入れ替える理由

針の角度は Math.atan2(x - cx, cy - y) で真上を0度・時計回りを正に合わせており、引数を元の順のままにすると角度の原点と向きが画面とずれます(この面のアニメーションは9本で9つの中で最多、累積面積は13.382%です)。

function apply() {
  var angle = MIN + (MAX - MIN) * value / 100; // パーセント値を角度へ
  root.style.setProperty('--angle', String(Math.round(angle)));
  root.style.setProperty('--fill', (value / 100).toFixed(3));
  knob.setAttribute('aria-valuenow', String(value));
  knob.setAttribute('aria-valuetext', VAL_TEXT.replace('{v}', String(value)));
  valueEl.textContent = value + '%';
}
function angleOf(x, y) {
  var r = ring.getBoundingClientRect();
  var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
  var deg = Math.atan2(x - cx, cy - y) * 180 / Math.PI; // 真上が0度、時計回りが正
  return Math.max(MIN, Math.min(MAX, deg));
}

04プッシュフェーダー

チャンネルフェーダーを左右へ押すミキサーコンソールの調節器で、0から10までの値は aria-valuenow と目盛りの下の数字の両方に現れます。

プッシュフェーダー
新しいタブで開く

04 · プッシュフェーダー — 計測 累積面積 16.537% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="slider"aria-valuenowpointerdowntranslateX

移動と潰れを2つの要素へ分けた設計

キャップの移動はラッパーの translateX、端でのクッションの潰れはキャップ自身の scaleX(.82) に分けてあり、scaleX が自分の中心を基準に効く性質があるため1つの要素へまとめると端の潰れが中央側へ寄った見た目になります。

@keyframes pfTravel {
  0%   { transform: translateX(0); }
  5%   { transform: translateX(0); }
  44%  { transform: translateX(calc(100% - 48px)); }
  55%  { transform: translateX(calc(100% - 48px)); }
  89%  { transform: translateX(0); }
  100% { transform: translateX(0); }
}
@keyframes pfCushion {
  0%   { transform: scaleX(1); }
  3%   { transform: scaleX(.82); }
  9%   { transform: scaleX(1); }
  44%  { transform: scaleX(1); }
  48%  { transform: scaleX(.82); }
  54%  { transform: scaleX(1); }
  89%  { transform: scaleX(1); }
  93%  { transform: scaleX(.82); }
  97%  { transform: scaleX(1); }
  100% { transform: scaleX(1); }
}

端に触れたときだけ鳴るクッション

クッションはポインターが実際に端へ届いた瞬間だけ180msのあいだ付いてから離れ、累積面積16.537%は2つの調節器のうち広く動くほうの値です。

  function travel() { return Math.max(0, wrap.clientWidth - cap.offsetWidth); }
  function human() { root.classList.remove('is-demo'); }
  function cushion() {
    cap.classList.add('is-press');
    clearTimeout(pressTimer);
    pressTimer = setTimeout(function () { cap.classList.remove('is-press'); }, 180);
  }
  function apply(px) {
    wrap.style.setProperty('--x', px + 'px');
    lastPx = px;
    value = travel() > 0 ? Math.round(px / travel() * MAX) : 0;
    well.setAttribute('aria-valuenow', String(value));
    nums.forEach(function (n, i) { n.classList.toggle('is-on', i === value); });
  }
  apply(value / MAX * travel());

05緊急バングボタン

赤いドームを押すと遠近の効いたシーンの奥へ translateZ(-120px) で沈み、警告灯が点き、押されたことは待機・作動の文字と aria-pressed で伝わります。

緊急バングボタン
新しいタブで開く

05 · 緊急バングボタン — 計測 累積面積 4.902% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-pressedperspectivetranslateZbox-shadow

シーンの奥行きを作る3枚

ボタンのプラスチックの質感はハイライトの radial-gradient 2枚と厚い box-shadow で積み重ねてあり、台座の影はボタンと同じタイミングで scaleX(.55) へ縮んで奥行きを一緒に語ります。

.bb__well { position: relative; display: flex; justify-content: center; perspective: 600px; }
.bb__shadow {
  position: absolute; left: 50%; bottom: -8px; width: 92%; height: 14px;
  transform: translateX(-50%) scaleX(1); border-radius: 50%;
  background: rgba($subject-ink, .34);
  transition: transform $duration $easing, opacity $duration $easing;
}
.bb__btn {
  position: relative; display: block; width: clamp(88px, 24vw, 108px); height: clamp(64px, 17vw, 80px);
  border: 0; border-radius: 50%; padding: 0; cursor: pointer; touch-action: manipulation;
  background:
    radial-gradient(120% 90% at 32% 22%, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0) 46%),
    radial-gradient(140% 120% at 50% 118%, rgba($subject-ink, .5), rgba($subject-ink, 0) 52%),
    $color;
  box-shadow: 0 10px 0 rgba($subject-ink, .3), 0 14px 18px rgba($subject-ink, .35), inset 0 2px 3px rgba(255, 255, 255, .55), inset 0 -4px 6px rgba($subject-ink, .45);
  transition: transform $duration $easing, box-shadow $duration $easing;
}

沈んで戻る時間表

プレビューではボタンが6%で降り始めて14%で底に触れ、警告灯の点滅(12〜47%)が続きますが、強度平均35.4という落ち着いたテンポは非常停止のコントロールによく合います。

@keyframes bbSink {
  0%, 6%   { transform: translateZ(0); }
  14%, 46% { transform: translateZ(-120px); }
  54%, 100% { transform: translateZ(0); }
}
@keyframes bbShadow {
  0%, 6%   { transform: translateX(-50%) scaleX(1); opacity: 1; }
  14%, 46% { transform: translateX(-50%) scaleX(.55); opacity: .55; }
  54%, 100% { transform: translateX(-50%) scaleX(1); opacity: 1; }
}

06引いて開けるプルノブ

ノブを下へ引くと軸が伸び、引きが30ピクセルを超えるとロックが外れて蓋が開き、指を離すと元へ戻る、機械の蓋や金庫のロック解除ウィジェット向けの手触りです。

引いて開けるプルノブ
新しいタブで開く

06 · 引いて開けるプルノブ — 計測 累積面積 20.842% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-expandedpointerupscaletransition

引き1つが動かす移動と伸び

カスタムプロパティ --pull ひとつが translateY でノブを下ろすと同時に、scale(1, calc(1 + var(--pull) / 220)) で上の原点(transform-origin: 50% 0)から縦へ伸ばします。

.pk__knob {
  position: relative; width: clamp(56px, 15vw, 68px); height: clamp(36px, 10vw, 44px);
  border: 0; border-radius: $r-control; padding: 0; cursor: grab; touch-action: none;
  transform-origin: 50% 0; // 軸に付いているので伸びは上の原点から(非中心原点の理由)
  transform: translateY(calc(var(--pull, 0) * 1px)) scale(1, calc(1 + var(--pull, 0) / 220));
  background:
    radial-gradient(120% 140% at 30% 0%, rgba($stage-paper, .35), rgba($stage-paper, 0) 50%),
    $subject-ink;
  box-shadow: 0 6px 10px rgba($subject-ink, .4), inset 0 1px 0 rgba($subject-cream, .3), inset 0 -3px 0 rgba(0, 0, 0, .5);
  transition: transform $dur-base $easing;
}

しきい値とタップを分ける境目

ドラッグがいちばん深いときこの面は一瞬で面積18.543%を占めて9つの中で最も広く、しきい値の30ピクセルを超えて初めてロックが外れ、±6ピクセル以内の動きはタップとして開閉を切り替えます。

  knob.addEventListener('pointermove', function (e) {
    if (!dragging) { return; }
    var dy = e.clientY - startY;
    setPull(Math.min(PULL_MAX, Math.max(0, dy)));
  });
  knob.addEventListener('pointerup', function (e) {
    if (!dragging) { return; }
    dragging = false;
    var moved = e.clientY - startY;
    root.classList.remove('is-drag');
    setPull(0);
    if (moved >= THRESHOLD) { setOpen(true); }
    else if (moved >= -6 && moved < 6) { setOpen(knob.getAttribute('aria-expanded') !== 'true'); }
    if (knob.hasPointerCapture(e.pointerId)) { knob.releasePointerCapture(e.pointerId); }
  });

07シーソースイッチ

中央に支点を置き、左右のパッドを交互に押してAとBを選ぶ選択器で、選択は傾いた角度・沈んだパッド・下の文字の3つで読めます。

シーソースイッチ
新しいタブで開く

07 · シーソースイッチ — 計測 累積面積 19.144% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-pressedrotatetransform-origintransition

板が回る軸の位置

板の回転軸は transform-origin: 50% 84%、つまり台座の半球に触れる点に取ってあり、左右±8度の回転そのものが選択を示します。

.ss__plank {
  position: absolute; left: 0; right: 0; top: clamp(20px, 6.5vw, 26px); height: clamp(42px, 12vw, 52px);
  display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;
  padding: 0 $sp-2;
  background: linear-gradient(180deg, rgba($subject-cream, .55), rgba($subject-cream, 0) 32%, rgba($stage-ink, .18)), $color;
  border: 1px solid rgba($stage-ink, .25); border-radius: $r-control;
  box-shadow: 0 10px 18px rgba($stage-ink, .26), inset 0 1px 0 rgba($subject-cream, .7);
  transform: rotate(-8deg);
  transform-origin: 50% 84%;
  transition: transform $duration $easing;
}

data-side ひとつで分かれる画面

角度もパッドの沈みも文字も data-side 属性ひとつから分かれるのでJSが画面の文字を直接触らずに済み、累積面積19.144%のゆとりある動きがA/Bの選択器に合っています。

function pick(side) {
  root.classList.remove('is-demo');
  root.setAttribute('data-side', side);
  padA.setAttribute('aria-pressed', side === 'a' ? 'true' : 'false');
  padB.setAttribute('aria-pressed', side === 'b' ? 'true' : 'false');
}
padA.addEventListener('click', function () { pick('a'); });
padB.addEventListener('click', function () { pick('b'); });
root.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });

08ブレーカースイッチ

配電盤モジュール全体がひとつの role="switch" ボタンなのでハンドルを倒す行為がそのままクリックになり、倒せば一度引っかかってから重く落ちてOFF表示が点きます。

ブレーカースイッチ
新しいタブで開く

08 · ブレーカースイッチ — 計測 累積面積 7.954% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="switch"aria-checkedrepeating-linear-gradientsteps(1,end)

steps 一段で作る重さ

ハンドルの実際の動きは transition のタイミングを steps(1,end) の1段へ置いて落としており、連続イージングにすると手が押し切ったあともハンドルが少し耐えてからするりと下りる機械の重さが消えます。

.bk__lever {
  position: absolute; left: 50%; top: 50%; width: clamp(34px, 9vw, 40px); height: clamp(76px, 20vw, 88px);
  background: linear-gradient(90deg, rgba($stage-ink, .32), rgba($subject-cream, .5) 28%, rgba($subject-cream, .16) 55%, rgba($stage-ink, .38)), $color;
  border: 1px solid rgba($stage-ink, .45); border-radius: $r-sm;
  box-shadow: 0 8px 14px rgba($stage-ink, .55), inset 0 1px 0 rgba($subject-cream, .85);
  transform: translate(-50%, -50%) rotate(-26deg);
  transform-origin: 50% 50%;
  transition: transform $duration steps(1,end);
}

カット地点を真ん中へ置いた時間表

プレビューでは手が22%と64%でハンドルを押し切り、ハンドルは27%と70%のカットで±26度を反転し、板が2ピクセル沈む bkThud が重さを加えます(動いたフレーム16/23)。

@keyframes bkLever {
  0%, 25%   { transform: translate(-50%, -50%) rotate(-26deg); }
  27%, 68%  { transform: translate(-50%, -50%) rotate(26deg); }
  70%, 100% { transform: translate(-50%, -50%) rotate(-26deg); }
}
@keyframes bkHand {
  0%        { transform: translateY(-20px); }
  12%       { transform: translateY(0); }
  22%       { transform: translateY(16px); }
  30%, 52%  { transform: translateY(0); }
  64%       { transform: translateY(16px); }
  72%       { transform: translateY(0); }
  84%, 100% { transform: translateY(-20px); }
}

09カセットイジェクトボタン

イジェクトキーを押すとテープが飛び出して一度揺れて止まり、もう一度押すと収まる、レトロなミュージックプレイヤーの出し入れのインタラクションです。

カセットイジェクトボタン
新しいタブで開く

09 · カセットイジェクトボタン — 計測 累積面積 19.301% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-pressedcubic-bezier(.2,1.8,.3,1)translateYkeyframes

オーバーシュートが作る揺れ

飛び出す力は cubic-bezier(.2, 1.8, .3, 1) のオーバーシュートで、最初の弾みが目標の高さ --pop(-46px)を行き過ぎてから戻り、狭い画面(340px以下)では --pop が-26pxと浅くなります。

.cs__tape {
  position: absolute; left: 50%; bottom: $sp-2;
  width: clamp(150px, 42vw, 174px); height: clamp(84px, 23vw, 96px); box-sizing: border-box;
  background: linear-gradient(180deg, rgba($subject-cream, .35), rgba($subject-cream, 0) 30%, rgba($stage-ink, .2)), $color;
  border: 1px solid rgba($stage-ink, .35); border-radius: $r-sm;
  box-shadow: 0 10px 16px rgba($stage-ink, .5), inset 0 1px 0 rgba($subject-cream, .9);
  transform: translateX(-50%);
  // 収納(0)から取り出し(var(--pop))へ — オーバーシュートのイージングが揺れを作る
  transition: transform $duration cubic-bezier(.2,1.8,.3,1);
}

取り出しと収納の時間表

プレビューは20%・28%・36%の3度の揺れを経て42%で止まり86%で完全に収まりますが、強度平均113.7は9つの中でいちばん強い値です。

@keyframes csPop {
  0%, 6%    { transform: translateX(-50%) translateY(0); }
  20%       { transform: translateX(-50%) translateY(calc(var(--pop) - 12px)); }
  28%       { transform: translateX(-50%) translateY(calc(var(--pop) + 6px)); }
  36%       { transform: translateX(-50%) translateY(calc(var(--pop) - 4px)); }
  42%, 72%  { transform: translateX(-50%) translateY(var(--pop)); }
  86%, 100% { transform: translateX(-50%) translateY(0); }
}

どこで壊れるか — コードを移すと死ぬ動き

遠近は直接の親へ掛けないと効かない

05の沈み込みを作る宣言は transform: translateZ(-120px) の1行ですが、この1行は観察者までの距離を決める perspective がなければ画面上では何も起こしません。ソースでは perspective: 600px がボタンを直接抱える .bb__well に、06の蓋が回る perspective: 520px が .pk__lidWrap に掛かっています。CSSの perspective は直接の子にしか効かないので、コードを写すときにボタンをもう1枚の要素で包んだり、遠近をさらに上の祖先へ移した瞬間に沈み込みがまるごと消えます。1段はさむなら、その箱に transform-style: preserve-3d を設定する必要があります。実際に .is-demo を外した05を押して読んだ値もこの道筋どおりで、ボタンの変換行列は matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -120, 1) とZ軸-120を含み、もう一度押すと none へ戻ります(run/013/_probe/interact_04_06.json)。根拠はMDN の perspective 文書です。

ドラッグがスクロールと争う場所

03・04・06のように指に追従して動く操作要素には touch-action: none が1行ずつ添えてあります。この1行がないと、タッチでドラッグを始めた瞬間にブラウザがスクロールを奪いにきて pointermove が数フレームで切れ、ポインターキャプチャもこの争いを解決してはくれません(MDN Pointer events)。css 切り替え スイッチ を他所のレイアウトへ移し替えるときは、こうして添えてある1行まで一緒に持っていってはじめて生きたまま動きます。06を実際に引いてみると、ノブが44ピクセル下りて縦に1.2倍伸びたあと元へ戻るところまで同じファイルに記録されています。コントラストも .is-demo を外した状態で文字36か所を測り直した結果、AA(4.5:1)を下回る場所は一つもありませんでした(測定スクリプトは run/013/_probe_contrast/contrast.py)。

測った場所 文字 背景 コントラスト 判定
01 状態語「切」 #ded7c9 #252226 11.03:1 AA
03 目盛りの数字 0 #17141a #ff4d1f 5.5:1 AA
04 点灯した目盛りの数字 5 #17141a #ffffff 18.24:1 AA
05 警告灯の文字 #17141a #fff7e6 17.11:1 AA
07 パッド下の文字 #58565a #ffffff 7.27:1 AA

いちばん低いのは03・07のオレンジ舞台(#ff4d1f)に載る墨(#17141a)の文字で5.5:1、2面とも舞台上の文字を丸ごと墨で打ったおかげで基準を超えました。9つのSCSS原本とコンパイル済みのCSS、項目ごとのReact版を収めた圧縮ファイルのパスワードは 8a45qw2n です。

差し替え3つとアクセシビリティ

SCSS 冒頭の変数3行で雰囲気を替える

9つの style.scss は頭に同じ変数3行($duration・$easing・$color)を置き、下の宣言はすべてこの3つだけを参照します。01はイージングの枠に $ease-pop の値 cubic-bezier(.2, 1.4, .4, 1) を直接書き、05と06は3つめの枠をそれぞれ $error・$subject-blue として使っています。

名前 替える値 感触
duration $dur-quick 220ms → 120ms レバーとロッカーが機械部品のようにパキッと切れて落ちる
easing $ease-spring → $ease-pop イジェクトとバングボタンが飛び出す力がひと回り強くなる
color $stage-paper → $stage-yellow プラスチックの本体がクリームから工業用イエローへ変わる

動きを減らしたときに残る一枚

prefers-reduced-motion: reduce では9つのループがすべて止まり、それぞれ別の静止画が残ります。01は左へ傾いたレバーと消えたLED、02は上面が浮いた既定のロッカー、03は0%の針と実際の値の文字、04は真ん中の値5のキャップ、05は上がったままのボタン、06は閉じた蓋、07は左へ傾いた板、08は入(ハンドルは上)、09は収まったテープです。reduce ブロックの宣言ごとに !important を付けた理由は .is-demo が付く側のセレクタのほうが詳細度が高いからで、基準はWCAG 2.2 の相互作用アニメーション解説にあります。

色がなくても読める仕掛け

9つとも色を抜いた画面で状態が読めます。01・02はLEDの点とON・OFFの文字に状態語を role="status" で添え、03・04は値を数字と aria-valuenow で、03はさらに aria-valuetext が「0%」の形で読み上げられます。05は待機・作動の文字に aria-pressed、06はロック・開の文字に aria-expanded、07は傾きと沈みとA/Bの文字に aria-pressed を持たせ、08・09はハンドルの角度とテープの位置に状態の一文を添えました。スイッチとスライダーの役割の定義はMDN switch roleとMDN slider roleにそのまま従っています。

よくある質問

html css スイッチ のコードをそのままコピーして使えますか

使えます。圧縮ファイルには項目ごとにSCSS原本、コンパイル済みのCSS、HTML、React版(コンポーネントと module.scss)が入っているので、貼り付ければそのまま動きます。SCSS原本を使うときはファイル冒頭の @use のパスだけプロジェクトへ合わせれば十分で、アイコンだけの小さなボタンが必要になったらフローティング ボタン アイコン 9選が相方です。

フォーム送信にはどうつなぎますか

このコントロールの状態は、見た目を分ける属性ひとつだけで切り替わります。01・02・08は aria-checked、03・04は aria-valuenow、05・07・09は aria-pressed、06は aria-expanded なので、送信の時点でこの値を読んで隠しinputへ移せば済みます。JSなしでフォームだけで済ませたいなら、<input type="checkbox"> をラベルで包んでチェック状態を見た目に映す作りへ差し替えてください。書式全体を同じ手触りでそろえるときは入力欄 デザイン 9選が続きになります。

ダイヤルの回転範囲を270度より広げるには

3か所が一緒に動く必要があります。JSの MIN・MAX 定数(-135・135)が針と値の範囲で、目盛りリングの repeating-conic-gradient(27度間隔、目盛り11本)と進行アークの conic-gradient(270度の扇)が描き出す範囲です。この設計は下側の90度を空けてあるため、360度近くまで広げるとノブが下の数字を隠してしまいます。範囲を広げるときは、目盛りの数字を値の面へ移す作業まで一緒に計画しておくと安全です。

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

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