建構設定元件

基礎總覽:如何建構滑桿和核取方塊的設定元件。

在這篇文章中,我想分享如何為網頁建構「設定」元件,使其具備回應性、支援多種裝置輸入方式,且適用於各種瀏覽器。試用示範模式

示範

如果比較喜歡看影片,或是想預覽我們建構的 UI/UX,請觀看 YouTube 上的簡短逐步解說:

總覽

我將這個元件的各個層面分成以下幾節:

  1. 版面配置
  2. 顏色
  3. 自訂範圍輸入內容
  4. 自訂核取方塊輸入內容
  5. 無障礙考量
  6. JavaScript

版面配置

這是第一個完全採用 CSS 格線的 GUI 挑戰賽示範!以下是每個格線,並以 Chrome 格線開發人員工具醒目顯示:

彩色外框和間距疊加層,可協助顯示構成設定版面配置的所有方塊

僅供間隙

最常見的版面配置:

foo {
  display: grid;
  gap: var(--something);
}

我將這個版面配置稱為「僅適用於間距」,因為它只會使用格線在區塊之間新增間距。

有五種版面配置使用這項策略,如下所示:

以輪廓線醒目顯示垂直格線版面配置,並填滿間隙

包含每個輸入群組 (.fieldset-item) 的 fieldset 元素會使用 gap: 1px,在元素之間建立細線邊框。無須使用複雜的邊框解決方案!

填補空檔
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
邊框技巧
.grid {
  display: grid;

  & > .fieldset-item {
    background: var(--bg-surface-2);

    &:not(:last-child) {
      border-bottom: 1px solid var(--bg-surface-1);
    }
  }
}

自然格線換行

最複雜的版面配置是巨集版面配置,也就是 <main><form> 之間的邏輯版面配置系統。

將包裝內容置中

彈性方塊和格線都提供 align-itemsalign-content 的功能,處理包裝元素時,content 版面配置對齊方式會將空間分配給子項 (以群組形式)。

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

主要元素使用 place-content: center alignment 簡寫,因此子項會在單欄和雙欄版面配置中垂直和水平置中。

觀看上方影片,瞭解「內容」如何保持置中,即使發生換行也是如此。

重複自動調整 minmax

<form> 會為每個區段使用自適應格線版面配置。這個版面配置會根據可用空間,從一欄切換為兩欄。

form {
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
  align-items: flex-start;
  max-width: 89vw;
}

這個格線的 row-gap (--space-xl) 值與 column-gap (--space-xxl) 不同,可為回應式版面配置增添客製化風格。當欄堆疊時,我們希望間距較大,但不要像在寬螢幕上那樣大。

grid-template-columns 屬性會使用 3 個 CSS 函式:repeat()minmax()min()Una Kravets 撰寫了一篇很棒的版面配置網誌文章,並將這個概念稱為「RAM」

與 Una 的版面配置相比,我們的版面配置有 3 個特別新增項目:

  • 我們傳遞額外的 min() 函式。
  • 我們指定 align-items: flex-start
  • max-width: 89vw 樣式。

Evan Minto 在網誌文章「Intrinsically Responsive CSS Grid with minmax() and min()」中,詳細說明瞭額外的 min() 函式。建議您閱讀該文章。flex-start 對齊修正功能會移除預設的延展效果,因此這個版面配置的子項不需要等高,可以有自然的內建高度。如需瞭解如何新增對齊方式,請觀看這部 YouTube 影片。

max-width: 89vw值得在本貼文中稍作說明。 以下是套用和未套用樣式的版面配置:

發生什麼事了?指定 max-width 時,會為 auto-fit 版面配置演算法提供背景資訊、明確大小或明確大小,讓演算法瞭解空間可容納多少重複項目。雖然空間「全寬」似乎很明顯,但根據 CSS 格線規格,必須提供明確的大小或最大大小。我已提供最大尺寸。

那麼,為什麼要使用 89vw 呢?因為「這項功能」適用於我的版面配置。 我和其他幾位 Chrome 團隊成員正在調查為何 100vw 等較合理的值不夠用,以及這是否確實是錯誤。

間距

這個版面配置的大部分和諧感來自有限的間距調色盤,確切來說是 7 個。

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

這些流程與格線、CSS @nest@media 的第 5 級語法搭配使用,效果極佳。以下是完整 <main> 版面配置的一組樣式範例。

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);

  @media (width >= 540px) {
    & {
      padding: var(--space-lg);
    }
  }

  @media (width >= 800px) {
    & {
      padding: var(--space-xl);
    }
  }
}

網格,預設會適度填補內容周圍的空白區域 (如同行動裝置)。但隨著更多檢視區塊空間可用,它會增加邊框間距來展開。 2021 年的 CSS 表現相當亮眼!

還記得稍早的「僅適用於間隙」版面配置嗎?以下是這個元件中更完整的顯示方式:

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

顏色

這個設計適度運用色彩,既能充分表達意念,又不會過於複雜。我的做法如下:

:root {
  --surface1: lch(10 0 0);
  --surface2: lch(15 0 0);
  --surface3: lch(20 0 0);
  --surface4: lch(25 0 0);

  --text1: lch(95 0 0);
  --text2: lch(75 0 0);
}

我會使用數字為介面和文字顏色命名,而不是 surface-darksurface-darker 等名稱,因為在媒體查詢中,我會翻轉這些顏色,而淺色和深色將不再有意義。

我在偏好媒體查詢中翻轉這些值,如下所示:

:root {
  ...

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --surface2: lch(100 0 0);
      --surface3: lch(98 0 0);
      --surface4: lch(85 0 0);

      --text1: lch(20 0 0);
      --text2: lch(40 0 0);
    }
  }
}

在深入探討顏色語法細節之前,請務必先快速瞭解整體情況和策略。不過我好像講得太快了, 先回到前面一點。

LCH?

我們不會深入探討色彩理論,但 LCH 是以人為導向的語法,可滿足我們對色彩的感知方式,而非以數學方式測量色彩 (例如 255)。這項優勢十分明顯,因為人類可以更輕鬆地撰寫,而其他人也會配合這些調整。

pod.link/csspodcast 網頁的螢幕截圖,顯示「Color 2: Perception」集數
CSS Podcast 中瞭解感知色彩 (以及更多內容!)

在今天的示範中,我們將著重於語法和值,我會翻轉這些值來製作亮色和深色。我們來看看 1 種表面和 1 種文字顏色:

:root {
  --surface1: lch(10 0 0);
  --text1:    lch(95 0 0);

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --text1:    lch(40 0 0);
    }
  }
}

--surface1: lch(10 0 0) 會轉譯為 10% 亮度、0 色度和 0 色調:非常深色的無色灰色。接著,在淺色模式的媒體查詢中,亮度會透過 --surface1: lch(90 0 0); 翻轉為 90%。這就是策略的重點。首先,請先變更這 2 個主題之間的明度,同時維持設計要求的對比度,或可維持無障礙功能的對比度。

lch() 的優點是亮度以人為導向,因此我們對 % 的變化感到滿意,因為這項變化在知覺上和一致性上都是 % 的差異。hsl() 例如不夠可靠

如要進一步瞭解色彩空間,請參閱這篇文章lch()敬請期待!

CSS 目前完全無法存取這些顏色。 請容我再次強調:我們無法存取大多數現代螢幕中三分之一的色彩。而且這些色彩並非一般色彩,而是螢幕可顯示的最鮮豔色彩。我們的網站色彩黯淡,是因為螢幕硬體的演進速度比 CSS 規格和瀏覽器實作更快。

Lea Verou

使用 color-scheme 的自動調整式表單控制項

許多瀏覽器 (目前為 Safari 和 Chromium) 都提供深色主題控制項,但您必須在 CSS 或 HTML 中指定設計使用這些控制項。

上圖顯示開發人員工具「樣式」面板中的屬性效果。這個範例使用 HTML 標記,我認為這通常是較好的位置:

<meta name="color-scheme" content="dark light">

如要瞭解詳情,請參閱 color-scheme 這篇文章 (作者:Thomas Steiner)。除了深色核取方塊輸入內容,還有更多好處!

CSS accent-color

表單元素最近出現活動accent-color 是單一 CSS 樣式,可變更瀏覽器輸入元素中使用的色調。如要進一步瞭解這項功能,請參閱 GitHub 上的這篇文章。我已將其納入這個元件的樣式。隨著瀏覽器支援這項功能,我的核取方塊會更符合主題,並顯示粉紅色和紫色。

input[type="checkbox"] {
  accent-color: var(--brand);
}

Linux 版 Chromium 的螢幕截圖,顯示粉紅色核取方塊

抽色,並修正漸層和 focus-within

顏色用得越少,越能達到畫龍點睛的效果。我喜歡透過色彩豐富的 UI 互動來實現這一點。

上述影片中包含多個層面的 UI 回饋和互動,可透過下列方式為互動增添個人風格:

  • 醒目顯示背景資訊。
  • 提供 UI 回饋,說明值在範圍內的「飽和」程度。
  • 提供使用者介面意見回饋,指出欄位接受輸入內容。

為了在與元素互動時提供意見回饋,CSS 會使用 :focus-within 虛擬類別變更各種元素的外觀。讓我們來分析 .fieldset-item,這非常有趣:

.fieldset-item {
  ...

  &:focus-within {
    background: var(--surface2);

    & svg {
      fill: white;
    }

    & picture {
      clip-path: circle(50%);
      background: var(--brand-bg-gradient) fixed;
    }
  }
}

當這個元素的其中一個子項具有焦點內:

  1. .fieldset-item背景會指派對比度較高的表面顏色。
  2. 巢狀 svg 會填滿白色,以提高對比度。
  3. 巢狀 <picture> clip-path 會展開為完整圓形,背景則會填滿明亮的固定漸層。

自訂範圍

假設有下列 HTML 輸入元素,我將說明如何自訂其外觀:

<input type="range">

這個元素有 3 個部分需要自訂:

  1. 範圍元素 / 容器
  2. 追蹤
  3. Thumb

範圍元素樣式

input[type="range"] {
  /* style setting variables */
  --track-height: .5ex;
  --track-fill: 0%;
  --thumb-size: 3ex;
  --thumb-offset: -1.25ex;
  --thumb-highlight-size: 0px;

  appearance: none;         /* clear styles, make way for mine */
  display: block;
  inline-size: 100%;        /* fill container */
  margin: 1ex 0;            /* ensure thumb isn't colliding with sibling content */
  background: transparent;  /* bg is in the track */
  outline-offset: 5px;      /* focus styles have space */
}

CSS 的前幾行是樣式的自訂部分,希望清楚標示這些部分能有所幫助。其餘樣式大多是重設樣式,可為建構元件的複雜部分提供一致的基礎。

追蹤樣式

input[type="range"]::-webkit-slider-runnable-track {
  appearance: none; /* clear styles, make way for mine */
  block-size: var(--track-height);
  border-radius: 5ex;
  background:
    /* hard stop gradient:
        - half transparent (where colorful fill we be)
        - half dark track fill
        - 1st background image is on top
    */
    linear-gradient(
      to right,
      transparent var(--track-fill),
      var(--surface1) 0%
    ),
    /* colorful fill effect, behind track surface fill */
    var(--brand-bg-gradient) fixed;
}

訣竅在於「顯示」鮮豔的填滿顏色。做法是在頂端使用硬停止漸層。在填滿百分比之前,漸層會呈現透明,之後則會使用未填滿的軌跡表面顏色。在未填滿的表面後方,是等待透明度顯示的全寬顏色。

軌跡填滿樣式

我的設計需要 JavaScript,才能維持填滿樣式。只有 CSS 策略,但需要將拇指元素的高度設為與軌道相同,而我無法在這些限制內找到和諧感。

/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')

/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
  const max = slider.getAttribute('max') || 10;
  const percent = slider.value / max * 100;

  return `${parseInt(percent)}%`;
};

/* on page load, set the fill amount */
sliders.forEach(slider => {
  slider.style.setProperty('--track-fill', rangeToPercent(slider));

  /* when a slider changes, update the fill prop */
  slider.addEventListener('input', e => {
    e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
  })
})

我認為這項更新能帶來不錯的視覺效果。滑桿不需 JavaScript 即可正常運作,且不需要 --track-fill 屬性,如果沒有這個屬性,滑桿就只會沒有填滿樣式。如果 JavaScript 可用,請填入自訂屬性,同時觀察任何使用者變更,並將自訂屬性與值同步。

這篇 CSS-Tricks 上的文章 (作者:Ana Tudor),說明如何只使用 CSS 解決軌跡填滿問題。我也覺得這個 range 元素非常鼓舞人心。

縮圖樣式

input[type="range"]::-webkit-slider-thumb {
  appearance: none; /* clear styles, make way for mine */
  cursor: ew-resize; /* cursor style to support drag direction */
  border: 3px solid var(--surface3);
  block-size: var(--thumb-size);
  inline-size: var(--thumb-size);
  margin-top: var(--thumb-offset);
  border-radius: 50%;
  background: var(--brand-bg-gradient) fixed;
}

這些樣式大多是為了製作美觀的圓圈。 您會再次看到固定的背景漸層,統一顯示動態的拇指、軌跡和相關聯 SVG 元素的顏色。我將互動的樣式分開,有助於隔離用於懸停醒目的技術:box-shadow

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

::-webkit-slider-thumb {
  

  /* shadow spread is initally 0 */
  box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

  /* if motion is OK, transition the box-shadow change */
  @media (--motionOK) {
    & {
      transition: box-shadow .1s ease;
    }
  }

  /* on hover/active state of parent, increase size prop */
  @nest input[type="range"]:is(:hover,:active) & {
    --thumb-highlight-size: 10px;
  }
}

目標是為使用者意見回饋提供易於管理且會動態顯示的視覺重點。 使用方塊陰影時,我能避免觸發版面配置。我會建立不模糊的陰影,並與拇指元素的圓形形狀相符,藉此達成這個目標。然後,我會在滑鼠懸停時變更並轉換其擴散大小。

如果核取方塊的醒目顯示效果也能這麼簡單就好了…

跨瀏覽器選取器

我發現需要使用這些 -webkit--moz- 選擇器,才能在不同瀏覽器中保持一致性:

input[type="range"] {
  &::-webkit-slider-runnable-track {}
  &::-moz-range-track {}
  &::-webkit-slider-thumb {}
  &::-moz-range-thumb {}
}

自訂核取方塊

假設有下列 HTML 輸入元素,我會說明如何自訂其外觀:

<input type="checkbox">

這個元素有 3 個部分需要自訂:

  1. 核取方塊元素
  2. 相關聯的標籤
  3. 醒目顯示效果

核取方塊元素

input[type="checkbox"] {
  inline-size: var(--space-sm);   /* increase width */
  block-size: var(--space-sm);    /* increase height */
  outline-offset: 5px;            /* focus style enhancement */
  accent-color: var(--brand);     /* tint the input */
  position: relative;             /* prepare for an absolute pseudo element */
  transform-style: preserve-3d;   /* create a 3d z-space stacking context */
  margin: 0;
  cursor: pointer;
}

transform-styleposition 樣式會準備稍後要導入的虛擬元素,以設定醒目顯示的樣式。否則,我大多會提供一些微小的風格建議。我希望游標是指針、外框偏移,預設核取方塊太小,而且如果accent-color支援,請將這些核取方塊納入品牌配色。

核取方塊標籤

請務必為核取方塊提供標籤,原因有 2 個。第一個是代表核取方塊值用途,回答「開啟或關閉什麼?」第二個原因是使用者體驗,網路使用者已習慣透過相關聯的標籤與核取方塊互動。

輸入
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
標籤
<label for="text-notifications">
  <h3>Text Messages</h3>
  <small>Get notified about all text messages sent to your device</small>
</label>

在標籤上,加入 for 屬性,並依 ID 指向核取方塊:<label for="text-notifications">。在核取方塊中,同時使用名稱和 ID,確保各種工具和技術 (例如滑鼠或螢幕閱讀器) 都能找到核取方塊: <input type="checkbox" id="text-notifications" name="text-notifications">:hover:active 等等,連結後即可免費使用,增加表單的互動方式。

核取方塊醒目顯示

我想讓介面保持一致,而且滑桿元素有很棒的縮圖醒目顯示效果,我想搭配核取方塊使用。縮圖可使用 box-shadowspread 屬性,放大及縮小陰影。不過,由於我們的核取方塊是正方形,而且應該是正方形,因此這個效果在這裡不適用。

我使用虛擬元素和大量複雜的 CSS,達成了相同的視覺效果:

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

input[type="checkbox"]::before {
  --thumb-scale: .01;                        /* initial scale of highlight */
  --thumb-highlight-size: var(--space-xl);

  content: "";
  inline-size: var(--thumb-highlight-size);
  block-size: var(--thumb-highlight-size);
  clip-path: circle(50%);                     /* circle shape */
  position: absolute;                         /* this is why position relative on parent */
  top: 50%;                                   /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
  left: 50%;
  background: var(--thumb-highlight-color);
  transform-origin: center center;            /* goal is a centered scaling circle */
  transform:                                  /* order here matters!! */
    translateX(-50%)                          /* counter balances left: 50% */
    translateY(-50%)                          /* counter balances top: 50% */
    translateZ(-1px)                          /* PUTS IT BEHIND THE CHECKBOX */
    scale(var(--thumb-scale))                 /* value we toggle for animation */
  ;
  will-change: transform;

  @media (--motionOK) {                       /* transition only if motion is OK */
    & {
      transition: transform .2s ease;
    }
  }
}

/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
  --thumb-scale: 1;
}

建立圓形虛擬元素很簡單,但將其放置在所附加元素後方則較為困難。以下是修正前後的畫面:

這絕對是微互動,但對我來說,保持視覺一致性很重要。動畫縮放技術與我們在其他地方使用的技術相同。我們將自訂屬性設為新值,並根據動態偏好設定,讓 CSS 轉換該屬性。這裡的重點功能是 translateZ(-1px)。父項建立 3D 空間,這個虛擬元素子項則透過在 z 空間中稍微向後放置自身,輕觸進入該空間。

無障礙設定

這部 YouTube 影片清楚示範了如何使用滑鼠、鍵盤和螢幕閱讀器與這個設定元件互動。我會在這裡說明一些詳細資料。

HTML 元素選項

<form>
<header>
<fieldset>
<picture>
<label>
<input>

這些提示和訣竅可協助使用者瀏覽工具。部分元素會提供互動提示,部分元素會連結互動性,部分元素則有助於形塑螢幕閱讀器瀏覽的無障礙樹狀結構。

HTML 屬性

我們可以隱藏螢幕閱讀器不需要的元素,在本例中為滑桿旁的圖示:

<picture aria-hidden="true">

上述影片示範了 Mac OS 的螢幕閱讀器流程。請注意,輸入焦點會直接從一個滑桿移到下一個滑桿。這是因為我們隱藏了可能位於下一個滑桿位置途中的圖示。如果沒有這項屬性,使用者就必須停止、聆聽並略過可能無法看到的圖片。

SVG 是一堆數學,讓我們新增 <title> 元素,以便滑鼠懸停時顯示標題,並加入人類可解讀的註解,說明數學式建立的內容:

<svg viewBox="0 0 24 24">
  <title>A note icon</title>
  <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

除此之外,我們已使用足夠的清楚標記 HTML,因此表單在滑鼠、鍵盤、電玩遊戲控制器和螢幕閱讀器上都能順利通過測試。

JavaScript

已說明如何透過 JavaScript 管理軌跡填滿顏色,現在來看看相關的 JavaScript <form>

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

每當表單有互動和變更時,控制台都會將表單記錄為物件,存入表格中,方便您在提交至伺服器前輕鬆檢查。

console.table() 結果的螢幕截圖,表單資料會顯示在表格中

結論

現在您已瞭解我的做法,您會怎麼做呢?這讓元件架構變得很有趣!誰要用自己最愛的架構,製作第一個含有插槽的版本?🙂

讓我們多元化方法,學習在網路上建構內容的所有方式。 建立試聽版、在 Twitter 上傳送連結給我,我會將連結新增至下方的「社群混音」部分!

社群重混作品

  • @tomayac,他們在核取方塊標籤的懸停區域方面有自己的風格!這個版本沒有元素之間的懸停間距:demosource