switch コンポーネントの作成

レスポンシブでアクセスしやすい switch コンポーネントを構築する方法の基本的な概要。

この投稿では、Switch コンポーネントを構築する方法についての考え方をご紹介します。 デモをお試しください

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> デモ

動画で視聴したい場合は、この投稿の YouTube バージョンをご利用ください。

概要

スイッチはチェックボックスに似た働きをする 明示的に表現します。

このデモでは、大部分に <input type="checkbox" role="switch"> を使用します。 機能があり、CSS や JavaScript をコードに追加する必要がない 完全に機能し アクセスできるようにしますCSS の読み込みによる右から左へのサポート 多岐にわたります。JavaScript を読み込むと切り替えられます ドラッグ可能で具体的なものにします。

カスタム プロパティ

次の変数は、スイッチのさまざまな部分と、 。トップレベル クラスである .gui-switch には、使用されるカスタム プロパティが含まれています。 一元化された構成を行うためのエントリ ポイントと、 カスタマイズが可能です。

トラック

長さ(--track-size)、パディング、2 色:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

サムネイル

サイズ、背景色、操作のハイライトの色:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

モーションの軽減

明確なエイリアスを追加して繰り返しを減らすため、 PostCSS を使用すると、メディアクエリをカスタム プロパティに プラグインをベースとする メディアクエリの仕様 5:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

マークアップ

<input type="checkbox" role="switch"> 要素を <label> - チェックボックスとラベルの関連付けを回避するために関係をバンドル ユーザーがラベルを操作して目的の操作を行えるようにし、 入力を切り替えます。


自然でスタイル化されていない
ラベルとチェックボックスがあります

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> には、 API および state.「 ブラウザが checked プロパティと input イベントoninputonchanged など)。

レイアウト

Flexbox gridcustom プロパティは このコンポーネントのスタイルを維持するうえで 重要な役割を果たします値を一元化し、名前を付ける あいまいな計算や面積に調整でき、小さなカスタム プロパティ コンポーネントを簡単にカスタマイズできる API。

.gui-switch

このスイッチの最上位レイアウトはフレックスボックスです。クラス .gui-switch には、 子がプロパティの計算に使用するプライベートおよびパブリックのカスタム プロパティ できます。

水平ラベルとスイッチにオーバーレイし、レイアウトを表示している Flexbox DevTools
分散します。

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

Flexbox レイアウトの拡張と変更は、Flexbox レイアウトの変更に似ています。 たとえば、スイッチの上または下にラベルを配置したり、 flex-direction:

垂直ラベルとスイッチの上に重ねて表示されている Flexbox DevTools。

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

トラック

チェックボックス入力のスタイルは、スイッチ トラックとして設定されています。 appearance: checkbox を使用し、代わりに独自のサイズを指定します。

スイッチ トラックの上に重ねて表示された、名前付きのグリッド トラックを表示する Grid DevTools
「track」という名前が付けられた地域が含まれます。

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

トラックでは、親指で操作できる 1 つのセルのグリッドのトラック領域も 1 つずつ作成されます。

サムネイル

スタイル appearance: none は、 できます。このコンポーネントでは 疑似要素:checked 入力に対する疑似クラス 置き換えます。

つまみは input[type="checkbox"] にアタッチされる疑似要素の子であり、 グリッド領域を占有することで、トラックの下ではなく上に配置されます。 track:

CSS グリッド内に配置された疑似要素のつまみを表示している DevTools。

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

スタイル

カスタム プロパティを使用すると、色に適応する汎用性の高いスイッチ コンポーネントを使用できます さまざまなオプションが用意されています

スイッチとそのライトモードのライトモードとダークモードの比較
構成されます。

タップ操作のスタイル

モバイルでは、ブラウザでタップのハイライト表示やテキスト選択機能がラベルに追加され、 できます。スタイルと視覚インタラクションに関するフィードバックに 悪影響が及びました このスイッチは必要ありません。数行の CSS で、それらの効果を取り除き、 独自の cursor: pointer スタイル:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

これらのスタイルは有用な視覚的効果があるため、必ずしも削除することをおすすめします。 やり取りのフィードバック。削除する場合は、必ずカスタムの代替テキストを指定してください。

トラック

この要素のスタイルは主に形状と色に関するものであり、この要素からアクセスします。 親 .gui-switch から、 cascade

トラックのサイズと色がカスタムになっているスイッチのバリエーション。

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

切り替えトラックのカスタマイズ オプションには、 カスタムプロパティを定義します。appearance: none に追加されていないため、border: none が追加されています すべてのブラウザのチェックボックスから枠線を削除します。

サムネイル

つまみ要素はすでに右側の track にありますが、円のスタイルが必要です。

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

円のつまみ疑似要素がハイライト表示された DevTools。

インタラクション

カスタム プロパティを使用して、カーソルを合わせるインタラクションを準備する サムネイル位置の変化などですまた、ユーザーの設定は、 チェックボックスをオンにしてから、 カーソルを合わせた時のハイライト表示のスタイルを選択します。

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

サムネイルの位置

カスタム プロパティは、親指の位置を決める単一のソース メカニズムを提供します。 おすすめします。自由に使えるトラックとつまみのサイズは、 以下のように、つまみをトラック内で適切にオフセットして維持します。 0%100%

input 要素は位置変数 --thumb-position を所有し、つまみ 疑似要素ではこれを translateX の位置として使用します。

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

--thumb-position を CSS と疑似クラスから自由に変更できるようになりました。 チェックボックス要素に指定します。この要素で以前に条件付きで transition: transform var(--thumb-transition-duration) ease を設定したため、これらの変更は 変更するとアニメーションで表示されることがあります。

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

この連携による連携はうまくいっていると思っています。thumb 要素: 1 つのスタイル(translateX の位置)のみを扱う場合です。この入力では、すべての 複雑さと計算が軽減されます

業種

サポートは、回転を追加する修飾子クラス -vertical を使用して行われました。 CSS が input 要素に変換します。

ただし、要素が 3D 回転してもコンポーネント全体の高さは変わりませんが、 ブロック レイアウトが崩れることがあります。--track-size--track-padding 変数。アプリケーションに必要な最小容量を計算する レイアウト内に配置されるように垂直方向のボタンを配置する必要があります。

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL)右から左

CSS の友人である Elad Schecter 氏と、私は 右から左への変換を行う CSS 変換を使用してサイドメニューをスライドアウトする 言語を切り替えることで 変数です。これは、CSS には論理プロパティ変換がないためです。 あり得ませんElad はカスタム プロパティ値を使うことを考えていました 独自のカスタム ディメンションを 1 か所で管理できるよう、 記述できますこの切り替えでも同じ手法を使いましたが、 うまくいったと思う:

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

--isLTR というカスタム プロパティの初期値は 1 です。つまり、 true になります。次に、CSS を使用して 疑似クラス :dir() コンポーネントが右から左へのレイアウト内にある場合、値は -1 に設定されます。

--isLTR を動作させるには、変換内の calc() 内で使用します。

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

垂直スイッチの回転が反対側の位置になります レイアウトで必要な値の数です。

thumb 疑似要素の translateX 変換も、次のように更新する必要があります。 次の反対側の要件を考慮してください

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

このアプローチでは、論理 CSS のようなコンセプトに関するすべてのニーズを解決することはできませんが、 構築できます。このサービスには、 多くの組織向けの DRY 原則 構築できます

組み込みの input[type="checkbox"] を使用するには、これが必要です さまざまな状態(:checked:disabled:indeterminate:hover:focus は意図的に放置されており、 そのオフセットに対してのみ調整が行われます。フォーカス リングは Firefox では見栄えが良く、 Safari:

Firefox と Safari で、スイッチにフォーカス リングが表示されているスクリーンショット。

オン

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

この状態は on 状態を表します。この状態では、入力「トラック」は 背景はアクティブな色に設定され、つまみの位置は「 あります。

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

無効

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

:disabled ボタンを使用すると、見た目が変わるだけでなく、 要素 immutable.Interaction の不変性はブラウザからは解放されますが、 appearance: none を使用するため、視覚的状態にはスタイルが必要です。

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

ダークモードのスイッチを無効 / オン / オフの状態で表示
構成されます。

この状態は、ダークモードとライトモードの両方が無効で、かつ チェックされた状態を表します。スタイルとして、これらの状態を緩和するために最小限のスタイルを選択しました。 スタイルの組み合わせによるメンテナンスの負担を軽減できます。

不確定

忘れがちな状態は :indeterminate で、チェックボックスのいずれも持たない オフにします。これは楽しい状態で、魅力的で気取らない状態です。良い 状態の間に隠れた状態を示すことがあります。

チェックボックスを不確定に設定するのは簡単ではありません。設定できるのは JavaScript だけです。

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

トラックのつまみが
未定であることを示します。

私にとっては、この州は控えめで親しみやすいので、 中央にあるスイッチのつまみの位置を調整します。

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

カーソルを合わせる

マウスオーバー操作では、接続された UI を視覚的にサポートする必要があります。 インタラクティブな UI の方向を示します。このスイッチでは、 ラベルまたは入力にカーソルを合わせると、半透明のリングが表示されます。このカーソルを合わせると すると、インタラクティブなつまみ要素に方向が指示されます。

「ハイライト」効果は box-shadow で行われます。無効になっていない入力にカーソルを合わせると、--highlight-size のサイズを大きくします。ユーザーが動きに問題がなければ、box-shadow が遷移して大きくなります。ユーザーが動きに反応しない場合は、ハイライトがすぐに表示されます。

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

スイッチのインターフェースが物理サーバーをエミュレートしようとすると、不思議に感じるかもしれません。 インターフェース。特に、トラック内に円があるこの種のインターフェースです。iOS で正解 スイッチを左右にドラッグして 選択肢があります。反対に、ドラッグ ジェスチャーが操作中に 試みても何も起こりません。

ドラッグ可能な親指

つまみ疑似要素は .gui-switch > input から位置を受け取ります。 スコープが var(--thumb-position) の場合、JavaScript はインライン スタイル値を 入力により、つまみの位置が動的に更新され、つまみが追従しているように見える ポインタ操作です。ポインタを離したら、インライン スタイルと カスタム プロパティを使用して、ドラッグがオフとオンのどちらに近かったかを判断する --thumb-position。これがソリューションのバックボーンですポインタ イベント 条件付きでポインタの位置を追跡して、CSS カスタム プロパティを変更できます。

このスクリプトが表示される前は、コンポーネントは完全に動作していたため 既存の動作を維持するには ラベルをクリックして入力を切り替えます。JavaScript では google.com に 費用を節約できます

touch-action

ドラッグはカスタムのジェスチャーであるため、 touch-action の特典。このスイッチでは、横方向の操作を行うと スクリプトによって処理されるか、垂直方向の切り替えでキャプチャされた垂直方向のジェスチャーが処理されます。 あります。touch-action を使用すると、処理するジェスチャーをブラウザに伝えることができます。 これにより、スクリプトが競合なしで操作を処理できるようになります。

次の CSS は、ポインタによる操作が この切り替えトラック内では、垂直方向の移動を処理し、水平方向の操作は行いません。 あります。

.gui-switch > input {
  touch-action: pan-y;
}

目的の結果、水平方向の操作を行うと、 できます。ポインタは、入力内から垂直方向にスクロールし、 横長の動画はカスタムで処理されます

Google Pixel の価値スタイルのユーティリティ

セットアップ時やドラッグ時に、計算されたさまざまな数値を取得する必要がある できます。以下の JavaScript 関数は、計算されたピクセル値を返します。 CSS プロパティを指定できますこれは、次のようにセットアップ スクリプトで使用されます。 getStyle(checkbox, 'padding-left')

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

window.getComputedStyle() が 2 番目の引数であるターゲット疑似要素を受け入れる様子に注目してください。JavaScript が要素から、さらには疑似要素からも非常に多くの値を読み取ることができる点が優れています。

dragging

これはドラッグ ロジックの中核部分であり、注意すべき点がいくつかあります。 関数イベント ハンドラから呼び出す必要があります。

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

スクリプト ヒーローは state.activethumb です。このスクリプトが配置されている小さな円です。 ポインタとともに位置を指定できますswitches オブジェクトは Map() であり、 キーは .gui-switch であり、値はキャッシュに保存された境界とサイズであり、 スクリプトが効率的になります。右から左への表記は同じカスタム プロパティを使用して処理される CSS が --isLTR であり、それを使用してロジックを反転して続行できることを RTL をサポートしています。event.offsetX もデルタを含むため有益です。 親指の位置を決めるのに便利な値です。

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

以下の CSS の最終行では、thumb 要素で使用されるカスタム プロパティを設定します。この そうしないと、時間の経過に伴って値の割り当てが遷移しますが、 イベントにより一時的に --thumb-transition-duration0s に設定され、以下の内容が削除されました やり取りが遅くなっていたでしょう。

dragEnd

ユーザーがスイッチの外側までドラッグして離すには、 登録する必要があるグローバル ウィンドウ イベント:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

ユーザーが自由なドラッグ操作が可能になり、 インターフェースがスマートになるからです処理にそれほど時間がかからなかった ただし、開発段階で慎重に検討する必要があったため、 プロセスです

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

要素の操作が完了しました。確認された入力を設定する時間です プロパティを実装し、すべてのジェスチャー イベントを削除します。チェックボックスは state.activethumb.checked = determineChecked()

determineChecked()

dragEnd によって呼び出されるこの関数は、つまみ電流の位置を決定します。 はトラックの境界内であり、次の値以上である場合は true を返します。 少し進むと、

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

参考情報

初期の HTML 構造が原因で、ドラッグ操作に少々コードのデメリットが生じていました。 主に入力をラベルでラップします。ラベル(親であること) 入力の後にクリック操作を受け取ります。Deployment の dragEndのアクティビティで、padRelease()がおかしな音として検知されました 使用します。

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

後でクリックするラベルのチェックボックスをオフにし ユーザーが行った操作を確認します

もう一度やり直す場合は、JavaScript で DOM を調整することが考えられるかもしれません (ラベルのクリック自体を処理する要素を作成するため) 組み込みの動作とも競合しません

この種の JavaScript は私が書くのが最も好きで、管理したくありません 条件付きイベントのバブリング:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

まとめ

この小さなスイッチ コンポーネントが、すべての GUI の課題の中で最も大きな仕事になりました。 完了です。どのようにやり方をしたかわかったので、どのように感じますか? ‽ 🙂?

アプローチを多様化して、ウェブで構築するすべての方法を学びましょう。 デモを作成し、ツイートしてリンクを送ってください 下の [コミュニティリミックス]セクションに アクセスしてください

コミュニティ リミックス

リソース

.gui-switch のソースコードは GitHub