建立工具提示元件

概略說明如何建構可自動調整顏色且可存取的工具提示自訂元素。

在本篇文章中,我想分享如何建立可自適應顏色且符合無障礙設計的 <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 ; has tooltip: Hey, 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>

圖片的螢幕截圖,工具提示為「The GUI Challenges skull 標誌」的工具提示。

這裡我將 <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, tooltip!」。螢幕閱讀器不會中斷文字,也不會將文字識別為工具提示內容。

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

下方是更新後的無障礙樹狀結構,連結文字後方現在有一個半形分號,以及工具提示「含有工具提示」的提示:

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,可更妥善地在視窗中定位項目。在此之前,我會製作工具提示。

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

建立示範、張貼推文 連結,以便我們將其新增至下方的社群重混專區!

社群重混作品

尚無任何內容

資源