
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
transformyopacity. - Evitar animar dimensiones como
widthoheight. - 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:
- Cargando.
- Contenido disponible.
- 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.