Saltar al contenido principal

Confirm

El momento "¿estás seguro?", en una sola pieza. Un Dialog que se comporta como alerta, un botón de cancelar y uno de acción, un guardia opcional de escribir para confirmar, y un hook que lo convierte en una promesa.

1'use client';
2
3import { useState } from 'react';
4import { Confirm } from '@/components/ui/confirm';
5
6function TrashIcon({ size = 16 }: { size?: number }) {
7 return (
8 <svg
9 xmlns="http://www.w3.org/2000/svg"
10 width={size}
11 height={size}
12 viewBox="0 0 24 24"
13 fill="none"
14 stroke="currentColor"
15 strokeWidth={1.5}
16 strokeLinecap="round"
17 strokeLinejoin="round"
18 aria-hidden="true"
19 >
20 <path d="M3 6h18" />
21 <path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
22 <path d="M19 6l-1 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1L5 6" />
23 <path d="M10 11v6" />
24 <path d="M14 11v6" />
25 </svg>
26 );
27}
28
29export function Default() {
30 const [result, setResult] = useState<string | null>(null);
31
32 return (
33 <div className="flex w-full max-w-md flex-col items-start gap-3">
34 <Confirm
35 title="Delete project"
36 description="The repository, its deployments and every environment variable go with it. This cannot be undone."
37 icon={<TrashIcon />}
38 variant="destructive"
39 confirmText="Delete project"
40 onConfirm={() =>
41 new Promise<void>((resolve) => {
42 setTimeout(() => {
43 setResult('Deleted.');
44 resolve();
45 }, 900);
46 })
47 }
48 onCancel={() => setResult('Kept it.')}
49 >
50 <Confirm.Trigger className="border-border hover:bg-muted rounded-md border px-3 py-1.5 text-sm transition-colors">
51 Delete project
52 </Confirm.Trigger>
53 </Confirm>
54 {result && <p className="text-muted-foreground text-xs">{result}</p>}
55 </div>
56 );
57}

Instalación

pnpm dlx nachui add confirm

Anatomía

1import { Confirm } from '@/components/ui/confirm';
1<Confirm
2 title="Eliminar proyecto"
3 description="Esto no se puede deshacer."
4 variant="destructive"
5 confirmText="Eliminar"
6 onConfirm={deleteProject}
7>
8 <Confirm.Trigger>Eliminar</Confirm.Trigger>
9</Confirm>
Con solo un trigger como hijo, el panel se arma solo: ícono si le pasaste uno, título, descripción, el guardia de texto cuando hay requireText, y abajo Cancelar y la acción. El panel tiene role="alertdialog", así que los lectores de pantalla lo anuncian como algo que espera una respuesta, y Escape y el overlay cuentan como cancelar.
Pasale un Confirm.Content para reordenar el footer o meter algo tuyo entre la descripción y los botones.
1<Confirm title="¿Cerrar sesión en todos lados?" onConfirm={signOutAll}>
2 <Confirm.Trigger>Cerrar sesión</Confirm.Trigger>
3 <Confirm.Content>
4 <p className="text-muted-foreground text-sm">Tus otras 3 sesiones terminan ahora.</p>
5 <Dialog.Footer>
6 <Confirm.Cancel>Dejarlas</Confirm.Cancel>
7 <Confirm.Action>Cerrar sesión</Confirm.Action>
8 </Dialog.Footer>
9 </Confirm.Content>
10</Confirm>

Composición

Un hybrid es composición visible. Abrí confirm.tsx y estos son los elements que lo forman, en este orden:
Confirm
├── Dialog
│ ├── Confirm.Trigger → Dialog.Trigger
│ └── Confirm.Content → Dialog.Content (role="alertdialog")
│ ├── Dialog.Header
│ │ ├── Dialog.Title
│ │ └── Dialog.Description
│ ├── Input (solo con requireText)
│ └── Dialog.Footer
│ ├── Confirm.Cancel → Button (outline)
│ └── Confirm.Action → Button (default o destructive)
└── ConfirmProvider + useConfirm()

Variantes

variant="destructive" pinta el botón de acción con los tokens destructivos y manda el foco inicial a Cancelar, así un Enter de más no borra nada. La variante por defecto enfoca la acción, porque ahí el camino rápido es el correcto.

Confirmación asíncrona

Devolvé una promesa desde onConfirm y el panel la espera. La acción muestra el spinner, los dos botones se deshabilitan, y Escape y el overlay dejan de cerrarlo. Se cierra cuando la promesa resuelve, y si rechaza se queda abierto con los botones de vuelta, para reintentar o salir.
1<Confirm
2 title="Eliminar proyecto"
3 variant="destructive"
4 onConfirm={async () => {
5 await api.projects.delete(id);
6 }}
7/>

Escribir para confirmar

requireText agrega un input debajo de la descripción y mantiene la acción deshabilitada hasta que lo tipeado coincide exacto. Enter dentro del input confirma una vez que coincide.
1'use client';
2
3import { useState } from 'react';
4import { Confirm } from '@/components/ui/confirm';
5
6const PROJECT = 'ignaciofigueroa.dev';
7
8export function WithText() {
9 const [result, setResult] = useState<string | null>(null);
10
11 return (
12 <div className="flex w-full max-w-md flex-col items-start gap-3">
13 <Confirm
14 title={`Transfer ${PROJECT}`}
15 description="Ownership moves to the other team and you lose access to the settings."
16 variant="destructive"
17 confirmText="Transfer"
18 requireText={PROJECT}
19 onConfirm={() => setResult(`${PROJECT} transferred.`)}
20 onCancel={() => setResult('Nothing changed.')}
21 >
22 <Confirm.Trigger className="border-border hover:bg-muted rounded-md border px-3 py-1.5 text-sm transition-colors">
23 Transfer project
24 </Confirm.Trigger>
25 </Confirm>
26 {result && <p className="text-muted-foreground text-xs">{result}</p>}
27 </div>
28 );
29}

El hook

Envolvé la app, o la parte que hace preguntas, en ConfirmProvider. Después useConfirm() te da una función que abre el panel y resuelve true al confirmar y false al cancelar, con Escape o con el overlay. Las llamadas se encolan, un panel a la vez.
1const confirm = useConfirm();
2
3async function remove(id: string) {
4 const ok = await confirm({
5 title: '¿Eliminar este comentario?',
6 variant: 'destructive',
7 confirmText: 'Eliminar',
8 });
9 if (ok) await api.comments.delete(id);
10}

Three drafts in the archive.

1'use client';
2
3import { useState } from 'react';
4import { ConfirmProvider, useConfirm } from '@/components/ui/confirm';
5
6function ArchiveButton() {
7 const confirm = useConfirm();
8 const [status, setStatus] = useState('Three drafts in the archive.');
9
10 const archive = async () => {
11 const ok = await confirm({
12 title: 'Archive the draft?',
13 description: 'It leaves the list but stays searchable.',
14 confirmText: 'Archive',
15 });
16 setStatus(ok ? 'Archived. Four drafts in the archive.' : 'Still in the list.');
17 };
18
19 return (
20 <div className="flex flex-col items-start gap-3">
21 <button
22 type="button"
23 onClick={() => void archive()}
24 className="border-border hover:bg-muted rounded-md border px-3 py-1.5 text-sm transition-colors"
25 >
26 Archive draft
27 </button>
28 <p className="text-muted-foreground text-xs">{status}</p>
29 </div>
30 );
31}
32
33export function Hook() {
34 return (
35 <ConfirmProvider>
36 <div className="w-full max-w-md">
37 <ArchiveButton />
38 </div>
39 </ConfirmProvider>
40 );
41}

Referencia de API

Confirm

PropTypeDefaultDescription
titleReactNode-La pregunta, también el nombre accesible
descriptionReactNode-Qué pasa si dicen que sí
iconReactNode-Se muestra en un círculo arriba del título
variant'default' | 'destructive''default'Tono de la acción y dónde cae el foco
confirmTextstring'Confirm'Etiqueta del botón de acción
cancelTextstring'Cancel'Etiqueta del botón de cancelar
requireTextstring-Texto que hay que escribir antes de confirmar
labelsPartial<{ confirm; cancel; typeToConfirm }>InglésTodos los textos visibles, typeToConfirm es una función
openboolean-Estado abierto controlado
defaultOpenbooleanfalseEstado abierto cuando no es controlado
onOpenChange(open: boolean) => void-Se llama cuando cambia el estado abierto
onConfirm() => void | Promise<void>-Lo llama la acción; una promesa lo deja pendiente
onCancel() => void-Lo llaman cancelar, Escape y el overlay
classNamestring-Clases para el panel por defecto
childrenReactNode-Un Confirm.Trigger y, opcionalmente, un Confirm.Content

Confirm.Trigger

PropTypeDefaultDescription
asChildbooleanfalseUsa el elemento hijo como trigger
classNamestring-Clases CSS adicionales

Confirm.Content

PropTypeDefaultDescription
childrenReactNodeCancelar y AcciónReemplaza el footer; el encabezado y el guardia quedan
classNamestring-Clases CSS adicionales

Confirm.Cancel

PropTypeDefaultDescription
childrenReactNodecancelTextEtiqueta
classNamestring-Clases CSS adicionales
...--Cualquier prop de Button salvo variant

Confirm.Action

PropTypeDefaultDescription
childrenReactNodeconfirmTextEtiqueta
classNamestring-Clases CSS adicionales
...--Cualquier prop de Button salvo variant

useConfirm

FirmaDescripción
useConfirm(): (options: ConfirmOptions) => Promise<boolean>Abre un panel, resuelve true al confirmar y false en cualquier otro caso
ConfirmOptions acepta title, description, icon, variant, confirmText, cancelText, requireText y labels, con el mismo significado que las props de arriba. Necesita un ConfirmProvider por encima.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue