建立工具提示元件

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

在這篇文章中,我想分享我對於如何建立可調色且無障礙的 <tool-tip> 自訂元素有何感想。歡迎試用示範版查看原始碼

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

如果你偏好使用影片,也可以觀看這篇 YouTube 文章:

總覽

工具提示是非強制回應的非強制性非互動式疊加層,含有補充資訊至使用者介面。這個項目預設會隱藏,並在相關聯的元素懸停或聚焦時取消隱藏。您無法選取工具提示或直接與相關互動。工具提示不會取代標籤或其他高價值資訊,使用者應能在沒有工具提示的情況下完成所有工作。

建議做法:請務必為輸入內容加上標籤,
請勿:採用工具提示而非標籤

切換提示與工具提示

如同許多元件,工具提示本身俱有不同的說明,例如 MDNWAI ARIASarah HigleyInclusive Components 中一樣。我喜歡工具提示和切換提示之間的差異。工具提示應包含非互動式的補充資訊,切換提示則可包含互動性和重要資訊。這主要原因是無障礙設計,也就是使用者預期如何前往彈出式視窗,以及存取其中的資訊和按鈕。切換提示很快就會變得複雜。

以下影片是「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, 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 工具提示」。

我通常會使用屬性 (而非類別) 處理這類項目,這樣 <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>

螢幕截圖中的工具提示:「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, 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;
}

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

更新 Chrome 開發人員工具無障礙功能樹狀結構的螢幕截圖,其中連結文字已改善分段,「top ; Has tooltip: Hey, a tooltip!」。

現在,螢幕閱讀器使用者聚焦於連結時,會顯示「top」並稍微暫停,然後說出「has tooltip: view, tooltips」。這樣螢幕閱讀器使用者就能取得實用的使用者體驗提示。「疑慮」可讓連結文字和工具提示進一步區隔開來。此外,系統朗讀「含有工具提示」時,如果先前已聽過「含有工具提示」,螢幕閱讀器也可以輕鬆取消朗讀。隨著您已看到補充訊息,將滑鼠懸停及取消懸停的速度非常令人印象深刻。感覺就好像有一樣的使用者體驗。

風格

<tool-tip> 元素會成為其代表補充訊息的元素的子項,所以我們先從疊加效果的基本要件開始。使用 position absolute 來匯出文件:

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

如果父項並非堆疊的結構定義,工具提示會自行定位到最靠近的那一個,這不是我們想要的。系統會在區塊上提供新的選取器,這有助於 :has()

瀏覽器支援

  • 105
  • 105
  • 121
  • 15.4

資料來源

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

請注意,這樣做會設定「關閉」狀態,因為「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-index 對戰,而 anchor API 則可更妥善地定位視窗內容。在此之前,我會製作工具提示

讓我們帶您更多元的方法,並瞭解運用網路打造網站的所有方式。

請建立示範並透過 Twitter 推文連結,我就能將這項工具新增至下方的「社群重混」部分!

社群重混作品

這裡還沒有內容。

資源