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 を提供することで、この問題を解決することを目的としています。他の要素の位置とサイズに基づいて、1 つの要素の位置とサイズを設定できます。

ツールチップの構造を詳しく示すブラウザ ウィンドウのモックアップを示す画像。

ブラウザ サポート

CSS アンカー ポジショニング API は、Chrome Canary の [試験運用版のウェブ プラットフォームの機能] フラグで試すことができます。このフラグを有効にするには、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%);
}

コンテナを移動しても、ほとんどのものが元の場所のままになります。

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

<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 のソリューションは適応しません。応答しません。アンカー要素がビューポートで切断される場合はどうなりますか?

次に、これにどのように対応するかを決める必要があります。検討すべき質問や意思決定の数が増え始めています。1 つの要素を別の要素に固定するだけです。理想的には、ソリューションは周囲の環境に合わせて調整し、対応します。

こうした問題を軽減するために、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 は必要ありません。
  • ブラウザがガイダンスに基づいて最適な位置を判断します。
  • サードパーティの依存関係が不要
  • ラッパー要素がない。
  • 最上位レイヤ内の要素で動作します。

上記で解決しようとした問題を再現して対処しましょう。代わりに、アンカーを下ろしたボートの例えを使用してください。これらは、アンカーされた要素とアンカーを表します。水は、そのブロックを表します。

まず、アンカーの定義方法を選択する必要があります。これは、アンカー要素に anchor-name プロパティを設定することで、CSS 内で行うことができます。ダッシュ付き識別子の値を受け入れます。

.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 に相当します。
  • フォールバック: 長さまたは割合を指定できるオプションのフォールバック値です。

アンカー要素のインセット プロパティtoprightbottomleft、またはそれらの論理同等物)の値として anchor 関数を使用します。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 を使用したコンテキスト メニューから始めましょう。矢印付きのボタンをクリックするとコンテキスト メニューが表示されます。そのメニューには、展開できる独自のメニューがあります。

ここで重要なのはマークアップではありません。ただし、3 つのボタンでそれぞれ popovertarget を使用しています。次に、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 をアニメーション化したり、ビューの遷移をアニメーション化したりできる個別のアニメーションが適しています。

棒グラフの計算

アンカー ポジショニングでできることのもう 1 つは、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 を使用してハンドルをドラッグ可能にしています。ただし、<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> 要素のスタイル設定などです。ご意見、ご感想をお寄せください。

写真提供: CHUTTERSNAPUnsplash