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 Anchor Positioning API は、要素をテザリングするための CSS API を提供することで、この問題を解決することを目的としています。他の要素の位置とサイズに基づいて、1 つの要素の位置とサイズを設定する手段を提供します。

この画像は、ツールチップの構成を詳しく記述したブラウザ ウィンドウのモックアップです。

ブラウザ サポート

Chrome Canary では、「試験運用版ウェブ プラットフォーム機能」フラグを使って CSS anchor Positioning API を試すことができます。このフラグを有効にするには、Chrome Canary を開いて chrome://flags にアクセスします。次に、[試験運用版ウェブ プラットフォーム機能] フラグを有効にします。

また、Oddbird のチームが開発中のポリフィルもあります。github.com/oddbird/css-anchor-positioning のリポジトリを確認してください。

アンカーのサポートの有無は、以下で確認できます。

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

この API はまだ試験運用版であるため、変更される可能性があります。この記事では、重要な部分について大まかに説明します。また、現在の実装は CSS ワーキング グループの仕様と完全には同期していません。

問題

この操作が必要な理由代表的なユースケースは、ツールチップやツールチップのようなエクスペリエンスの作成です。ツールチップを、参照先のコンテンツにテザリングすることをおすすめします。多くの場合、要素を別の要素にテザリングする方法が必要になります。また、ページを操作しても、たとえばユーザーが UI をスクロールしたりサイズ変更したりしても、そのテザリングが途切れないことが期待されます。

もう 1 つの問題は、テザリングされた要素をビュー内に保持する必要がある場合です。たとえば、ツールチップを開くと、ビューポートの境界によってクリップされた場合などです。ユーザーにとっては良いエクスペリエンスとは言えないかもしれません。ツールチップを調整します。

現在のソリューション

現在、この問題へのアプローチにはいくつかの方法があります。

まずは、基本的な「アンカーをラップする」アプローチです。両方の要素を取得して、コンテナでラップします。次に、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);

このことから、次のような疑問が浮かび上がります。

  • スタイルはいつ計算されるのですか?
  • スタイルを計算するにはどうすればよいですか?
  • スタイルはどのくらいの頻度で計算されますか?

これで解決するでしょうか?ユースケースには合っているかもしれませんが、問題が 1 つあります。それは、Google のソリューションが適応しないということです。反応しない。アンカー要素がビューポートによって切り取られた場合

今度は、これにどう対応するか、どのように反応するかを決める必要があります。下す必要のある質問や決定の数は増え続けています。必要な作業は、ある要素を別の要素に固定することだけです。理想的な環境では、ソリューションが周囲に適応し、調整されるのです。

この問題を軽減するには、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 プロパティを設定します。dashed-ident 値を受け入れます。

.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 と等しくなります。
  • Fallback: 長さまたは割合を指定する代替値です(省略可)。

anchor 関数は、アンカー要素のインセット プロパティ(toprightbottomleft、またはそれらと論理的に同等のもの)の値として使用します。また、calcanchor 関数を使用することもできます。

.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 関数を使用できます。たとえば、ボートをアンカーの幅の 4 倍にするには、次のようにします。

.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 を使用することで、無料で実行できます。たとえば、bottomanchor を使用する場合は、topauto に設定します。

.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 を使用しているボタンが 3 つあります。次に、popover 属性を使用した 3 つの要素を使用します。これにより、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;
}

しかし、これをさらに進めるにはどうすればよいでしょうか。なんらかの指示オーバーレイに使用できます。ツールチップを使ってスポット間を移動したり、コンテンツを更新したりできます。コンテンツをクロスフェードできます。ここでは、display のアニメーション化View Transitions を可能にする独立したアニメーションを使用できます。

棒グラフの計算

アンカーの配置では、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 があります。ただし、レイアウトの更新はアンカーの位置設定によって処理されます。

ハンドルのサイズ変更

1 つの要素だけに固定する必要はありません。1 つの要素に複数のアンカーを使用できます。棒グラフの例を見ればお気づきでしょうか。ツールチップはグラフに固定され、次に適切な棒に固定されました。この概念をもう少し深く掘り下げると、これを使用して要素のサイズを変更できます。

アンカー ポイントをカスタムのサイズ変更ハンドルのように扱い、inset 値を利用することもできます。

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

このデモでは、GreenSock Draggable によってハンドルが Draggable になります。ただし、<img> 要素は、ハンドル間のギャップを埋めるように調整されたコンテナを埋めるようにサイズ変更されます。

SelectMenu ですか?

最後に紹介するのは、今後の予定に関する予告です。ただし、フォーカス可能なポップオーバーを作成すると、アンカーの位置が設定されます。スタイル設定可能な <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 を機能させるには、他に何が必要ですか?次の記事で保存しますただし、スタイル設定が可能な選択要素も追加される予定です。どうぞご期待ください。


以上です。

ウェブ プラットフォームは進化を続けています。CSS のアンカー配置は、UI コントロールの開発方法を改善するうえで非常に重要です。そうすれば、こうした厄介な決断から解放されます。これまでは不可能だったことも可能になります。たとえば、<select> 要素のスタイル設定などです。ご意見をお聞かせください。

写真撮影: CHUTTERSNAP(出典: Unsplash