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つの条件が必要です:
- 配置対象に
position: absoluteかposition: fixedを設定。 position-anchorでアンカーにリンク。- insetプロパティ(
top、right、bottom、left)で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-areaのspan-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-nameはdashed-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-name、position-anchor、position-area)でユースケースの80%をカバーし、@position-tryフォールバックが残りの20%を処理。Popover APIと組み合わせれば、JavaScript一行なしで完全なツールチップ・ドロップダウン・ポップオーバーが実現できます。
2026年の新規プロジェクトで、アンカーポジショニングのためにFloating UIを入れる理由?もうありません。ブラウザがネイティブに処理し、より高速で、コードも少ない。
既存プロジェクトのマイグレーションも簡単です。computePosition()をCSSプロパティに置き換え、スクロール/リサイズリスナーを削除し、依存関係からポジショニングライブラリを外す。ユーザーはよりスムーズなスクロールを体感し、バンドルは小さくなり、コードはスッキリします。
ツールチップポジショニング戦争、終結です。CSSの勝ち。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう