透過 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 中試用「Experimental Web Platform 功能」旗標後方的 CSS 錨定定位 API。如要啟用該標記,請開啟 Chrome Canary 並前往 chrome://flags。接著啟用「Experimental web platform features」旗標。

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

或者,您可以在 HTML 中使用 anchor 屬性定義錨定標記。屬性值是錨定元素的 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 值,即可免費進行翻轉。舉例來說,如果您將 bottom 設為 anchor,請將 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 的內容選單開始著手。做法如下:點選有 V 形圖案的按鈕,就會顯示內容選單。並透過專屬選單加以展開。

這裡並不是重點。但每個按鈕都使用 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。請嘗試使用選取項目變更內容位置。您選擇的選項會更新格線對齊方式。這會影響錨定標記的位置。

專注並追蹤

這個示範會結合 CSS 基本功能,藉由導入 :has() 的功能。其概念是為聚焦的 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 Anchor 定位,

開始引進「:has()」等內容時,很不錯。您可以在開啟時旋轉標記:

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

接下來你可從哪裡著手?我們還需要做什麼,才能讓 select 正常運作?我們會在下一篇文章儲存。不過別擔心,我們即將推出可設定樣式的精選元素。敬請期待!


就是這麼簡單!

網路平台不斷在演進。CSS 錨定位置是改善使用者介面控制項開發方式的重要一環。就能省去一些棘手的決策。但它還能讓您執行以前從未做過的事情。例如設定 <select> 元素的樣式!請提供您寶貴的意見。

相片來源:CHUTTERSNAP (於 Unsplash 上)