拆解 GitHub 前端:Popover API、Anchor Positioning 能取代套件嗎?
從 tooltip 的 Popover 到選單的 Anchor Positioning:GitHub 挑選低風險元件漸進升級的實戰觀察
Leo Chiu · · 6 min
現在原生的 HTML/CSS 越來越完整,以前需要搭配許多套件、JS 才能實現 dialog、popover 等功能,現在這些原生 HTML/CSS 都已經進入 Baseline,平常開發已經可以正常使用。
但我就好奇現在業界會選擇使用原生的寫法,還是仍然使用套件?
所以我以 GitHub 當作標竿,看一下 GitHub 有用到哪些原生的 HTML 跟 CSS。
GitHub 的 tooltip 是用 Popover API 做的嗎?
Popover API 是瀏覽器內建的浮層機制,元素加上 popover 屬性就會進 top layer,不用自己處理 z-index,popover="auto" 還會在點外面或按 Esc 時自動關閉。
會。GitHub 的許多頁面都能看到 popover 這個屬性,repo 首頁、Issue 頁面、PR Files,只要是 tooltip,基本上都是使用 popover。
它們的來源有兩種:
- Primer 的
TooltipV2,包成一個<span popover="auto"> - 自家的
<tool-tip>web component,設成popover="manual"再配sr-only
這兩種都沒用 popovertarget,這個屬性可以讓按鈕不寫 JS 就開關 popover,但只認「點擊」。
tooltip 是滑鼠移上去才出現,所以 GitHub 得自己監聽 hover,再用 JS 呼叫 showPopover() 把它打開。
tooltip 的位置也是用 JS 算的,在 Issues 列表上打開「Compact display density」按鈕的 tooltip,它的 computed position-anchor 是 normal,座標是打開時寫進 inline style 的 top / left:
我照 GitHub 的標記做了一個 demo,不過位置改用 CSS Anchor Positioning 決定,搭配 position-try-fallbacks 可以在撞到邊緣時自動位移,避免跑版:
靠近畫面上緣時,tooltip 會自己翻到下面(position-try-fallbacks)。
<button
style={{ anchorName: "--tip" }}
onMouseEnter={() => tip.current?.showPopover()}
>
hover me
</button>
<span
ref={tip}
popover="auto"
style={{ positionAnchor: "--tip", positionArea: "top" }}
className="[position-try-fallbacks:flip-block]"
>
tooltip
</span>這個 demo 的進出場動畫用 @starting-style 搭配 transition-behavior: allow-discrete。因為 popover 平常是 display: none,一般的 transition 接不到進場狀態。
@starting-style:讓元素從 display: none 淡入
popover 平常掛著 display: none,打開瞬間才變成 display: block。
因為元素一出現就已經是最終樣式,中間沒有過渡起點,一般的 transition 做不出進場動畫。@starting-style 就是用來補這段,告訴瀏覽器元素剛出現時要從哪組樣式開始過渡。
.tooltip {
opacity: 0;
transition:
opacity 150ms,
display 150ms allow-discrete,
overlay 150ms allow-discrete;
}
.tooltip:popover-open {
opacity: 1;
@starting-style {
opacity: 0;
}
}allow-discrete 讓 display 和 overlay 這種離散屬性也能參與 transition,退場時會等淡出動畫跑完才收掉。
GitHub 的 stylesheet 裡也有這套寫法,寫在 IssueViewer 頂部的標籤列(topContainerChips):用 display … allow-discrete 配 @starting-style 讓它淡入,再為 prefers-reduced-motion 關掉動畫。
Anchor Positioning 是什麼?不用 JS 也能對齊按鈕
Anchor Positioning 是 CSS 的定位機制,讓浮層直接對齊指定的元素。以前做 tooltip 或選單,通常要用 JS 去量按鈕座標,不然就是載入 Floating UI,現在用 CSS 就能綁定目標元素:
.trigger {
anchor-name: --trigger;
}
.tooltip {
position-anchor: --trigger;
position-area: top;
position-try-fallbacks: flip-block;
}position-area 的用法很直覺,把 anchor 周圍切成 3×3 的九宮格,anchor 本身在正中間,只要指定要塞進哪一格就行:
┌─────────┬─────────┬──────────┐
│top left │ top │top right │
├─────────┼─────────┼──────────┤
│ left │ anchor │ right │
├─────────┼─────────┼──────────┤
│bot left │ bottom │bot right │
└─────────┴─────────┴──────────┘top、bottom、left、right:靠在該側,對齊 anchor 的中心線。top span-right:貼在上方,並從 anchor 左緣往右延伸,選單很常見這種對齊(後面TopLayerDemo的 popover 選單就是bottom span-right)。block-start、inline-end這類邏輯屬性也支援,碰到 RTL 語系會自動翻面。
position-try-fallbacks 是空間不夠時的備案,像是 flip-block 會垂直翻轉,flip-inline 會水平翻轉。
tooltip 靠在螢幕邊緣會自己換邊,靠的就是
position-try-fallbacks這個機制。
點右邊九宮格可以看左邊方塊對應的位置變化,全程沒有寫任何 JS 算座標:
position-area: top;那 GitHub 自己用在哪?是選單。點開 Issues 列表上的篩選選單,看裡面的 DOM 結構:
- 容器用
<div role="dialog" aria-labelledby>,沒用原生<dialog> - 樣式是
position: fixed,不在 top layer(:popover-open和:modal都沒中) - 有設
position-anchor,所以定位用了 Anchor Positioning - 裡面用
role="combobox"的 input 配listbox/option,沒用<select>或<datalist>
等於它跟 tooltip 剛好相反,用 Anchor Positioning 算位置,但沒進 top layer。
沒進 top layer 會遇到什麼問題,看下面這個實驗就清楚了,兩邊選單都包在 overflow: hidden 裡面:
兩個選單各有四個項目,但容器只有 96px 高。左邊的被切掉,右邊的不會;點空白處或按 Esc 也只有右邊會自己關。
左邊的 position: absolute 選單直接被容器裁掉,右邊的 popover 被拉到 top layer,不受容器限制,點外面或按 Esc 也會自己關閉。
另外選單也沒用到九宮格。我量到它的 position-area computed 值是 none,所以它具體怎麼靠 anchor 排位置,我沒有確認。
View Transitions:DOM 改變時自動補動畫
View Transitions 的機制是讓瀏覽器在 DOM 改變前後各抓一張快照,自動補上過渡動畫。
同一頁裡的 DOM 更新用 document.startViewTransition() 包起來,多頁之間的跳轉則可以用 @view-transition { navigation: auto } 直接套用。
@view-transition {
navigation: auto;
}
.card-thumbnail {
view-transition-name: card;
}stylesheet 裡唯一實際設定的 view-transition-name 在 Copilot 的 DashboardListView 上。
下面做了一個洗牌的對照,可以切換看有沒有開 startViewTransition 的效果。每個方塊都給了獨立的 view-transition-name,瀏覽器才能認出順序改變並做出平移:
- 1
- 2
- 3
- 4
- 5
- 6
content-visibility:畫面外先不 render
PR Files 的每個檔案區塊都加了 content-visibility: auto,讓畫面外的 diff 先不 render,再用 contain-intrinsic-size 先預留高度,捲動條才不會亂跳。左側檔案樹的每一列也加了同一個屬性:
content-visibility: auto 對效能的影響很直觀。
下面 demo 會把 4000 列內容渲染兩次,記錄瀏覽器排版花了多久。數字是你當前設備的實測結果,因為畫面外的節點不需要排版,列數越多差距越明顯:
- 一般
- –
- content-visibility
- –
其他用到的 CSS
@container:依元件所在容器的寬度調整樣式,同一張卡片放側欄排直的、放主內容區排橫的:has():依裡面有什麼設定外層樣式,例如表單有錯誤欄位時整個外框變紅@layer:宣告樣式的優先順序,不用靠選擇器權重和!important互相壓subgrid:讓子元素沿用父層 grid 的欄列線,一排卡片的標題和按鈕能橫向對齊color-mix():直接在 CSS 混色,hover 色和淺色變體可以從主色衍生field-sizing:讓<textarea>隨輸入內容自動變高,不用 JS 算scrollHeightanimation-timeline:讓動畫進度綁定捲動位置,不是時間,閱讀進度條不用監聽 scroll
沒用到的原生 HTML
我把 GitHub 的頁面和點開的選單看過一輪,底下這些都沒出現:
<dialog>、<details>、<select>、<search>、<progress>- Invoker Commands(
command/commandfor)和interestfor inert、hidden="until-found"、contenteditable="plaintext-only"<script type="speculationrules">
最值得講的是 dialog
在沒用到的功能裡,我覺得 <dialog> 的落差最大。GitHub 的篩選選單依然是 <div role="dialog">,很多行為就得自己寫程式補齊:
| 行為 | 原生 <dialog> + showModal() | div role="dialog" |
|---|---|---|
| 蓋在其他東西上面 | 自動進 top layer,不受 overflow 和 z-index 影響 | 靠 position: fixed 和 z-index |
| 背景不能操作 | 其餘頁面自動變 inert | 自己在背景加 inert 或 aria-hidden |
| 焦點被困在裡面 | 瀏覽器處理 | 自己寫 focus trap |
| Esc 關閉 | 內建 | 自己監聽鍵盤 |
| 遮罩 | ::backdrop | 自己多放一個 div |
<dialog id="filter">
<h2>Filter by labels</h2>
<button commandfor="filter" command="close">Close</button>
</dialog>
<button commandfor="filter" command="show-modal">Labels</button>開關甚至能用 Invoker Commands 寫成宣告式,一行 JS 都不用寫。
下面把兩種做法並排。分別打開後連按幾次 Tab 再按 Esc,對話框裡會即時顯示焦點跑到哪裡、Esc 有沒有作用:
GitHub 的 stylesheet 裡有 ::backdrop 規則,但 DOM 裡沒有任何 <dialog>,推測多半是給 popover 用的(popover 自己也有 ::backdrop)。
其他沒用到的標籤像 <details>、<search>、inert、hidden="until-found" 也是類似狀況,瀏覽器本來就已經處理好焦點、鍵盤與無障礙。
GitHub 沒換成原生 <dialog>,我的猜測是歷史包袱加上對跨瀏覽器相容性的保守考量。選單現有的鍵盤控制、搜尋、多選、非同步載入,在 React 元件裡都已經寫好了,換成原生等於整套重寫。這只是推測,單看掃描數據看不出具體原因。
小結
- Popover API:tooltip 幾乎都用了,hover 時由 JS 呼叫
showPopover(),位置也是 JS 算好寫進 inline style - Anchor Positioning:用在選單,讓選單對齊觸發按鈕,但選單本身沒進 top layer
- 沒用到的原生 HTML:
<dialog>、<details>、<select>都沒出現,選單和對話框還是div加 ARIA - CSS:
content-visibility確實套在 PR 的檔案區塊和檔案樹上,@starting-style和 View Transitions 的規則寫在 IssueViewer 和 Copilot 的元件裡,我沒有看到它們實際渲染出來
看起來 GitHub 是挑風險小的地方先換。tooltip 換成 popover 幾乎沒有副作用;選單和 dialog 牽涉焦點、鍵盤操作和既有的 React 元件,就還沒動。
想在自己的專案導入的話,可以照同樣的順序:先用 popover 做 tooltip,再用 Anchor Positioning 拿掉定位用的 JS,最後才考慮把選單換成原生 <dialog>。
Reference
- Popover API — MDN
- Top layer — MDN
- CSS anchor positioning — MDN
- position-area — MDN
- position-try-fallbacks — MDN
- @starting-style — MDN
- View Transition API — MDN
- content-visibility — MDN
<dialog>— MDN
每兩週,分享寫作與所見。每天進步一點點,一起在終點遇見更好的自己。

Leo ChiuSenior Software Engineer @ KKday
資深軟體工程師,任職於 KKday Growth Team,主導網站 SEO / AEO(AI 搜尋優化)策略落地。平常在團隊裡推動 AI agent 的開發流程與效能優化。


