Componente
Confirmar según el riesgo
Un botón destructivo que pide más cuanto más grave es la acción: lo reversible se deshace, lo medio se confirma con un segundo clic y lo irreversible, escribiendo el nombre.
- El plus
- La fricción crece con la consecuencia, y nadie espera ni cuenta segundos.
- Lente
- Proporción
- Stack
- React · CSS · Web Animations API
El patrón
Borrar, cancelar o descartar casi siempre pasa por el mismo modal: "¿Estás seguro?", con dos botones. Funciona la primera vez. A la décima, la mano hace clic en "Sí" antes de leer, y el modal deja de proteger.
Otra variante pide mantener el botón presionado hasta que se llena. Protege mejor, pero hace esperar con el dedo apretado incluso cuando la acción no lo amerita.
El plus
No todas las acciones destructivas pesan lo mismo, así que el botón no pide lo mismo para todas. Hay tres niveles: , y .
El costo extra nunca es tiempo: es un clic, y escribir solo cuando de verdad importa.
Lo reversible se deshace
Descartar un borrador se rehace en un minuto. Pedir confirmación ahí es ruido: el botón ejecuta al primer clic y ofrece Deshacer durante 5 segundos.
Un relleno azul claro cruza el botón mientras se puede deshacer. Cuando llega al final, la acción queda hecha.
Dos clics, no un doble clic
Cancelar algo que deja registro merece un segundo de atención. El primer clic arma el botón: dice "Confirmar eliminación" y un relleno rojo claro empieza a cruzarlo. Si haces el segundo clic antes de que llegue al final, se confirma; si no, se desarma solo. Clic fuera, Esc o perder el foco también lo desarman.
El riesgo de este patrón es el doble clic: dos clics rápidos lo confirmarían sin que nadie leyera nada. Por eso el segundo clic solo cuenta si llega después de 400 ms.
const confirm = () => {
// Un segundo clic más rápido que esto es un doble clic, no una decisión.
if (performance.now() - armedAt.current < 400) return;
setPhase('confirming');
};
Pruébalo: haz doble clic sobre el botón. Se arma, pero no se confirma.
El texto cambia donde pasa el relleno
El texto empieza gris y se vuelve rojo justo en el borde del relleno, letra por letra. No se calcula nada por letra: el botón tiene tres capas con el mismo contenido, y cada una se recorta con clip-path.
- Base: gris, la única que lee el lector de pantalla.
- Relleno: rojo claro con el texto en rojo. Se descubre de izquierda a derecha.
- Barrido: rojo con el texto claro. Aparece al confirmar.
fill.animate(
[{ clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0% 0 0)' }],
{ duration: 4000, easing: 'linear', fill: 'forwards' },
);
Encima va un borde interno de 1,5 px en su propia capa: en gris es un 6 % de negro, en rojo un anillo blanco. Si fuera un borde del botón, el relleno lo taparía.
Lo irreversible se escribe
Borrar algo sin vuelta atrás pide algo que no se hace por inercia: escribir el nombre. El botón abre un panel en la misma página, sin modal, y el botón rojo solo se activa cuando el nombre coincide.
La ayuda dice cuántas letras faltan, y mayúsculas y tildes no cuentan: la fricción está en escribirlo, no en adivinar cómo. Pegar también está permitido; bloquearlo castiga a quien usa un gestor de contraseñas o un lector de pantalla.
Estados
| Estado | Qué cambia |
|---|---|
| Reposo | Gris, con sombra y borde interno. La pista "2 clics" dice el costo antes de tocar. |
| Hover | Fondo rojo suave: anuncia que es destructivo, no ejecuta nada. |
| Foco | Anillo azul de 2 px, distinto del rojo de peligro. |
| Armado | El relleno avanza en 4 s y el texto pasa de gris a rojo a su paso. |
| Confirmando | El rojo barre el botón de izquierda a derecha. |
| Cargando | Todo el botón en rojo, con anillo blanco: "Eliminando…". |
| Éxito | Check y "Archivo eliminado". |
| Error | Ícono y texto, no solo color. Un clic vuelve a armarlo. |
| Movimiento reducido | Sin barridos: el estado cambia de golpe y la pista cuenta los segundos. |
Cada cambio se anuncia en una región role="status", y la acción ocurre al soltar el clic, nunca al presionar. Activa "Sin movimiento" en el escenario para ver la versión sin animación.
Tokens
Sin colores sueltos. En reposo: --color-bg-control, --color-text-control, --color-line-edge y --color-shadow-control. En el relleno: --color-bg-danger-progress y --color-text-danger. En el barrido: --color-bg-danger, --color-text-on-danger y --color-line-on-danger. Todos tienen valor en claro y en oscuro, y las duraciones salen de --duration-fast y --duration-base, que valen 0 con movimiento reducido.