Diálogo neobrutalista
Caja de borde grueso con sombra dura que entra con rebote, banda de título tipo ventana y botones que se hunden.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useId, useRef, type CSSProperties } from "react";
/** Diálogo neobrutalista: caja de borde grueso con sombra dura que entra con un pequeño rebote, banda de título y botones que se hunden. Sobre <dialog> nativo. React + TypeScript, sin dependencias. */
export type CapaDialogoBrutalProps = {
trigger?: string;
title?: string;
description?: string;
accept?: string;
onAccept?: () => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gccdb-root { box-sizing: border-box; display: inline-block; font: 700 16px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #111; }
.gccdb-root *, .gccdb-root *::before, .gccdb-root *::after { box-sizing: border-box; }
.gccdb-root button { font: inherit; }
.gccdb-root :focus-visible { outline: 4px solid #2f5bff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gccdb-dlg { animation: none !important; } }
.gccdb-btn { min-height: 50px; padding: 0 24px; border: 4px solid #111; background: #ffe14d; color: #111; font-weight: 900; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; box-shadow: 5px 5px 0 #111; transition: transform .1s, box-shadow .1s; }
.gccdb-btn:hover { background: #ffd21f; }
.gccdb-btn:active { transform: translate(5px, 5px); box-shadow: 0 0 0 #111; }
.gccdb-btn[data-v="white"] { background: #fff; }
.gccdb-btn[data-v="pink"] { background: #ff90e8; }
.gccdb-dlg { margin: auto; inset: 0; width: min(460px, calc(100vw - 32px)); padding: 0; border: 4px solid #111; background: #fff; color: #111; box-shadow: 10px 10px 0 #111; animation: gccdb-in .35s cubic-bezier(.34, 1.7, .64, 1); }
.gccdb-dlg::backdrop { background: repeating-linear-gradient(45deg, rgba(17, 17, 17, .55) 0 10px, rgba(17, 17, 17, .45) 10px 20px); }
.gccdb-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 4px solid #111; background: #8bc8ff; font-weight: 900; text-transform: uppercase; letter-spacing: .06em; font-size: 13px; }
.gccdb-x { width: 30px; height: 30px; padding: 0; border: 3px solid #111; background: #fff; cursor: pointer; font-weight: 900; line-height: 1; }
.gccdb-x:hover { background: #ff9a8a; }
.gccdb-body { padding: 22px 20px 8px; }
.gccdb-title { margin: 0 0 8px; font-size: 28px; line-height: 1; font-weight: 900; letter-spacing: -.04em; text-transform: uppercase; }
.gccdb-desc { margin: 0; font-weight: 600; }
.gccdb-foot { display: flex; flex-wrap: wrap; gap: 12px; padding: 18px 20px 22px; }
@keyframes gccdb-in { from { transform: translate(-14px, 18px) rotate(-3deg) scale(.92); opacity: 0; } }`;
export function CapaDialogoBrutal({ trigger = "Abrir aviso", title = "¡Ojo con esto!", description = "Vas a publicar los cambios para todo el equipo. Sin marcha atrás, sin drama: se puede revertir desde el historial.", accept = "Publicar ya", onAccept, className = "", style }: CapaDialogoBrutalProps) {
const id = useId();
const ref = useRef<HTMLDialogElement>(null);
return (
<div className={`gccdb-root ${className}`} style={style}>
<style>{CSS}</style>
<button className="gccdb-btn" type="button" onClick={() => ref.current?.showModal()}>{trigger}</button>
<dialog ref={ref} className="gccdb-dlg" aria-labelledby={`${id}-t`} onClick={(event) => event.target === ref.current && ref.current?.close()}>
<div className="gccdb-bar">
<span>aviso.exe</span>
<button className="gccdb-x" type="button" aria-label="Cerrar" onClick={() => ref.current?.close()}>×</button>
</div>
<div className="gccdb-body">
<h2 className="gccdb-title" id={`${id}-t`}>{title}</h2>
<p className="gccdb-desc">{description}</p>
</div>
<div className="gccdb-foot">
<button className="gccdb-btn" data-v="pink" type="button" onClick={() => { onAccept?.(); ref.current?.close(); }}>{accept}</button>
<button className="gccdb-btn" data-v="white" type="button" onClick={() => ref.current?.close()}>Mejor no</button>
</div>
</dialog>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/capa-dialogo-brutalEjemplo de uso de Diálogo neobrutalista
Después de copiar capa-dialogo-brutal.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { CapaDialogoBrutal } from "./capa-dialogo-brutal";
export default function Ejemplo() {
return <CapaDialogoBrutal trigger="Ejemplo" />;
}Propiedades de CapaDialogoBrutal
CapaDialogoBrutal acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
trigger? | string | Opcional. |
title? | string | Opcional. |
description? | string | Opcional. |
accept? | string | Opcional. |
onAccept? | () => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 62 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Diálogo neobrutalista por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Usa el elemento <dialog> nativo, con foco y cierre con Escape gestionados por el navegador.
- Es un componente de cliente de Next.js: incluye la directiva "use client".