Cómo crear animaciones suaves con transition

Crear una interfaz agradable no depende solo de elegir buenos colores, una tipografía legible o una estructura clara. También influye mucho cómo responde la web cuando una persona interactúa con ella. Un botón que cambia de estado de forma brusca puede sentirse rígido. En cambio, un botón que cambia de color o se desplaza ligeramente con suavidad transmite una sensación más cuidada y profesional.

Aquí es donde entra en juego transition, una de las propiedades más útiles de CSS para crear pequeñas animaciones entre dos estados. Con ella podemos suavizar cambios de color, movimientos, escalados, opacidades o sombras sin necesidad de utilizar JavaScript ni crear animaciones complejas con @keyframes.

Si estás empezando a trabajar con movimiento en interfaces, te recomiendo leer también esta guía sobre animaciones CSS desde cero, donde explico las bases para entender cuándo usar transition, cuándo usar animation y qué propiedades conviene animar.

En este artículo vamos a centrarnos en cómo crear animaciones suaves con transition, cómo escribir su sintaxis correctamente, qué errores evitar y cómo aplicarla en botones, enlaces, tarjetas, menús e imágenes.

Qué es transition en CSS

La propiedad transition permite que un cambio entre dos estados de un elemento no ocurra de golpe, sino de forma progresiva.

Por ejemplo, imagina un botón con un color de fondo rosa. Cuando pasas el cursor por encima, ese botón cambia a morado. Sin transición, el cambio es inmediato. Con transition, el navegador genera los estados intermedios y el cambio se percibe de manera más suave.

.button {
  background-color: #cc2b5e;
  transition: background-color 0.3s ease;
}

.button:hover {
  background-color: #753a88;
}

En este ejemplo, el cambio de color tarda 0.3s y utiliza una curva de movimiento ease. Es un detalle pequeño, pero cambia mucho la percepción de la interfaz.

La idea principal es sencilla: transition suaviza el paso de un valor CSS a otro. No crea una animación compleja por sí sola, sino que anima un cambio que ya existe.

Por eso suele utilizarse en estados como:

  • :hover
  • :focus
  • :active
  • clases añadidas con JavaScript
  • cambios visuales en componentes interactivos

Es una propiedad especialmente útil para microinteracciones: botones, enlaces, tarjetas, menús, iconos, tooltips o pequeños efectos visuales.

Diferencia entre transition y animation

Una duda muy habitual es cuándo conviene usar transition y cuándo es mejor usar animation.

Aunque ambas sirven para crear movimiento en CSS, no tienen el mismo propósito.

transition se utiliza cuando queremos animar el cambio entre dos estados. Por ejemplo, un botón en reposo y ese mismo botón en estado hover.

.card {
  transform: translateY(0);
  transition: transform 0.25s ease;
}

.card:hover {
  transform: translateY(-6px);
}

Aquí solo hay dos estados: la tarjeta en su posición inicial y la tarjeta ligeramente elevada. Este es un caso perfecto para usar transition.

En cambio, animation se utiliza cuando necesitamos una secuencia más elaborada, varios pasos intermedios o una repetición. Por ejemplo, un loader girando, un icono latiendo en bucle o una animación de entrada con diferentes fases.

Regla práctica para elegir

Puedes quedarte con esta idea:

Si hay dos estados, usa transition.
Si hay una secuencia o repetición, usa animation.

Para un efecto hover, lo normal es usar transition. Para un loader infinito, probablemente usarás @keyframes y animation.

Esta diferencia es importante porque muchas veces se usa animation para resolver efectos que podrían hacerse de forma más simple, limpia y mantenible con transition.

Sintaxis de transition

La propiedad transition es una abreviatura que agrupa varias propiedades relacionadas con la transición.

Su sintaxis habitual es:

transition: propiedad duración función-de-tiempo retraso;

Por ejemplo:

transition: transform 0.3s ease-in-out 0s;

Este código indica que la propiedad transform debe cambiar durante 0.3s, con una curva ease-in-out y sin retraso.

También se puede escribir de forma separada:

.element {
  transition-property: transform;
  transition-duration: 0.3s;
  transition-timing-function: ease-in-out;
  transition-delay: 0s;
}

Ambas formas son correctas, pero en proyectos reales suele utilizarse la versión abreviada porque es más compacta y fácil de leer.

transition-property: qué propiedad se anima

transition-property indica qué propiedad CSS queremos animar.

transition-property: background-color;

También podemos animar varias propiedades separándolas con comas:

transition-property: background-color, transform;

Una opción muy común es usar all:

transition: all 0.3s ease;

Aunque puede parecer cómodo, conviene tener cuidado. all indica que cualquier propiedad que cambie será animada. Esto puede provocar resultados inesperados si más adelante modificas otras propiedades del elemento.

Por eso, en muchos casos es mejor indicar exactamente qué propiedades quieres animar:

transition: background-color 0.3s ease, transform 0.3s ease;

El código queda más claro, más predecible y más fácil de mantener.

transition-duration: cuánto dura la transición

transition-duration define cuánto tarda la transición en completarse. Puede expresarse en segundos o milisegundos.

transition-duration: 300ms;

O también:

transition-duration: 0.3s;

Para microinteracciones, como botones o enlaces, suelen funcionar bien valores entre 150ms y 300ms.

Si la transición es demasiado rápida, apenas se nota. Si es demasiado lenta, puede hacer que la interfaz se sienta pesada.

.button {
  transition: background-color 180ms ease, transform 180ms ease;
}

Este tipo de duración suele funcionar muy bien para botones porque aporta suavidad sin hacer esperar a la persona usuaria.

transition-timing-function: cómo se mueve la transición

La función de tiempo define cómo progresa la transición durante su duración. Es decir, si empieza rápido, termina lento, mantiene velocidad constante o combina aceleración y desaceleración.

Algunos valores habituales son:

transition-timing-function: ease;
transition-timing-function: linear;
transition-timing-function: ease-in;
transition-timing-function: ease-out;
transition-timing-function: ease-in-out;

El valor ease suele funcionar bien en muchos casos. Sin embargo, para interfaces más cuidadas, ease-out y ease-in-out pueden dar una sensación más natural.

Por ejemplo:

.card {
  transition: transform 0.25s ease-out;
}

ease-out hace que el movimiento empiece con más energía y termine suavemente. Es muy útil para tarjetas que se elevan, elementos que aparecen o pequeños desplazamientos.

transition-delay: retrasar el inicio

transition-delay permite retrasar el inicio de la transición.

transition-delay: 0.1s;

Puede ser útil cuando quieres crear un efecto escalonado, por ejemplo en una lista de elementos que aparecen de forma progresiva.

.item {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.item.is-visible {
  opacity: 1;
  transform: translateY(0);
}

El retraso puede aportar ritmo visual, pero conviene usarlo con moderación. Si todo tarda demasiado en responder, la experiencia puede sentirse lenta.

Cómo crear un hover suave con transition

Uno de los usos más habituales de transition es crear un hover suave en botones, enlaces o tarjetas.

Veamos un ejemplo sencillo:

.button {
  display: inline-block;
  padding: 0.85rem 1.4rem;
  border-radius: 999px;
  background-color: #cc2b5e;
  color: #ffffff;
  text-decoration: none;
  transition: background-color 0.25s ease, transform 0.25s ease;
}

.button:hover {
  background-color: #753a88;
  transform: translateY(-2px);
}

Aquí ocurren dos cosas: cambia el color de fondo y el botón se desplaza ligeramente hacia arriba.

El resultado es una interacción sencilla, pero más fluida y agradable que un cambio instantáneo.

Añadir también el estado focus

Un error frecuente es diseñar solo el estado :hover y olvidarse de las personas que navegan con teclado.

Por eso, siempre que tenga sentido, conviene añadir también :focus-visible:

.button:hover,
.button:focus-visible {
  background-color: #753a88;
  transform: translateY(-2px);
}

Así el botón responde tanto al cursor como al foco de teclado. Es un pequeño detalle que mejora la accesibilidad y hace que la interfaz sea más coherente.

Si este tema te interesa, también puedes conectar este tipo de decisiones con el diseño de interacciones y prototipos. En esta guía sobre cómo pasar de wireframe a prototipo interactivo en Figma explico cómo pensar mejor los estados y flujos antes de llevarlos al código.

Propiedades recomendadas para animaciones suaves CSS

No todas las propiedades CSS se animan igual de bien. Algunas obligan al navegador a recalcular el layout de la página, mientras que otras son más eficientes.

Para conseguir animaciones suaves, normalmente conviene priorizar:

  • transform
  • opacity

Con transform podemos mover, escalar, rotar o inclinar un elemento sin modificar directamente el flujo del documento.

.card:hover {
  transform: scale(1.03);
}

Con opacity podemos crear efectos de aparición y desaparición:

.tooltip {
  opacity: 0;
  transition: opacity 0.2s ease;
}

.tooltip.is-visible {
  opacity: 1;
}

Mejor transform que top, left o margin

Aunque se pueden animar propiedades como width, height, margin, padding, top o left, no siempre es recomendable. Estos cambios pueden afectar al layout y hacer que la animación sea menos fluida.

Por ejemplo, en lugar de mover un elemento con top:

.box:hover {
  top: -10px;
}

es preferible usar transform:

.box:hover {
  transform: translateY(-10px);
}

El resultado visual puede ser parecido, pero la segunda opción suele ser más estable y eficiente.

Ejemplos prácticos de CSS transitions

A continuación tienes varios ejemplos que puedes adaptar a tus propios proyectos.

Enlace con cambio de color suave

.link {
  color: #cc2b5e;
  text-decoration: none;
  transition: color 0.2s ease;
}

.link:hover,
.link:focus-visible {
  color: #753a88;
}

Este patrón funciona muy bien en enlaces dentro de artículos, menús o llamadas a la acción secundarias.

Tarjeta que se eleva al pasar el cursor

.card {
  padding: 1.5rem;
  border-radius: 1rem;
  background-color: #ffffff;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  transform: translateY(0);
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.14);
}

Este efecto es muy útil en tarjetas de servicios, entradas destacadas del blog o proyectos de portfolio. La clave está en que el movimiento sea sutil. No hace falta desplazar mucho el elemento para que se entienda que es interactivo.

Imagen con zoom suave

.image-wrapper {
  overflow: hidden;
  border-radius: 1rem;
}

.image-wrapper img {
  display: block;
  width: 100%;
  transition: transform 0.4s ease;
}

.image-wrapper:hover img {
  transform: scale(1.06);
}

El contenedor tiene overflow: hidden para evitar que la imagen se salga visualmente al escalar. Es un recurso muy usado en galerías, cards de blog y proyectos visuales.

Menú desplegable con opacidad y desplazamiento

.dropdown {
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.dropdown.is-open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

Este ejemplo es útil cuando el estado del menú se controla con una clase, por ejemplo mediante JavaScript.

Si estás trabajando con rutas, navegación o enlaces internos en proyectos React, también puede interesarte esta guía sobre React Router Hash Link y enlaces ancla en React, donde el comportamiento de navegación puede combinarse con detalles visuales como el desplazamiento suave.

Buenas prácticas para usar transition

Una transición bien aplicada puede mejorar mucho la experiencia de usuario. Pero si se usa sin intención, también puede generar ruido o distracción.

Usa transiciones con un propósito

No todos los elementos necesitan moverse. La animación debe ayudar a entender mejor una acción, no convertirse en un adorno constante.

Antes de añadir una transición, pregúntate:

  • ¿Ayuda a comprender mejor la interacción?
  • ¿Hace que el cambio de estado sea más claro?
  • ¿Refuerza la acción de la persona usuaria?
  • ¿Puede resultar molesta si se repite muchas veces?

Una buena transición suele sentirse natural. Está ahí, mejora la experiencia, pero no roba protagonismo.

Mantén duraciones cortas

Para botones, enlaces e iconos, normalmente basta con una duración entre 150ms y 300ms.

Para elementos más grandes, como modales, paneles o menús desplegables, puedes usar valores algo mayores, como 300ms o 400ms.

.modal {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

La transición debe acompañar la experiencia, no ralentizarla.

Evita abusar de transition: all

Aunque transition: all parece una solución rápida, puede generar efectos inesperados.

transition: all 0.3s ease;

Es mejor indicar las propiedades concretas:

transition: background-color 0.3s ease, transform 0.3s ease;

Así el código es más explícito y fácil de mantener.

Respeta la preferencia de movimiento reducido

No todas las personas viven el movimiento en pantalla de la misma forma. Algunas pueden sentirse incómodas con animaciones excesivas.

Por eso conviene tener en cuenta prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

Esto reduce al mínimo las animaciones para quienes han indicado en su sistema que prefieren menos movimiento.

Errores comunes al usar transition

Aunque transition es sencilla, hay varios errores habituales que pueden hacer que no funcione como esperas.

Poner la transición solo en :hover

Este es uno de los errores más comunes:

.button:hover {
  background-color: #753a88;
  transition: background-color 0.3s ease;
}

El problema es que la transición solo se declara en el estado hover. Lo recomendable es definirla en el estado base:

.button {
  background-color: #cc2b5e;
  transition: background-color 0.3s ease;
}

.button:hover {
  background-color: #753a88;
}

Así el navegador puede animar tanto la entrada como la salida del estado.

Intentar animar display

No se puede animar de forma fluida un cambio de display: none a display: block.

Esto no generará una transición suave:

.menu {
  display: none;
  transition: display 0.3s ease;
}

.menu.is-open {
  display: block;
}

Una alternativa más adecuada es combinar opacity, transform, visibility y pointer-events:

.menu {
  opacity: 0;
  transform: translateY(10px);
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s ease;
}

.menu.is-open {
  opacity: 1;
  transform: translateY(0);
  visibility: visible;
  pointer-events: auto;
}

Crear movimientos demasiado exagerados

Una transición no tiene que ser espectacular para funcionar. De hecho, muchas veces cuanto más sutil, mejor.

Por ejemplo, una tarjeta que se desplaza 40px puede sentirse exagerada. En cambio, un desplazamiento de 4px, 6px u 8px suele ser suficiente.

.card:hover {
  transform: translateY(-6px);
}

La suavidad no depende solo de la duración. También depende de la distancia, la curva de movimiento y el contexto.

Cómo combinar varias transiciones

Puedes aplicar varias transiciones a un mismo elemento separándolas con comas.

.button {
  background-color: #cc2b5e;
  color: #ffffff;
  transform: translateY(0);
  transition:
    background-color 0.25s ease,
    color 0.25s ease,
    transform 0.25s ease;
}

.button:hover {
  background-color: #753a88;
  color: #ffffff;
  transform: translateY(-2px);
}

También puedes usar duraciones diferentes para cada propiedad:

.card {
  transition:
    transform 0.25s ease-out,
    box-shadow 0.35s ease;
}

En este caso, el movimiento de la tarjeta es un poco más rápido que el cambio de sombra. Son detalles pequeños, pero ayudan a que la interfaz se sienta más cuidada.

Cómo organizar tus transiciones en proyectos reales

Cuando una web crece, no conviene escribir duraciones y curvas distintas en cada componente sin ningún criterio. Lo ideal es crear cierta coherencia visual.

Una buena práctica es definir variables CSS:

:root {
  --transition-fast: 150ms ease;
  --transition-base: 250ms ease;
  --transition-slow: 400ms ease;
}

Después puedes reutilizarlas:

.button {
  transition: background-color var(--transition-base), transform var(--transition-base);
}

Esto ayuda a mantener una experiencia consistente en toda la interfaz.

También puedes crear utilidades reutilizables:

.u-transition {
  transition: transform 0.25s ease, opacity 0.25s ease;
}

Eso sí, conviene no abusar de clases genéricas si estás trabajando con componentes muy específicos. Lo importante es que el sistema sea claro y fácil de mantener.

Si además estás guardando preferencias visuales en el navegador, como modo claro, modo oscuro o estados personalizados, puedes complementar este tema con el artículo sobre cómo usar localStorage y sessionStorage en proyectos JavaScript.

Preguntas frecuentes sobre transition CSS

¿Qué es transition en CSS?

transition es una propiedad de CSS que permite suavizar el cambio entre dos estados de un elemento. Por ejemplo, puede hacer que un botón cambie de color de forma progresiva al pasar el cursor por encima.

Se utiliza mucho para crear animaciones suaves en botones, enlaces, tarjetas, menús, imágenes y otros elementos interactivos.

¿Cuál es la diferencia entre transition y animation?

transition sirve para animar el cambio entre dos estados, como normal y hover. animation, en cambio, permite crear secuencias más complejas mediante @keyframes, repeticiones o varios pasos intermedios.

Si necesitas suavizar un cambio simple, usa transition. Si necesitas una secuencia, usa animation.

¿Qué propiedades conviene animar para conseguir transiciones suaves?

Las propiedades más recomendadas suelen ser transform y opacity, porque permiten crear efectos visuales fluidos sin modificar directamente el layout.

Por ejemplo, para mover un elemento es mejor usar transform: translateY() que cambiar top o margin. Para mostrar u ocultar algo suavemente, suele ser mejor usar opacity que intentar animar display.

Cuando una pequeña transición mejora toda la experiencia

Crear animaciones suaves con transition no consiste en llenar una web de efectos. Consiste en diseñar cambios de estado que ayuden a entender mejor lo que ocurre en la interfaz.

Un botón que responde con suavidad confirma una acción. Una tarjeta que se eleva ligeramente indica que puede interactuarse con ella. Un menú que aparece de forma gradual evita una sensación brusca. Un enlace que cambia de color de manera fluida hace que la navegación se sienta más cuidada.

La clave está en usar transition CSS con intención: elegir bien qué propiedad animar, cuánto debe durar, qué curva de movimiento encaja mejor y cómo afecta esa decisión a la accesibilidad y al rendimiento.

En definitiva, las CSS transitions son una herramienta sencilla, potente y muy útil para mejorar la calidad percibida de una web. No necesitas crear grandes efectos para que una interfaz se sienta más profesional. A veces, una transición de 200ms, aplicada en el lugar adecuado, marca la diferencia entre una experiencia rígida y una experiencia fluida, clara y agradable.

Qué partes de CSS funcionan realmente en email marketing

Cuando venimos del desarrollo web, maquetar un email puede sentirse como volver varios años atrás. En una web actual damos por hecho que podemos usar Flexbox, Grid, variables CSS, animaciones, fuentes externas, pseudo-elementos, componentes interactivos y media queries avanzadas. Sin embargo, en email marketing la pregunta no es “¿puedo escribir este CSS?”, sino algo bastante más práctico: ¿lo van a interpretar correctamente Gmail, Outlook, Apple Mail, Yahoo y otros clientes de correo?

Y aquí empieza el verdadero reto. El CSS en email existe, funciona y es necesario, pero no funciona con la misma libertad que en una página web. Por eso, cuando hablamos de css email, css compatible email o css outlook, en realidad estamos hablando de compatibilidad, pruebas y decisiones de diseño mucho más conservadoras.

Un email no se renderiza en un navegador universal. Se abre en muchos clientes distintos, cada uno con sus propias reglas, filtros y limitaciones. Lo que se ve perfecto en Apple Mail puede romperse en Outlook. Lo que funciona en Gmail móvil puede comportarse de forma diferente en un webmail corporativo. Por eso, para diseñar emails eficaces, conviene pensar menos como si estuviéramos creando una landing page y más como si estuviéramos construyendo una pieza de comunicación resistente.

Si ya has trabajado con maquetación de emails o estás empezando a explorar herramientas como MJML, te puede resultar útil complementar este artículo con la guía sobre qué es MJML y por qué facilita la maquetación de emails responsive, donde explico cómo este framework ayuda a reducir parte de la complejidad técnica.

Por qué CSS en email marketing no funciona igual que en una web

En desarrollo web, el navegador interpreta HTML y CSS siguiendo estándares bastante consistentes. Puede haber diferencias entre Chrome, Safari, Firefox o Edge, pero en general existe una base común razonablemente estable. En email marketing, en cambio, la situación es bastante más irregular.

Cada cliente de correo puede modificar, filtrar o ignorar partes del HTML y del CSS. Algunos aceptan estilos en la etiqueta <style>. Otros los procesan de forma parcial. Algunos respetan propiedades como border-radius, mientras que otros las ignoran en determinados contextos. Y luego está Outlook, que durante años ha sido uno de los grandes dolores de cabeza para quienes maquetan emails.

El problema no es solo CSS, sino el motor de renderizado

Una de las razones por las que el CSS en email es tan impredecible está en el motor que utiliza cada cliente para mostrar el mensaje. Apple Mail, por ejemplo, suele ofrecer buen soporte porque se apoya en WebKit. En cambio, algunas versiones clásicas de Outlook para Windows han usado Microsoft Word como motor de renderizado, lo que explica muchas de sus limitaciones con propiedades modernas de CSS.

Esto significa que un mismo email puede verse muy bien en un cliente y romperse en otro. No necesariamente porque el código esté mal escrito, sino porque el entorno donde se abre el email no interpreta CSS como lo haría un navegador moderno.

Por eso, cuando diseñamos una campaña de email marketing, no basta con abrir el archivo HTML en el navegador y comprobar que se ve bien. Esa prueba solo nos dice cómo lo interpreta el navegador, no cómo lo interpretará Outlook, Gmail, Apple Mail o un cliente de correo corporativo.

La mentalidad correcta: diseñar para resistir

En email marketing conviene trabajar con una lógica de degradación elegante. Es decir, el email puede verse más bonito en clientes modernos, pero debe seguir siendo legible, claro y funcional en clientes más restrictivos.

No pasa nada si una sombra decorativa no aparece. Tampoco es grave si un borde redondeado se muestra como un rectángulo. Lo importante es que el mensaje se entienda, que el botón principal sea visible y que la jerarquía visual siga funcionando.

La pregunta clave sería: si se eliminan los efectos decorativos, ¿el email sigue cumpliendo su función? Si la respuesta es sí, vamos por buen camino.

La base más fiable: tablas, atributos HTML y CSS inline

Aunque pueda sonar anticuado, la estructura más fiable para un email sigue estando basada en tablas HTML. No porque las tablas sean mejores desde el punto de vista semántico, sino porque muchos clientes de correo las interpretan de forma más estable que los layouts modernos con div, Flexbox o Grid.

En email marketing, las tablas no se usan para representar datos, sino para controlar la estructura visual. Es una práctica heredada, sí, pero todavía muy útil cuando necesitamos que una newsletter se vea correctamente en distintos entornos.

Si estás comparando enfoques de maquetación, también puedes revisar el artículo sobre MJML vs HTML tradicional para emails, donde explico las ventajas y limitaciones de trabajar directamente con HTML frente a usar una herramienta que abstrae parte del código.

Qué significa usar CSS inline en email

Usar CSS inline significa aplicar los estilos directamente sobre cada elemento HTML. En una web esto sería poco recomendable por mantenimiento, escalabilidad y separación de responsabilidades. En email, sin embargo, sigue siendo una de las prácticas más seguras.

<td style="font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; color: #333333;">
  Texto del email
</td>

La razón es sencilla: algunos clientes de correo eliminan o modifican estilos declarados en el <head>. En cambio, los estilos inline tienen más posibilidades de sobrevivir al procesamiento del cliente.

Esto no significa que todo el CSS deba ir siempre inline y que no podamos usar clases. Significa que los estilos esenciales deben estar lo más cerca posible del elemento que los necesita. Por ejemplo: tamaño de texto, color, fuente, espaciado básico, fondo y estilos principales de botones.

Cuándo sí tiene sentido usar estilos en la etiqueta style

Aunque el CSS inline es la base más segura, los estilos dentro de <style> siguen teniendo utilidad. Pueden utilizarse para media queries, ajustes responsive, modo oscuro, clases auxiliares o mejoras progresivas para clientes modernos.

La diferencia está en no depender exclusivamente de ellos. Una buena regla práctica sería esta: lo esencial va inline; lo complementario puede ir en <style>.

Por ejemplo, un botón debe seguir pareciendo un botón aunque una media query no se aplique. Una columna debe seguir mostrando el contenido aunque no se apile exactamente como esperábamos. Y un bloque de texto debe seguir siendo legible aunque no cargue la fuente personalizada.

Propiedades CSS que suelen funcionar bien en email

A pesar de las limitaciones, hay muchas propiedades CSS que sí funcionan de manera bastante fiable en email marketing. La clave está en usarlas con sentido común y evitar que el diseño dependa de propiedades demasiado modernas o frágiles.

Tipografía básica

Las propiedades relacionadas con texto suelen ser de las más seguras. Entre ellas encontramos:

  • font-family
  • font-size
  • font-weight
  • line-height
  • color
  • text-align
  • text-decoration
  • text-transform

Estas propiedades son fundamentales para construir jerarquía visual. Permiten diferenciar títulos, subtítulos, párrafos, enlaces y llamadas a la acción sin depender de estructuras complejas.

Eso sí, conviene usar fuentes de sistema o definir buenos fallbacks. Las fuentes externas pueden funcionar en algunos clientes, pero no en todos. Por eso, si una marca utiliza una tipografía personalizada, lo más prudente es acompañarla siempre de alternativas seguras:

style="font-family: 'MiFuente', Arial, Helvetica, sans-serif;"

De esta manera, si la fuente principal no carga, el email seguirá viéndose correctamente.

Colores, fondos simples y bordes

También suelen funcionar bien propiedades como color, background-color, border, padding, width o vertical-align. Son propiedades sencillas, pero muy útiles para construir una experiencia visual clara.

El uso de background-color es especialmente importante porque permite crear bloques visuales sin depender de imágenes. En email marketing, siempre conviene que el diseño funcione aunque las imágenes no carguen. Por eso, un botón con color de fondo y texto real suele ser más robusto que una imagen que contiene el texto del botón.

Este principio también está muy relacionado con la accesibilidad. Si el mensaje principal está dentro de una imagen y esa imagen no se carga, el usuario puede quedarse sin información clave. En cambio, si el texto es real, el contenido sigue estando disponible.

Espaciado con padding

El padding suele ser más fiable que el margin, especialmente cuando se aplica sobre celdas <td>. Por ejemplo:

<td style="padding: 24px 32px;">
  Contenido del bloque
</td>

En cambio, confiar demasiado en márgenes puede producir resultados inconsistentes, sobre todo en Outlook. Por eso, en emails complejos se suele controlar el espaciado mediante tablas, celdas, atributos y padding.

No es tan limpio como trabajar con CSS moderno, pero es mucho más previsible.

Propiedades CSS que funcionan, pero con reservas

Hay un segundo grupo de propiedades que pueden funcionar bien en muchos clientes, pero no conviene tratarlas como base estructural. Son útiles para mejorar el acabado visual, pero no deberían sostener la comprensión del email.

Border-radius

border-radius se utiliza mucho para botones, tarjetas o imágenes redondeadas. En muchos clientes funciona correctamente, pero en otros puede fallar o requerir soluciones específicas.

La recomendación práctica es sencilla: puedes usar border-radius, pero el email no debería depender de él. Si un botón pierde las esquinas redondeadas, debe seguir pareciendo un botón. Si una tarjeta se muestra con esquinas rectas, el contenido debe seguir siendo claro.

En otras palabras, los bordes redondeados son una mejora visual, no una garantía estructural.

Box-shadow

Las sombras pueden aportar profundidad y separar visualmente bloques, pero no son una propiedad especialmente fiable en email. Algunos clientes las muestran correctamente, otros las ignoran y otros pueden modificar su comportamiento tras una actualización.

Por eso, si quieres usar box-shadow, hazlo como recurso decorativo. No lo utilices como único mecanismo para diferenciar una tarjeta del fondo. Es mejor combinarlo con un color de fondo, un borde suave o un espaciado claro.

Así, si la sombra no aparece, el diseño no se rompe.

Media queries

Las media queries son muy útiles para adaptar newsletters a móvil. Permiten ajustar tamaños de texto, apilar columnas, modificar anchos o mejorar zonas táctiles.

Sin embargo, no todos los clientes de correo las soportan igual. Por eso, es recomendable diseñar emails que ya sean legibles sin depender por completo de media queries. Una estructura híbrida, fluida o mobile-first suele ser más segura que un diseño rígido que solo funciona si se aplican todos los estilos responsive.

Si estás empezando con este tipo de maquetación, puedes ampliar esta parte con la guía sobre cómo crear tu primera newsletter responsive con MJML, especialmente si buscas una forma más sencilla de trabajar con columnas, secciones y comportamiento móvil.

Modo oscuro

El modo oscuro es uno de los puntos más delicados en email marketing. Algunos clientes respetan tus colores, otros los modifican automáticamente y otros invierten parcialmente fondos, textos o imágenes.

Esto puede provocar problemas de contraste, logotipos que se ven mal o botones que pierden claridad. Por eso, conviene definir colores de fondo explícitos, probar imágenes en claro y oscuro, evitar textos sobre imágenes sin fallback y revisar siempre el email en varios clientes.

El modo oscuro no debe tratarse como un detalle menor. Cada vez más usuarios lo tienen activado por defecto, así que ignorarlo puede afectar directamente a la legibilidad de una campaña.

CSS que conviene evitar en email marketing

Ahora viene la parte más importante cuando hablamos de CSS compatible con email: qué conviene evitar o, al menos, no usar como base del diseño.

Flexbox y Grid como estructura principal

Flexbox y Grid son dos herramientas fundamentales en desarrollo web moderno, pero en email marketing no son la opción más segura para la estructura principal.

Pueden funcionar en algunos clientes, especialmente en entornos más modernos, pero su soporte no es suficientemente uniforme como para utilizarlos como base de una newsletter que debe verse bien en Gmail, Outlook, Apple Mail y webmails variados.

La recomendación práctica es clara: usa tablas para la estructura principal y reserva Flexbox o Grid para casos muy controlados, siempre que hayas validado previamente el soporte en los clientes más importantes para tu audiencia.

Si vienes de trabajar mucho con layouts web, puede que esta limitación resulte frustrante. Pero en email marketing la prioridad no es usar la técnica más moderna, sino conseguir que el mensaje llegue de forma estable.

Position, float y layouts complejos

Propiedades como position: absolute, position: fixed, float o z-index pueden generar comportamientos imprevisibles en email. En una web permiten crear interfaces ricas y composiciones complejas. En un email, en cambio, suelen aumentar el riesgo de roturas.

Un email no necesita comportarse como una aplicación. Necesita comunicar una idea, mantener la identidad visual de la marca y dirigir a una acción clara. Cuanto más complejo sea el layout, más probable será que algo falle en algún cliente.

Pseudo-elementos y selectores avanzados

Selectores como :hover, :focus, :nth-child, combinadores complejos o pseudo-elementos como ::before y ::after no deberían formar parte de la base de un email comercial.

En desarrollo web son recursos muy útiles. De hecho, si te interesa este enfoque visual, en el blog también puedes leer sobre pseudo-elementos en CSS y cómo ayudan a crear ilustraciones más complejas. Pero en email marketing conviene ser mucho más prudente.

Si necesitas una decoración visual importante, probablemente sea mejor resolverla con HTML real, una imagen optimizada o una estructura más simple.

Variables CSS, animaciones y funciones modernas

Las custom properties, calc(), clamp(), filtros, máscaras, animaciones, transiciones o efectos avanzados pueden funcionar en algunos clientes, pero no deberían ser necesarios para una campaña de email marketing convencional.

El problema no es que estén “prohibidos”, sino que añaden incertidumbre. Y en email, la incertidumbre visual suele traducirse en pérdida de control sobre la experiencia final.

Cuando dudes, aplica esta regla: si una propiedad mejora el email pero no es imprescindible, puedes planteártela como mejora progresiva. Si una propiedad es imprescindible para que el email se entienda, debería ser muy compatible.

Outlook: el gran filtro de compatibilidad

Cuando se habla de CSS en Outlook, es importante aclarar que Outlook no es un único entorno. Existe Outlook clásico para Windows, el nuevo Outlook para Windows, Outlook.com, Outlook para Mac y las apps móviles de Outlook. Y no todos se comportan igual.

El problema histórico está sobre todo en Outlook clásico para Windows. Sus limitaciones han hecho que muchas plantillas de email tengan que incorporar soluciones específicas, comentarios condicionales o incluso VML para ciertos fondos y botones.

Qué suele romperse en Outlook

En Outlook conviene revisar especialmente:

  • anchos de imágenes;
  • fondos con imágenes;
  • espaciados verticales inesperados;
  • líneas blancas entre bloques;
  • botones con bordes redondeados;
  • columnas;
  • sombras y efectos decorativos;
  • fuentes personalizadas;
  • márgenes.

Esto no significa que debamos diseñar emails feos o excesivamente básicos. Significa que debemos diseñar con una base robusta y dejar los detalles más delicados como mejoras visuales.

Cómo trabajar mejor con Outlook

Para mejorar la compatibilidad con Outlook, conviene seguir algunas buenas prácticas:

  • usar tablas para la estructura principal;
  • definir anchos de imágenes con cuidado;
  • aplicar CSS inline en elementos clave;
  • evitar depender de margin para el espaciado principal;
  • crear botones con texto real;
  • probar el email antes del envío;
  • asumir que algunos efectos visuales no se mostrarán igual.

No se trata de diseñar únicamente para Outlook, sino de evitar que Outlook destruya la experiencia básica del mensaje.

Ejemplo de estructura segura para un bloque de email

Un bloque de email relativamente seguro podría tener una estructura como esta:

<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f5f5f5;">
  <tr>
    <td align="center" style="padding: 32px 16px;">
      <table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="width: 100%; max-width: 600px; background-color: #ffffff;">
        <tr>
          <td style="padding: 32px; font-family: Arial, Helvetica, sans-serif; color: #222222;">
            <h1 style="margin: 0 0 16px; font-size: 28px; line-height: 34px; font-weight: bold;">
              Título principal
            </h1>
            <p style="margin: 0 0 24px; font-size: 16px; line-height: 24px;">
              Texto introductorio del email con una estructura sencilla y compatible.
            </p>
            <table role="presentation" cellspacing="0" cellpadding="0" border="0">
              <tr>
                <td style="background-color: #cc2b5e; padding: 12px 24px;">
                  <a href="https://ejemplo.com" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px; color: #ffffff; text-decoration: none; display: inline-block;">
                    Ver más
                  </a>
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>

Desde una mentalidad web moderna, este código puede parecer repetitivo. Pero en email marketing responde a una necesidad concreta: conseguir que el diseño sea estable en el mayor número posible de clientes.

Checklist de CSS compatible para email marketing

Antes de enviar una campaña, conviene revisar una pequeña lista de comprobación. No sustituye a las pruebas reales, pero ayuda a detectar problemas comunes.

Estructura

  • ¿El layout principal está basado en tablas?
  • ¿El ancho máximo está controlado?
  • ¿El email se lee bien aunque no carguen las imágenes?
  • ¿Las columnas se comportan correctamente en móvil?

Estilos

  • ¿Los estilos esenciales están inline?
  • ¿La tipografía tiene fuentes fallback?
  • ¿Los colores de texto y fondo están definidos de forma explícita?
  • ¿Los botones tienen texto real?
  • ¿El espaciado depende más de padding que de margin?

Compatibilidad

  • ¿El diseño funciona sin sombras?
  • ¿El diseño funciona sin bordes redondeados?
  • ¿El contenido se entiende sin fuentes externas?
  • ¿Las media queries mejoran el diseño, pero no lo sostienen por completo?
  • ¿Se ha probado en Gmail, Outlook, Apple Mail y móvil?

Accesibilidad y experiencia

  • ¿El texto tiene buen contraste?
  • ¿Los enlaces son claros?
  • ¿Los botones tienen un tamaño cómodo para móvil?
  • ¿Las imágenes tienen texto alternativo útil?
  • ¿El orden de lectura tiene sentido?

Esta parte también conecta con una idea muy importante en diseño web y experiencia de usuario: reducir la fricción. Un email con una jerarquía clara, botones visibles y contenido fácil de escanear reduce la carga cognitiva y ayuda al usuario a tomar decisiones con menos esfuerzo.

Preguntas frecuentes sobre CSS en email

¿Puedo usar CSS en emails HTML?

Sí, puedes usar CSS en emails HTML, pero con más restricciones que en una web. Lo más recomendable es utilizar CSS inline para los estilos esenciales, mantener una estructura simple y probar el resultado en distintos clientes de correo antes del envío.

¿Flexbox y Grid funcionan en email marketing?

Pueden funcionar en algunos clientes, pero no son la opción más segura para la estructura principal de una campaña. Para emails comerciales que deben verse correctamente en muchos entornos, sigue siendo más fiable trabajar con tablas HTML y CSS inline.

¿Por qué Outlook rompe tantos emails?

Porque algunas versiones de Outlook, especialmente las clásicas de escritorio para Windows, no interpretan el HTML y el CSS como un navegador moderno. Esto puede afectar a fondos, espaciados, imágenes, bordes, fuentes y otros estilos visuales.

Diseñar emails con CSS es aceptar las reglas del medio

El CSS en email marketing no va de demostrar cuánto CSS sabemos usar. Va de tomar buenas decisiones para que el mensaje llegue bien, se lea bien y funcione en contextos muy diferentes.

Una newsletter no necesita comportarse como una web moderna. Necesita cargar correctamente, mantener la identidad visual de la marca, guiar la lectura y facilitar una acción clara. Para conseguirlo, muchas veces el mejor camino es aceptar las limitaciones del medio: tablas para la estructura, CSS inline para lo esencial, estilos progresivos para clientes modernos y pruebas constantes antes de enviar.

La clave no está en renunciar al diseño, sino en diseñar con realismo. Puedes usar colores, tipografías cuidadas, jerarquía visual, botones atractivos, fondos sólidos, imágenes optimizadas y pequeños detalles decorativos. Pero conviene evitar que la experiencia dependa de propiedades frágiles como sombras, layouts modernos, animaciones o fuentes externas.

En definitiva, el CSS que realmente funciona en email marketing es el que entiende su contexto. No el más moderno, no el más elegante, no el más parecido al de una landing page. El que funciona es el CSS que sobrevive a Gmail, se adapta a móvil, no se rompe en Outlook y mantiene intacto lo más importante: el mensaje.

Skeleton loaders con CSS: animaciones para mejorar la percepción de carga

Cuando una página web necesita unos segundos para mostrar su contenido, cada instante cuenta. Aunque el tiempo de espera real sea breve, una interfaz completamente vacía puede hacer que el usuario piense que algo no funciona, que la conexión se ha interrumpido o que la aplicación se ha quedado bloqueada.

Los skeleton loaders con CSS ofrecen una solución visual a este problema. En lugar de mostrar una pantalla en blanco o un icono giratorio, presentan una representación simplificada de la estructura que ocupará el contenido definitivo. De este modo, el usuario puede anticipar qué aparecerá y percibe que la aplicación está respondiendo.

Estos elementos también reciben nombres como skeleton screens, loading placeholders o pantallas esqueleto. Su función no es reducir directamente el tiempo de carga, sino mejorar la percepción de velocidad y proporcionar continuidad visual durante la espera.

En este artículo veremos qué es un skeleton loader, cuándo conviene utilizarlo, cómo crearlo únicamente con HTML y CSS y qué aspectos deben tenerse en cuenta para que la animación sea accesible, eficiente y coherente con el diseño de la interfaz.

Si todavía no tienes claros los fundamentos de las animaciones web, puedes comenzar por esta guía básica de animaciones CSS, donde encontrarás una introducción a las transiciones, los fotogramas clave y las propiedades animables.

¿Qué es un skeleton loader?

Un skeleton loader es una interfaz temporal que imita la distribución del contenido que todavía no está disponible. Normalmente está formado por bloques de color neutro que representan imágenes, títulos, párrafos, avatares, botones o tarjetas.

Por ejemplo, mientras una aplicación carga la información de un perfil, puede mostrar:

  • Un círculo en el lugar donde aparecerá el avatar.
  • Un rectángulo para representar el nombre.
  • Varias líneas horizontales simulando una descripción.
  • Un bloque más grande reservado para una imagen.
  • Una forma rectangular en el lugar donde aparecerá un botón.

Cuando los datos terminan de cargarse, estos elementos desaparecen y son sustituidos por el contenido real.

La principal diferencia entre un skeleton loader CSS y un indicador de carga tradicional es la cantidad de contexto que ofrecen. Un spinner comunica que existe un proceso en curso, pero no explica qué contenido aparecerá después. El skeleton, en cambio, presenta una estructura aproximada de la interfaz final.

Skeleton loader, spinner y barra de progreso

Aunque estos tres recursos se utilizan para representar estados de carga, no cumplen exactamente la misma función.

Un spinner es adecuado para acciones breves y poco complejas, como enviar un formulario, validar unos datos o guardar una configuración. Resulta sencillo y ocupa poco espacio, pero no informa sobre la estructura que se mostrará después.

Una barra de progreso es útil cuando se conoce el porcentaje de avance de una operación. Por ejemplo, durante la subida de un archivo, la instalación de un recurso o la exportación de un documento.

El skeleton screen CSS es especialmente apropiado cuando se está cargando contenido estructurado, como:

  • Listados de productos.
  • Tarjetas de noticias.
  • Perfiles de usuario.
  • Resultados de búsqueda.
  • Publicaciones de una red social.
  • Paneles de administración.
  • Tablas con información remota.
  • Galerías de imágenes.
  • Comentarios o conversaciones.

La elección debe depender del tipo de espera y del contexto. No se trata de sustituir todos los indicadores de carga por skeletons, sino de utilizar cada patrón donde resulte más comprensible.

En la práctica, una misma aplicación puede combinar diferentes soluciones. Un listado de artículos puede mostrar tarjetas esqueleto mientras se descargan los datos, mientras que el botón para guardar un artículo puede utilizar un pequeño spinner acompañado del texto «Guardando».

Por qué los skeleton loaders mejoran la experiencia de usuario

Un skeleton loader no acelera una petición HTTP ni reduce el tamaño de un archivo JavaScript. Sin embargo, puede conseguir que el proceso de espera resulte menos frustrante.

Esto sucede porque la percepción del tiempo no depende únicamente de los segundos transcurridos. También está relacionada con la información visual que recibe la persona durante ese periodo.

Cuando una interfaz muestra una estructura reconocible, transmite varias señales positivas:

  • La aplicación ha respondido.
  • El contenido se está preparando.
  • La distribución de la página ya está definida.
  • No es necesario actualizar la pantalla.
  • El resultado aparecerá en una posición predecible.

Una pantalla vacía obliga al usuario a interpretar qué está sucediendo. Un skeleton loader reduce esa incertidumbre al ofrecer una respuesta visual inmediata.

Reducción de los saltos de contenido

Uno de los problemas habituales en las interfaces dinámicas es que los elementos cambien de posición a medida que se descargan imágenes, textos o componentes.

Estos movimientos pueden generar una experiencia incómoda. El usuario puede estar a punto de pulsar un botón y, justo antes de hacerlo, el contenido se desplaza porque aparece una imagen que todavía no tenía espacio reservado.

Un skeleton loader puede ayudar a evitar este comportamiento cuando se diseña con unas dimensiones similares a las del elemento definitivo.

Por ejemplo, si una tarjeta va a contener una imagen con una relación de aspecto de 16:9, el bloque provisional debería reservar esa misma proporción:

.skeleton-image {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 0.75rem;
}

La propiedad aspect-ratio permite mantener el espacio necesario antes de que llegue la imagen real. Esto favorece una interfaz más estable y predecible.

No obstante, el skeleton solo ayudará a reducir los desplazamientos si su estructura se corresponde con el contenido final. Si el placeholder tiene una altura de 200 píxeles y la imagen definitiva ocupa 400, el salto visual seguirá produciéndose.

Sensación de progreso

Una pantalla vacía transmite inactividad. Una animación de carga CSS suave, en cambio, comunica que el sistema continúa trabajando.

El usuario no sabe necesariamente cuánto falta para que aparezcan los datos, pero observa una respuesta visual inmediata. Esa sensación de progreso puede ser suficiente para evitar que cierre la página o repita una acción innecesariamente.

Aun así, un skeleton loader no debe utilizarse para ocultar problemas graves de rendimiento. Si una pantalla tarda demasiado en cargar, será necesario revisar las peticiones, el tamaño de los recursos, el procesamiento de datos y la arquitectura de la aplicación.

La animación mejora la espera, pero no sustituye una estrategia real de optimización.

Cómo crear un skeleton loader básico con CSS

Un skeleton loader sencillo puede construirse con un elemento HTML y unas pocas propiedades CSS.

Partimos de una tarjeta que representa una noticia o un artículo:

<article class="skeleton-card" aria-hidden="true">
  <div class="skeleton skeleton-card__image"></div>

  <div class="skeleton-card__content">
    <div class="skeleton skeleton-card__title"></div>
    <div class="skeleton skeleton-card__text"></div>
    <div class="skeleton skeleton-card__text skeleton-card__text--short"></div>
  </div>
</article>

La clase .skeleton contiene los estilos compartidos por todos los bloques:

.skeleton {
  background-color: #e5e7eb;
  border-radius: 0.5rem;
}

Después definimos las dimensiones de cada elemento:

.skeleton-card {
  width: min(100%, 22rem);
  overflow: hidden;
  border: 1px solid #e5e7eb;
  border-radius: 1rem;
  background-color: #ffffff;
}

.skeleton-card__image {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 0;
}

.skeleton-card__content {
  display: grid;
  gap: 0.75rem;
  padding: 1.25rem;
}

.skeleton-card__title {
  width: 70%;
  height: 1.5rem;
}

.skeleton-card__text {
  width: 100%;
  height: 0.875rem;
}

.skeleton-card__text--short {
  width: 60%;
}

Con este código obtenemos una representación estática de la tarjeta. El usuario puede identificar dónde aparecerán la imagen, el título y la descripción, incluso antes de que los datos estén disponibles.

Sin embargo, todavía podemos mejorar el componente incorporando una animación que transmita actividad.

Crear el efecto de pulso

La opción más sencilla consiste en modificar progresivamente la opacidad de los bloques:

.skeleton {
  background-color: #e5e7eb;
  border-radius: 0.5rem;
  animation: skeleton-pulse 1.5s ease-in-out infinite;
}

@keyframes skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.55;
  }
}

Este efecto funciona bien en interfaces minimalistas y requiere muy poco código. La animación es discreta y no distrae demasiado del resto de la página.

No obstante, conviene no reducir excesivamente la opacidad. El bloque debe seguir siendo visible durante toda la animación para conservar su función estructural.

Si necesitas profundizar en la sintaxis y el funcionamiento de los fotogramas clave, puedes consultar el artículo sobre cómo funciona @keyframes en CSS.

Crear un efecto shimmer con gradiente

Otro patrón muy utilizado es el efecto shimmer. Consiste en desplazar una franja luminosa sobre el bloque para simular que el contenido se está preparando.

.skeleton {
  position: relative;
  overflow: hidden;
  background-color: #e5e7eb;
  border-radius: 0.5rem;
}

.skeleton::after {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    90deg,
    transparent,
    rgb(255 255 255 / 55%),
    transparent
  );
  content: "";
  transform: translateX(-100%);
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  100% {
    transform: translateX(100%);
  }
}

El pseudoelemento ::after ocupa toda la superficie del bloque. El gradiente comienza fuera del componente y se desplaza horizontalmente mediante transform.

Esta solución evita añadir un elemento HTML adicional únicamente para crear el brillo. El contenido semántico permanece más limpio y el efecto decorativo queda completamente controlado desde CSS.

Además, animar transform suele ser una opción más adecuada que modificar continuamente propiedades relacionadas con el tamaño o la posición del documento. Puedes ampliar esta idea en el artículo sobre por qué conviene animar transform y opacity antes que width o height.

Personalizar el skeleton con variables CSS

Cuando existen varios skeleton loaders en una aplicación, conviene centralizar sus estilos mediante propiedades personalizadas:

:root {
  --skeleton-background: #e5e7eb;
  --skeleton-highlight: rgb(255 255 255 / 60%);
  --skeleton-radius: 0.5rem;
  --skeleton-duration: 1.6s;
}

.skeleton {
  position: relative;
  overflow: hidden;
  background-color: var(--skeleton-background);
  border-radius: var(--skeleton-radius);
}

.skeleton::after {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    90deg,
    transparent,
    var(--skeleton-highlight),
    transparent
  );
  content: "";
  transform: translateX(-100%);
  animation: skeleton-shimmer var(--skeleton-duration) ease-in-out infinite;
}

De esta manera, resulta más sencillo adaptar el componente a la identidad visual del proyecto o modificar globalmente la velocidad, el color y el radio de los bloques.

También permite mantener una apariencia coherente cuando el loading placeholder se utiliza en diferentes secciones de la aplicación.

Cómo construir skeleton loaders reutilizables

En una aplicación real probablemente necesitaremos algo más que una única tarjeta. Por ese motivo, es recomendable pensar en el skeleton como un pequeño sistema de componentes.

Podemos crear variantes para textos, imágenes y avatares:

.skeleton--text {
  width: 100%;
  height: 0.875rem;
}

.skeleton--heading {
  width: 65%;
  height: 1.75rem;
}

.skeleton--avatar {
  width: 3rem;
  height: 3rem;
  flex-shrink: 0;
  border-radius: 50%;
}

.skeleton--thumbnail {
  width: 7rem;
  aspect-ratio: 1;
}

Después podemos combinar estas piezas en diferentes estructuras sin repetir todos los estilos:

<div class="profile-skeleton" aria-hidden="true">
  <div class="skeleton skeleton--avatar"></div>

  <div class="profile-skeleton__content">
    <div class="skeleton skeleton--heading"></div>
    <div class="skeleton skeleton--text"></div>
    <div class="skeleton skeleton--text profile-skeleton__short-line"></div>
  </div>
</div>
.profile-skeleton {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
}

.profile-skeleton__content {
  display: grid;
  flex: 1;
  gap: 0.625rem;
}

.profile-skeleton__short-line {
  width: 45%;
}

Este enfoque permite mantener una base común mientras se crean composiciones diferentes para perfiles, artículos, productos o comentarios.

Skeleton para una lista de elementos

Si una pantalla debe mostrar varias tarjetas, podemos repetir el mismo patrón:

<section class="skeleton-list" aria-label="Cargando artículos">
  <article class="skeleton-card" aria-hidden="true">
    <!-- Bloques del skeleton -->
  </article>

  <article class="skeleton-card" aria-hidden="true">
    <!-- Bloques del skeleton -->
  </article>

  <article class="skeleton-card" aria-hidden="true">
    <!-- Bloques del skeleton -->
  </article>
</section>
.skeleton-list {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, 18rem), 1fr)
  );
  gap: 1.5rem;
}

El uso de auto-fit y minmax() permite que el listado se adapte a diferentes tamaños de pantalla sin depender de un gran número de media queries.

Evitar que todos los bloques parezcan idénticos

Si todas las líneas de texto tienen exactamente la misma longitud, el skeleton puede resultar demasiado artificial. Una ligera variación ayuda a representar el contenido real de manera más creíble:

.skeleton-line:nth-child(2) {
  width: 92%;
}

.skeleton-line:nth-child(3) {
  width: 78%;
}

.skeleton-line:nth-child(4) {
  width: 55%;
}

La variación debe ser moderada. El objetivo no es reproducir el contenido definitivo con exactitud, sino ofrecer una aproximación visual de su jerarquía y distribución.

También conviene evitar generar anchuras completamente aleatorias en cada renderizado. Si el skeleton cambia de forma cada vez que aparece, la interfaz puede transmitir falta de consistencia.

Accesibilidad en los skeleton loaders

Una animación de carga CSS no debería evaluarse únicamente por su apariencia. También debe funcionar correctamente para personas que utilizan lectores de pantalla o que presentan sensibilidad al movimiento.

Los bloques decorativos del skeleton no contienen información útil. Por tanto, no deberían anunciarse uno por uno mediante tecnologías de asistencia.

Podemos ocultarlos con aria-hidden="true":

<div class="skeleton-card" aria-hidden="true">
  <!-- Elementos visuales -->
</div>

Al mismo tiempo, el contenedor que está esperando los datos puede indicar que se encuentra ocupado:

<section class="articles" aria-busy="true">
  <div class="skeleton-card" aria-hidden="true">
    <!-- Skeleton -->
  </div>
</section>

Cuando el contenido real esté disponible, el atributo deberá cambiar a:

<section class="articles" aria-busy="false">
  <!-- Contenido definitivo -->
</section>

Comunicar el estado sin describir cada bloque

En determinadas interfaces puede ser conveniente incluir un mensaje accesible que indique que los datos se están cargando:

<p class="sr-only" role="status">
  Cargando artículos, por favor espera.
</p>

La clase .sr-only mantiene el texto disponible para lectores de pantalla, aunque no aparezca visualmente:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

No es necesario anunciar cada elemento del skeleton. Un único mensaje general suele ser suficiente para informar del estado de la interfaz.

Cuando la carga termine, el mensaje puede actualizarse o retirarse. Si se produce un error, deberá sustituirse por una explicación clara de lo ocurrido.

Respetar prefers-reduced-motion

Las personas que han configurado su sistema para reducir el movimiento deberían poder utilizar la página sin animaciones continuas innecesarias.

Podemos desactivar el efecto mediante la consulta prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  .skeleton,
  .skeleton::after {
    animation: none;
  }
}

El skeleton seguirá cumpliendo su función como placeholder estático, pero dejará de desplazarse o cambiar de opacidad.

Esta adaptación es especialmente importante cuando existen muchos elementos animados al mismo tiempo. Una cuadrícula con diez o veinte skeletons puede generar un movimiento constante que resulte incómodo para algunos usuarios.

En la guía sobre animaciones CSS accesibles y prefers-reduced-motion encontrarás una explicación más detallada sobre cómo detectar y respetar esta preferencia.

Rendimiento de las animaciones de carga

Aunque el skeleton aparece precisamente durante una espera, eso no significa que su rendimiento sea irrelevante. Una animación poco optimizada puede consumir recursos, provocar tirones o competir con otras tareas que el navegador está ejecutando.

Para crear una animación eficiente conviene:

  • Priorizar transform y opacity.
  • Evitar animar dimensiones como width o height.
  • No crear un número excesivo de elementos.
  • Utilizar pseudoelementos cuando sea posible.
  • Detener la animación cuando el componente desaparezca.
  • Evitar sombras y filtros demasiado complejos.
  • No animar elementos que permanecen fuera del área visible.

El efecto shimmer mostrado anteriormente utiliza transform: translateX(), una elección adecuada para desplazar la franja luminosa.

Si quieres profundizar en este tema, puedes consultar el artículo sobre animaciones CSS y rendimiento, donde se explica qué propiedades resultan más apropiadas y cuáles pueden provocar recálculos de diseño innecesarios.

Utilizar will-change con moderación

También debemos evitar aplicar will-change indiscriminadamente. Esta propiedad permite avisar al navegador de que un elemento va a cambiar, pero mantenerla en decenas de componentes puede aumentar el consumo de memoria.

En la mayoría de skeleton loaders sencillos no es necesario utilizarla. Si las pruebas de rendimiento demuestran que aporta una mejora, podría aplicarse al pseudoelemento animado:

.skeleton::after {
  will-change: transform;
}

No debe añadirse como una optimización automática. Antes de incorporarla, conviene comprobar el comportamiento real mediante las herramientas de desarrollo del navegador.

¿Cuántos skeletons deberían mostrarse?

No existe un número universal. Dependerá del diseño y de la cantidad de contenido visible inicialmente.

Como referencia, suele ser suficiente representar los elementos que cabrían en la primera pantalla. Mostrar veinte tarjetas animadas cuando el usuario solo puede ver tres aumenta el trabajo del navegador sin aportar información adicional.

También puede utilizarse un skeleton simplificado para el contenido que queda fuera de la vista o retrasar su renderizado hasta que se aproxime al viewport.

El objetivo es representar la espera, no duplicar toda la página con elementos provisionales.

Errores frecuentes al diseñar skeleton screens

Un skeleton loader puede empeorar la experiencia si no guarda relación con la interfaz real o si permanece demasiado tiempo en pantalla.

Utilizar una estructura diferente al contenido final

El error más evidente consiste en mostrar un skeleton con unas dimensiones que no coinciden con el componente definitivo.

Si el placeholder presenta una imagen cuadrada, pero el contenido final utiliza una imagen horizontal, la tarjeta cambiará de forma al completarse la carga. En ese caso, el skeleton no evita los saltos visuales, sino que los provoca.

La estructura provisional debería aproximarse a:

  • La altura del contenido.
  • La proporción de las imágenes.
  • El número de líneas de texto.
  • La posición de botones y avatares.
  • Los márgenes y espacios internos.
  • La jerarquía visual del componente.

No es necesario que la copia sea exacta, pero sí que conserve la distribución principal.

Mantener el skeleton aunque la carga haya fallado

El skeleton representa un estado temporal. Si la petición produce un error, la interfaz debe sustituirlo por un mensaje claro y una posible acción de recuperación.

Mantener la animación indefinidamente hace creer que el contenido continúa cargándose cuando, en realidad, el proceso ha fallado.

Un componente debería contemplar al menos tres estados:

  1. Cargando.
  2. Contenido disponible.
  3. Error de carga.

En algunos casos también será necesario representar un estado vacío, cuando la petición funciona correctamente pero no devuelve resultados.

Usar animaciones demasiado rápidas

Un shimmer muy veloz puede resultar agresivo y llamar más la atención que el contenido principal. Por el contrario, una animación excesivamente lenta puede parecer bloqueada.

Una duración situada aproximadamente entre 1.4s y 2s suele producir un efecto suave, aunque debe ajustarse según el tamaño del componente y la distancia recorrida:

.skeleton::after {
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
}

La animación debe acompañar la espera sin convertirse en el elemento protagonista.

Mostrar skeletons para cargas casi instantáneas

Si el contenido aparece en unas pocas décimas de segundo, el skeleton puede producir un destello molesto. El usuario verá brevemente una estructura gris que desaparece casi de inmediato.

Para evitarlo, algunas aplicaciones retrasan ligeramente la aparición del placeholder. Si los datos llegan antes de ese umbral, el skeleton nunca se muestra.

Esta lógica no puede resolverse únicamente con CSS, porque depende del estado y de la duración de la petición. Debe controlarse desde JavaScript o desde el framework utilizado.

Utilizar skeletons sin una necesidad real

No todas las esperas requieren una pantalla esqueleto. Añadirla de forma automática a cualquier interacción puede complicar el código y sobrecargar visualmente la interfaz.

Antes de implementarla, conviene valorar si realmente ayuda al usuario. El artículo sobre cuándo utilizar animaciones CSS y cuándo evitarlas puede ayudarte a tomar esta decisión con mayor criterio.

Cuándo conviene utilizar un skeleton loader

Los skeleton loaders funcionan especialmente bien cuando la estructura del contenido se conoce antes de recibir los datos.

Son una buena opción para:

  • Tarjetas de artículos o productos.
  • Cabeceras de perfiles.
  • Conversaciones y mensajes.
  • Tablas y paneles.
  • Feeds de contenido.
  • Resultados paginados.
  • Componentes cargados de forma diferida.
  • Galerías de imágenes.
  • Paneles con estadísticas.
  • Secciones personalizadas para usuarios registrados.

En cambio, pueden no ser la mejor solución cuando:

  • La operación es prácticamente instantánea.
  • No se conoce la estructura del resultado.
  • Se dispone de un porcentaje de progreso real.
  • El usuario debe esperar a que termine una acción concreta.
  • La pantalla contiene una única operación pequeña.
  • El placeholder provocaría más movimiento que la propia carga.

Un botón que está enviando un formulario no necesita transformarse en una pantalla esqueleto. En ese contexto, un texto como «Enviando…» acompañado de un indicador pequeño será más comprensible.

También puedes consultar la guía sobre cómo crear loaders animados solo con CSS para conocer otras alternativas visuales y elegir el indicador más adecuado para cada situación.

Cómo integrar el skeleton loader con JavaScript

CSS se ocupa de la apariencia, pero normalmente JavaScript controla cuándo debe mostrarse el placeholder.

Un ejemplo sencillo podría ser:

<section id="articles" aria-busy="true">
  <p class="sr-only" role="status">
    Cargando artículos.
  </p>

  <div id="articles-skeleton" class="skeleton-list">
    <!-- Skeletons -->
  </div>

  <div id="articles-content" hidden></div>
</section>
const section = document.querySelector("#articles");
const skeleton = document.querySelector("#articles-skeleton");
const content = document.querySelector("#articles-content");

async function loadArticles() {
  try {
    const response = await fetch("/api/articles");

    if (!response.ok) {
      throw new Error("No se han podido cargar los artículos");
    }

    const articles = await response.json();

    content.innerHTML = articles
      .map(
        (article) => `
          <article>
            <h2>${article.title}</h2>
            <p>${article.description}</p>
          </article>
        `
      )
      .join("");

    skeleton.remove();
    content.hidden = false;
    section.setAttribute("aria-busy", "false");
  } catch (error) {
    skeleton.remove();
    content.hidden = false;
    content.textContent = error.message;
    section.setAttribute("aria-busy", "false");
  }
}

loadArticles();

En una aplicación desarrollada con React, Vue, Angular o cualquier otro framework, el principio será el mismo: renderizar el skeleton durante el estado de carga y sustituirlo cuando los datos estén disponibles.

La clave está en evitar que el contenido real y el placeholder aparezcan simultáneamente o que el skeleton continúe animándose después de haber sido ocultado.

Evitar la inyección directa de contenido no confiable

El ejemplo anterior utiliza innerHTML para mantener el código breve. En una aplicación real, no deberías insertar directamente datos externos sin haberlos validado o escapado.

Cuando el contenido procede de una API que no controlas por completo, utiliza métodos seguros para crear los elementos o las herramientas de renderizado proporcionadas por tu framework.

El skeleton loader se ocupa del estado visual de carga, pero no elimina la necesidad de aplicar buenas prácticas de seguridad.

Buenas prácticas para un loading placeholder CSS efectivo

Un buen skeleton loader debería ser discreto, representativo y temporal. Para conseguirlo, podemos seguir estas recomendaciones:

  • Mantener una estructura similar al contenido real.
  • Utilizar colores neutros compatibles con el tema visual.
  • Reservar las proporciones de imágenes y bloques.
  • Evitar animaciones excesivamente llamativas.
  • Respetar la preferencia de movimiento reducido.
  • Ocultar los elementos decorativos a lectores de pantalla.
  • Comunicar correctamente el estado de carga.
  • Contemplar errores y estados vacíos.
  • Reducir los placeholders situados fuera del área visible.
  • Sustituir el skeleton tan pronto como los datos estén preparados.
  • Comprobar el contraste en temas claros y oscuros.
  • Probar el resultado en dispositivos con menos recursos.

También conviene adaptar los colores al modo oscuro. Un gris pensado para un fondo blanco puede generar demasiado contraste dentro de una interfaz oscura:

@media (prefers-color-scheme: dark) {
  :root {
    --skeleton-background: #2d3340;
    --skeleton-highlight: rgb(255 255 255 / 8%);
  }
}

En proyectos con un selector de tema propio, será preferible vincular las variables a una clase o atributo:

[data-theme="dark"] {
  --skeleton-background: #2d3340;
  --skeleton-highlight: rgb(255 255 255 / 8%);
}

Esta solución permite que el skeleton responda al tema seleccionado dentro de la aplicación, independientemente de la configuración general del sistema operativo.

Preguntas frecuentes sobre skeleton loaders con CSS

¿Un skeleton loader mejora la velocidad real de una página?

No. Un skeleton loader no reduce por sí mismo el tiempo necesario para descargar datos, ejecutar JavaScript o renderizar imágenes. Su función consiste en mejorar la percepción de carga y proporcionar información visual durante la espera.

Para mejorar la velocidad real deben aplicarse otras medidas, como optimizar imágenes, reducir dependencias, utilizar caché, dividir el código, mejorar las consultas o disminuir el tiempo de respuesta del servidor.

¿Es mejor utilizar un skeleton loader o un spinner?

Depende del contexto. Un skeleton loader suele funcionar mejor cuando se está cargando contenido estructurado cuya distribución ya conocemos. Un spinner resulta más adecuado para operaciones breves, acciones sobre botones o procesos donde no es posible anticipar el resultado visual.

Ambos patrones pueden convivir dentro de la misma aplicación si se utilizan de forma coherente.

¿Se puede crear un skeleton screen únicamente con CSS?

Sí. La estructura visual y la animación pueden crearse con HTML y CSS mediante bloques, gradientes, pseudoelementos y @keyframes.

Sin embargo, para decidir cuándo aparece o desaparece el skeleton normalmente será necesario utilizar JavaScript, un framework frontend o algún sistema de renderizado que gestione el estado de la petición.

Diseñar la espera también forma parte de la experiencia

La carga no es un momento ajeno al diseño de una interfaz. Desde que el usuario solicita un contenido hasta que puede interactuar con él, cada estado forma parte de la experiencia.

Los skeleton loaders con CSS permiten transformar una pantalla vacía en una estructura comprensible. Bien utilizados, anticipan el contenido, reducen los cambios visuales y transmiten que el sistema continúa respondiendo.

Sin embargo, su valor no depende únicamente de añadir un gradiente animado. Un skeleton efectivo debe respetar la forma del contenido real, ser accesible, consumir pocos recursos y desaparecer correctamente tanto cuando la petición termina como cuando se produce un error.

La mejor animación de carga no es la más llamativa, sino la que acompaña la espera sin convertirse en protagonista. Cuando el usuario apenas repara en ella y encuentra el contenido exactamente donde esperaba, el skeleton loader está cumpliendo su función.