Dropdown компонент

Floating UI — это библиотека для позиционирования плавающих элементов в браузере, таких как тултипы, модальные окна, меню и dropdown. Она обеспечивает точное управление положением элементов относительно их опорных точек, учитывая размеры окна, прокрутку, ограничения видимой области и перекрытия.

Dropdown-компонент представляет собой элемент, который раскрывается при взаимодействии с триггером (кнопкой или ссылкой) и отображает список опций. Floating UI позволяет создавать динамичные, адаптивные и безопасно позиционируемые dropdown-меню.


Установка и базовая инициализация

Для работы с Floating UI необходимо установить пакет:

npm install @floating-ui/dom

Импортируем необходимые функции:

import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
  • computePosition — ключевая функция для вычисления позиции плавающего элемента относительно опорного.
  • offset — позволяет задать смещение по оси X и Y.
  • flip — автоматически изменяет позицию элемента, если он не помещается в видимую область.
  • shift — корректирует положение, чтобы элемент не выходил за границы экрана.
  • autoUpdate — обеспечивает автоматическое пересчитывание позиции при изменении размеров окна или содержимого.

Структура Dropdown

Dropdown состоит из двух основных частей:

  1. Триггер — элемент, который открывает меню (button, a или div).
  2. Плавающее меню — список опций (ul/div), который позиционируется относительно триггера.

Пример базовой разметки:

<button id="dropdown-trigger">Меню</button>
<ul id="dropdown-menu" style="display: none;">
  <li>Опция 1</li>
  <li>Опция 2</li>
  <li>Опция 3</li>
</ul>

Вычисление позиции

Для открытия dropdown и корректного позиционирования используется computePosition:

const trigger = document.getElementById('dropdown-trigger');
const menu = document.getElementById('dropdown-menu');

function openDropdown() {
  menu.style.display = 'block';
  
  computePosition(trigger, menu, {
    placement: 'bottom-start',
    middleware: [
      offset(5),
      flip(),
      shift({ padding: 10 })
    ]
  }).then(({ x, y }) => {
    Object.assign(menu.style, {
      left: `${x}px`,
      top: `${y}px`
    });
  });
}

Пояснение параметров

  • placement — направление относительно триггера (top, bottom, left, right) с уточнением (start, end).

  • middleware — цепочка промежуточных функций, которые корректируют позицию.

    • offset добавляет смещение.
    • flip изменяет направление при нехватке места.
    • shift с padding предотвращает выход элемента за границы видимой области.

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

Чтобы меню оставалось корректно позиционированным при изменении размеров окна или скролле, используется autoUpdate:

const cleanup = autoUpdate(trigger, menu, () => {
  computePosition(trigger, menu, {
    placement: 'bottom-start',
    middleware: [offset(5), flip(), shift({ padding: 10 })]
  }).then(({ x, y }) => {
    Object.assign(menu.style, { left: `${x}px`, top: `${y}px` });
  });
});

// Для закрытия dropdown:
function closeDropdown() {
  menu.style.display = 'none';
  cleanup(); // отключаем автоматическое обновление
}

autoUpdate автоматически отслеживает:

  • изменение размеров окна;
  • скролл родительских элементов;
  • изменения размеров триггера или меню.

Управление открытием и закрытием

Dropdown обычно открывается по клику на триггер, а закрывается при повторном клике, выборе опции или клике вне меню:

trigger.addEventListener('click', () => {
  if (menu.style.display === 'block') {
    closeDropdown();
  } else {
    openDropdown();
  }
});

document.addEventListener('click', (event) => {
  if (!trigger.contains(event.target) && !menu.contains(event.target)) {
    closeDropdown();
  }
});

Адаптивное поведение

Floating UI позволяет создавать dropdown, который адаптируется к ограниченному пространству. Например:

computePosition(trigger, menu, {
  placement: 'bottom-start',
  middleware: [
    offset(5),
    flip({ fallbackPlacements: ['top-start', 'right-start', 'left-start'] }),
    shift({ padding: 10 })
  ]
});
  • fallbackPlacements задаёт список альтернативных направлений, если основное не помещается.
  • shift обеспечивает плавное смещение в пределах окна.

Работа с анимацией и transitions

Поскольку Floating UI управляет только позицией, анимация открытия dropdown выполняется через CSS:

#dropdown-menu {
  transition: opacity 0.2s ease, transform 0.2s ease;
  opacity: 0;
  transform: scaleY(0);
  transform-origin: top;
}

#dropdown-menu.show {
  opacity: 1;
  transform: scaleY(1);
}

И синхронизация с JS:

function openDropdown() {
  menu.style.display = 'block';
  menu.classList.add('show');
  computePosition(trigger, menu, { placement: 'bottom-start', middleware: [offset(5), flip(), shift({ padding: 10 })] })
    .then(({ x, y }) => Object.assign(menu.style, { left: `${x}px`, top: `${y}px` }));
}

function closeDropdown() {
  menu.classList.remove('show');
  setTimeout(() => menu.style.display = 'none', 200);
}

Поддержка nested dropdown

Floating UI позволяет строить вложенные меню, где каждый уровень позиционируется относительно родительской опции:

const parentOption = document.querySelector('.parent-option');
const subMenu = document.querySelector('.sub-menu');

parentOption.addEventListener('mouseenter', () => {
  computePosition(parentOption, subMenu, {
    placement: 'right-start',
    middleware: [offset(2), flip(), shift({ padding: 5 })]
  }).then(({ x, y }) => Object.assign(subMenu.style, { left: `${x}px`, top: `${y}px` }));
  subMenu.style.display = 'block';
});

parentOption.addEventListener('mouseleave', () => {
  subMenu.style.display = 'none';
});

Особенности Accessibility

Dropdown должен быть доступным для клавиатурной навигации:

  • tabindex="0" для опций;
  • управление фокусом с клавишами стрелок (ArrowUp, ArrowDown);
  • закрытие по Escape.
menu.addEventListener('keydown', (event) => {
  const items = Array.from(menu.querySelectorAll('li'));
  let index = items.indexOf(document.activeElement);
  
  if (event.key === 'ArrowDown') {
    index = (index + 1) % items.length;
    items[index].focus();
  } else if (event.key === 'ArrowUp') {
    index = (index - 1 + items.length) % items.length;
    items[index].focus();
  } else if (event.key === 'Escape') {
    closeDropdown();
    trigger.focus();
  }
});

Интеграция с фреймворками

Floating UI можно использовать с React, Vue или Svelte через адаптеры или напрямую, создавая компоненты с реактивным состоянием открытия и закрытия, и синхронизацией позиции через computePosition.

В React пример минимального dropdown:

import { useRef, useState, useEffect } from 'react';
import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';

function Dropdown() {
  const [open, setOpen] = useState(false);
  const triggerRef = useRef(null);
  const menuRef = useRef(null);

  useEffect(() => {
    if (open && triggerRef.current && menuRef.current) {
      const cleanup = autoUpdate(triggerRef.current, menuRef.current, () => {
        computePosition(triggerRef.current, menuRef.current, {
          placement: 'bottom-start',
          middleware: [offset(5), flip(), shift({ padding: 10 })]
        }).then(({ x, y }) => Object.assign(menuRef.current.style, { left: `${x}px`, top: `${y}px` }));
      });
      return cleanup;
    }
  }, [open]);

  return (
    <>
      <button ref={triggerRef} onCl ick={() => setOpen(!open)}>Меню</button>
      <ul ref={menuRef} style={{ display: open ? 'block' : 'none' }}>
        <li>Опция 1</li>
        <li>Опция 2</li>
      </ul>
    </>
  );
}

Этот подход обеспечивает чистое управление состоянием и точное позиционирование даже при изменении размера окна или содержимого dropdown.


Floating UI предоставляет мощные инструменты для создания полностью кастомизируемых dropdown-меню с точной поддержкой позиционирования, адаптивности, анимации и доступности, позволяя реализовать сложные пользовательские интерфейсы без ручного расчёта координат и учета множества граничных случаев.