透過 CSS 錨定位置互相共用元素

您目前如何將一個元素連結至另一個元素?您可以嘗試追蹤其位置,或使用某種形式的包裝函式元素。

<!-- index.html -->
<div class="container">
  <a href="/link" class="anchor">I’m the anchor</a>
  <div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
  position: relative;
}
.anchored {
  position: absolute;
}

這些解決方案通常不理想。需要 JavaScript 或額外的標記。CSS 錨點定位 API 旨在解決這個問題,提供可連結元素的 CSS API。可讓您根據其他元素的位置和大小,設定一個元素的位置和大小。

圖片顯示模擬的瀏覽器視窗,詳細說明工具提示的結構。

瀏覽器支援

您可以在 Chrome Canary 中試用 CSS 錨點定位 API,這項功能位於「Experimental Web Platform Features」標記後方。如要啟用該標記,請開啟 Chrome Canary 並前往 chrome://flags。然後啟用「實驗性網站平台功能」旗標。

Oddbird 團隊也正在開發polyfill。請務必前往 github.com/oddbird/css-anchor-positioning 查看存放區。

您可以使用下列方法檢查是否支援錨點:

@supports(anchor-name: --foo) {
  /* Styles... */
}

請注意,此 API 仍處於實驗階段,可能會有所變動。本文將概略說明重要的部分。目前的實作方式也未完全與 CSS 工作群組規格同步。

問題

為什麼需要這麼做?其中一個常見用途,就是建立工具提示或類似工具提示的體驗。在這種情況下,您通常會將工具提示連結至參照的內容。您通常需要某種方式將元素連結至另一個元素。您也希望與網頁互動時不會中斷連結,例如使用者捲動或調整 UI 大小。

另一個問題是,如果您想確保繫結元素會保留在檢視畫面中,例如您開啟工具提示,但它會遭到檢視區範圍裁剪。這可能會讓使用者體驗不佳。您希望工具提示能隨之調整。

目前的解決方案

目前有幾種方法可解決這個問題。

首先,我們要介紹最基本的「包裝錨點」方法。您可以將這兩個元素取出,並將它們包裝在容器中。接著,您可以使用 position 將工具提示相對於錨點定位。

<div class="containing-block">
  <div class="tooltip">Anchor me!</div>
  <a class="anchor">The anchor</a>
</div>
.containing-block {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
}

您可以移動容器,大部分情況下,所有內容都會保留在您想要的位置。

如果您知道錨點的位置,或可以以某種方式追蹤錨點,則可以採用其他方法。您可以將其傳遞至自訂屬性的工具提示。

<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
  --anchor-width: 120px;
  --anchor-top: 40vh;
  --anchor-left: 20vmin;
}

.anchor {
  position: absolute;
  top: var(--anchor-top);
  left: var(--anchor-left);
  width: var(--anchor-width);
}

.tooltip {
  position: absolute;
  top: calc(var(--anchor-top));
  left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
  transform: translate(-50%, calc(-100% - 10px));
}

但如果不知道錨點的位置怎麼辦?您可能需要透過 JavaScript 介入。您可以執行類似以下程式碼的操作,但這表示樣式開始從 CSS 流出,並進入 JavaScript。

const setAnchorPosition = (anchored, anchor) => {
  const bounds = anchor.getBoundingClientRect().toJSON();
  for (const [key, value] of Object.entries(bounds)) {
    anchored.style.setProperty(`--${key}`, value);
  }
};

const update = () => {
  setAnchorPosition(
    document.querySelector('.tooltip'),
    document.querySelector('.anchor')
  );
};

window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);

這會引發一些問題:

  • 何時計算樣式?
  • 如何計算樣式?
  • 我要多久計算一次樣式?

這樣解決問題了嗎?這可能適用於您的用途,但有一個問題:我們的解決方案無法調整。沒有回應。如果錨定元素遭到可視區域截斷,該怎麼辦?

接下來,您需要決定是否要回應,以及如何回應。您需要回答的問題和做出的決策數量開始增加。您只需要將一個元素連結至另一個元素即可。在理想情況下,您的解決方案會根據周遭環境調整及做出反應。

為了減輕這方面的痛苦,您可以使用 JavaScript 解決方案來解決問題。這會導致在專案中加入依附元件的成本,而且可能會因使用方式而導致效能問題。舉例來說,某些套件會使用 requestAnimationFrame 來維持正確的位置。也就是說,您和您的團隊必須熟悉套件及其設定選項。因此,您的問題和決策可能不會減少,但會有所變更。這就是 CSS 錨點定位的「原因」之一。這樣一來,您在計算位置時,就不會需要考慮效能問題。

以下是使用「floating-ui」這個常用套件 (用於解決這個問題) 的程式碼:

import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';

const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')

const updatePosition = () => {  
  computePosition(anchor, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip()]
  })
    .then(({x, y}) => {
      Object.assign(tooltip.style, {
        left: `${x}px`,
        top: `${y}px`
      })
  })
};

const clean = autoUpdate(anchor, tooltip, updatePosition);

請嘗試在使用該程式碼的這個示範中重新調整錨點位置。

「工具提示」的運作方式可能不如預期。它會對 y 軸上的可視區域外部做出反應,但不會對 x 軸做出反應。深入研究說明文件,很可能就能找到適合自己的解決方案。

不過,尋找適合專案的套件可能會耗費大量時間。這需要額外決定,而且如果無法按照預期運作,可能會讓您感到挫折。

使用錨定定位

輸入 CSS 錨點定位 API。這個概念是將樣式保留在 CSS 中,並減少需要做出的決策次數。您希望達到相同的結果,但目標是改善開發人員體驗。

  • 不需要 JavaScript。
  • 讓瀏覽器根據您的指示找出最佳位置。
  • 不再有第三方依附元件
  • 沒有包裝元素。
  • 適用於頂層元素。

讓我們重新建立上述問題,並嘗試解決。但請改用船隻與錨的類比。這些元素分別代表錨定元素和錨點。水代表包含區塊。

首先,您需要選擇如何定義錨點。您可以在 CSS 中設定錨點元素的 anchor-name 屬性,即可執行這項操作。可接受 破折線標示的識別項值。

.anchor {
  anchor-name: --my-anchor;
}

或者,您也可以使用 anchor 屬性在 HTML 中定義錨點。屬性值是錨點元素的 ID。這會建立隱含錨點。

<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>

定義錨點後,您可以使用 anchor 函式。anchor 函式採用 3 個引數:

  • 錨點元素:要使用的錨點的 anchor-name,或者您可以省略值,使用 implicit 錨點。您可以透過 HTML 關係定義,也可以使用具有 anchor-name 值的 anchor-default 屬性。
  • 錨點側:您要使用的錨點位置關鍵字。這可以是 toprightbottomleftcenter 等,也可以傳遞百分比。例如,50% 等於 center
  • 備用值:這是可選的備用值,可接受長度或百分比。

您可以使用 anchor 函式做為錨定元素的內嵌屬性 (toprightbottomleft 或其邏輯等效值) 的值。您也可以在 calc 中使用 anchor 函式:

.boat {
  bottom: anchor(--my-anchor top);
  left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}

 /* alternative with anchor-default */
.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: calc(anchor(center) - (var(--boat-size) * 0.5));
}

由於沒有 center 內嵌屬性,因此如果您知道錨定元素的大小,可以使用 calc。為何不使用 translate?您可以使用以下做法:

.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
}

不過,瀏覽器不會考量錨定元素的轉換位置。在考量位置備用和自動定位時,您就會明白為何這麼做很重要。

您可能注意到上述使用了自訂屬性 --boat-size。不過,如果您想讓錨點元素的大小取決於錨點的大小,也可以存取該大小。您可以使用 anchor-size 函式,而非自行計算。舉例來說,假設船隻的寬度是錨的四倍:

.boat {
  width: calc(4 * anchor-size(--my-anchor width));
}

您也可以使用 anchor-size(--my-anchor height) 存取高度。您可以使用它來設定任一軸的大小,也可以設定兩者的大小。

如果您想將錨點設為 absolute 定位元素,規則是元素不得為同層元素。在這種情況下,您可以使用具有 relative 定位功能的容器包裝錨點。接著,您就可以將錨點設為該圖片。

<div class="anchor-wrapper">
  <a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>

請查看這部示範影片,您可以拖曳錨點,船隻就會跟隨。

追蹤捲動位置

在某些情況下,錨點元素可能位於捲動容器內。同時,錨定元素可能位於該容器之外。由於捲動動作會在版面配置的不同執行緒上發生,因此您需要一種追蹤方式。anchor-scroll 屬性可以執行這項操作。您可以在錨定元素上設定這個屬性,並為其指定要追蹤的錨點值。

.boat { anchor-scroll: --my-anchor; }

請試試這個示範,您可以使用角落中的核取方塊,開啟或關閉 anchor-scroll

不過,這項比喻在本例中並不完全適用,因為在理想情況下,您的船和錨都會在水中。此外,Popover API 等功能可讓您保持相關元素的緊密連結。不過,錨點定位可用於頂層的元素。這是 API 的其中一個主要優點:能夠將不同流程中的元素綁定在一起。

請參考這個示範,其中包含捲動容器,以及含有工具提示的錨點。彈出式工具提示元素可能不會與錨點一併顯示:

但您會發現彈出式視窗會追蹤各自的錨點連結。您可以調整捲動容器的大小,系統會自動更新位置。

備用廣告排序和自動廣告排序

這就是錨點位置的力量提升的關鍵。position-fallback 可根據您提供的一組備用項目,為錨定元素定位。您可以透過樣式引導瀏覽器,讓瀏覽器為您找出位置。

這裡的常見用途是,在錨點上方或下方顯示工具提示。這項行為取決於工具提示是否會遭到容器裁剪。這個容器通常是可視區域。

如果您深入研究上一個示範的程式碼,就會發現其中使用了 position-fallback 屬性。如果您捲動容器,可能會發現這些錨定的彈出式視窗會跳動。當各自的錨點靠近檢視區邊界時,就會發生這種情況。此時,彈出式視窗會嘗試調整,以便停留在檢視區中。

在建立明確的 position-fallback 之前,錨點定位功能也會提供自動定位功能。您可以在錨點函式和相反的內嵌屬性中使用 auto 值,免費取得翻轉效果。舉例來說,如果您將 anchor 用於 bottom,請將 top 設為 auto

.tooltip {
  position: absolute;
  bottom: anchor(--my-anchor auto);
  top: auto;
}

自動定位的替代做法是使用明確的 position-fallback。您必須定義位置備用集合。瀏覽器會逐一檢查這些位置,直到找到可用的一個位置為止,然後套用該位置。如果找不到可用的鍵盤,系統會預設使用定義的第一個鍵盤。

嘗試在上方和下方顯示工具提示的 position-fallback 可能會如下所示:

@position-fallback --top-to-bottom {
  @try {
    bottom: anchor(top);
    left: anchor(center);
  }

  @try {
    top: anchor(bottom);
    left: anchor(center);
  }
}

將這項屬性套用至工具提示的效果如下:

.tooltip {
  anchor-default: --my-anchor;
  position-fallback: --top-to-bottom;
}

使用 anchor-default 表示您可以將 position-fallback 重複用於其他元素。您也可以使用範圍限定的自訂屬性來設定 anchor-default

請再看看這部使用船隻的示範影片。有 position-fallback 組合。當您變更錨點的位置時,船隻會調整位置,以便停留在容器內。請嘗試變更邊框間距值,以調整主體邊框間距。請注意瀏覽器如何修正位置。位置會隨著容器的格狀對齊方式而變更。

這次 position-fallback 會以順時針方向嘗試位置,因此會比較冗長。

.boat {
  anchor-default: --my-anchor;
  position-fallback: --compass;
}

@position-fallback --compass {
  @try {
    bottom: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    right: anchor(left);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }
}


範例

瞭解錨點定位的主要功能後,我們來看看除了工具提示之外的其他實用範例。這些範例旨在激發你的靈感,讓你瞭解如何使用錨點定位。要進一步改善規格,最理想的方式就是收集真實使用者的意見回饋。

內容選單

讓我們從使用 Popover API 的內容功能表開始。點選帶有箭頭的按鈕時,會顯示內容選單。而該選單會提供可展開的選單。

標記並非這裡的重點。但您有三個按鈕都使用 popovertarget。接著,您有三個使用 popover 屬性的元素。這樣一來,您就能在不使用任何 JavaScript 的情況下開啟內容選單。如下所示:

<button popovertarget="context">
  Toggle Menu
</button>        
<div popover="auto" id="context">
  <ul>
    <li><button>Save to your Liked Songs</button></li>
    <li>
      <button popovertarget="playlist">
        Add to Playlist
      </button>
    </li>
    <li>
      <button popovertarget="share">
        Share
      </button>
    </li>
  </ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>

您現在可以定義 position-fallback,並在內容選單之間共用。我們也確保為彈出式視窗取消設定任何 inset 樣式。

[popovertarget="share"] {
  anchor-name: --share;
}

[popovertarget="playlist"] {
  anchor-name: --playlist;
}

[popovertarget="context"] {
  anchor-name: --context;
}

#share {
  anchor-default: --share;
  position-fallback: --aligned;
}

#playlist {
  anchor-default: --playlist;
  position-fallback: --aligned;
}

#context {
  anchor-default: --context;
  position-fallback: --flip;
}

@position-fallback --aligned {
  @try {
    top: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }

  @try {
    top: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(bottom);
    left: anchor(right);
  }

  @try {
    right: anchor(left);
    bottom: anchor(bottom);
  }
}

@position-fallback --flip {
  @try {
    bottom: anchor(top);
    left: anchor(left);
  }

  @try {
    right: anchor(right);
    bottom: anchor(top);
  }

  @try {
    top: anchor(bottom);
    left: anchor(left);
  }

  @try {
    top: anchor(bottom);
    right: anchor(right);
  }
}

這樣一來,您就能取得自適應巢狀內容選單 UI。請嘗試使用選取器變更內容位置。您選擇的選項會更新格線對齊方式。這會影響錨點定位方式,進而影響彈出式視窗的位置。

聚焦和追蹤

這個示範會透過引入 :has() 結合 CSS 基本元素。其概念是針對有焦點的 input 轉換視覺指標

方法是在執行階段設定新的錨點。在這個示範中,系統會在輸入焦點時更新受限自訂屬性。

#email {
    anchor-name: --email;
  }
  #name {
    anchor-name: --name;
  }
  #password {
    anchor-name: --password;
  }
:root:has(#email:focus) {
    --active-anchor: --email;
  }
  :root:has(#name:focus) {
    --active-anchor: --name;
  }
  :root:has(#password:focus) {
    --active-anchor: --password;
  }

:root {
    --active-anchor: --name;
    --active-left: anchor(var(--active-anchor) right);
    --active-top: calc(
      anchor(var(--active-anchor) top) +
        (
          (
              anchor(var(--active-anchor) bottom) -
                anchor(var(--active-anchor) top)
            ) * 0.5
        )
    );
  }
.form-indicator {
    left: var(--active-left);
    top: var(--active-top);
    transition: all 0.2s;
}

但您可以如何進一步提升?您可以將其用於某些形式的教學疊加層。工具提示可在各個興趣點之間移動,並更新內容。您可以使用交疊淡出/淡入效果。您可以使用可displayView 轉場製作動畫的獨立動畫。

長條圖計算

錨點定位的另一個有趣用途,就是與 calc 結合使用。假設您在圖表中使用了一些彈出式視窗來註解圖表。

您可以使用 CSS minmax 追蹤最高和最低值。相關的 CSS 可能會如下所示:

.chart__tooltip--max {
    left: anchor(--chart right);
    bottom: max(
      anchor(--anchor-1 top),
      anchor(--anchor-2 top),
      anchor(--anchor-3 top)
    );
    translate: 0 50%;
  }

系統會使用部分 JavaScript 更新圖表值,並使用部分 CSS 設定圖表樣式。不過,錨點位置會為我們處理版面配置更新。

大小調整控點

您不必只錨定至一個元素。您可以為元素使用多個錨點。您可能已經在長條圖範例中注意到這點。工具提示會錨定在圖表和相應的長條圖上。如果您進一步瞭解這個概念,就可以用來調整元素大小。

您可以將定位點視為自訂調整大小的控制點,並傾向使用 inset 值。

.container {
   position: absolute;
   inset:
     anchor(--handle-1 top)
     anchor(--handle-2 right)
     anchor(--handle-2 bottom)
     anchor(--handle-1 left);
 }

在這個示範中,GreenSock Draggable 會讓手把具備可拖曳功能。不過,<img> 元素會調整大小,以填滿容器,並調整填滿手柄之間的空白。

選取選單?

最後一項功能是對未來的預告。不過,您可以建立可聚焦的彈出式視窗,現在您已可使用錨點定位。您可以建立可設定樣式的 <select> 元素基礎。

<div class="select-menu">
<button popovertarget="listbox">
 Select option
 <svg>...</svg>
</button>
<div popover="auto" id="listbox">
   <option>A</option>
   <option>Styled</option>
   <option>Select</option>
</div>
</div>

使用隱含的 anchor 可簡化這項作業。不過,初步的 CSS 可能會像這樣:

[popovertarget] {
 anchor-name: --select-button;
}
[popover] {
  anchor-default: --select-button;
  top: anchor(bottom);
  width: anchor-size(width);
  left: anchor(left);
}

將 Popover API 的功能與 CSS 錨點定位功能結合,就能達到這個效果。

您可以開始引入 :has() 之類的東西,您可以在開啟時旋轉標記:

.select-menu:has(:open) svg {
  rotate: 180deg;
}

接下來要去哪裡?我們還需要做什麼才能讓 select 正常運作?我們會在下一篇文章中介紹這項功能。不過別擔心,我們會推出可設定樣式的 Select 元素。敬請持續鎖定!


就是這麼簡單!

網路平台不斷進化,CSS 錨點定位是改善 UI 控制項開發方式的重要一環。讓您不必做出一些棘手的決定。但也能讓你執行先前無法執行的操作。例如設定 <select> 元素的樣式!請提供您寶貴的意見。

相片來源:CHUTTERSNAP 發布於 Unsplash 網站