跳到主要內容

小細節:Tooltip、捲軸、游標、間距

四個最常被跳過、但一眼就看得出有沒有做的地方。附上可以直接抄的 CSS 與判斷規則。

約 10 分鐘 · micro-details.md

這四樣有個共同點:做了不會被稱讚,沒做會被一眼看穿。 它們也正好是 AI 生出來的介面最常整個略過的部分,因為它們不在任何 wireframe 上。

一、Tooltip

不要用 title=

瀏覽器原生的 title 屬性給你:約一秒不可調整的延遲、作業系統的字體、你控制不了的位置、完全無法設定樣式,以及在觸控裝置上什麼都沒有

data-tip 加 CSS 偽元素,純 CSS 就能做完:

[data-tip] { position: relative; }

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  translate: -50% 4px;
  width: max-content;
  max-width: 260px;
  padding: var(--ds-space-2) var(--ds-space-3);
  border-radius: var(--ds-tooltip-radius);
  background: var(--ds-tooltip-bg);
  color: var(--ds-tooltip-ink);
  font-size: var(--ds-step--1);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms, translate 120ms;
}

[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  opacity: 1;
  translate: -50% 0;
  transition-delay: 320ms;   /* 出現才延遲,消失要立刻 */
}

transition-delay 只加在顯示上:滑鼠掃過一排工具列不該一路閃出五個泡泡,但離開時應該立刻消失。

四條使用規則

  1. Tooltip 只能重複或縮短畫面上已經有的資訊。 任何完成任務必需的資訊都不能只放在裡面。
  2. 一定要 :focus-visible 也會出現,否則鍵盤使用者永遠看不到。
  3. 觸控沒有 hover。 手機上 tooltip-only 的內容等於不存在。重要的話就寫在畫面上。
  4. 圖示按鈕的無障礙名稱來自 aria-label,tooltip 是額外的視覺提示,不是替代品。
<button aria-label="刪除" data-tip="刪除,無法復原">
  <span aria-hidden="true">✕</span>
</button>

三個常見錯誤

  • 停用的按鈕不會觸發 hover → tooltip 要包在外層的 <span>
  • 父層有 overflow: hidden → tooltip 被切掉。這種情況要改用真的 popover
  • tooltip 裡放連結 → 滑鼠移過去的路上它就消失了。需要互動就是 popover,不是 tooltip

二、捲軸

為什麼要處理

不處理的話,Windows 上是一條又寬又灰、不屬於你任何色票的直條,而且吃掉約 15px 的寬度;macOS 上則是不動就看不見。同一個版面在兩個系統上看起來像兩個產品。

.ds-scroll {
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;                                   /* Firefox */
  scrollbar-color: var(--ds-scrollbar-thumb) transparent;  /* Firefox */
}

.ds-scroll::-webkit-scrollbar { width: 10px; height: 10px; }
.ds-scroll::-webkit-scrollbar-track { background: transparent; }
.ds-scroll::-webkit-scrollbar-thumb {
  background: var(--ds-scrollbar-thumb);
  border-radius: 999px;
  /* 透明邊框 + background-clip 是唯一能讓 webkit 滑塊內縮的方法,
     padding 在這裡完全沒有作用 */
  border: 2px solid transparent;
  background-clip: content-box;
}

四種處理,怎麼選

做法 什麼時候用
主題化(預設) 大部分情況。看得見、跟著主題走
hover 才出現 密集的後台,多個並排面板
完全隱藏 只在內容本身就能證明「還有更多」的地方
保留槽位 內容會從一頁變兩頁的頁面

隱藏捲軸的唯一合法情況

.ds-scroll[data-scrollbar='hidden'] {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.ds-scroll[data-scrollbar='hidden']::-webkit-scrollbar { display: none; }

只有當內容自己會說「還有更多」時才可以用。 橫向的篩選列可以,因為下一個被切一半的項目就是提示;輪播可以,因為有箭頭。

長文和不確定長度的清單絕對不行。 在桌機用滑鼠的人,捲軸是唯一知道「下面還有東西」的線索,藏掉它等於藏掉內容。

要藏就一定要補一個視覺訊號 —— 箭頭、露出半個的下一項,或邊緣淡出:

.scroll-fade::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(transparent, var(--ds-canvas));
}

兩個沒人記得但很有效的屬性

overscroll-behavior: contain;   /* 捲到底不會帶著整頁一起動 */
scrollbar-gutter: stable;       /* 內容變長時版面不會往左跳 15px */

第二個尤其明顯:頁面從一屏變兩屏時捲軸出現,整個版面往左位移 —— 這是很多人感覺「網站怪怪的」但說不出原因的來源。

三、游標

游標是最便宜的 affordance,也是最常做錯的。

三個經典錯誤

錯誤 後果
<div onclick> 保留文字 I 型游標 看起來不能點,一半的人不會去試
停用的按鈕還是 pointer 看起來能點、點了沒反應 → 使用者認為壞掉了
送出中沒有任何游標變化 使用者再點一次 → 重複訂單

一次設定好

:where(button, summary, label[for], [role='button'], [role='tab'], a[href]) {
  cursor: pointer;
}
:where(input:not([type='range']):not([type='checkbox']), textarea) {
  cursor: text;
}
:where(:disabled, [aria-disabled='true']) { cursor: not-allowed; }
:where([aria-busy='true']) { cursor: progress; }
:where([aria-busy='true']) * { pointer-events: none; }
:where([draggable='true']) { cursor: grab; }
:where([draggable='true']):active { cursor: grabbing; }

倒數第二行很重要:aria-busy 同時做兩件事 —— 游標變成 progress,而且內部不再吃點擊。少了第二件,使用者會連按五次。

progresswait 的差別

  • progress:在忙,但你還能操作別的地方。九成的情況用這個。
  • wait:整個介面卡住。應該極少出現,出現了通常代表你該改成非同步。

拖曳的兩個細節

grab 按下去要變成 grabbing,否則拖曳過程感覺沒有回應。而且拖曳時要 user-select: none,不然會把旁邊的文字一起選起來。

拖曳也一定要有鍵盤替代路徑。分隔線給它 tabindex="0" 和方向鍵處理,只要幾行。

自訂游標

--ds-cursor-pointer: url("data:image/svg+xml;utf8,<svg …/></svg>") 3 2, pointer;

url() 後面那兩個數字是 hotspot(實際點擊的座標),寫錯的話每一次點擊都會偏幾個像素。

代價是真實的:失去作業系統的游標尺寸與高對比設定、主執行緒忙碌時會延遲、和使用者在其他地方看到的不一致。活動頁和形象站可以玩,每天要用八小時的工具不要。

四、間距

為什麼會亂

每個元素都自帶 margin → margin collapse 產生沒人預期的結果 → 有人用一個說不出理由的數字「修好」某一個間隙。半年後同一頁有十四種垂直間距。

三條規則

一、容器負責間距,元素不帶 margin。

.ds-flow > * + * { margin-block-start: var(--ds-flow-space, var(--ds-space-4)); }

只在兄弟之間加間距,第一個之前與最後一個之後永遠沒有多餘的空隙 —— 所以這個區塊丟到任何地方都不會多一截。

二、只用刻度上的值。 出現 13px 就代表有人在推像素而不是做決定。

三、空間屬於它所群組的東西。 標題屬於它下面的段落,不屬於上面的區塊,所以上方的空間要比下方大:

.ds-flow > :where(h2, h3, h4) { margin-block-start: var(--ds-space-8); }
.ds-flow > :where(h2, h3, h4) + * { margin-block-start: var(--ds-space-3); }

上下一樣大的話,讀者無法從版面看出哪一段屬於哪個標題 —— 這就是「一整頁看起來像一堵牆」的技術原因。

檢查方法

grep -rnE "(margin|padding|gap): *[0-9]+px" src/

只要出現不在刻度上的數字,就是有人在憑感覺推像素。

檢查清單

  • 沒有用瀏覽器原生 title= 當 tooltip
  • tooltip 在 :focus-visible 也會出現
  • 沒有任何必要資訊只存在於 tooltip 裡
  • 所有捲動區域都有處理捲軸樣式
  • 隱藏捲軸的地方都有另一個「還有更多」的訊號
  • 捲動容器有 overscroll-behavior: contain
  • 可點的東西都是 pointer,停用的都是 not-allowed
  • 處理中的狀態有 aria-busy 且不再吃點擊
  • 拖曳有 grab / grabbing 切換,而且有鍵盤替代路徑
  • 間距全部來自刻度,沒有裸數字
  • 標題上方的空間大於下方

參考模組:Tooltip捲軸樣式游標語意間距節奏

顯示設定

這裡改的每一項,會即時套用到站上所有預覽。

風格

密度

圓角

動態

系統層級的「減少動態效果」永遠優先於這裡的設定。

語言