Skeleton
Grey shapes shimmer where content will land. First paint placeholders for cards, text, and avatars.
Variants: defaultcircletext (default: default)
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Card placeholder while loading:
```tsx import { Skeleton } from '@/components/atoms/Skeleton';
<div aria-busy="true" aria-live="polite"> <Skeleton variant="default" /> <Skeleton variant="default" /> </div> ```
## Accesibilidad
- Mark the loading region with `aria-busy="true"` (and optionally `aria-live`) on the container — skeleton shapes alone are not announced as “loading”. - Replace skeletons with real content; do not leave them permanently on screen.
### Correcto
- aria-hidden='true' automatico — screen readers ignoran los skeletons - prefers-reduced-motion desactiva el pulso (queda bloque estatico) - Replicar la estructura del contenido real — mismos gaps, heights, widths - Reemplazar skeleton por contenido real sin cambio de layout (CLS = 0)
### Evitar
- No usar Skeleton como indicador de error — es solo para loading - No dejar skeletons permanentemente — siempre debe haber timeout o fallback - No animar con duraciones menores a 500ms — el pulso se vuelve molesto
## Cuándo no usar
- Indeterminate wait without layout reservation → `Spinner`. - Avatar-specific loading face → `Avatar skeleton` prop when that control owns the slot.
## Criterio de uso
- Usa Skeleton cuando conoces la forma del contenido que llegará y quieres reservar su espacio desde el primer paint. - Haz que la geometría del placeholder se parezca al contenido real; una barra genérica no explica una card compleja y aumenta el cambio visual. - Retíralo cuando el contenido esté listo y conserva `aria-busy` en el contenedor durante la transición.
## Gotchas
- Skeleton es decorativo: el estado de carga debe comunicarse en el contenedor, no con una colección de formas sin nombre. - No lo uses como animación permanente ni para bloquear acciones que ya podrían estar disponibles. - **Nota**: variant='text' tiene height: 1em por defecto — escala con el font-size del contenedor. Solo necesitas width. - **Nota**: 10 lineas de CSS. El componente mas ligero del DS. El consumidor controla todo el dimensionamiento via inline style. - **Nota**: No requiere GSAP. Es puro CSS @keyframes. La duracion (1s) es mas rapida que el Avatar skeleton (1.5s) — el pulso se siente mas energico.
Uso
import { Skeleton } from '@/components/atoms/Skeleton';
<div aria-busy="true">
<Skeleton variant="circle" className="w-10 h-10" />
<Skeleton variant="text" className="w-40" />
</div>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| variant | select | default | `default`, `circle`, `text` | Placeholder geometry. | default for blocks/cards; circle for avatars; text for line stacks. Default: default. |
Gotchas
- a11y
Mark the loading region with aria-busy on the container; skeleton itself is decorative.
- layout
Size via className/width styles to match the real component footprint — avoid layout shift when content swaps in.
Anatomía CSS
<div class="skeleton"> </div>
| Clase | Propósito |
|---|---|
skeleton | root |
skeleton--circle | modifier |
skeleton--text | modifier |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #e5e5e5 | border |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
@keyframes skeleton-pulse | | |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Skeleton
Loading placeholder. Pulse animation on muted background.
Consumer sets width/height/border-radius via style or className.
Parts: .skeleton
------------------------------------------------------------------------- */
.skeleton {
display: block;
background-color: #e5e5e5;
border-radius: 8px;
animation: skeleton-pulse 1000ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ---- Shape helpers ---- */
.skeleton--circle {
border-radius: 9999px;
}
.skeleton--text {
height: 1em;
border-radius: 4px;
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
}
}
Codigo fuente
import { type HTMLAttributes } from 'react'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export type SkeletonProps = { variant?: 'default' | 'circle' | 'text'; className?: string; } & HTMLAttributes<HTMLDivElement>; export function Skeleton({ variant = 'default', className, ...props }: SkeletonProps) { return ( <div aria-hidden="true" className={cn( 'skeleton', variant !== 'default' && `skeleton--${variant}`, className, )} {...props} /> ); }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Skeleton) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
@mediaon(prefers-reduced-motion: reduce)— not a Designer breakpoint — moved to head Custom Code block
Tras pegar: Create component → nombre Atom / Skeleton → Publish.