Follow cursor эффект

Эффект follow cursor (следования за курсором) в контексте библиотеки Floating UI реализуется через динамическое позиционирование плавающего элемента относительно текущего положения указателя мыши. В отличие от классического привязывания к DOM-элементу, здесь в качестве «якоря» используется виртуальная точка, координаты которой обновляются при каждом событии mousemove.

Floating UI предоставляет механизм виртуальных элементов (virtual elements), позволяющий рассчитывать позицию так, как будто существует реальный DOM-узел.


Виртуальный элемент как основа

Виртуальный элемент — это объект с методом getBoundingClientRect, возвращающим координаты, имитирующие положение элемента на странице.

Простейшая реализация:

const virtualElement = {
  getBoundingClientRect() {
    return {
      x: cursorX,
      y: cursorY,
      width: 0,
      height: 0,
      top: cursorY,
      left: cursorX,
      right: cursorX,
      bottom: cursorY,
    };
  },
};

Ключевая идея: координаты cursorX и cursorY обновляются при движении мыши, а Floating UI использует эти значения для вычисления позиции всплывающего элемента.


Базовая реализация follow cursor

import { computePosition } from '@floating-ui/dom';

const floatingEl = document.querySelector('#tooltip');

let cursorX = 0;
let cursorY = 0;

const virtualElement = {
  getBoundingClientRect() {
    return {
      x: cursorX,
      y: cursorY,
      width: 0,
      height: 0,
      top: cursorY,
      left: cursorX,
      right: cursorX,
      bottom: cursorY,
    };
  },
};

document.addEventListener('mousemove', async (event) => {
  cursorX = event.clientX;
  cursorY = event.clientY;

  const { x, y } = await computePosition(virtualElement, floatingEl);

  Object.assign(floatingEl.style, {
    left: `${x}px`,
    top: `${y}px`,
  });
});

Оптимизация производительности

Частые вызовы computePosition могут приводить к нагрузке на CPU. Используются несколько техник оптимизации:

1. requestAnimationFrame

let frameId = null;

document.addEventListener('mousemove', (event) => {
  cursorX = event.clientX;
  cursorY = event.clientY;

  if (frameId) return;

  frameId = requestAnimationFrame(async () => {
    const { x, y } = await computePosition(virtualElement, floatingEl);

    Object.assign(floatingEl.style, {
      left: `${x}px`,
      top: `${y}px`,
    });

    frameId = null;
  });
});

2. Минимизация перерасчетов layout

Floating UI уже оптимизирован, но важно:

  • не вызывать лишние DOM-операции
  • избегать чтения layout сразу после записи

Использование middleware

Follow cursor почти всегда требует корректировки позиции, чтобы элемент не перекрывал курсор и оставался в пределах экрана.

offset

Сдвиг от курсора:

import { offset } from '@floating-ui/dom';

computePosition(virtualElement, floatingEl, {
  middleware: [offset(10)],
});

flip

Автоматическое изменение стороны:

import { flip } from '@floating-ui/dom';

middleware: [flip()];

shift

Предотвращает выход за границы viewport:

import { shift } from '@floating-ui/dom';

middleware: [shift()];

Полный пример

import { computePosition, offset, flip, shift } from '@floating-ui/dom';

computePosition(virtualElement, floatingEl, {
  middleware: [
    offset(12),
    flip(),
    shift({ padding: 8 }),
  ],
});

Контроль позиции относительно курсора

По умолчанию позиция вычисляется так, будто курсор — это элемент нулевого размера. Для более точного поведения используются стратегии:

Смещение относительно курсора

offset(({ placement }) => {
  if (placement === 'top') return 10;
  return 5;
});

Привязка к определенной стороне

computePosition(virtualElement, floatingEl, {
  placement: 'top-start',
});

Обработка разных типов указателей

Follow cursor может работать не только с мышью:

Touch события

document.addEventListener('touchmove', (event) => {
  const touch = event.touches[0];
  cursorX = touch.clientX;
  cursorY = touch.clientY;
});

Pointer events (рекомендуется)

document.addEventListener('pointermove', (event) => {
  cursorX = event.clientX;
  cursorY = event.clientY;
});

Pointer Events объединяют мышь, стилус и касание.


Управление видимостью элемента

Часто всплывающий элемент отображается только при наведении:

document.addEventListener('mouseenter', () => {
  floatingEl.style.display = 'block';
});

document.addEventListener('mouseleave', () => {
  floatingEl.style.display = 'none';
});

Либо через opacity:

floatingEl.style.opacity = '1';
floatingEl.style.pointerEvents = 'none';

Плавное следование (инерция)

Резкое следование за курсором может выглядеть неестественно. Добавляется сглаживание:

let currentX = 0;
let currentY = 0;

function animate() {
  currentX += (cursorX - currentX) * 0.1;
  currentY += (cursorY - currentY) * 0.1;

  const virtualElement = {
    getBoundingClientRect() {
      return {
        x: currentX,
        y: currentY,
        width: 0,
        height: 0,
        top: currentY,
        left: currentX,
        right: currentX,
        bottom: currentY,
      };
    },
  };

  computePosition(virtualElement, floatingEl).then(({ x, y }) => {
    floatingEl.style.left = `${x}px`;
    floatingEl.style.top = `${y}px`;
  });

  requestAnimationFrame(animate);
}

animate();

Ограничение зоны движения

Иногда требуется, чтобы элемент следовал только в пределах контейнера:

const rect = container.getBoundingClientRect();

cursorX = Math.max(rect.left, Math.min(event.clientX, rect.right));
cursorY = Math.max(rect.top, Math.min(event.clientY, rect.bottom));

Частые проблемы

1. Дергание элемента

  • Причина: частые перерисовки
  • Решение: requestAnimationFrame

2. Смещение относительно курсора

  • Причина: отсутствие offset
  • Решение: добавить middleware

3. Выход за границы экрана

  • Причина: нет shift или flip
  • Решение: добавить middleware

4. Плохая производительность

  • Причина: тяжелые вычисления в обработчике
  • Решение: кэширование, уменьшение логики в mousemove

Расширенные сценарии

Привязка к курсору с учетом размеров элемента

Можно учитывать размер всплывающего блока:

offset(({ rects }) => {
  return rects.floating.height / 2;
});

Динамическое изменение позиции

placement: cursorX > window.innerWidth / 2 ? 'left' : 'right'

Follow cursor + tooltip логика

  • появление с задержкой
  • скрытие при остановке курсора
  • адаптивная позиция

Архитектурные особенности

  • Virtual element полностью отделяет позиционирование от DOM
  • Floating UI работает одинаково для реальных и виртуальных якорей
  • Middleware формирует цепочку трансформаций координат
  • Follow cursor — частный случай общей системы позиционирования

Ключевые моменты

  • Курсор представляется как виртуальный элемент
  • Координаты обновляются через события указателя
  • computePosition вычисляет итоговую позицию
  • Middleware управляет поведением (смещение, границы, флип)
  • Производительность контролируется через requestAnimationFrame

Такая архитектура позволяет реализовывать сложные интерфейсные эффекты: кастомные тултипы, контекстные подсказки, интерактивные элементы, реагирующие на движение пользователя в реальном времени.