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 をスクロールしたりサイズを変更したりした場合など、ページを操作してもそのつなぎ目が途切れることがないようにする必要があります。

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

現在のソリューション

現在、この問題に取り組む方法はいくつかあります。

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

コンテナを移動しても、ほとんどの部分は必要な場所にとどまります。

もう 1 つのアプローチは、アンカーの位置がわかっている場合や、なんらかの方法でトラッキングできる場合です。カスタム プロパティを使用してツールチップに渡すことができます。

<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));
}
Codepen の web-dot-dev による Pen NWMdwLe を参照してください。

しかし、アンカーの位置が不明な場合はどうすればよいでしょうか。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 が使用されます。つまり、お客様とそのチームがパッケージとその構成オプションをよく理解しておく必要があります。その結果、疑問や意思決定が減るのではなく、変化していきます。これは「なぜ」のご覧ください。掲載順位を計算するときにパフォーマンスの問題を考える必要がなくなります。

この問題の一般的なパッケージである「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 をアニメーション化したり、ビュー遷移をアニメーション化できる非連続アニメーションを使用することもできます。

棒グラフによる計算

アンカーの配置では、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