Popover

Pronto

Conteúdo flutuante interativo aberto por click. Suporta forms, listas, ações múltiplas. Diferente de Tooltip: persiste até dismissed.

Usar quando

Menu de ações (kebab), filtros avançados em popup, mini-form contextual, configurações rápidas.

Não usar quando

Hint estático (use Tooltip). Form longo (use Drawer/Modal). Confirmação destrutiva (use Modal).

Variantes (clique pra abrir)

Menu de ações
Mini form (configuração rápida)
Date filter

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Openclick no triggeropacity 0→1 + scale 0.95→1 + translateY 4→0200ms cubic-bezier(0.32,0.72,0,1)
Closeclick outside / Esc / item selectedopacity 1→0150ms ease
Auto-positionopen near edgeDetecta colisão com viewport e flipa ladoinstant antes do show
Item hovermouseenter no itemBackground → surfaceHover100ms ease
Keyboard nav↑↓Move foco entre items do menuinstant

Acessibilidade

Acessibilidade — checklist

Teclado
Enter / SpaceAbre popover
EscFecha
↑ / ↓Navega items (se menu)
TabFoco no próximo item, Tab fora fecha
ARIA esperado
  • Trigger: aria-haspopup="menu" aria-expanded="true|false"
  • Popup: role="menu" ou role="dialog"
  • Items: role="menuitem"
  • aria-controls="popover-id"
Notas
  • Click outside fecha (registrar listener no document).
  • Esc fecha + foco volta ao trigger.
  • Em mobile, popover pode virar bottom sheet.
  • Diferente de tooltip: popover é interativo (foco vai pra dentro).

Código

'use client';
import { useState, useEffect, useRef } from 'react';
import { T, RADIUS } from '@/lib/tokens';

export function Popover({ trigger, children, position = 'bottom-start' }) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    function onDoc(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    }
    if (open) document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
      <span onClick={() => setOpen(!open)} aria-haspopup="menu" aria-expanded={open}>
        {trigger}
      </span>
      {open && (
        <div role="menu" style={{
          position: 'absolute', top: '100%', left: 0, marginTop: 4,
          background: T.surface, border: `1px solid ${T.border}`,
          borderRadius: RADIUS.md, boxShadow: T.shadowMd,
          padding: 4, minWidth: 180, zIndex: 50,
        }}>
          {children}
        </div>
      )}
    </div>
  );
}

Regras

Faça

  • Click outside fecha (com listener no document).
  • Esc fecha + foco volta ao trigger.
  • Auto-position: detecta colisão com viewport e flipa.
  • Mín 180px de width pra menu.
  • Item destrutivo separado por divider + cor carmim.
  • Em mobile: vire bottom sheet.

Não faça

  • Não use popover pra hint (use Tooltip).
  • Não force scroll dentro de popover sem max-height.
  • Não tenha múltiplos popovers abertos.
  • Não esqueça click outside (popover preso = bug).
  • Não use popover pra form complexo (use Drawer/Modal).

Tokens usados

TokenValorPapel
T.surface#FFFFFFbackground
T.border#E3DFD2borda
T.surfaceHover#F7F5ECitem hover
T.shadowMd0 4px 14px ...sombra
T.carmim#A32E2Eitem destrutivo
RADIUS.md12borderRadius