Envías un toast inofensivo de “Guardado”. Luego llegan tickets de soporte: el toast cubre el botón de pago en móvil, se apila debajo de un modal, se niega a descartarse y anima como una máquina tragamonedas. Si alguna vez una notificación UI se convirtió en un incidente, bienvenido.
Los toasts parecen adorno de frontend, pero se comportan como infraestructura de producción: necesitan colocación predecible, capas con sentido, modos de fallo depurables y salvaguardas para accesibilidad y preferencias de movimiento. Así es como los construyes para que no te muerdan a las 2 a.m.
Una demo estática pequeña de un diseño “apilamiento en esquina superior derecha”. Esto no es un componente completo; es la forma a la que aspiras.
Qué es un toast (y qué no es)
Un toast es una notificación transitoria que confirma que algo ocurrió. No es un diálogo. No es un error de formulario. No es un pedido de permisos. Debe aparecer, ofrecer el contexto justo y apartarse sin exigir atención.
El modelo mental claro: un toast es una línea de registro con piel de UI. Esperas que esté ordenado, con límite de frecuencia y legible. Si es ruidoso, persistente o bloquea al usuario, has creado un modal con problemas de negación.
Regla con criterio: si el usuario debe actuar, no uses un toast. Usa UI inline (para validación de formularios) o un modal (para acciones seguras/irreversibles). Los toasts son para “FYI” y “hecho”.
Hay un segundo trabajo, menos glamuroso: los toasts son una herramienta del presupuesto de errores. Son cómo expones reintentos, fallos parciales y modos degradados sin hundir el flujo. Pero solo si se comportan de forma consistente entre páginas, modales y junglas de z-index.
Broma #1: Un toast que no se puede descartar es solo un modal que fue a la escuela de arte.
Hechos interesantes y breve historia
Los toasts parecen haber surgido con las aplicaciones web modernas. No fue así. El patrón ha estado rebotando en sistemas de UI durante décadas.
- Android popularizó “Toast” como una primitiva de UI en versiones tempranas de la plataforma, moldeando cómo los desarrolladores web hablan de mensajes transitorios.
- Las primeras aplicaciones de escritorio usaban “balloon tips” en las bandejas del sistema; la idea era la misma: estado efímero sin bloquear el trabajo.
- El término “toast” probablemente quedó porque “aparece y desaparece”—una notificación pequeña y rápida, no una alerta completa.
- Las UIs web inicialmente confiaban en alert() porque los navegadores lo ofrecían gratis; eso entrenó a una generación a interrumpir usuarios sin razón.
- Las transformaciones CSS se volvieron el estándar para animaciones porque típicamente evitan el recálculo de diseño y reducen el stuttering.
- El auge de las SPA hizo necesarios sistemas globales de notificaciones; las transiciones de página ya no reinician el estado, así que las colas importan.
- Los safe-area insets llegaron con las muescas y esquinas redondeadas; los toasts que los ignoran se ven bien en escritorios y terribles en iPhones.
- “prefers-reduced-motion” se volvió una expectativa estándar tras años de quejas vestibulares; ignorarlo ya es un bug real de accesibilidad.
«La esperanza no es una estrategia.» — General Gordon R. Sullivan
Puedes esperar que tus toasts no colisionen con otros overlays. O puedes ingenierarlo.
Primitivas de diseño: contenedor, pila y áreas seguras
Si quieres toasts fiables, deja de esparcirlos por árboles de componentes aleatorios. Dales un hogar: una región dedicada al toast posicionada respecto al viewport, no respecto a cualquier flexbox que hoy envuelva el contenido de la página.
El contrato del contenedor
El contenedor de toasts debe:
- Ser
position: fixed(o ocasionalmenteabsolutedentro de una raíz conocida) para anclarse al viewport. - Tener un
insetpredecible y soportar áreas seguras. - No robar clics de la página excepto donde el toast necesite interactuar.
- Definir un ancho máximo y permitir envoltura.
Esqueleto CSS en el que puedes confiar
cr0x@server:~$ cat toast.css
:root{
--toast-gap: 10px;
--toast-edge: 12px;
--toast-max-width: 420px;
--toast-z: 1000; /* You’ll still need a z-index policy. */
}
.toast-region{
position: fixed;
z-index: var(--toast-z);
inset: var(--toast-edge);
display: flex;
flex-direction: column;
gap: var(--toast-gap);
pointer-events: none;
/* Safe-area: protect against notches + rounded corners */
padding:
calc(env(safe-area-inset-top) + 0px)
calc(env(safe-area-inset-right) + 0px)
calc(env(safe-area-inset-bottom) + 0px)
calc(env(safe-area-inset-left) + 0px);
}
.toast{
pointer-events: auto;
width: min(var(--toast-max-width), 100%);
background: rgba(20,25,34,.95);
border: 1px solid rgba(39,50,68,.9);
border-radius: 12px;
box-shadow: 0 14px 40px rgba(0,0,0,.5);
padding: 10px 12px;
}
Observa el movimiento que te salva de bloquear clics por accidente: la región obtiene pointer-events: none, los toasts individuales lo recuperan. Esa es la diferencia entre “UX agradable” y “por qué no puedo pulsar el botón de pago”.
Áreas seguras: trata el móvil como producción, no como demo
En teléfonos con muescas, el viewport que ves no es el viewport real. env(safe-area-inset-*) existe por una razón. Si lo ignoras, tus toasts se esconderán bajo la muesca y se verán rotos. Peor aún, quedarán parcialmente no clicables, que es el tipo de bug que arruina tu día porque es “intermitente” según el dispositivo y la orientación.
Apilamiento: orden, espaciado y políticas de máximo visible
El apilamiento no es “solo flexbox”. El apilamiento es una decisión de producto que se convierte en una decisión operativa cuando un fallo en el backend genera 20 toasts de error en cinco segundos. Necesitas reglas: orden, cuenta máxima visible, comportamiento de colapso y política de desestimación.
Elige un orden y sé consistente
Elige uno:
- Más nuevo arriba: bueno para feedback de “estado más reciente”. Los usuarios ven la acción más reciente primero.
- Más nuevo abajo: bueno para historias cronológicas. La pila crece hacia abajo como un registro.
Lo que sea que elijas, codifícalo en el CSS y en el orden del DOM. No intentes “arreglarlo” con transformaciones y márgenes negativos; así obtendrás un orden de foco extraño y lectores de pantalla anunciando mensajes en una secuencia distinta a la que ve el usuario.
Usa gap, no márgenes ni offsets absolutos
Usa display: flex y gap. Es limpio, legible y no acumula bugs de espaciado cuando los toasts cambian de altura.
Planifica la “tormenta de toasts”
Las tormentas de toasts ocurren. Son causadas por reintentos, bucles de validación, acciones masivas o un servicio que devuelve 429 brevemente y tu UI narra cada intento.
Establece una cuenta máxima visible (a menudo 3–5). Si hay más, o bien:
- Colapsa en un solo toast: “5 notificaciones más…” con una opción para abrir un panel.
- Pon en cola y muestra después, pero cuidado: los errores retrasados pueden aparecer después de que el usuario se haya ido, lo que parece que estás embrujando la app.
| Política | Qué experimentan los usuarios | Riesgo operativo | Mi opinión |
|---|---|---|---|
| Apilamiento ilimitado | Pantalla llena de toasts | Bloquea UI, hunde rendimiento | Evitar. Así un “incidente menor” se convierte en “escalada ejecutiva”. |
| Máx visible + eliminar antiguos | Mensajes más recientes visibles | Puede ocultar el toast raíz del problema | Aceptable para toasts de éxito de baja severidad; peligroso para errores. |
| Máx visible + colapsar resto | Pila limpia + resumen | Más complejidad UI | Mejor por defecto cuando los errores pueden dispararse. |
| Poner en cola y mostrar después | Mensajes llegan tarde | Causalidad confusa | Sólo para info no crítica como “sincronización completada”. |
Espaciado y legibilidad: diseña para contenido variable
Un toast puede ser “Guardado” o una explicación de varias frases sobre un fallo parcial. Tu diseño debe sobrevivir a los quiebres de línea. Usa min() para el ancho y permite que la altura crezca naturalmente. No limites la altura a menos que proporciones una opción “más”; de lo contrario ocultas la única parte útil del mensaje.
Variantes de colocación sin copiar/pegar CSS
Necesitarás variantes de colocación. Producto lo pedirá. Luego soporte lo pedirá. Luego tu cordura lo pedirá, porque bottom-center en móvil es distinto a top-right en escritorio.
El enfoque equivocado es un archivo CSS separado por variante de colocación. El enfoque correcto es: un componente de región único, la colocación controlada por atributos data y un pequeño conjunto de variables CSS.
Define las colocaciones como alineamientos, no como coordenadas
Piénsalo en términos de:
- Dirección del eje principal (
flex-direction) - Alineación del eje cruzado (
align-items) - Dónde se sitúa la región (
top/bottom/left/rightvíainset)
cr0x@server:~$ cat toast-placements.css
.toast-region{
--_x: end; /* start | center | end */
--_y: start; /* start | end */
--_dir: column; /* column | column-reverse */
--_inset-top: var(--toast-edge);
--_inset-right: var(--toast-edge);
--_inset-bottom: var(--toast-edge);
--_inset-left: var(--toast-edge);
position: fixed;
z-index: var(--toast-z);
top: var(--_inset-top);
right: var(--_inset-right);
bottom: var(--_inset-bottom);
left: var(--_inset-left);
display: flex;
flex-direction: var(--_dir);
gap: var(--toast-gap);
pointer-events: none;
justify-content: flex-start;
align-items: flex-end;
}
/* placements */
.toast-region[data-placement="top-right"]{
--_inset-bottom: auto;
--_inset-left: auto;
--_dir: column;
align-items: flex-end;
}
.toast-region[data-placement="top-left"]{
--_inset-bottom: auto;
--_inset-right: auto;
--_dir: column;
align-items: flex-start;
}
.toast-region[data-placement="bottom-right"]{
--_inset-top: auto;
--_inset-left: auto;
--_dir: column-reverse;
align-items: flex-end;
}
.toast-region[data-placement="bottom-center"]{
--_inset-top: auto;
left: var(--toast-edge);
right: var(--toast-edge);
bottom: var(--toast-edge);
--_dir: column-reverse;
align-items: center;
}
.toast{
pointer-events: auto;
width: min(var(--toast-max-width), 100%);
}
Dos puntos que importan en producción:
- Las pilas bottom suelen querer
column-reversepara que los toasts nuevos aparezcan cerca del borde, no debajo de los antiguos donde el usuario no los verá. - Bottom-center necesita restricciones de ancho completo mediante left/right inset y
align-items: center; de lo contrario lucharás con “¿por qué esto no está centrado?” por el resto de tu carrera.
Evita la “ruleta de colocación responsive”
Algunos equipos mueven la región de toast según puntos de quiebre: top-right en escritorio, bottom-center en móvil. Eso puede estar bien. Pero hazlo intencionalmente y mantén la dirección de la animación consistente (más sobre esto más adelante). Los usuarios notan cuando una notificación se teletransporta de una esquina a otra entre páginas u orientaciones.
Animaciones: rápidas, reversibles y respetuosas
Las animaciones de toast son donde las buenas intenciones mueren. Un toast necesita aparecer rápido, no llamar la atención y nunca hacer que la UI parezca más lenta de lo que es.
Usa transform + opacity, no propiedades de layout
Animar top, height o margin fuerza recálculos de layout. En una página haciendo trabajo real (tablas, gráficos, cabeceras sticky), así es como obtienes jank. Mantente en transform y opacity para el propio toast.
Entrada y salida deben ser simétricas
Cuando un toast se descarta, debería irse de una forma que coincida con cómo llegó. La simetría reduce la sensación de “¿qué acaba de pasar?”.
cr0x@server:~$ cat toast-motion.css
.toast{
transform-origin: top right;
will-change: transform, opacity;
}
.toast[data-state="entering"]{
animation: toast-in 220ms cubic-bezier(.2,.9,.2,1) both;
}
.toast[data-state="exiting"]{
animation: toast-out 180ms cubic-bezier(.2,.9,.2,1) both;
}
@keyframes toast-in{
from{ opacity: 0; transform: translateY(-8px) scale(.98); }
to{ opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out{
from{ opacity: 1; transform: translateY(0) scale(1); }
to{ opacity: 0; transform: translateY(-6px) scale(.98); }
}
@media (prefers-reduced-motion: reduce){
.toast[data-state="entering"],
.toast[data-state="exiting"]{
animation: none;
}
}
Ese bloque de prefers-reduced-motion no es opcional. Es tu pacto de “sin sorpresas” con usuarios que sufren mareos por las animaciones de UI. Trátalo como tratas los timeouts: una pequeña configuración que evita un número desproporcionado de incidentes.
Animación consciente de la dirección (sin crear 12 variantes)
Si colocas toasts abajo, probablemente quieras que suban ligeramente, no que caigan desde arriba como un ladrillo. Puedes lograrlo con una sola variable para el offset Y.
cr0x@server:~$ cat toast-directional-motion.css
.toast-region{ --toast-enter-y: -8px; --toast-exit-y: -6px; }
.toast-region[data-placement^="bottom"]{
--toast-enter-y: 8px;
--toast-exit-y: 6px;
}
@keyframes toast-in{
from{ opacity: 0; transform: translateY(var(--toast-enter-y)) scale(.98); }
to{ opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out{
from{ opacity: 1; transform: translateY(0) scale(1); }
to{ opacity: 0; transform: translateY(var(--toast-exit-y)) scale(.98); }
}
No animes movimiento horizontal salvo que tengas una razón fuerte. El movimiento lateral se interpreta como “gesto de deslizamiento”, y los usuarios intentarán interactuar con él como tal.
Broma #2: Si tu animación de toast dura más que la llamada API, felicitaciones—has creado un amplificador de latencia UI.
Capas y contextos de apilamiento: el impuesto del z-index
La mayoría de los bugs de toast en apps reales no tienen que ver con el componente toast. Tienen que ver con contextos de apilamiento. Específicamente: el toast está en el lugar correcto, pero detrás de otra cosa. O delante de algo que no debería. O desaparece cuando un ancestro recibe transform.
Entiende al enemigo: contextos de apilamiento
Un nuevo contexto de apilamiento puede crearse por cosas como:
positionconz-index(en ciertas combinaciones)transformfilteropacity < 1isolation: isolatecontain: paint(y similares)
Si tu región de toasts vive dentro de un subárbol que tiene cualquiera de estos, tu “overlay global” es de repente local. Ahí es cuando los modales se comen los toasts, o los toasts aparecen detrás de un header sticky con un heroico z-index: 999999.
Elige una política de capas como adulto
Aquí hay una política que sobrevive en apps medianas a grandes:
- Define una pequeña escala de z-index en un solo lugar (tokens): base, dropdown, sticky, modal, toast, tooltip.
- Renderiza la región de toasts lo más cerca posible de
<body>(patrón portal), para que no quede atrapada por contextos de apilamiento padres. - Nunca “arregles” el apilamiento con números gigantes aleatorios. Es como agregar más niveles de RAID porque el primero se sintió solo.
| Capa | z-index típico | Notas |
|---|---|---|
| Contenido de la página | 0 | Por defecto; evita establecer z-index salvo que sea necesario. |
| Header sticky | 100 | Hazlo aburrido; mantenlo bajo. |
| Menús desplegables | 200 | Debería sobreponerse al header. |
| Backdrop de modal + modal | 400–600 | Los backdrops no deberían exceder tooltips/toasts salvo que quieras silencio. |
| Toasts | 700 | ¿Visibles por encima de modales? Decide. Yo prefiero sí para estado no bloqueante, pero no para avisos de seguridad. |
| Tooltips | 800 | Deberían ganar a los toasts si se superponen. |
Elige un orden que coincida con la semántica de tu producto. La clave es que esté documentado, compartido y aplicado. Si no, cada equipo lo reinventará y tu z-index se volverá un vertedero.
Eventos de puntero y comportamiento de clic a través
Los toasts flotan sobre el contenido. A veces eso está bien. A veces rompe la llamada a la acción principal en el peor momento posible. La solución rara vez es “mueve el toast.” Usualmente es “evita que el contenedor capture la entrada”.
El contenedor debe dejar pasar clics
Aplica pointer-events: none en la región y pointer-events: auto en el toast. Esto hace que el espacio vacío de la región sea transparente a los clics.
Haz los toasts interactivos intencionalmente
Si el toast tiene acciones (Deshacer, Reintentar), debe ser accesible por teclado y lector de pantalla. Eso significa:
- Los botones de acción son botones reales.
- El cierre es un botón, no un div con onClick y una plegaria.
- Los estados de foco son visibles.
Si tu toast es puramente informativo, considera hacerlo no interactivo para evitar clics accidentales. No pongas un botón de cerrar en todo “porque eso es lo que hacen los toasts.” Si se descarta automáticamente y es inofensivo, déjalo ser un fantasma.
Accesibilidad: regiones live, foco y lectores de pantalla
Los toasts son una trampa para la accesibilidad porque son transitorios y no necesariamente se activan por una acción directa y obvia del usuario. Necesitas decidir cómo deben anunciarse y debes evitar robar el foco a menos que sea un mensaje realmente crítico.
Usa regiones aria-live correctamente
Un patrón común:
- Toasts de éxito/info:
aria-live="polite" - Toasts de error:
aria-live="assertive"solo cuando son urgentes
“Assertive” puede interrumpir lectores de pantalla. Úsalo como si llamases al ingeniero de guardia: solo cuando importe.
No robes el foco para toasts estándar
Robar foco es la forma de romper formularios y la navegación por teclado. Si un usuario está escribiendo en un campo y le quitas el foco para un toast, has creado un bug de accesibilidad y un impuesto a la productividad.
Pero proporciona una forma de alcanzarlos
Si los toasts incluyen acciones, los usuarios necesitan una manera razonable de llegar a ellos. Dos enfoques que funcionan:
- Un botón de notificaciones que abre un panel con toasts recientes (navegable por teclado).
- Un enlace “Saltar a notificaciones” para usuarios de teclado en apps donde los toasts son críticos y frecuentes.
Además: respeta las preferencias de movimiento del usuario. Eso también es accesibilidad. Si animas, asumes las consecuencias.
Rendimiento y fiabilidad: evita tormentas de reflow
Los toasts son pequeños. Por eso los equipos se descuidan. Luego los publican en cada página, y de repente una tormenta de toasts se convierte en una fiesta del hilo principal.
Lo que realmente duele
- Animar propiedades de layout.
- Medir el DOM cada frame para calcular offsets de apilamiento.
- Renderizar docenas de sombras con blur en dispositivos de gama baja.
- Provocar recálculo de estilos alternando clases globales en
bodypor cada toast.
Lo que escala bien
- Usa layout flex + gap; sin matemáticas manuales de apilamiento.
- Anima cada toast solo con transform/opacity.
- Limita toasts visibles; colapsa el desbordamiento.
- Prefiere un contenedor único con un conjunto estable de reglas CSS; manipula solo atributos data para estado.
Blur y sombras: usa con moderación
Los filtros blur pueden ser caros. Las box-shadows grandes pueden ser caras. Puedes mantener el aspecto de “tarjeta flotante” sin freír GPUs móviles usando sombras moderadas y evitando backdrop blurs a menos que hayas probado en dispositivos de gama baja.
Tres micro-historias corporativas desde el campo
Micro-historia #1: El incidente causado por una suposición equivocada
Un equipo de producto lanzó un nuevo sistema de toasts como parte de un rediseño. Se veía genial en storybook. Se veía genial en escritorio. En su mente estaba posicionado “globalmente” porque se renderizaba dentro del componente de layout que cada ruta usaba.
Luego un flujo de pago introdujo un nuevo envoltorio “secure step” que aplicó transform: translateZ(0) para suavizar una transición. Ese envoltorio también alojaba un widget de verificación basado en iframe con sus propias reglas de capas. La región de toasts ahora estaba dentro de un elemento transformado, lo que significó que ya no se comportaba como un overlay verdadero del viewport.
Los síntomas fueron raros: a veces el toast aparecía detrás del widget de verificación; a veces se recortaba en el límite del envoltorio; a veces tenía desplazamiento. Soporte reportó “mensajes de error faltantes”, que es la clase de frase que hace que los ingenieros de guardia se queden mirando al vacío un momento.
La suposición equivocada era simple: “position: fixed siempre es relativo al viewport.” No siempre—las transformaciones pueden cambiar esa relación. La solución no fue un z-index mayor. La solución fue mover la región de toasts fuera del subárbol transformado (portal al root del documento), y luego documentar una regla: “no aplicar transforms en la raíz de la app a menos que controles todos los overlays”.
Micro-historia #2: La optimización que salió mal
Otra organización decidió “optimizar” el rendimiento de los toasts precomputando alturas y posicionando absolutamente cada toast con un translateY. La idea: nada de layout, solo transforms. Incluso añadieron will-change en todos lados porque “hace que las animaciones sean más suaves”.
Funcionó en demos. Funcionó con uno o dos toasts. Luego la app se encontró en producción con un escenario real: una integración generó un lote de advertencias tras una importación masiva. Los toasts tenían texto de longitud variable, algunos se envolvían, otros no. La altura precomputada era incorrecta cuando las fuentes cargaban tarde, cuando la localización cambiaba la longitud de las líneas, o cuando los usuarios hacían zoom en la página.
La pila derivó. Los toasts se solaparon. Las animaciones de cierre dejaron agujeros. Peor aún, el “will-change en todos lados” promovía demasiadas capas, lo que aumentó el uso de memoria y degradó el desplazamiento en dispositivos de gama media.
La solución fue aburrida: volver a flexbox + gap, aceptar que el layout existe y limitar toasts visibles. La verdadera ganancia de rendimiento vino de no renderizar 20 toasts con sombras pesadas, no de pretender que el layout se podía sustituir por matemáticas ingeniosas.
Micro-historia #3: La práctica aburrida pero correcta que salvó el día
Un equipo tenía una política de capas escrita. Nada fancy: un documento corto y un pequeño archivo de tokens de z-index. Los toasts se renderizaban en la raíz del documento. Los modales tenían niveles definidos. Los tooltips tenían niveles definidos. Todos se quejaban de que era “proceso”.
Luego llegó un rediseño mayor: nueva navegación, nuevo header sticky, nuevo overlay de búsqueda, nuevos modales de onboarding. La clase de semana donde cada merge request toca CSS. El equipo esperaba bugs de capas UI. Tuvieron casi ninguno.
Cuando apareció un bug (un tooltip renderizando bajo un toast), la solución fue rápida porque la política dejó claro quién estaba equivocado: el token de z-index del tooltip se aplicó mal en un componente. Sin adivinanzas. Sin escaladas. Sin “funciona en mi máquina”.
La práctica aburrida salvó el día porque redujo el espacio de soluciones. En trabajo de fiabilidad, eso es oro.
Tareas prácticas con comandos: inspeccionar, reproducir, decidir
No puedes SREear fuera de un bug de CSS, pero puedes depurarlo con intención. Abajo hay tareas prácticas que puedes ejecutar durante desarrollo o en CI para diagnosticar problemas de toast. Cada una incluye el comando, qué significa la salida y la decisión que tomas.
Tarea 1: Confirma que el CSS del toast realmente se publica (sanidad del bundle)
cr0x@server:~$ ls -lh dist/assets | grep -E 'toast|main'
-rw-r--r-- 1 cr0x cr0x 312K Nov 12 09:14 main.8c1b1a.css
-rw-r--r-- 1 cr0x cr0x 14K Nov 12 09:14 toast.2a9f0c.css
Qué significa: la hoja de estilo del toast existe y está siendo producida por la build.
Decisión: si falta, tu bug de “toast sin estilos” es de build/pipeline/config, no de lógica CSS. Arregla el orden de importación o la configuración del bundler antes de tocar z-index.
Tarea 2: Revisa el orden de importación de CSS (el problema de sobreescritura silenciosa)
cr0x@server:~$ rg -n "toast\.css|toast\.scss|@import.*toast" src
src/app.tsx:7:import "./toast.css";
src/app.tsx:8:import "./app.css";
Qué significa: los estilos del toast se cargan antes que los estilos de la app; los estilos de la app pueden sobreescribirlos.
Decisión: si app.css contiene reglas genéricas como button{} o .card{} podrías estar sobreescribiendo estilos del toast. Invierte el orden o aumenta la especificidad intencionalmente (no accidentalmente).
Tarea 3: Valida que la región se renderiza en la raíz del documento (chequeo del portal)
cr0x@server:~$ rg -n "createPortal|#toast-root|toast-root" src
src/ui/toast/ToastProvider.tsx:22:return createPortal(region, document.getElementById("toast-root")!);
src/index.html:15:<div id="toast-root"></div>
Qué significa: tienes un punto de montaje DOM dedicado y un portal.
Decisión: si no ves esto, probablemente estás colocando toasts dentro de árboles de componentes que acabarán adquiriendo transforms/overflow y los recortarán.
Tarea 4: Detecta recortes por overflow en el shell de la app
cr0x@server:~$ rg -n "overflow:\s*(hidden|clip|auto)" src/layout
src/layout/AppShell.css:41:overflow: hidden;
src/layout/Content.css:12:overflow: auto;
Qué significa: partes de tu layout crean contextos de recorte.
Decisión: si los toasts se renderizan dentro de esos elementos, serán recortados. Mueve la región de toasts al body/portal, o elimina overflow hidden de ancestros si es factible.
Tarea 5: Encuentra transforms que crean una trampa para position: fixed
cr0x@server:~$ rg -n "transform:|filter:|opacity:\s*0\." src/layout src/pages
src/layout/SecureWrap.css:9:transform: translateZ(0);
src/pages/Onboarding.css:18:opacity: 0.98;
Qué significa: estos pueden crear contextos de apilamiento; las transformaciones pueden afectar el comportamiento de hijos fixed según la estructura.
Decisión: si la región de toasts está dentro de estos wrappers, muévela fuera. Si debe estar dentro, deja de usar transforms en el wrapper y anima un hijo en su lugar.
Tarea 6: Confirma que los tokens de z-index están centralizados y no son improvisados
cr0x@server:~$ rg -n "z-index:\s*[0-9]{4,}" src
src/components/LegacyModal.css:3:z-index: 99999;
src/components/HelpWidget.css:8:z-index: 1000000;
Qué significa: alguien está improvisando con z-index.
Decisión: reemplaza con valores basados en tokens; de lo contrario el z-index de tu toast será una carrera armamentista sin fin.
Tarea 7: Revisa la captura de pointer-events que bloquea la UI
cr0x@server:~$ rg -n "pointer-events:\s*(auto|none)" src/ui/toast
src/ui/toast/toast.css:12:pointer-events: none;
src/ui/toast/toast.css:24:pointer-events: auto;
Qué significa: la región se deja pasar al clic, los toasts son interactivos.
Decisión: si no ves esta separación, arréglala antes de publicar. Los bugs de bloqueo de clics son visibles al cliente instantáneamente.
Tarea 8: Verifica que exista soporte para reduced-motion
cr0x@server:~$ rg -n "prefers-reduced-motion" src/ui/toast
src/ui/toast/toast-motion.css:21:@media (prefers-reduced-motion: reduce){
Qué significa: respetas las preferencias de movimiento del usuario.
Decisión: si falta, añádelo. Esto no es pulir; es prevenir regresiones de accesibilidad y quejas por movimiento.
Tarea 9: Asegúrate de no animar propiedades de layout
cr0x@server:~$ rg -n "@keyframes|transition:" src/ui/toast
src/ui/toast/toast-motion.css:9:animation: toast-in 220ms cubic-bezier(.2,.9,.2,1) both;
Qué significa: tienes animaciones. Ahora inspecciona qué animan.
Decisión: si encuentras top, height o margin en keyframes, refactoriza a transform/opacity para reducir jank y thrash de layout.
Tarea 10: Valida que esté presente el manejo de safe-area
cr0x@server:~$ rg -n "safe-area-inset" src/ui/toast
src/ui/toast/toast.css:16:padding: calc(env(safe-area-inset-top) + 0px) ...
Qué significa: la región de toasts no se esconderá bajo muescas y esquinas redondeadas.
Decisión: si falta y soportas web móvil, añádelo. Este es un bug de “funciona en portátil” que se convierte en “¿por qué los usuarios están confundidos?”.
Tarea 11: Usa Lighthouse CI para detectar regresiones por tormentas de toasts
cr0x@server:~$ npx lighthouse http://localhost:4173 --only-categories=performance --view=false
Performance: 86
First Contentful Paint: 1.4 s
Total Blocking Time: 220 ms
Cumulative Layout Shift: 0.02
Qué significa: tienes números base de rendimiento.
Decisión: si Total Blocking Time sube tras introducir efectos fancy (filters, sombras grandes), reduce efectos. Los toasts no valen una app más lenta.
Tarea 12: Perfila una tormenta de toasts en Chrome headless (traza real)
cr0x@server:~$ node scripts/toast-storm.js
Rendered 50 toasts in 2.3s
Main-thread long tasks: 7
Worst long task: 182ms
Qué significa: renderizar/animaciones están causando tareas largas bajo carga.
Decisión: limita toasts visibles, elimina efectos caros y asegúrate de no medir layout repetidamente en JS.
Tarea 13: Confirma que el servidor no envía CSP que bloquee estilos inline (si dependes de ellos)
cr0x@server:~$ curl -I http://localhost:8080 | rg -i "content-security-policy"
Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'
Qué significa: los estilos están limitados a CSS self-hosted. Los estilos inline están bloqueados.
Decisión: si tu sistema de toasts inyecta estilos inline para colocación/animación, puede fallar en producción. Prefiere clases CSS/atributos data sobre inyección inline de estilos.
Tarea 14: Verifica que la región de toasts exista una sola vez (sin duplicados)
cr0x@server:~$ rg -n "id=\"toast-root\"" -S .
./src/index.html:15:<div id="toast-root"></div>
Qué significa: tienes un contenedor root.
Decisión: si existen múltiples roots en plantillas, renderizarás varias regiones y te preguntaras por qué los toasts se duplican. Arregla la plantilla/layout primero.
Guía rápida de diagnóstico
Cuando el comportamiento de los toasts está roto, no empieces ajustando curvas de animación. Trátalo como un outage: identifica rápidamente la clase de fallo y luego afina.
Primero: ¿es visible y está en el lugar correcto?
- Revisa presencia en el DOM: ¿está el elemento toast en el DOM?
- Revisa posición computada: ¿la región es
position: fixedy anclada donde esperas? - Revisa restricciones del viewport: ¿está recortado por overflow o un ancestro transformado?
Si falta: es un problema de render/state (lógica JS, montaje del portal, render condicional), no CSS.
Si está presente pero en lugar incorrecto: es colocación del contenedor o atrapamiento de position: fixed debido a transforms.
Segundo: ¿está detrás o por encima de lo incorrecto?
- Inspecciona contextos de apilamiento: busca transforms, opacity, filters en ancestros.
- Audita la política de z-index: ¿alguien usa
z-index: 999999y gana?
Si está detrás de un modal: decide si eso es correcto desde producto. Luego ajusta tokens de z-index. No publiques números aleatorios.
Tercero: ¿está rompiendo entrada o accesibilidad?
- Eventos de puntero: ¿puedes clicar a través del espacio vacío de la región?
- Teclado: ¿puedes tabular sin trampas de foco?
- Lector de pantalla: ¿anuncia con cortesía, no spamea?
Si bloquea clics: arregla pointer-events en el contenedor.
Si roba foco: deja de enfocar toasts por defecto; usa un panel para notificaciones con acciones.
Cuarto: ¿está entrecortado bajo carga?
- Prueba de tormenta de toasts: renderiza 20–50 toasts; busca tareas largas.
- Propiedades de animación: confirma solo transform/opacity.
- Limita visibles: máx 3–5; colapsa el resto.
Si está entrecortado: reduce efectos, limita visibles, evita thrash de layout y blur caro.
Errores comunes: síntoma → causa raíz → solución
1) El toast aparece detrás del modal
Síntoma: el toast está en el DOM pero no visible cuando hay modales.
Causa raíz: escala de z-index inconsistente; el modal está por encima del toast, o el toast está atrapado en un contexto de apilamiento inferior.
Solución: mueve la región de toasts al root del documento vía portal y define tokens de z-index. Decide explícitamente si los toasts deben superar modales.
2) El toast se recorta en el borde de un contenedor
Síntoma: el toast se “corta” cuando está cerca del borde de la página o dentro de un wrapper de layout.
Causa raíz: un ancestro tiene overflow: hidden/clip o el toast no está verdaderamente fijado al viewport.
Solución: renderiza los toasts fuera del contenedor que recorta; elimina overflow hidden de ancestros si el layout lo permite.
3) El toast bloquea clics en la página
Síntoma: el usuario no puede clicar botones cerca de la región de toasts aun cuando ningún toast cubre el botón.
Causa raíz: el contenedor de toasts captura pointer-events en toda la región.
Solución: pointer-events: none en el contenedor, pointer-events: auto en el toast.
4) Las animaciones de toast se sienten lentas y hacen la app “pesada”
Síntoma: frames perdidos, stutter durante scroll, jank cuando aparecen múltiples toasts.
Causa raíz: animar propiedades de layout o usar efectos caros (blur, sombras grandes) combinados con demasiados toasts simultáneos.
Solución: anima transform/opacity, reduce efectos pesados, limita toasts visibles y evita mediciones por frame en JS.
5) Un toast nuevo aparece “debajo” del viejo (dirección de pila incorrecta)
Síntoma: el usuario no nota el mensaje nuevo porque aparece lejos del borde.
Causa raíz: el orden del DOM y la dirección flex no coinciden con la semántica de colocación.
Solución: para colocaciones inferiores usa flex-direction: column-reverse (o invierte el orden del DOM) para que los toasts nuevos aparezcan cerca del borde inferior.
6) El contenido del toast se solapa o colapsa cuando el texto se envuelve
Síntoma: mensajes largos se solapan con el botón de cerrar o se truncan impredeciblemente.
Causa raíz: posicionamiento absoluto dentro del toast, altura fija o columnas de grid inflexibles.
Solución: usa grid CSS con columnas sensatas (icono | contenido | acciones), permite que el contenido se envuelva, evita alturas fijas.
7) El lector de pantalla anuncia cada toast como una emergencia
Síntoma: la tecnología asistiva interrumpe constantemente.
Causa raíz: usar aria-live="assertive" para actualizaciones rutinarias.
Solución: usa polite para toasts normales; reserva assertive para fallos verdaderamente urgentes. Considera limitar la tasa de anuncios.
8) Los toasts se duplican al navegar
Síntoma: tras cambios de ruta, existen múltiples regiones; los toasts se muestran por duplicado.
Causa raíz: el proveedor de toasts se monta por ruta, no en la raíz de la app; múltiples #toast-root en las páginas.
Solución: monta el proveedor una sola vez en el top-level. Asegura un único #toast-root en el HTML.
Listas de verificación / plan paso a paso
Lista de verificación de construcción: el conjunto “publicar sin remordimientos”
- La región de toasts está montada una vez en la raíz del documento (portal).
- La región usa
position: fixedy safe-area insets. - La región deja pasar clics: contenedor
pointer-events: none, toastauto. - La pila usa flex + gap; no cálculos manuales de offset Y.
- Las colocaciones se controlan por un único
data-placemento variante de clase. - Máx de toasts visibles establecido (3–5) con política de colapso.
- Animaciones usan transform + opacity; soporte reduced-motion.
- z-index usa tokens; no hay números gigantes en componentes aleatorios.
- Accesibilidad: estrategia de región live definida; el foco no se roba por defecto.
Paso a paso: implementar colocaciones y movimiento sin caos
- Crea la región: una sola
.toast-regioncon posicionamiento fijo y padding de safe-area. - Implementa el apilamiento: columna flex con gap; decide newest-top vs newest-bottom por colocación.
- Añade variantes de colocación: reglas
data-placementpara top-right, top-left, bottom-right, bottom-center. - Añade atributos de estado:
data-state="entering|steady|exiting"para hooks de animación. - Enlaza animaciones: keyframes usando transform/opacity, y override para reduced-motion.
- Define política de z-index: crea tokens y elimina valores z-index descontrolados.
- Prueba tormentas de toasts: renderiza 50 toasts en dev; confirma sin solapamientos, sin jank, sin bloqueo de clics.
- Prueba overlays: abre modales, dropdowns, tooltips; verifica que las reglas de capas sean correctas.
- Prueba área segura móvil: dispositivos con muesca, orientación landscape, zoom.
- Pase de accesibilidad: navegación por teclado, anuncios de lector de pantalla, preferencias de movimiento.
Lista operativa: cuando producto cambia requisitos
- Si quieren mensajes persistentes: rutea a un panel de notificaciones, no aumentes la duración de los toasts.
- Si quieren acciones inline: asegura que los botones sean reales y accesibles por teclado; evita que el toast entero sea el único objetivo clicable.
- Si quieren más ubicaciones: añade variantes vía variables; no bifurques CSS por colocación.
- Si quieren contenido más rico: limita ancho, permite envoltura y prueba con cadenas localizadas.
Preguntas frecuentes
1) ¿Deben los toasts aparecer por encima o por debajo de los modales?
Decide según la semántica. Si el modal es una interacción crítica (aprobación de pago, seguridad), mantén los toasts debajo. Para modales no bloqueantes, colocar los toasts arriba puede estar bien. Sea lo que sea, codifícalo en tokens de z-index y no dejes que los equipos improvisen.
2) ¿Por qué mi toast fijo se mueve cuando hago scroll dentro de un contenedor?
Porque no está realmente fijado al viewport. Si la región de toasts está dentro de un ancestro transformado o un contenedor con scroll, la posición fixed puede comportarse como relativa a ese ancestro. Renderiza la región en el root del documento y elimina transforms de ancestros cuando sea posible.
3) ¿Cuántos toasts deberían ser visibles a la vez?
Tres a cinco. Más que eso se vuelve spam UI y deuda de rendimiento. Si legítimamente hay más, colápsalos en un resumen o muévelos a un panel de notificaciones.
4) ¿Los toasts de éxito deben descartarse automáticamente?
Normalmente sí, con una duración corta. Los errores son más delicados: los errores descartados automáticamente se pasan por alto. Si el usuario debe actuar, no uses un toast; usa UI inline o un panel con estado persistente.
5) ¿Puedo animar la altura para un colapso más suave?
Puedes, pero es una fuente común de jank porque dispara layout. Si lo haces, mantén pocos elementos, prueba en dispositivos de gama baja y prefiere transform/opacity en el propio toast. Un colapso “suave” que deja caer frames no es suave.
6) ¿Por qué mis toasts se superponen cuando el texto se envuelve?
Normalmente porque haces offsets manuales o posicionamiento absoluto dentro del toast. Usa un layout natural (grid/flex) y permite que la altura del toast se expanda. Usa flex + gap para apilar.
7) ¿Cuál es la mejor colocación para móvil?
Bottom-center es común porque los pulgares y la atención están más bajos en la pantalla. Pero puede colisionar con navegación inferior y áreas de gestos del SO. Respeta safe-area insets y prueba en landscape. Las colocaciones superiores reducen colisiones de gestos pero pueden entrar en conflicto con barras de dirección y muescas. Elige uno y verifícalo en dispositivos.
8) ¿Deben los toasts ser clicables completos?
Sólo si lo deseas de verdad. Objetivos clicables en todo el toast pueden causar navegación accidental, especialmente cuando los toasts aparecen donde el usuario ya está pulsando. Prefiere botones explícitos (Deshacer, Reintentar, Ver detalles).
9) ¿Cómo evito toasts duplicados al navegar?
Montar el proveedor de toasts una sola vez en la raíz de la app y renderizar en un único elemento toast root. Las duplicaciones suelen venir de proveedores por ruta o múltiples #toast-root en templates.
10) ¿Cómo evito que los toasts cubran UI importante?
Primero: configuración de pointer-events para que no bloqueen clics fuera del toast. Segundo: elige colocaciones que eviten CTAs principales (a menudo top-right en escritorio, bottom-center en móvil). Tercero: considera offsets adaptativos cuando hay navegación inferior, pero no sobre-ingenierices—prueba y elige defaults estables.
Conclusión: próximos pasos que realmente ayudan
Si tu sistema de toasts es frágil, no es porque CSS sea difícil. Es porque el resto de la UI es un sistema con restricciones reales: contextos de apilamiento, overlays, accesibilidad y rendimiento bajo ráfagas. Trata los toasts como un overlay de primera clase, no como un adorno de última hora.
Haz esto a continuación:
- Mueve la región de toasts a un portal en la raíz del documento y aplica un único montaje.
- Adopta una política de tokens de z-index y elimina números gigantescos improvisados.
- Usa flex + gap para apilar, limita toasts visibles, colapsa desbordamiento.
- Anima con transform/opacity, añade soporte reduced-motion y prueba una tormenta de toasts.
- Corrige pointer-events para que la región deje pasar clics.
Luego ejecuta la guía de diagnóstico una vez—adrede—antes de que la producción te la haga mirar.