建立切換元件

說明如何建構回應式及無障礙切換元件的基本總覽。

在這篇文章中,我想分享如何建構切換元件的想法。 立即試用

示範

如果您喜歡看影片,請參考這篇文章的 YouTube 版本:

總覽

切換函式與核取方塊類似 但明確代表「開啟」和「關閉」狀態的布林值

此示範內容的大多數使用 <input type="checkbox" role="switch"> 它的優點是不需要 CSS 或 JavaScript 且功能完全可用載入 CSS 支援從右至左 語言、產業、動畫等載入 JavaScript 可立即切換 可拖曳和實體

自訂屬性

下列變數代表切換鈕的各個部分,以及 只要設定成「自動重新啟動」 和「在主機維護期間」選項即可做為頂層類別,.gui-switch 包含使用的自訂屬性 整個元件子項,以及集中化的進入點 。

追蹤

長度 (--track-size)、邊框間距和兩種顏色:

.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>,合併關係,避免使用核取方塊和標籤關聯 同時讓使用者能與標籤互動 來切換輸入鈕

A 罩杯
隨機且未加上樣式的標籤和核取方塊

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

<input type="checkbox"> 預先建構了 API狀態。 會管理 checked敬上 屬性和 input 事件 例如 oninputonchanged

版面配置

FlexboxGridcustom 資源 維持元件的樣式能以集中方式處理值、提供名稱 以便進行模稜兩可的計算或區域,並啟用小型自訂屬性 可輕鬆自訂元件的 API。

.gui-switch

切換按鈕的頂層版面配置為 Flexbox。.gui-switch 類別包含 子項用來計算其依附元件的私有和公開自訂屬性 版面配置

Flexbox 開發人員工具與水平標籤和切換按鈕重疊,顯示版面配置
太空的分佈情形

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

擴充和修改 Flexbox 版面配置,就如同變更任何 Flexbox 版面配置。 例如,將標籤放在切換鈕上方或下方,或是 flex-direction:

Flexbox 開發人員工具與垂直標籤和切換鈕重疊。

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

追蹤

核取方塊輸入會移除一般開關樣式,並將其樣式設為開關軌 appearance: checkbox,改為提供其自己的大小:

疊加在切換軌道上的格狀開發人員工具,顯示已命名的格線軌
名為「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;
}

同時會建立一個賽道以一個格狀格線軌跡區域建立路徑 聲明。

縮圖

樣式 appearance: none 也會移除 。此元件會使用 pseudo-element:checked 對輸入值套用虛擬類別 會替換這個視覺指標

拇指是附加至 input[type="checkbox"] 的虛擬元素子項, 堆疊在曲目上方,而非顯示在曲目下方,聲明格線區域 track:

開發人員工具:在 CSS 格線內,顯示虛擬元素指標。

.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);
}

切換測試群組的各種自訂選項如下: 自訂屬性已新增 border: none,因為 appearance: none 不會 取消勾選所有瀏覽器核取方塊的邊框。

縮圖

拇指元素已位於右側 track,但需要圓形樣式:

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

醒目顯示圓形拇指虛擬元素的開發人員工具。

互動

使用自訂屬性來為會懸停顯示的互動做好準備 高亮度和拇指位置改變。使用者偏好設定 轉換前檢查功能 動態或懸停醒目顯示樣式

.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));
}

我們現在可免費變更 CSS 和虛擬類別中的 --thumb-position 核取方塊元素中。由於我們先前已有條件地在這個元素上設定 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 元素為 只有一種樣式,也就是 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 中沒有邏輯屬性轉換 也可能永遠小艾想要使用自訂屬性值 反轉百分比,讓使用者能管理我們自訂的營業地點 邏輯轉換的邏輯我在這次切換機會也使用相同的技巧 結果非常好:

.gui-switch {
  --isLTR: 1;

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

名為 --isLTR 的自訂屬性一開始包含 1 的值,表示該屬性為 true,因為預設版面配置是從左到右。接著使用 CSS 虛擬類別 :dir() 當元件位於從右到左的版面配置中時,值會設為 -1

在轉換的 calc() 內使用 --isLTR,將 --isLTR 用於行動:

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

現在,垂直切換鈕應採用相反方向的旋轉角度 都是由右至左的版面配置所設計

拇指虛擬元素上的 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 狀態。在這個狀態下,輸入內容「track」 背景設為使用中的色彩,而拇指位置設為 。

.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 自訂屬性。

因為在顯示這段指令碼之前,元件就已能 100% 正常運作 但要維持既有的運作模式,需要花費不少心力,例如 按一下標籤即可切換輸入模式我們的 JavaScript 不應在 導致現有功能無法負擔

touch-action

拖曳是一種自訂手勢,非常適合用來 touch-action 福利。切換到橫向手勢時 交由指令碼處理,或是擷取到垂直切換的垂直手勢 變數。透過 touch-action,我們可以告訴瀏覽器要處理哪些手勢 以便指令碼在不競爭的情況下處理手勢。

下列 CSS 會指示瀏覽器在指標手勢從 在這個切換軌中,處理垂直手勢、只針對橫向執行任何操作 對象:

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

所需結果為水平手勢,且不會同時平移或捲動 頁面。指標可以從輸入內容中垂直捲動,並捲動 但橫向是自訂的

像素值樣式公用程式

在設定和拖曳期間,需要擷取各種計算的數字值 從元素擷取出來下列 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() 如何接受第二個引數 (目標虛擬元素)。十分簡潔,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-transition-duration暫時設為 0s,正在移除內容 那樣會是很緩慢的互動

dragEnd

為了讓使用者能夠拖曳到靠近開關的位置再放開, 必須註冊的全域視窗事件:

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

  dragEnd(event)
})

我認為在確保使用者能自由拖曳或 相關介面的智慧功能已充分考量這一點沒什麼時間也沒關係 切換後,在開發階段需要審慎評估 上傳資料集之後,您可以運用 AutoML 自動完成部分資料準備工作

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 結構的關係,拖曳手勢造成一些程式碼債 主要來說是將輸入內容納入標籤中標籤,即父項 元素就會在輸入完成後收到點擊互動。結尾處 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 挑戰 到目前為止!現在你知道我怎麼了,這樣會如何 🙂?

讓我們來體驗多元的方法,瞭解透過網路建立內容的所有方式。 建立示範、將 Twitter 推文連結,我們就會為您新增 前往下方的社群重混專區!

社群重混作品

資源

從以下網站找出 .gui-switch 原始碼: GitHub