Skeleton
BetaReserva el lugar del contenido que está cargando, con su forma.
registry/ds/skeleton.tsximport { Skeleton } from "@/registry/ds/skeleton";
export default function SkeletonBasic() {
return (
<div role="status" aria-busy="true" aria-label="Cargando curso" className="flex w-80 flex-col gap-4 rounded-2xl border border-border p-5">
<Skeleton variant="rect" height={120} />
<div className="flex items-center gap-3">
<Skeleton variant="circle" width={40} height={40} />
<div className="flex flex-1 flex-col gap-2">
<Skeleton width="60%" className="text-md" />
<Skeleton width="40%" className="text-sm" />
</div>
</div>
<div className="flex flex-col gap-2 text-sm">
<Skeleton />
<Skeleton />
<Skeleton width="80%" />
</div>
</div>
);
}- Imita la forma y el tamaño del contenido real: así nada salta al llegar.
- Para listas, cards y perfiles. Para una acción puntual, Spinner.
- Máximo 3–5 filas repetidas: más no aporta.
- Si el contenido tarda más de ~10 s, mostrá un mensaje, no un skeleton eterno.
Igual que el contenido
El skeleton copia la grilla del contenido real: mismas alturas y anchos, para
que nada salte cuando llegan los datos. El contenedor lleva aria-busy.
"use client";
import { useState } from "react";
import { Avatar } from "@/registry/ds/avatar";
import { Chip } from "@/registry/ds/badge";
import { Skeleton } from "@/registry/ds/skeleton";
import { Switch } from "@/registry/ds/switch";
const docentes = [
{ nombre: "Julia Arce", materia: "Cálculo II", estado: "success" as const },
{ nombre: "Martín Ferro", materia: "Física I", estado: "warning" as const },
{ nombre: "Lucía Paz", materia: "Programación II", estado: "success" as const },
];
/** El skeleton copia la grilla del contenido real: mismas alturas y anchos, así nada salta cuando llegan los datos */
export default function SkeletonList() {
const [loaded, setLoaded] = useState(false);
return (
<div className="flex w-80 flex-col gap-4">
<Switch size="sm" label="Datos cargados" checked={loaded} onCheckedChange={setLoaded} />
<ul className="divide-y divide-border-subtle rounded-2xl border border-border bg-bg-elevated px-4" aria-busy={!loaded} aria-label="Docentes">
{docentes.map((d) => (
<li key={d.nombre} className="flex items-center gap-3 py-3">
{loaded ? (
<>
<Avatar size="sm" name={d.nombre} />
<div className="min-w-0 flex-1 text-sm">
<p className="truncate font-medium text-fg">{d.nombre}</p>
<p className="truncate text-xs text-fg-muted">{d.materia}</p>
</div>
<Chip size="sm" status={d.estado}>
{d.estado === "success" ? "Disponible" : "Ocupado"}
</Chip>
</>
) : (
<>
<Skeleton variant="circle" width={32} height={32} />
<div className="flex flex-1 flex-col gap-1.5">
<Skeleton width="55%" className="text-sm" />
<Skeleton width="35%" className="text-xs" />
</div>
<Skeleton variant="rect" width={72} height={22} className="rounded-full" />
</>
)}
</li>
))}
</ul>
</div>
);
}| Parte | Propiedad | Token |
|---|---|---|
| Hueso | Fondo, pulsa | bg/surface |
Radios: texto radius-sm, rectángulo radius-md, círculo radius-full.
El hueso de texto mide 1em de alto: heredá el tamaño con la clase del texto real.
Instalación
npx shadcn@latest add https://polen.cdc.cool/r/skeleton.jsonPlayground
Configurá el componente y copiá el snippet: se genera del mismo estado que renderiza el preview.
variant
<Skeleton width="240px" />| Prop | Tipo | Default | Descripción |
|---|---|---|---|
variant | "text" | "rect" | "circle" | "text" | Forma. |
width · height | string | number | — | Cualquier unidad CSS. El texto usa 1em de alto. |
Cada hueso es aria-hidden. El estado de carga lo anuncia el contenedor:
role="status", aria-busy="true" y un aria-label que diga qué se carga.
(Un aria-label sobre un <div> sin rol no vale: necesita el rol.)