Back

Uma Abordagem Leve para Tooltips em React

Uma Abordagem Leve para Tooltips em React

Você precisa de um tooltip. Talvez seja uma dica para um botão de ícone, ou contexto para um rótulo truncado. Antes de recorrer a um framework de UI completo ou a uma biblioteca legada como Tippy.js, considere quão pouco código você realmente precisa.

Este guia apresenta padrões leves de tooltip em React—desde a abordagem nativa mais simples até hooks personalizados mínimos e bibliotecas headless de tooltip como Floating UI. Cada etapa adiciona capacidade sem inflar seu bundle.

Principais Conclusões

  • O atributo nativo title fornece tooltips sem custo, mas carece de estilização e suporte a teclado
  • Tooltips apenas com CSS oferecem estilização e estados de foco sem JavaScript, mas não conseguem lidar com colisão de viewport
  • Um hook personalizado mínimo oferece controle programático mantendo o tamanho do bundle pequeno
  • Floating UI fornece detecção de colisão e consciência de viewport em aproximadamente 3kB
  • Sempre priorize acessibilidade usando atributos ARIA adequados e nunca coloque informações críticas apenas em tooltips

Comece com o Atributo Title Nativo

O atributo title integrado do navegador é a linha de base sem JavaScript:

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

Isso funciona, mas é limitado. O tooltip aparece após um atraso, você não pode estilizá-lo, e ele não aparece no foco do teclado. Ainda assim, para casos verdadeiramente simples, não custa nada.

Tooltips React Apenas com CSS

Para tooltips estilizados sem lógica JavaScript, o CSS lida com estados básicos de hover:

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;
  }
}

Este padrão apenas com CSS respeita prefers-reduced-motion e é acionado tanto em hover quanto em focus. Para acessibilidade de tooltip React, o role="tooltip" e aria-label no botão fornecem contexto para leitores de tela.

A limitação? Sem detecção de colisão. Se o tooltip estiver próximo à borda do viewport, ele é cortado.

Um Hook Personalizado Mínimo

Quando você precisa de controle programático sem uma biblioteca, um pequeno hook funciona bem:

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 }
}

Uso:

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>
  )
}

A relação aria-describedby conecta o botão ao seu tooltip para tecnologia assistiva. Isso importa: tooltips devem complementar, não substituir, rótulos acessíveis. Nunca coloque informações críticas apenas em um tooltip.

Bibliotecas Headless de Tooltip: Floating UI

Quando você precisa de posicionamento adequado—detecção de colisão, inversão, deslocamento—Floating UI é a escolha moderna. É o sucessor do Popper.js e pesa cerca de 3kB.

Tooltips React com Floating UI fornecem primitivos sem opiniões sobre estilização:

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>
      )}
    </>
  )
}

Isso lida com limites de viewport automaticamente. O middleware flip reposiciona quando o espaço acaba, enquanto shift mantém o tooltip visível ao longo do eixo.

Para segurança com SSR, os hooks do Floating UI não acessam window ou document durante a renderização. Se você está construindo uma solução personalizada, proteja essas referências em useEffect ou useLayoutEffect.

Quando Usar Portals

Se o pai do seu tooltip tem overflow: hidden ou contextos de empilhamento complexos, renderize o tooltip em um portal:

import { createPortal } from 'react-dom'

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

Isso escapa de contêineres com recorte. Floating UI lida com posicionamento independentemente de onde o tooltip é renderizado no DOM.

Por Que Não Tippy.js?

Tippy.js e react-popper serviram bem por anos, mas são efetivamente legados. Floating UI oferece o mesmo mecanismo de posicionamento com uma pegada menor e melhor integração com React através de hooks. Para novos projetos, bibliotecas headless de tooltip como Floating UI ou Radix UI Tooltip são a escolha prática.

Escolhendo Sua Abordagem

Combine complexidade com necessidade:

  • title nativo: Custo zero, controle zero
  • Apenas CSS: Estilizado, acessível, sem lógica de posicionamento
  • Hook personalizado: Controle total, código mínimo, posicionamento manual
  • Floating UI: Detecção de colisão, consciência de viewport, ~3kB

Conclusão

Tooltips não precisam ser pesados. Comece simples, adicione capacidade quando o caso de uso exigir, e mantenha a acessibilidade no centro de cada implementação. O atributo nativo title funciona para casos básicos, soluções apenas com CSS lidam com necessidades de estilização, e quando você precisa de detecção de colisão, Floating UI entrega sem o inchaço de bibliotecas legadas.

Perguntas Frequentes

Use aria-describedby para conectar o elemento gatilho ao conteúdo do tooltip. Adicione role tooltip ao próprio elemento tooltip. Mantenha o texto do tooltip conciso e suplementar. Nunca coloque informações essenciais apenas em tooltips, pois usuários que dependem de teclados ou leitores de tela podem perder conteúdo atrasado ou apenas em hover.

Tooltips apenas com CSS carecem de detecção de colisão. O tooltip se posiciona em relação ao seu pai sem verificar os limites do viewport. Use Floating UI com os middlewares flip e shift para reposicionar automaticamente tooltips quando eles seriam cortados. Alternativamente, renderize tooltips em um portal para escapar de contêineres com overflow hidden.

Floating UI é a escolha recomendada para novos projetos. É o sucessor do Popper.js que alimentava o Tippy.js e oferece um tamanho de bundle menor, cerca de 3kB, com melhor integração React através de hooks. Tippy.js ainda funciona, mas é considerado legado e carrega mais peso do que alternativas modernas.

Use portals quando os pais do tooltip têm overflow hidden, overflow scroll, ou contextos de empilhamento complexos que causam recorte. Portals renderizam o tooltip diretamente em document.body, escapando de quaisquer restrições de contêiner. Floating UI lida com posicionamento corretamente independentemente de onde o tooltip aparece na árvore 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