Skeleton loading con Claude Code: React, CLS y accesibilidad
Implementa skeleton loading con Claude Code: ejemplo React, CLS, accesibilidad, errores comunes y verificación.
El skeleton loading muestra la estructura aproximada de una pantalla mientras los datos todavía se están cargando. Dicho de forma sencilla: reserva el espacio de la imagen, el título, el resumen y las acciones antes de que llegue el contenido real.
Un spinner solo dice “algo está pasando”. Un skeleton dice además “aquí aparecerá este tipo de contenido”. Si una imagen, un bloque publicitario o una respuesta de API entra en un espacio ya reservado, la página tiene menos riesgo de saltar. Esa estabilidad visual está relacionada con las guías de web.dev sobre Cumulative Layout Shift y Core Web Vitals.
Esta guía explica cómo pedir a Claude Code una implementación práctica de skeleton loading, con ejemplos de React y CSS que puedes copiar, notas de accesibilidad, casos de fallo y comprobaciones ligeras. Para el contexto relacionado, consulta optimización de rendimiento, lazy loading de imágenes y el flujo de accesibilidad.
Divide el trabajo antes de implementar
Un skeleton no es solo un rectángulo gris. Una función real necesita estados de carga, éxito, vacío y error, y esos estados deben compartir una distribución estable. Si pides a Claude Code “haz un skeleton bonito”, puede crear un shimmer atractivo pero olvidar el estado de error, la reducción de movimiento o los mensajes para lectores de pantalla.
flowchart LR
P["Prompt para Claude Code"] --> S["skeleton de tamaño similar"]
S --> D["datos cargados"]
D --> E["estado vacío"]
D --> X["estado de error"]
S --> A["aria-busy / status"]
S --> M["prefers-reduced-motion"]
S --> C["revisión de CLS"]
Empieza con un prompt que defina alcance y verificación:
Read the existing card/list components before editing.
Implement skeleton loading only for the article cards list.
Keep the skeleton dimensions close to the loaded content.
Handle loading, empty, error, and success states.
Respect prefers-reduced-motion and avoid layout shift.
Add a small Playwright check if the project already uses Playwright.
Do not change unrelated styles, routing, or data fetching.
prefers-reduced-motion es una condición CSS que indica si el usuario ha pedido reducir el movimiento en el sistema o el navegador. Los efectos shimmer intensos pueden ser molestos para algunas personas, así que conviene revisar la referencia de MDN sobre prefers-reduced-motion y ofrecer una versión sin animación.
Casos de uso realistas
El skeleton loading funciona mejor cuando el usuario puede intuir qué viene, pero los datos aún no están listos.
| Caso de uso | Qué reservar | Cuidado con |
|---|---|---|
| Listas de tarjetas de artículos | Miniatura, título de dos líneas, resumen, etiqueta | Mantén fija la altura del área multimedia |
| Dashboards | Tarjetas KPI, marcos de gráficos, actividad reciente | No muestres números parciales que puedan confundir |
| Grillas ecommerce | Imagen, nombre, precio, valoración | Evita mostrar precio o stock obsoleto durante el refresco |
| Tablas de administración | Cabecera, filas, zona de acciones | Si cambia mucho el número de filas, revisa paginación y filtros |
| Landing pages de consultoría | Casos, CTA, FAQ | Un CTA tardío puede empujar la ruta de conversión hacia abajo |
En un flujo de consulta de ClaudeCodeLab, la mejora más clara vino de reservar antes el espacio de las tarjetas y del CTA. El error fue hacer un skeleton más alto que el contenido final: durante la carga parecía ordenado, pero al terminar la página saltaba hacia arriba. Un skeleton no es decoración; es un contrato de layout.
Ejemplo React para copiar
Pega este ejemplo en src/App.tsx dentro de un proyecto Vite + React + TypeScript. Simula latencia con setTimeout y permite cambiar entre éxito, vacío y error. Este es el nivel de detalle de estado que conviene dar a Claude Code antes de modificar un repositorio real.
import { useEffect, useState } from "react";
import "./skeleton-demo.css";
type Article = {
id: number;
title: string;
description: string;
tag: string;
};
type LoadState = "loading" | "success" | "empty" | "error";
const demoArticles: Article[] = [
{
id: 1,
title: "Crear cambios de UI más seguros con Claude Code",
description: "Lee los componentes existentes y mejora solo la experiencia de carga.",
tag: "UX",
},
{
id: 2,
title: "Reservar espacio de imagen sin aumentar CLS",
description: "Fija alturas de medios, títulos y resúmenes antes de que lleguen los datos.",
tag: "Performance",
},
{
id: 3,
title: "Estados de carga accesibles",
description: "Combina aria-busy, mensajes status y comportamiento con menos movimiento.",
tag: "A11y",
},
];
function SkeletonLine({ width = "100%" }: { width?: string }) {
return <span className="sk-line" style={{ width }} aria-hidden="true" />;
}
function ArticleCardSkeleton() {
return (
<article className="article-card is-skeleton" aria-hidden="true">
<div className="sk-media" />
<div className="article-card__body">
<SkeletonLine width="46%" />
<SkeletonLine />
<SkeletonLine width="86%" />
<SkeletonLine width="32%" />
</div>
</article>
);
}
function ArticleCard({ article }: { article: Article }) {
return (
<article className="article-card">
<div className="article-card__media">{article.tag}</div>
<div className="article-card__body">
<p className="article-card__tag">{article.tag}</p>
<h2>{article.title}</h2>
<p>{article.description}</p>
</div>
</article>
);
}
export default function App() {
const [state, setState] = useState<LoadState>("loading");
const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => {
const timer = window.setTimeout(() => {
setArticles(demoArticles);
setState("success");
}, 1200);
return () => window.clearTimeout(timer);
}, []);
const reloadAs = (nextState: LoadState) => {
setState("loading");
setArticles([]);
window.setTimeout(() => {
setArticles(nextState === "success" ? demoArticles : []);
setState(nextState);
}, 700);
};
return (
<main className="demo-shell">
<div className="demo-toolbar" aria-label="Cambiar estado visible">
<button onClick={() => reloadAs("success")}>Éxito</button>
<button onClick={() => reloadAs("empty")}>Vacío</button>
<button onClick={() => reloadAs("error")}>Error</button>
</div>
<section
aria-busy={state === "loading"}
aria-describedby="article-list-status"
className="article-grid"
>
<p id="article-list-status" className="sr-only" role="status">
{state === "loading" ? "Cargando lista de artículos" : "Lista de artículos cargada"}
</p>
{state === "loading" &&
Array.from({ length: 3 }).map((_, index) => (
<ArticleCardSkeleton key={index} />
))}
{state === "success" &&
articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
{state === "empty" && (
<div className="state-panel">Todavía no hay artículos para mostrar.</div>
)}
{state === "error" && (
<div className="state-panel" role="alert">
No se pudo cargar la lista de artículos. Inténtalo más tarde.
</div>
)}
</section>
</main>
);
}
El detalle clave de accesibilidad es no anunciar el skeleton completo. Un lector de pantalla no necesita escuchar que hay tres líneas grises. En este ejemplo las tarjetas skeleton usanaria-hidden, mientras un únicorole="status" comunica el estado de la lista. La referencia útil es ARIA status role en MDN.
CSS para tamaño estable y movimiento moderado
Guarda este CSS comosrc/skeleton-demo.css. Lo importante es mantener parecidos elmin-height, el alto del área multimedia y el espaciado interno entre el estado cargando y el estado final. El shimmer es discreto y se detiene si el usuario prefiere menos movimiento.
:root {
color: #18212f;
background: #f6f7f9;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button {
min-height: 40px;
border: 1px solid #b8c2d6;
border-radius: 8px;
background: #ffffff;
color: #18212f;
padding: 0 14px;
font-weight: 700;
}
.demo-shell {
width: min(1040px, calc(100% - 32px));
margin: 40px auto;
}
.demo-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.article-card {
min-height: 316px;
overflow: hidden;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
}
.article-card__media,
.sk-media {
display: grid;
min-height: 148px;
place-items: center;
background: #dfe7f3;
color: #39506f;
font-weight: 800;
}
.article-card__body {
display: grid;
gap: 10px;
padding: 18px;
}
.article-card__tag {
color: #3b6b4f;
font-size: 0.875rem;
font-weight: 800;
}
.article-card h2 {
min-height: 56px;
margin: 0;
font-size: 1.16rem;
line-height: 1.45;
}
.article-card p {
margin: 0;
line-height: 1.7;
}
.sk-line,
.sk-media {
border-radius: 8px;
background: linear-gradient(90deg, #d9e0ea 25%, #edf1f7 37%, #d9e0ea 63%);
background-size: 240% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.sk-line {
display: block;
height: 16px;
}
.state-panel {
min-height: 180px;
display: grid;
place-items: center;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
padding: 24px;
text-align: center;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
@keyframes skeleton-shimmer {
from {
background-position: 120% 0;
}
to {
background-position: -120% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.sk-line,
.sk-media {
animation: none;
background: #d9e0ea;
}
}
Este CSS prioriza un layout estable por encima de un brillo dramático. Si las líneas skeleton tienen anchos demasiado aleatorios, el contenido final parece pertenecer a otro diseño. Es más práctico reservar un título de dos líneas, un resumen de dos líneas y un área multimedia predecible.
Comprobación mínima con Playwright
Si el proyecto ya usa Playwright, añade una prueba enfocada. No demuestra que el CLS real sea perfecto, pero detecta regresiones obvias antes de la revisión.
import { expect, test } from "@playwright/test";
test("article skeleton keeps a stable card area", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Cargando lista de artículos")).toBeAttached();
await expect(page.locator(".is-skeleton")).toHaveCount(3);
const firstBox = await page.locator(".article-card").first().boundingBox();
expect(firstBox?.height).toBeGreaterThan(280);
await page.getByRole("button", { name: "Error" }).click();
await expect(page.getByRole("alert")).toContainText("No se pudo cargar");
});
El CLS real también depende de imágenes, anuncios, fuentes, scripts de terceros, red y dispositivos. Usa esta prueba como alarma temprana y apóyate después en la guía de web.dev y datos de producción.
Errores comunes
El primer error es que el skeleton no coincida con el contenido final. Durante la carga se ve limpio, pero la tarjeta real se estira o se contrae. Reserva dimensiones de imagen conwidth, height oaspect-ratio, y reserva espacio para anuncios y CTA antes de que aparezcan.
El segundo error es mostrar skeleton para todas las peticiones rápidas. Si normalmente terminan en 100 ms, el skeleton puede crear parpadeo. En producción usa reglas como “mostrar después de 300 ms”, “solo en la primera carga” o “mantener datos anteriores durante el refresco”.
El tercer error es generar demasiado ruido de accesibilidad. Si cada tarjeta tiene su propiorole="status", la tecnología asistiva puede repetir mensajes. Mantén el mensaje vivo a nivel de lista y oculta las formas puramente visuales.
El cuarto error es olvidar la ruta de fallo. Si la API falla y el skeleton queda para siempre, el usuario cree que la página sigue funcionando. Diseña error, vacío y reintento como estados separados.
El quinto error es pedir a Claude Code que decida la prioridad de producto. Claude Code puede leer archivos y generar código, pero una persona decide qué CTA debe seguir visible, cuánto espacio de anuncio reservar y qué contenido es seguro mostrar primero.
Prompt de revisión para Claude Code
Después de implementar, cambia a modo revisión:
Review only the skeleton loading changes.
Check whether loaded content and skeleton content reserve similar space.
Check loading, success, empty, and error states.
Check reduced-motion behavior and ARIA announcements.
Point out any code that may increase CLS or create repeated screen reader messages.
Return findings with file names and exact lines.
Así Claude Code pasa de implementador a crítico. En sitios de contenido, anuncios, artículos relacionados, CTA e imágenes lazy comparten el mismo flujo visual. Usa la guía de CSS y estrategias de testing para separar comprobaciones visuales, lectura asistiva y regresión.
Relación con monetización
El skeleton loading también protege la ruta de ingresos. Si un CTA de consultoría, una tarjeta de producto, un formulario de newsletter o un anuncio aparece tarde y empuja el artículo, el lector pierde su posición o hace clic donde no quería. Eso afecta confianza y conversión.
Para trabajo individual, empieza con la cheatsheet gratuita de Claude Code y fija un proceso de revisión. Para equipos que quieran convertir skeleton loading, lazy images, Core Web Vitals y accesibilidad en un flujo común, usa formación y consultoría de Claude Code. Masa puede revisar el repositorio y convertir las mejoras en prompts, componentes y checks prácticos.
Resumen
Un buen skeleton loading no es un truco para esconder la espera. Reserva espacio parecido a la pantalla final, reduce incertidumbre y evita movimientos evidentes. Con Claude Code, entrega al mismo tiempo el componente objetivo, los estados, las dimensiones, los requisitos de accesibilidad y los comandos de verificación.
Al probar este flujo, lo más útil fue fijar primero la altura de medios y títulos, anunciar la carga a nivel de lista y detener la animación para usuarios con movimiento reducido. Cuando solo pulí el shimmer, los estados vacío y error quedaron para después y la revisión tardó más. Pide implementación y verificación juntas.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.