はじめに
「ツールチップが画面端で見切れる」「ドロップダウンが親要素の外に飛び出す」「スクロールすると吹き出しの位置がズレる」——こうした要素の位置決めに関する不具合に悩まされた経験はないでしょうか。
CSSのposition: absoluteだけで解決しようとすると、画面のリサイズやスクロール、要素のサイズ変化など、あらゆるケースを自前で計算する羽目になります。この面倒な座標計算を肩代わりしてくれるのが、今回紹介するFloating-UIです。
とはいえ、説明を読むより実際に触ってみた方が早いと思います。placementやflip・shiftの設定をその場で切り替えて、ツールチップの位置がどう変わるか確認できるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
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
Floating-UIのサンプルを動かす
まずは素のJavaScript(@floating-ui/dom)で、computePositionによる位置計算を実際に触ってみましょう。placementのセレクトボックスで基準となるボタンに対する配置方向を切り替えたり、flip・shiftミドルウェアのチェックボックスをオン・オフしたりすると、ツールチップの位置がその場で変わります。
要点だけを抜き出すと、次のようになります。offsetで余白を作り、flip・shiftをmiddleware配列に加えるかどうかで挙動を切り替えています。
import { computePosition, offset, flip, shift } from '@floating-ui/dom'
const middleware = [offset(8)]
if (flipEnabled) middleware.push(flip())
if (shiftEnabled) middleware.push(shift({ padding: 8 }))
computePosition(button, tooltip, {
placement, // 'top' | 'right' | 'bottom' | 'left' など
middleware,
}).then(({ x, y }) => {
Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px` })
})
実際に動かせるのが下のサンプルです。placementをleftに変えたり、flip・shiftのチェックを外したりして反応を確かめてみてください。
placementをleftに切り替えるとツールチップがボタンの左側に回り込み、flipのチェックを外した状態で画面の端にウィンドウ幅を狭めると、はみ出しても反転せずにそのまま突き抜けてしまう様子が確認できます。shiftを外すと、画面端で吹き出しがボタンから左右にずれて見切れることもあります。
続いてReact版です。@floating-ui/reactのuseFloatingフックとuseClick・useDismiss・useInteractionsフックを使って、クリックで開閉するポップオーバーを作ります。
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement,
middleware: [offset(8), flip(), shift({ padding: 8 })],
})
const click = useClick(context)
const dismiss = useDismiss(context)
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss])
実際に動かせるのが下のサンプルです。placementのセレクトを変えてからボタンをクリックし、ポップオーバーの開く位置を確認してみてください。
useClickとuseDismissをuseInteractionsに渡すだけで、クリックでの開閉と外側クリックでの閉じる処理を自前で書く必要がなくなっている点がポイントです。middlewareのoffset(8)をoffset(24)のように大きくすると、ボタンとポップオーバーの間隔が広がることも確認できます。
基本的な使い方
まずは素の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を覗いてみることをおすすめします。