はじめに
「ツールチップが画面端で見切れる」「ドロップダウンが親要素の外に飛び出す」「スクロールすると吹き出しの位置がズレる」——こうした要素の位置決めに関する不具合に悩まされた経験はないでしょうか。
CSSのposition: absoluteだけで解決しようとすると、画面のリサイズやスクロール、要素のサイズ変化など、あらゆるケースを自前で計算する羽目になります。この面倒な座標計算を肩代わりしてくれるのが、今回紹介するFloating-UIです。
Floating-UIとは
Floating-UIは、ツールチップやポップオーバー、ドロップダウン、セレクトメニューといった「浮遊要素(floating element)」を、基準となる要素(reference element)に対して正確に配置するためのJavaScriptライブラリです。人気の位置計算ライブラリだったPopper.jsの後継として開発されており、より軽量でツリーシェイクしやすい設計に生まれ変わっています。
コアはフレームワーク非依存で、React・Vue・React Native向けの公式バインディングも用意されているため、どのようなプロジェクトにも導入しやすいのが魅力です。
主な特徴
- ミドルウェアによる柔軟な位置調整 -
offset(余白の追加)、flip(はみ出す場合に反転配置)、shift(画面内に収まるようスライド)などの機能を組み合わせて、複雑な配置ロジックを宣言的に構築できます - 軽量かつツリーシェイク可能 - 必要な機能だけをインポートできるため、バンドルサイズを最小限に抑えられます
- アクセシビリティへの配慮 - React版では
useRoleやuseDismissといったフックが用意されており、ARIA属性やキーボード操作への対応がしやすくなっています - マルチプラットフォーム対応 - DOM向けの
@floating-ui/domだけでなく、React・Vue・React Native向けのパッケージも公式に提供されています
インストール
用途に応じて、以下のパッケージから選択します。
# バニラJavaScript / DOM操作の場合
npm install @floating-ui/dom
# Reactでインタラクション込みで使う場合
npm install @floating-ui/react
# Reactで位置計算のみ使う場合
npm install @floating-ui/react-dom
# Vueの場合
npm install @floating-ui/vue
基本的な使い方
まずは素のJavaScriptで、ボタンに対してツールチップを配置する例を見てみましょう。computePositionが座標計算のコアAPIです。
import { computePosition, offset, flip, shift } from '@floating-ui/dom';
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
computePosition(button, tooltip, {
placement: 'top',
middleware: [
offset(8), // 基準要素との間に8pxの余白を作る
flip(), // 画面からはみ出す場合は配置を反転
shift({ padding: 8 }), // 画面内に収まるようスライド
],
}).then(({ x, y }) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});
});
placementで基本の配置方向を指定し、middlewareに必要な調整ロジックを配列で渡すだけです。ミドルウェアは配列の順番通りに適用されるため、前段の結果を踏まえて次の計算が行われます。
実践的なユースケース
実務では、ReactのuseFloatingフックとuseInteractionsフックを組み合わせて、開閉状態やイベントの管理まで一括で行うのが定番です。ここではホバーで表示するツールチップコンポーネントを作成してみます。
import { useState } from 'react';
import {
useFloating,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
offset,
flip,
shift,
} from '@floating-ui/react';
function Tooltip({ label, children }: { label: string; children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: 'top',
middleware: [offset(6), flip(), shift({ padding: 8 })],
});
// マウスホバー・フォーカス・Escキー・ARIA属性を一括で管理する
const hover = useHover(context);
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });
const { getReferenceProps, getFloatingProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);
return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
{children}
</button>
{isOpen && (
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className="tooltip"
>
{label}
</div>
)}
</>
);
}
useHoverやuseFocus、useDismissはそれぞれホバー表示・フォーカス表示・Escキーでの非表示といった振る舞いを担当し、useRoleが適切なARIA属性を自動で付与してくれます。これらはツリーシェイク可能な個別フックなので、ドロップダウンメニューであればuseClickとuseListNavigationを組み合わせるなど、コンポーネントの性質に合わせて必要なものだけを選んで組み立てられます。
セレクトボックスの選択肢一覧や、通知バッジのポップオーバーなど、「基準要素の近くに、画面内に収まる形で何かを表示したい」というUIパターンであれば、ほぼそのままこの構成が応用できます。
まとめ
Floating-UIは、ツールチップやポップオーバーの位置計算という地味ながら奥が深い課題を、ミドルウェアという宣言的な仕組みで解決してくれるライブラリです。offset・flip・shiftといった基本のミドルウェアを押さえるだけでも、多くの配置問題は解消できます。
さらにReact版ではuseFloatingとuseInteractionsによって、位置計算とインタラクション・アクセシビリティ対応をまとめて実装できるのも大きな強みです。自前でツールチップやドロップダウンを実装して座標計算に悩んでいるなら、一度公式ドキュメントのTutorialを覗いてみることをおすすめします。