12k
All articles

Легковесный подход к всплывающим подсказкам в React

Сравнение нативного title, CSS-паттернов, хуков и Floating UI для создания доступных тултипов в React с определением коллизий и минимальным бандлом.

OpenReplay Team
OpenReplay Team
Легковесный подход к всплывающим подсказкам в React

Вам нужна всплывающая подсказка (tooltip). Возможно, это подсказка для кнопки-иконки или контекст для обрезанной метки. Прежде чем обращаться к полноценному UI-фреймворку или устаревшей библиотеке вроде Tippy.js, подумайте, как мало кода вам на самом деле нужно.

Это руководство рассматривает легковесные паттерны всплывающих подсказок в React — от простейшего нативного подхода до минимальных пользовательских хуков и headless-библиотек для всплывающих подсказок, таких как Floating UI. Каждый шаг добавляет возможности без раздувания вашего бандла.

Ключевые выводы

  • Нативный атрибут title предоставляет всплывающие подсказки с нулевой стоимостью, но не имеет стилизации и поддержки клавиатуры
  • CSS-only всплывающие подсказки предлагают стилизацию и состояния фокуса без JavaScript, но не могут обрабатывать коллизии с viewport
  • Минимальный пользовательский хук дает программный контроль при сохранении малого размера бандла
  • Floating UI обеспечивает обнаружение коллизий и осведомленность о viewport примерно в 3 КБ
  • Всегда приоритизируйте доступность, используя правильные ARIA-атрибуты, и никогда не размещайте критическую информацию только во всплывающих подсказках

Начните с нативного атрибута Title

Встроенный в браузер атрибут title — это базовый вариант без JavaScript:

<button title="Save your changes">💾</button>

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

CSS-Only всплывающие подсказки в React

Для стилизованных всплывающих подсказок без логики JavaScript, CSS обрабатывает базовые состояния наведения:

function IconButton({ label, children }) {
  return (
    <button className="tooltip-trigger" aria-describedby="tooltip-id">
      {children}
      <span className="tooltip" id="tooltip-id" role="tooltip">{label}</span>
    </button>
  )
}
.tooltip-trigger {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}

.tooltip-trigger:hover .tooltip,
.tooltip-trigger:focus .tooltip {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .tooltip {
    transition: none;
  }
}

Этот CSS-only паттерн учитывает prefers-reduced-motion и срабатывает как при наведении, так и при фокусе. Для доступности всплывающих подсказок в React, role="tooltip" и aria-label на кнопке обеспечивают контекст для программ чтения с экрана.

Ограничение? Нет обнаружения коллизий. Если всплывающая подсказка находится рядом с краем viewport, она обрезается.

Минимальный пользовательский хук

Когда вам нужен программный контроль без библиотеки, хорошо работает небольшой хук:

import { useState, useCallback } from 'react'

function useTooltip() {
  const [isOpen, setIsOpen] = useState(false)

  const triggerProps = {
    onMouseEnter: useCallback(() => setIsOpen(true), []),
    onMouseLeave: useCallback(() => setIsOpen(false), []),
    onFocus: useCallback(() => setIsOpen(true), []),
    onBlur: useCallback(() => setIsOpen(false), []),
  }

  return { isOpen, triggerProps }
}

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

function TooltipButton({ hint, children }) {
  const { isOpen, triggerProps } = useTooltip()
  const id = `tooltip-${React.useId()}`

  return (
    <span style={{ position: 'relative', display: 'inline-block' }}>
      <button {...triggerProps} aria-describedby={isOpen ? id : undefined}>
        {children}
      </button>
      {isOpen && (
        <span id={id} role="tooltip" className="tooltip">
          {hint}
        </span>
      )}
    </span>
  )
}

Связь aria-describedby соединяет кнопку с её всплывающей подсказкой для вспомогательных технологий. Это важно: всплывающие подсказки должны дополнять, а не заменять доступные метки. Никогда не размещайте критическую информацию только во всплывающей подсказке.

Headless-библиотеки для всплывающих подсказок: Floating UI

Когда вам нужно правильное позиционирование — обнаружение коллизий, переворачивание, смещение — Floating UI является современным выбором. Это преемник Popper.js и весит около 3 КБ.

Всплывающие подсказки Floating UI React дают вам примитивы без навязывания стилизации:

import {
  useFloating,
  offset,
  flip,
  shift,
  useHover,
  useFocus,
  useInteractions,
} from '@floating-ui/react'
import { useState } from 'react'

function Tooltip({ label, children }) {
  const [isOpen, setIsOpen] = useState(false)

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(6), flip(), shift()],
  })

  const hover = useHover(context)
  const focus = useFocus(context)
  const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus])

  return (
    <>
      <span ref={refs.setReference} {...getReferenceProps()}>
        {children}
      </span>
      {isOpen && (
        <div
          ref={refs.setFloating}
          style={floatingStyles}
          role="tooltip"
          {...getFloatingProps()}
        >
          {label}
        </div>
      )}
    </>
  )
}

Это автоматически обрабатывает границы viewport. Middleware flip перепозиционирует элемент, когда заканчивается место, а shift сохраняет видимость всплывающей подсказки вдоль оси.

Для безопасности SSR хуки Floating UI не обращаются к window или document во время рендеринга. Если вы создаете пользовательское решение, защитите эти ссылки в useEffect или useLayoutEffect.

Когда использовать порталы

Если родитель вашей всплывающей подсказки имеет overflow: hidden или сложные контексты наложения, рендерите всплывающую подсказку в портале:

import { createPortal } from 'react-dom'

{isOpen && createPortal(
  <div ref={refs.setFloating} style={floatingStyles} role="tooltip">
    {label}
  </div>,
  document.body
)}

Это избегает обрезающих контейнеров. Floating UI обрабатывает позиционирование независимо от того, где всплывающая подсказка рендерится в DOM.

Почему не Tippy.js?

Tippy.js и react-popper хорошо служили годами, но они фактически устарели. Floating UI предлагает тот же движок позиционирования с меньшим размером и лучшей интеграцией с React через хуки. Для новых проектов headless-библиотеки для всплывающих подсказок, такие как Floating UI или Radix UI Tooltip, являются практичным выбором.

Выбор подхода

Сопоставьте сложность с потребностью:

  • Нативный title: Нулевая стоимость, нулевой контроль
  • CSS-only: Стилизованный, доступный, без логики позиционирования
  • Пользовательский хук: Полный контроль, минимальный код, ручное позиционирование
  • Floating UI: Обнаружение коллизий, осведомленность о viewport, ~3 КБ

Заключение

Всплывающие подсказки не должны быть тяжелыми. Начните с простого, добавляйте возможности, когда этого требует случай использования, и держите доступность в центре каждой реализации. Нативный атрибут title работает для базовых случаев, CSS-only решения обрабатывают потребности в стилизации, а когда вам нужно обнаружение коллизий, Floating UI предоставляет это без раздувания устаревших библиотек.

Часто задаваемые вопросы

Как сделать всплывающие подсказки доступными для пользователей программ чтения с экрана?

Используйте aria-describedby для связи элемента-триггера с содержимым всплывающей подсказки. Добавьте role tooltip к самому элементу всплывающей подсказки. Делайте текст подсказки кратким и дополнительным. Никогда не размещайте важную информацию только во всплывающих подсказках, поскольку пользователи, полагающиеся на клавиатуру или программы чтения с экрана, могут пропустить отложенный или активируемый только наведением контент.

Почему моя всплывающая подсказка обрезается у края экрана?

CSS-only всплывающие подсказки не имеют обнаружения коллизий. Всплывающая подсказка позиционируется относительно своего родителя без проверки границ viewport. Используйте Floating UI с middleware flip и shift для автоматического перепозиционирования всплывающих подсказок, когда они иначе обрезались бы. Альтернативно рендерите всплывающие подсказки в портале, чтобы избежать контейнеров с overflow hidden.

Должен ли я использовать Tippy.js или Floating UI для новых React-проектов?

Floating UI является рекомендуемым выбором для новых проектов. Это преемник Popper.js, который питал Tippy.js, и предлагает меньший размер бандла около 3 КБ с лучшей интеграцией React через хуки. Tippy.js всё ещё работает, но считается устаревшим и несёт больший вес, чем современные альтернативы.

Когда следует рендерить всплывающие подсказки в React-портале?

Используйте порталы, когда родители всплывающих подсказок имеют overflow hidden, overflow scroll или сложные контексты наложения, которые вызывают обрезание. Порталы рендерят всплывающую подсказку непосредственно в document.body, избегая любых ограничений контейнера. Floating UI корректно обрабатывает позиционирование независимо от того, где всплывающая подсказка появляется в дереве DOM.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.