Back

CSS Anchor Positioning: JavaScriptツールチップライブラリの終焉(完全ガイド)

フロントエンド開発者なら、ツールチップのポジショニングで消耗した経験、一度はあるんじゃないでしょうか。ボタンに紐づくドロップダウン、トリガーに追従するツールチップ、特定の要素に張り付くポップオーバー。解決策はいつも同じでした——JavaScriptライブラリを入れて、スクロール・リサイズのたびに座標計算して、オーバーフロー検出を処理して、z-indexが壊れないことを祈る。これが10年以上続いた苦行です。

Floating UI(Popper.jsの後継)のコアパッケージだけでnpmの週間ダウンロード数は2,300万超え。CSSがネイティブにできなかったことを補うために、これだけのプロジェクトがライブラリに依存していたわけです。でも、その時代は終わりました。

CSS Anchor Positioning APIがBaseline 2026としてChrome 125+、Firefox 147+、Safari 26で完全サポート。フラグの裏に隠れた実験的機能じゃありません。プロダクション対応済みで、WebにおけるポジショニングUIの作り方を根本から変える仕様です。

このガイドでは、コアAPI、高度なフォールバック戦略、Popover API連携、JSライブラリからのマイグレーションパターン、そして知らないと確実にハマるエッジケースまで全部カバーします。


CSS Anchor Positioningが解決する問題

このAPIがなかった時代、ある要素を別の要素基準で配置する手段は3つしかありませんでした:

1. Absolute Positioning + 手動オフセット — 壊れやすく、スクロールでずれ、オーバーフロー対応不可。

/* クラシックなハック */ .tooltip-wrapper { position: relative; } .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); }

静的なレイアウトでは動きますが、トリガー要素がビューポートの端にあったり、ページがスクロールしたり、コンテナにoverflow: hiddenが設定されていると破綻します。

2. JavaScriptによる位置計算 — 10年間の業界標準でした。

// Floating UIが内部でやっていること(簡略版) function updatePosition(anchor, floating) { const anchorRect = anchor.getBoundingClientRect(); const floatingRect = floating.getBoundingClientRect(); let top = anchorRect.bottom + 8; let left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2; // オーバーフロー検出 if (top + floatingRect.height > window.innerHeight) { top = anchorRect.top - floatingRect.height - 8; // 上にフリップ } if (left < 0) left = 8; // 右にシフト if (left + floatingRect.width > window.innerWidth) { left = window.innerWidth - floatingRect.width - 8; // 左にシフト } floating.style.top = `${top}px`; floating.style.left = `${left}px`; } // スクロール、リサイズ、DOM変更のたびに実行が必要 window.addEventListener('scroll', () => updatePosition(anchor, floating), { passive: true }); window.addEventListener('resize', () => updatePosition(anchor, floating)); const observer = new ResizeObserver(() => updatePosition(anchor, floating)); observer.observe(anchor);

動くことは動きます。でも、スクロールするたびにJSがレイアウト計算を回しているんです。ブラウザのレンダリングパイプラインに「割り込ませてもらいます!」と妨害しているようなものです。

3. CSS Anchor Positioning — ネイティブソリューション。

.trigger { anchor-name: --my-trigger; } .tooltip { position: fixed; position-anchor: --my-trigger; bottom: anchor(top); left: anchor(center); translate: -50% 0; /* 自動オーバーフロー処理 */ position-try-fallbacks: flip-block; }

JavaScript不要。スクロールリスナー不要。ResizeObserver不要。getBoundingClientRect()不要。ブラウザがレンダリングパイプライン内で全部やってくれます。本来こうあるべきだったんですが、やっと実現しました。


コアAPI:3つのビルディングブロック

CSS Anchor Positioningの構成要素は3つだけ。アンカーの宣言アンカー基準のポジショニングオーバーフローの処理

1. アンカーを宣言する

どんな要素でもanchor-nameプロパティで名前を付ければアンカーになります:

.profile-avatar { anchor-name: --avatar; } .settings-gear { anchor-name: --settings-btn; }

ルール:

  • 名前はCSSのdashed-ident構文(--プレフィックス)を使います。
  • 一つの要素に設定できるアンカー名は一つだけです。
  • アンカー名はポジショニングされた要素と同じcontaining blockスコープに存在する必要があります。

HTMLインラインでも宣言できます:

<button style="anchor-name: --menu-trigger">メニュー</button>

2. アンカー基準でポジショニング

要素をアンカーに対して配置するには、3つの条件が必要です:

  1. 配置対象にposition: absoluteposition: fixedを設定。
  2. position-anchorでアンカーにリンク。
  3. insetプロパティ(toprightbottomleft)でanchor()関数を使用。
.status-badge { position: fixed; position-anchor: --avatar; /* アバターの右下隅に配置 */ top: anchor(bottom); left: anchor(right); }

anchor()関数はアンカーのジオメトリを参照するサイドキーワードを受け取ります:

anchor()の値意味
anchor(top)アンカーの上端
anchor(bottom)アンカーの下端
anchor(left)アンカーの左端
anchor(right)アンカーの右端
anchor(center)アンカーの中心点(該当軸上)
anchor(start)論理的な開始位置(書字方向対応)
anchor(end)論理的な終了位置

パーセンテージも使えます:

.tooltip { position: fixed; position-anchor: --trigger; /* アンカーの左端から25%の位置 */ left: anchor(25%); bottom: anchor(top); }

position-areaショートハンド

一般的な配置には、position-areaがよりシンプルなメンタルモデルを提供します。個々のサイドではなく、アンカー周囲の3×3グリッドで考えます:

┌──────────┬──────────┬──────────┐
│ top left │ top      │ top right│
├──────────┼──────────┼──────────┤
│ left     │ center   │ right    │
├──────────┼──────────┼──────────┤
│ bottom   │ bottom   │ bottom   │
│ left     │          │ right    │
└──────────┴──────────┴──────────┘
/* アンカー下、中央揃えのツールチップ */ .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; } /* アンカー右のサイドバー */ .sidebar { position: fixed; position-anchor: --panel; position-area: right; } /* アイコン右上の通知バッジ */ .badge { position: absolute; position-anchor: --icon; position-area: top right; }

ほとんどのケースではposition-areaで十分。anchor()関数を直接使うのは、「アンカーの左端から20%」のようなピクセル単位の精度が必要なときだけです。

3. position-try-fallbacksでオーバーフロー処理

CSS Anchor Positioningの真騨はここです。JSライブラリに本当に勝つポイント。position-try-fallbacksは、配置された要素が画面外にはみ出す場合の挙動を定義します:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* 下にはみ出す場合、上にフリップ */ position-try-fallbacks: flip-block; }

ビルトイン戦略:

戦略挙動
flip-blockブロック軸で反対側にフリップ(上 ↔ 下)
flip-inlineインライン軸でフリップ(左 ↔ 右)
flip-block flip-inline両軸でフリップを試行

より細かい制御には、@position-tryでカスタムフォールバック位置を定義できます:

@position-try --above { position-area: top center; margin-bottom: 8px; } @position-try --left-side { position-area: left; margin-right: 8px; } @position-try --right-side { position-area: right; margin-left: 8px; } .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* 順番に試してフィットするものを選択 */ position-try-fallbacks: --above, --left-side, --right-side; }

ブラウザが各フォールバックを順番に評価し、はみ出さない最初のものを自動選択。レイアウト処理の中で勝手に行われるので、JavaScriptもrequestAnimationFrameもResizeObserverも一切不要です。


Popover APIとの統合

CSS Anchor PositioningはHTML Popover API(popover属性)と組み合わせると真価を発揮します。この2つが揃えば、JSゼロで完結するツールチップ/ドロップダウンが完成します:

<button popovertarget="user-menu" style="anchor-name: --menu-btn"> 設定 ⚙️ </button> <div id="user-menu" popover anchor="menu-btn"> <nav> <a href="/profile">プロフィール</a> <a href="/settings">設定</a> <a href="/logout">ログアウト</a> </nav> </div>
[popover] { position: fixed; position-anchor: --menu-btn; position-area: bottom left; margin-top: 4px; position-try-fallbacks: flip-block; }

無料で手に入るもの:

  • トップレイヤーレンダリング — z-index戦争終了。ポップオーバーはブラウザのトップレイヤーでレンダリングされます。
  • ライトディスミス — 外側クリックで自動的にポップオーバーが閉じます。
  • デフォルトでアクセシブル — キーボードナビゲーションとフォーカス管理はブラウザが処理。
  • アンカーポジショニング — メニューがトリガーに追従し、オーバーフローを処理し、スクロールに追従。

JavaScript一切不要。すべてこれだけで。

ツールチップパターン(Popover活用)

<button popovertarget="tip" popovertargetaction="toggle" style="anchor-name: --help-btn"> ヘルプ? </button> <div id="tip" popover="hint"> この操作は元に戻せません。 </div>
#tip { position: fixed; position-anchor: --help-btn; position-area: top center; margin-bottom: 8px; position-try-fallbacks: flip-block, flip-inline; background: var(--surface-inverse); color: var(--text-inverse); padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 240px; }

popover="hint"バリアントは非モーダルでフォーカスを奪いません。ツールチップに最適です。

注意: popover="hint"はInterop 2026のフォーカスエリアで、ブラウザサポートはまだ拡大中です。クロスブラウザ互換性が重要な場合は、popover="manual"にJavaScriptのshow/hideロジックをフォールバックとして使うのが安全です。


実践パターン

パターン1:サブメニュー付きドロップダウン

.nav-item { anchor-name: --nav-item; } .dropdown { position: fixed; position-anchor: --nav-item; position-area: bottom span-right; margin-top: 4px; position-try-fallbacks: flip-block; } .dropdown-item { anchor-name: --sub-trigger; } .submenu { position: fixed; position-anchor: --sub-trigger; position-area: right; margin-left: 2px; position-try-fallbacks: flip-inline; }

position-areaspan-rightキーワードに注目してください。アンカー位置から右方向に展開されます。まさにドロップダウンメニューのあるべき挙動です。サブメニューはflip-inlineでビューポートからはみ出す場合に右から左にフリップします。

パターン2:フォームフィールドバリデーションツールチップ

<div class="field"> <label for="email">メールアドレス</label> <input id="email" type="email" style="anchor-name: --email-input" required placeholder="[email protected]"> <div class="validation-msg" popover="hint" id="email-error"> 有効なメールアドレスを入力してください </div> </div>
#email-error { position: fixed; position-anchor: --email-input; position-area: right; margin-left: 12px; position-try-fallbacks: --below-field; background: var(--color-danger-surface); color: var(--color-danger-text); border: 1px solid var(--color-danger-border); padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 200px; } @position-try --below-field { position-area: bottom span-right; margin-top: 4px; margin-left: 0; }

デスクトップではバリデーションメッセージがインプットの右側に表示されます。モバイルや狭いビューポートでスペースがない場合、自動的にフィールドの下にフォールバックします。メディアクエリは不要です。

パターン3:Google Docsスタイルの文脈注釈

.comment-marker { anchor-name: --comment; background: var(--highlight-yellow); } .comment-thread { position: fixed; position-anchor: --comment; position-area: right; margin-left: 24px; width: 280px; position-try-fallbacks: --left-side; } @position-try --left-side { position-area: left; margin-right: 24px; }

JavaScriptライブラリからのマイグレーション

Floating UI、Tippy.jsなどを使っている場合、概念はこのようにマッピングされます:

Floating UI → CSS Anchor Positioning

Floating UIの概念CSSの同等機能
computePosition()position-anchor + anchor()
placement: 'bottom'position-area: bottom center
flip()ミドルウェアposition-try-fallbacks: flip-block
shift()ミドルウェアカスタム@position-try付きposition-try-fallbacks
offset(8)margin-top: 8px(または該当方向のmargin)
autoUpdate()ビルトイン(自動)
arrowミドルウェア::before疑似要素 + anchor()

マイグレーション例

Before(Floating UI):

import { computePosition, flip, shift, offset } from '@floating-ui/dom'; const button = document.querySelector('#trigger'); const tooltip = document.querySelector('#tooltip'); function update() { computePosition(button, tooltip, { placement: 'bottom', middleware: [offset(8), flip(), shift({ padding: 5 })], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); } const cleanup = autoUpdate(button, tooltip, update);

After(CSS Anchor Positioning):

<button style="anchor-name: --trigger">ホバーしてください</button> <div class="tooltip">ツールチップの内容</div>
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; position-try-fallbacks: flip-block, flip-inline; }

JS版はライブラリのインポート(Floating UIコア約3KB min+gzip)、auto-updateリスナーのセットアップ、アンマウント時のクリーンアップが必要。CSS版はプロパティ3つでJavaScriptはゼロ。この差はデカい。

矢印(Arrow)はどうする?

JavaScriptツールチップライブラリは通常arrowミドルウェアを提供します。CSS Anchor Positioningでは疑似要素を使います:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 12px; position-try-fallbacks: flip-block; } .tooltip::before { content: ''; position: absolute; bottom: 100%; left: anchor(--trigger center); translate: -50% 0; border: 6px solid transparent; border-bottom-color: var(--tooltip-bg); }

ポイント:矢印の疑似要素がanchor()でトリガーの中心に常にアラインされ続けます。オーバーフロー処理でツールチップの位置が変わっても、矢印は正しい位置をキープ。専用ミドルウェアで処理していたことが、CSSプロパティ1つで実現できてしまうわけです。


エッジケースと注意点

1. アンカーのスコープと可視性

アンカーは配置対象と同じcontaining blockスコープに存在する必要があります。アンカーがoverflow: hiddenを持つコンテナ内にある場合、配置対象はコンテナの外を「見る」ことができません。

解決策: 配置対象にposition: fixedを使いましょう。

2. 同じアンカー名を持つ複数要素

複数の要素が同じanchor-nameを持つ場合、DOM順序で最後の要素だけがアクティブなアンカーになります。

解決策: ユニークなアンカー名を使いましょう。

3. HTML anchor属性による暗黙的アンカリング

<button id="my-btn">クリック</button> <div anchor="my-btn" class="popup">コンテンツ</div>
.popup { position: fixed; position-area: bottom center; }

注意:HTML anchor属性は要素のID--なし)を使い、CSS anchor-namedashed-ident--あり)を使います。混同しないでください。

4. アニメーションの注意点

アンカー配置された要素のアニメーションには、@starting-styleで遷移の初期状態を定義します:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.2s; } .tooltip:popover-open { opacity: 1; transform: translateY(0); } @starting-style { .tooltip:popover-open { opacity: 0; transform: translateY(-4px); } }

5. 動的アンカー切替

どのアンカーに対して配置するかを動的に変更できます:

.contextual-toolbar { position: fixed; position-anchor: --active-selection; position-area: top center; margin-bottom: 8px; }
document.addEventListener('selectionchange', () => { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const marker = document.getElementById('selection-marker'); const rect = range.getBoundingClientRect(); marker.style.cssText = ` anchor-name: --active-selection; position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; pointer-events: none; `; } });

Mediumのフローティングフォーマットツールバーのような、ユーザーインタラクションに応じてアンカー位置が変わるコンテキストツールバーに有用なパターンです。


パフォーマンス:ネイティブがJSを圧倒する理由

CSS Anchor PositioningとJSベースのポジショニング、パフォーマンス差は「ちょっと速い」レベルじゃありません。アーキテクチャレベルで違うんです。

JavaScriptポジショニングのパイプライン

ユーザーがスクロール
  → スクロールイベント発火
    → JavaScriptがgetBoundingClientRect()を実行(レイアウト強制)
      → JavaScriptが新しい位置を計算
        → JavaScriptがelement.styleを更新(レイアウトトリガー)
          → ブラウザが再レンダリング

毎スクロールフレーム:Layout → JS → Layout → Paint → Composite

CSS Anchor Positioningのパイプライン

ユーザーがスクロール
  → ブラウザが通常のレイアウトパスで位置を更新
    → Paint → Composite

毎スクロールフレーム:Layout(ポジショニング含む) → Paint → Composite

実際にどれだけ違うのか

20個のツールチップ/ポップオーバーがあるページで:

  • JavaScript方式: 20個のスクロールリスナーが各自レイアウト再計算を強制。高速スクロール時にフレームドロップが発生。
  • CSS方式: スクロールリスナーゼロ。20個すべての位置が1回のレイアウトパスで解決。スクロール中のJavaScript実行ゼロ。

CPUが限られたモバイルデバイスで、メインスレッドの1ミリ秒が重要な環境で、この差がスムーズな60fpsスクロールと目に見えるカクつきの分かれ目になります。


ブラウザサポートとプログレッシブエンハンスメント

2026年3月現在、CSS Anchor Positioningのサポート状況:

  • Chrome/Edge 125+(2024年6月から)
  • Firefox 147+(2026年初頭から)
  • Safari 26+(2026年初頭から)

古いブラウザ向けには@supportsでフォールバックを提供します:

/* フォールバック */ .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); } /* プログレッシブエンハンスメント */ @supports (anchor-name: --a) { .tooltip { position: fixed; position-anchor: --trigger; position-area: top center; margin-bottom: 8px; translate: none; inset: auto; position-try-fallbacks: flip-block; } }

まとめ

CSS Anchor PositioningはFlexboxとGrid以来、CSSに追加された最も重要なレイアウトプリミティブです。10年以上JSが担当していた問題を、オーバーフロー対応含めてブラウザがネイティブに解決してくれます。

APIは拍子抜けするほどシンプル。プロパティ3つ(anchor-nameposition-anchorposition-area)でユースケースの80%をカバーし、@position-tryフォールバックが残りの20%を処理。Popover APIと組み合わせれば、JavaScript一行なしで完全なツールチップ・ドロップダウン・ポップオーバーが実現できます。

2026年の新規プロジェクトで、アンカーポジショニングのためにFloating UIを入れる理由?もうありません。ブラウザがネイティブに処理し、より高速で、コードも少ない。

既存プロジェクトのマイグレーションも簡単です。computePosition()をCSSプロパティに置き換え、スクロール/リサイズリスナーを削除し、依存関係からポジショニングライブラリを外す。ユーザーはよりスムーズなスクロールを体感し、バンドルは小さくなり、コードはスッキリします。

ツールチップポジショニング戦争、終結です。CSSの勝ち。

CSSFrontendWeb DevelopmentPerformanceBrowser APIUI

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう