В Floating UI концепция виртуальных элементов используется для управления позиционированием всплывающих компонентов, которые не обязательно существуют в DOM, но требуют точного расчёта координат относительно других элементов. Динамические виртуальные элементы позволяют создавать гибкие интерфейсы, где позиционирование зависит от данных, событий или пользовательских вычислений.
В Floating UI виртуальный элемент описывается объектом, который
реализует интерфейс VirtualElement. Основные свойства:
getBoundingClientRect(): функция, возвращающая объект
DOMRect или аналогичный, содержащий координаты и размеры
виртуального элемента.contextElement (опционально): элемент DOM, используемый
для расчёта относительных координат.Пример базового виртуального элемента:
const virtualElement = {
getBoundingClientRect: () => ({
x: 100,
y: 150,
width: 50,
height: 30,
top: 150,
left: 100,
right: 150,
bottom: 180,
}),
};
Такой объект можно передавать в функции computePosition
для вычисления позиции всплывающего окна:
import { computePosition, offset, flip, shift } from '@floating-ui/dom';
const floating = document.querySelector('#tooltip');
computePosition(virtualElement, floating, {
placement: 'top',
middleware: [offset(10), flip(), shift({ padding: 5 })],
}).then(({ x, y }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`,
});
});
Одним из ключевых преимуществ виртуальных элементов является
возможность динамически обновлять координаты. Для этого
достаточно, чтобы функция getBoundingClientRect возвращала
актуальные значения при каждом вызове computePosition.
Например:
const dynamicVirtualElement = {
getBoundingClientRect: () => {
const mouseX = window.mouseX || 0;
const mouseY = window.mouseY || 0;
return {
x: mouseX,
y: mouseY,
width: 0,
height: 0,
top: mouseY,
left: mouseX,
right: mouseX,
bottom: mouseY,
};
},
};
window.addEventListener('mousemove', (event) => {
window.mouseX = event.clientX;
window.mouseY = event.clientY;
computePosition(dynamicVirtualElement, floating, {
placement: 'right',
middleware: [offset(5), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`,
});
});
});
В этом примере всплывающий элемент следит за курсором мыши, при этом не существует в DOM как исходный элемент — позиция рассчитывается исключительно виртуально.
VirtualElement можно использовать не только для курсора, но и для сложных интерфейсных элементов:
function createTooltipVirtualElement(target) {
return {
getBoundingClientRect: () => target.getBoundingClientRect(),
};
}
const tooltipFloating = document.querySelector('#tooltip');
targetElement.addEventListener('mouseenter', () => {
const virtual = createTooltipVirtualElement(targetElement);
computePosition(virtual, tooltipFloating, {
placement: 'bottom',
middleware: [offset(10), flip()],
}).then(({ x, y }) => {
Object.assign(tooltipFloating.style, { left: `${x}px`, top: `${y}px` });
});
});
Floating UI предоставляет мощные middleware для коррекции положения, смещения и адаптации под границы окна. Виртуальные элементы полностью совместимы с этими инструментами:
offset(value) — добавляет смещение от виртуального
элемента.flip() — автоматически переворачивает положение при
нехватке места.shift({ padding }) — сдвигает элемент внутрь видимой
области.arrow({ element }) — позиционирует стрелку относительно
виртуального элемента.Комбинируя middleware с динамическими виртуальными элементами, можно создавать интерфейсы с адаптивным поведением, которые корректно реагируют на изменения DOM, размеры окна и взаимодействие пользователя.
Динамические виртуальные элементы могут часто пересчитывать позицию. Для улучшения производительности:
requestAnimationFrame вместо прямого
вызова на каждый mousemove.Пример оптимизированного обновления:
let rafId;
window.addEventListener('mousemove', (event) => {
window.mouseX = event.clientX;
window.mouseY = event.clientY;
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
computePosition(dynamicVirtualElement, floating, {
placement: 'right',
middleware: [offset(5), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`,
});
});
});
});
Виртуальные элементы особенно полезны, когда всплывающий элемент рендерится через портал или находится внутри Shadow DOM, где прямой DOM-контекст может быть недоступен. Позиция рассчитывается независимо от физического родителя, что упрощает интеграцию с библиотеками типа React, Vue или Svelte.
getBoundingClientRect может учитывать трансформации
родительских элементов или скролл.Динамические виртуальные элементы открывают возможности для создания интерактивных, высокоадаптивных интерфейсов, где всплывающие компоненты могут позиционироваться вне зависимости от DOM и при этом оставаться полностью совместимыми с системой middleware Floating UI.