建立工具提示元件

基礎總覽:如何建構可適應顏色且符合無障礙設計的工具提示自訂元素。

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

工具提示顯示在各種範例和配色方案中

如果比較喜歡看影片,可以觀看這篇貼文的 YouTube 版本:

總覽

工具提示是不具模式、不具阻斷性、不具互動性的疊加層,內含使用者介面的補充資訊。預設為隱藏,當滑鼠懸停在相關聯的元素上或聚焦時,就會顯示。工具提示無法直接選取或互動。工具提示並非標籤或其他重要資訊的替代方案,使用者應可在沒有工具提示的情況下,順利完成工作。

建議做法:一律標示輸入內容。
錯誤:依賴工具提示而非標籤

切換提示與工具提示

與許多元件一樣,工具提示的說明各有不同,例如 MDNWAI ARIASarah HigleyInclusive Components。我喜歡工具提示和切換提示之間的區隔。工具提示應包含非互動式補充資訊,切換提示則可包含互動式和重要資訊。造成這種差異的主要原因是無障礙功能,也就是使用者應如何導覽至彈出式視窗,並存取其中的資訊和按鈕。Toggletip 很快就會變得複雜。

以下是 Designcember 網站的切換提示影片,其中包含可互動的疊加層,使用者可以釘選開啟並探索,然後透過輕觸關閉或 Esc 鍵關閉:

這項 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>

現在螢幕閱讀器會將其視為工具提示。請參閱下列範例,瞭解第一個連結元素在樹狀結構中是否有可辨識的工具提示元素,第二個連結元素則否。第二個使用者沒有角色。在樣式部分,我們將改善這個樹狀檢視畫面。

Chrome 開發人員工具無障礙樹狀結構的螢幕截圖,代表 HTML。顯示文字為「top ; Has tooltip: 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>

螢幕截圖:連結右側的工具提示顯示「工具提示」。

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

最後,將 <tool-tip> 元素放在要提供工具提示的元素內。我在 <picture> 元素中放置圖片和 <tool-tip>,與有視覺能力的使用者分享 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>

圖片的螢幕截圖,工具提示顯示「The GUI Challenges skull logo」。

我在 <abbr> 元素內放置 <tool-tip>

<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 讀取附有工具提示的連結

上一節中,我們介紹了無障礙樹狀結構、工具提示角色和 inert,接下來要測試並驗證使用者體驗,確保工具提示訊息能適當顯示給使用者。測試後,我們無法判斷語音訊息的哪個部分是工具提示。在無障礙樹狀結構中進行偵錯時,也可以看到「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!」。

現在,螢幕閱讀器使用者將焦點移至連結時,螢幕閱讀器會說出「頂端」,並稍作停頓,然後宣布「有工具提示:看,工具提示」。這可為螢幕閱讀器使用者提供一些實用的 UX 提示。延遲時間可讓連結文字和工具提示之間有適當的間隔。此外,當系統播報「有工具提示」時,螢幕閱讀器使用者如果之前已聽過工具提示,可以輕鬆取消。這與快速懸停和取消懸停非常相似,因為您已看過補充訊息。這感覺像是良好的 UX 同位。

樣式

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

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

如果父項不是堆疊內容,工具提示會將自身定位在最近的堆疊內容,這並非我們所要的。區塊中新增的選取器可提供協助::has()

Browser Support

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

Source

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

請別太擔心瀏覽器支援問題。首先,請記住這些工具提示是補充資訊,如果無法運作,應該就沒問題。其次,在 JavaScript 區段中,我們會部署指令碼,為不支援 :has() 的瀏覽器填補所需功能。

接著,我們將工具提示設為非互動式,以免工具提示從父項元素竊取指標事件:

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(),這個指令碼也就能完全消失。

只有在瀏覽器不支援 :has() 時,這個 Polyfill 指令碼才會執行兩項作業。請先檢查是否支援 :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-index 衝突,以及使用 anchor API 更妥善地在視窗中放置項目。在此之前,我會製作工具提示。

讓我們多元化地運用各種方法,學習在網路上建構內容。

建立試聽版,然後在推特上傳送連結給我,我會將連結加到下方的社群混音區!

社群重混作品

目前沒有任何內容。

資源