建立工具提示元件

說明如何建構可自適應顏色且符合無障礙設計的工具提示自訂元素。

在本篇文章中,我想分享如何建立可自適應顏色且符合無障礙設計的 <tool-tip> 自訂元素。試用示範查看來源

工具提示會顯示在各種範例和色彩配置中

如果你偏好觀看影片,請參閱這篇文章的 YouTube 版本:

總覽

工具提示是一種非模式、非阻斷、非互動式疊加層,其中包含使用者介面的補充資訊。預設為隱藏,當滑鼠游標懸停或聚焦於相關元素時,就會顯示。您無法直接選取或與工具提示互動。工具提示不能取代標籤或其他高價值資訊,使用者應能在不使用工具提示的情況下,完整完成工作。

做法:一律為輸入內容加上標籤。
不要:使用工具提示代替標籤

Toggletip 與 Tooltip

與許多元件一樣,工具提示的定義也有多種,例如 MDNWAI ARIASarah Higley包容式元件。我喜歡將工具提示和切換提示分開。工具提示應包含非互動式補充資訊,而切換提示則可包含互動性和重要資訊。這項差異的主要原因是無障礙設計,使用者如何前往彈出式視窗,以及如何存取其中的資訊和按鈕。切換提示很快就會變得複雜。

以下是 Designcember 網站的切換提示影片;這是一個具有互動功能的疊加層,使用者可以將其固定開啟並進行探索,然後透過輕觸關閉或 Escape 鍵關閉:

這個 GUI 挑戰會使用工具提示,並盡可能使用 CSS 完成所有操作,以下是建構工具提示的方法。

標記

我選擇使用自訂元素 <tool-tip>。作者不必將自訂元素轉換為網路元件。瀏覽器會將 <foo-bar> 視為 <div>。您可以將自訂元素視為較不具體化的類別名稱。不涉及 JavaScript。

<tool-tip>A tooltip</tool-tip>

這就像一個內含文字的 div。我們可以透過新增 [role="tooltip"],將無障礙樹狀結構連結至支援的螢幕閱讀器。

<tool-tip role="tooltip">A tooltip</tool-tip>

如今,這會在螢幕閱讀器中識別為工具提示。請參閱以下範例,瞭解第一個連結元素的樹狀結構中含有已辨識的工具提示元素,而第二個連結元素則沒有。第二個沒有角色。在「樣式」部分,我們會改善這個樹狀檢視畫面。

代表 HTML 的 Chrome 開發人員工具無障礙樹狀圖螢幕截圖。顯示文字為「top」的連結;有工具提示:「Hey, a tooltip!」;可聚焦。其中包含「top」的靜態文字和工具提示元素。

接下來,我們需要讓工具提示無法聚焦。如果螢幕閱讀器無法瞭解工具提示角色,使用者就能將焦點放在 <tool-tip> 上來讀取內容,但使用者體驗不需要這麼做。螢幕閱讀器會將內容附加到父項元素,因此不需要將焦點設為可存取。我們可以使用 inert,確保使用者不會在分頁流程中意外看到這個工具提示內容:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Chrome 開發人員工具無障礙樹狀結構的另一張螢幕截圖,這次缺少工具提示元素。

接著,我選擇使用屬性做為介面,指定工具提示的位置。根據預設,所有 <tool-tip> 都會採用「頂端」位置,但您可以透過新增 tip-position 來自訂元素上的位置:

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

連結的螢幕截圖,右側有「A tooltip」的工具提示。

針對這類情況,我傾向使用屬性而非類別,這樣 <tool-tip> 就不會同時指派多個位置。只能有一個或沒有。

最後,請將 <tool-tip> 元素放入您要提供工具提示的元素中。我會將圖片和 <tool-tip> 放在 <picture> 元素中,以便與視障使用者分享 alt 文字:

<picture>
  <img alt="The GUI Challenges skull logo" width="100" src="...">
  <tool-tip role="tooltip" tip-position="bottom">
    The <b>GUI Challenges</b> skull logo
  </tool-tip>
</picture>

圖片的螢幕截圖,其中的工具提示顯示「GUI Challenges 骷髏標誌」。

這裡我將 <tool-tip> 放在 <abbr> 元素內:

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

螢幕截圖:段落中包含縮寫字詞 HTML,並在其上方顯示「超文本標記語言」的工具提示。

無障礙設定

由於我選擇建構工具提示,而非切換提示,因此這個部分會簡單許多。首先,讓我概略說明我們希望提供的使用者體驗:

  1. 在空間受限或介面雜亂的情況下,請隱藏輔助訊息。
  2. 當使用者將游標懸停、將焦點移至元素,或使用觸控方式與元素互動時,系統就會顯示訊息。
  3. 當滑鼠游標、焦點或觸控動作結束時,再次隱藏訊息。
  4. 最後,如果使用者已指定減少動作的偏好設定,請確保所有動作都會減少。

我們的目標是提供隨選的補充訊息。有視力且使用滑鼠或鍵盤的使用者,只要將滑鼠游標懸停在訊息上,即可看到訊息內容,並用眼睛閱讀。視障螢幕閱讀器使用者可以聚焦訊息,透過工具以聽覺方式接收訊息。

MacOS VoiceOver 讀取含有工具提示的連結的螢幕截圖

在上一節中,我們介紹了無障礙樹狀結構、工具提示角色和無效狀態,接下來要做的就是測試並驗證使用者體驗是否適當地向使用者顯示工具提示訊息。在測試時,我們無法確定音訊訊息的哪一部分是工具提示。在輔助工具樹狀結構中進行偵錯時,也可以看到這項問題。連結文字「top」會與「Look, tooltips!」一起執行,不會有任何延遲。螢幕閱讀器不會將文字分段或視為工具提示內容。

Chrome 開發人員工具無障礙樹狀結構的螢幕截圖,其中連結文字為「top Hey, a tooltip!」。

將螢幕閱讀器專屬的偽元素新增至 <tool-tip>,我們就能為視障使用者新增自己的提示文字。

&::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

您可以在下方看到更新後的無障礙樹狀結構,現在連結文字後面會加上分號,並顯示工具提示提示「Has tooltip:」。

Chrome 開發人員工具無障礙樹狀結構的更新版螢幕截圖,其中連結文字的用詞已改善為「top ; Has tooltip: Hey, a tooltip!&#39;」。

現在,當螢幕閱讀器使用者將焦點放在連結上時,系統會說出「top」,稍微停頓一下,然後宣布「has tooltip: look, tooltips」。這可為螢幕閱讀器使用者提供幾個不錯的使用者體驗提示。猶豫動作可讓連結文字和工具提示之間有明確的區隔。此外,當系統朗讀「has tooltip」時,如果螢幕閱讀器使用者之前已聽過這項資訊,他們可以輕鬆取消。這很像快速將游標懸停和移開,因為您已經看到補充訊息。這感覺就像是使用者體驗的平衡。

樣式

<tool-tip> 元素會是代表補充訊息的元素的子項,因此我們先從疊加效果的必要元素開始。使用 position absolute 將其從文件流程中移除:

tool-tip {
  position: absolute;
  z-index: 1;
}

如果父項不是堆疊內容,工具提示會將自身定位到最近的堆疊內容,這不是我們想要的結果。區塊上有一個新的選取器可提供協助,即 :has()

瀏覽器支援

  • Chrome:105。
  • Edge:105。
  • Firefox:121。
  • Safari:15.4。

資料來源

:has(> tool-tip) {
  position: relative;
}

請不要過度擔心瀏覽器支援問題。首先,請記住這些工具提示是輔助性質。如果無法解決問題,也沒關係。其次,在 JavaScript 部分,我們會部署指令碼,為不支援 :has() 的瀏覽器提供所需的 polyfill 功能。

接下來,我們要讓工具提示不具互動性,這樣工具提示就不會從父元素竊取指標事件:

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

接著,使用不透明度隱藏工具提示,以便我們透過交叉淡出效果轉換工具提示:

tool-tip {
  opacity: 0;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
}

:is():has() 在這裡扮演重責,讓包含父項元素的 tool-tip 能夠察覺使用者互動情形,進而切換子項工具提示的顯示/隱藏狀態。滑鼠使用者可以懸停,鍵盤和螢幕閱讀器使用者可以聚焦,而觸控使用者可以輕觸。

由於顯示和隱藏覆疊層功能可供視障使用者使用,現在是時候新增一些樣式,以便設定主題、位置,並在氣泡中加入三角形形狀。以下樣式會開始使用自訂屬性,並在現有基礎上加入陰影、字體排版和顏色,讓樣式看起來像浮動工具提示:

深色模式中浮動在「block-start」連結上方的工具提示螢幕截圖。

tool-tip {
  --_p-inline: 1.5ch;
  --_p-block: .75ch;
  --_triangle-size: 7px;
  --_bg: hsl(0 0% 20%);
  --_shadow-alpha: 50%;

  --_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
  --_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
  --_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
  --_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;

  pointer-events: none;
  user-select: none;

  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;

  position: absolute;
  z-index: 1;
  inline-size: max-content;
  max-inline-size: 25ch;
  text-align: start;
  font-size: 1rem;
  font-weight: normal;
  line-height: normal;
  line-height: initial;
  padding: var(--_p-block) var(--_p-inline);
  margin: 0;
  border-radius: 5px;
  background: var(--_bg);
  color: CanvasText;
  will-change: filter;
  filter:
    drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
    drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}

/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
  position: relative;
}

/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

/* prepend some prose for screen readers only */
tool-tip::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
  content: "";
  background: var(--_bg);
  position: absolute;
  z-index: -1;
  inset: 0;
  mask: var(--_tip);
}

/* top tooltip styles */
tool-tip:is(
  [tip-position="top"],
  [tip-position="block-start"],
  :not([tip-position]),
  [tip-position="bottom"],
  [tip-position="block-end"]
) {
  text-align: center;
}

主題調整

由於文字顏色會透過系統關鍵字 CanvasText 從頁面繼承,因此工具提示只需要管理幾種顏色。此外,由於我們已建立自訂屬性來儲存值,因此我們可以只更新這些自訂屬性,讓主題處理其餘部分:

@media (prefers-color-scheme: light) {
  tool-tip {
    --_bg: white;
    --_shadow-alpha: 15%;
  }
}

淺色和深色版本工具提示的並排螢幕截圖。

對於淺色主題,我們會將背景調整為白色,並調整陰影的不透明度,讓陰影變得較不明顯。

由右向左

為了支援從右到左的閱讀模式,自訂屬性會將文件方向的值分別儲存為 -1 或 1。

tool-tip {
  --isRTL: -1;
}

tool-tip:dir(rtl) {
  --isRTL: 1;
}

這可協助您放置工具提示:

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

以及三角形所在位置:

tool-tip[tip-position="right"]::after {
  --_tip: var(--_left-tip);
}

tool-tip[tip-position="right"]:dir(rtl)::after {
  --_tip: var(--_right-tip);
}

最後,也可以用於 translateX() 的邏輯轉換:

--_x: calc(var(--isRTL) * -3px * -1);

工具提示位置

請使用 inset-blockinset-inline 屬性,以邏輯方式放置工具提示,以便處理實體和邏輯工具提示位置。以下程式碼顯示四個位置的樣式,適用於從左到右和從右到左的方向。

靠上和區塊起始對齊

螢幕截圖:顯示左至右頂端位置和右至左頂端位置之間的差異。

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
  inset-inline-start: 50%;
  inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
  --_tip: var(--_bottom-tip);
  inset-block-end: calc(var(--_triangle-size) * -1);
  border-block-end: var(--_triangle-size) solid transparent;
}

靠右和內嵌式結尾對齊

螢幕截圖:顯示左至右右側位置和右至左內嵌式結尾位置之間的刊登位置差異。

tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
  inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
  --_tip: var(--_left-tip);
  inset-inline-start: calc(var(--_triangle-size) * -1);
  border-inline-start: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
  --_tip: var(--_right-tip);
}

靠下和區塊結尾對齊

螢幕截圖:顯示左至右底部位置和右至左區塊結尾位置之間的放置差異。

tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
  inset-inline-start: 50%;
  inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
  --_tip: var(--_top-tip);
  inset-block-start: calc(var(--_triangle-size) * -1);
  border-block-start: var(--_triangle-size) solid transparent;
}

靠左對齊和內嵌式起始對齊

螢幕截圖:顯示左到右左側位置和由右到左內嵌式開頭位置之間的刊登位置差異。

tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
  inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
  --_tip: var(--_right-tip);
  inset-inline-end: calc(var(--_triangle-size) * -1);
  border-inline-end: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
  --_tip: var(--_left-tip);
}

動畫

目前我們只切換了工具提示的顯示設定。在本節中,我們會先為所有使用者製作不透明度動畫,因為這是一般來說比較安全的減速動畫轉場效果。接著,我們會為轉換位置加上動畫效果,讓工具提示從父項元素滑出。

安全且有意義的預設轉場效果

為工具提示元素設定樣式,以便轉換不透明度和轉換效果,如下所示:

tool-tip {
  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

為轉場效果加入動態效果

針對可顯示工具提示的每個邊,如果使用者允許動畫,請為 translateX 屬性稍微調整位置,讓其從以下位置移動一小段距離:

@media (prefers-reduced-motion: no-preference) {
  :has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: 3px;
  }

  :has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: -3px;
  }

  :has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: -3px;
  }

  :has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: 3px;
  }
}

請注意,這是設定「out」狀態,因為「in」狀態位於 translateX(0)

JavaScript

我認為 JavaScript 是選用項目。這是因為這些工具提示都不需要讀取,即可在 UI 中完成工作。因此,如果工具提示完全失敗,也不會有太大影響。這也表示我們可以將工具提示視為逐步強化的內容。所有瀏覽器都會支援 :has(),這個指令碼也會完全消失。

polyfill 指令碼會執行兩項作業,但只有在瀏覽器不支援 :has() 時才會執行。首先,請檢查 :has() 支援情形:

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

接著,找出 <tool-tip> 的父項元素,並為其提供要使用的類別名稱:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))
}

接下來,請插入一組使用該類別名的樣式,模擬 :has() 選取器的確切行為:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))

  let styles = document.createElement('style')
  styles.textContent = `
    .has_tool-tip {
      position: relative;
    }
    .has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
      opacity: 1;
      transition-delay: 200ms;
    }
  `
  document.head.appendChild(styles)
}

就這樣,現在所有瀏覽器都會在不支援 :has() 時顯示工具提示。

結論

既然您知道我如何操作,那您會怎麼做呢? 🙂? 我真的很期待 popup API,讓切換提示更容易使用、頂層可避免 z 索引衝突,以及 anchor API,可更妥善地在視窗中定位項目。在此之前,我會製作工具提示。

讓我們多方嘗試,瞭解在網路上建構應用程式的所有方式。

請製作示範作品,並在推特上傳連結,我會將其加入下方的社群重混曲目錄!

社群重混作品

這裡尚無任何內容。

資源