Легковесный подход к всплывающим подсказкам в 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 соединяет кнопку с её всплывающей подсказкой для вспомогательных технологий. Это важно: всплывающие подсказки должны дополнять, а не заменять доступные метки. Никогда не размещайте критическую информацию только во всплывающей подсказке.
Discover how at OpenReplay.com.
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.