Back

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

Легковесный подход к всплывающим подсказкам в 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.

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

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

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. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay