Popover
BetaContenido secundario anclado a un botón — un filtro, un formulario corto, ayuda con links.
registry/ds/popover.tsximport { Button } from "@/registry/ds/button";
import { Checkbox } from "@/registry/ds/checkbox";
import { Popover, PopoverClose } from "@/registry/ds/popover";
export default function PopoverBasic() {
return (
<Popover trigger={<Button variant="stroke">Filtrar</Button>} title="Filtrar materias" description="Se aplica a la lista de abajo." closable>
<div className="flex flex-col gap-3">
<Checkbox size="sm" defaultChecked label="Con cupo disponible" />
<Checkbox size="sm" label="Solo electivas" />
<Checkbox size="sm" label="Modalidad virtual" />
</div>
<div className="flex justify-end gap-2 pt-1">
<PopoverClose>
<Button size="sm" variant="text">Limpiar</Button>
</PopoverClose>
<PopoverClose>
<Button size="sm">Aplicar</Button>
</PopoverClose>
</div>
</Popover>
);
}Popover, Tooltip o Modal
| Se abre con | Tiene controles | Bloquea la página | |
|---|---|---|---|
| Tooltip | hover / foco | No | No |
| Popover | clic | Sí | No |
| Modal | clic | Sí | Sí |
Usá Popover para tareas chicas que no merecen interrumpir: filtros, elegir una fecha, un formulario de un campo, ayuda con un link. Si la tarea es la principal de la pantalla, es un Modal; si no hay nada que tocar, un Tooltip.
Confirmación liviana
Para acciones reversibles o de poco costo, la confirmación va al lado del
botón, no en un Modal. Dos botones: Cancelar (text) y la acción.
Física I · Comisión 2
Martes 14:00 · Lab 2
"use client";
import { useState } from "react";
import { Chip } from "@/registry/ds/badge";
import { Button } from "@/registry/ds/button";
import { Popover, PopoverClose } from "@/registry/ds/popover";
/** Confirmación liviana, al lado de la acción. Para lo reversible o de poco costo; lo irreversible va a un Modal */
export default function PopoverConfirm() {
const [inscripta, setInscripta] = useState(true);
return (
<div className="flex items-center gap-4 rounded-xl border border-border bg-bg-elevated px-4 py-3">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-fg">Física I · Comisión 2</p>
<p className="text-xs text-fg-muted">Martes 14:00 · Lab 2</p>
</div>
{inscripta ? (
<Popover side="bottom" align="end" title="¿Darte de baja de Física I?" description="Liberás tu lugar. Podés volver a inscribirte mientras haya cupo." trigger={<Button size="sm" variant="stroke">Darme de baja</Button>}>
<div className="flex justify-end gap-2">
<PopoverClose>
<Button size="sm" variant="text">
Cancelar
</Button>
</PopoverClose>
<PopoverClose>
<Button size="sm" onClick={() => setInscripta(false)}>
Darme de baja
</Button>
</PopoverClose>
</div>
</Popover>
) : (
<div className="flex items-center gap-3">
<Chip size="sm" status="warning">
Sin inscripción
</Chip>
<Button size="sm" variant="tonal" onClick={() => setInscripta(true)}>
Inscribirme
</Button>
</div>
)}
</div>
);
}Ayuda contextual
Cuando un Tooltip queda corto (varios pasos, un link) y una página es
demasiado. Se abre con clic o Enter, nunca con hover, y el disparador tiene
aria-label.
import { Button } from "@/registry/ds/button";
import { HelpCircle } from "@/registry/ds/icons";
import { Input } from "@/registry/ds/input";
import { Link } from "@/registry/ds/link";
import { Popover } from "@/registry/ds/popover";
/** Ayuda contextual: más que un Tooltip (texto largo, un link) y menos que una página. Se abre con clic o Enter, no con hover */
export default function PopoverHelp() {
return (
<div className="w-80">
<Input
label={
<span className="inline-flex items-center gap-1.5">
CBU o alias
<Popover
size="lg"
side="right"
title="Dónde lo encontrás"
description="Son 22 dígitos, o el alias que elegiste en tu banco."
trigger={<Button variant="text" size="sm" iconOnly={<HelpCircle />} aria-label="Ayuda sobre CBU o alias" className="-my-2 size-7" />}
>
<ol className="list-decimal space-y-1 pl-4 text-sm text-fg-muted">
<li>Abrí la app de tu banco.</li>
<li>Entrá a la cuenta y tocá «Datos» o «Compartir».</li>
<li>Copiá el CBU completo o el alias.</li>
</ol>
<p className="pt-2 text-sm">
<Link href="#" external>
Qué es un CBU
</Link>
</p>
</Popover>
</span>
}
placeholder="0000003100012345678901"
inputMode="numeric"
/>
</div>
);
}Controlado
Con open y onOpenChange el padre decide cuándo abrir y puede reiniciar
estado al cerrar, como el "Copiado" de este ejemplo.
"use client";
import { useState } from "react";
import { Button } from "@/registry/ds/button";
import { ArrowUpRight } from "@/registry/ds/icons";
import { Input } from "@/registry/ds/input";
import { Popover } from "@/registry/ds/popover";
import { Switch } from "@/registry/ds/switch";
/** Controlado: `open` lo maneja el padre, así el estado interno (copiado) se reinicia al cerrar */
export default function PopoverShare() {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const link = "campus.edu/m/MAT201";
return (
<Popover
open={open}
onOpenChange={(o) => {
setOpen(o);
if (!o) setCopied(false);
}}
side="bottom"
align="start"
size="lg"
title="Compartir materia"
closable
trigger={
<Button variant="stroke" iconLeft={<ArrowUpRight />}>
Compartir
</Button>
}
>
<div className="flex flex-col gap-4">
<Input
size="sm"
aria-label="Enlace"
readOnly
value={link}
action={
<Button
size="sm"
variant="tonal"
onClick={() => {
navigator.clipboard?.writeText(link).catch(() => {});
setCopied(true);
}}
>
{copied ? "Copiado" : "Copiar"}
</Button>
}
/>
<Switch size="sm" label="Cualquiera con el enlace puede ver" supporting="Si lo apagás, solo alumnos inscriptos." defaultChecked />
<p className="sr-only" aria-live="polite">
{copied ? "Enlace copiado" : ""}
</p>
</div>
</Popover>
);
}Reglas
- Se ancla a lo que lo abrió y aparece cerca (8 px).
- Un solo popover abierto a la vez.
- Si tiene acciones ("Aplicar"), que cierren el popover.
- Contenido corto: si scrollea, es un Drawer.
| Parte | Medida |
|---|---|
| Card | md 288 · lg 320 · padding 16 · radius-lg 10 |
| Separación | 8 del disparador |
| Título / descripción | Text sm / Medium · Text sm |
| Cerrar | 24, a 12 del borde |
| Parte | Propiedad | Token |
|---|---|---|
| Card | Fondo | bg/elevated |
| Card | Borde | border/default |
| Card | Sombra | Elevation/lg |
| Título | Color | text/default |
| Descripción | Color | text/muted |
Instalación
npx shadcn@latest add https://polen.cdc.cool/r/popover.jsonSobre Base UI Popover.
Playground
Configurá el componente y copiá el snippet: se genera del mismo estado que renderiza el preview.
<Popover trigger={<Button variant="stroke">Filtrar</Button>} title="Filtrar materias" description="Se aplica a la lista de abajo." closable>
<Checkbox size="sm" label="Con cupo disponible" />
<PopoverClose>
<Button size="sm">Aplicar</Button>
</PopoverClose>
</Popover>| Prop | Tipo | Default | Descripción |
|---|---|---|---|
trigger | ReactElement | — | Requerido. El botón que lo abre. |
title · description | ReactNode | — | Nombran al popover para lectores de pantalla. |
side · align | string | "bottom" · "center" | Posición preferida; se voltea si no entra. |
size | "md" | "lg" | "md" | 288 o 320 de ancho. |
closable | boolean | false | Muestra la ✕. |
open · onOpenChange | boolean · (open) => void | — | Modo controlado. |
PopoverClose | — | — | Envuelve un Button para que cierre. |
role="dialog"no modal, nombrado por el título; el foco entra al abrir y vuelve al disparador al cerrar.- Esc y clic afuera cierran. Tab recorre el contenido y sale.
- El disparador lleva
aria-expandedyaria-haspopup.