Componentes · Ola 3 · Estados
Skeleton
Ocupa el lugar del contenido que todavía no llegó, con su forma. Un brillo tenue lo recorre de izquierda a derecha mientras el contenido llega; con movimiento reducido se queda quieto.
import { Skeleton } from '@archeblack/ui';
Ejemplos
Formas
text (una línea del texto actual), rect y circle en las tallas de Avatar.
<script lang="ts">
import { Skeleton } from '@archeblack/ui';
</script>
<div style="display: grid; gap: var(--arche-spacing-6); width: min(100%, 24rem)">
<!-- text: cada bloque mide una línea del texto actual. -->
<div>
<Skeleton width="70%" />
<Skeleton width="92%" />
<Skeleton width="48%" />
</div>
<!-- rect: una imagen, un gráfico. -->
<Skeleton shape="rect" height="6rem" />
<!-- circle: las tallas de Avatar. -->
<div style="display: flex; align-items: center; gap: var(--arche-spacing-3)">
<Skeleton shape="circle" size="sm" />
<Skeleton shape="circle" size="md" />
<Skeleton shape="circle" size="lg" />
</div>
</div> Una lista que carga
El contenedor lleva aria-busy y una región status dice qué carga; los bloques tienen la forma de cada fila.
Equipo
Cargando el equipo
<script lang="ts">
import { Avatar, Button, Skeleton } from '@archeblack/ui';
const team = [
{ name: 'Jimmy Mora', role: 'Administrador' },
{ name: 'Ada Lovelace', role: 'Desarrolladora' },
{ name: 'Grace Hopper', role: 'Revisora' }
];
let loading = $state(true);
let timer: ReturnType<typeof setTimeout> | undefined;
function reload() {
loading = true;
clearTimeout(timer);
timer = setTimeout(() => (loading = false), 2500);
}
$effect(() => {
reload();
return () => clearTimeout(timer);
});
</script>
<div
style="display: grid; justify-items: start; gap: var(--arche-spacing-4); width: min(100%, 24rem)"
>
<h4 id="team-title">Equipo</h4>
<!--
El contenedor que carga lleva aria-busy mientras espera. Los bloques están ocultos para el
lector; el texto «Cargando el equipo» de la región status dice qué pasa.
-->
<div aria-labelledby="team-title" aria-busy={loading} role="region" style="width: 100%">
<p role="status" class="visually-hidden">{loading ? 'Cargando el equipo' : ''}</p>
<ul style="display: grid; gap: var(--arche-spacing-3); margin: 0; padding: 0; list-style: none">
{#each team as person (person.name)}
<li style="display: flex; align-items: center; gap: var(--arche-spacing-3)">
{#if loading}
<Skeleton shape="circle" />
<div style="flex: 1">
<Skeleton width="60%" />
<Skeleton width="35%" style="font-size: var(--arche-font-size-xs)" />
</div>
{:else}
<Avatar name={person.name} alt="" />
<div style="flex: 1">
<p style="margin: 0; color: var(--arche-color-text-strong)">{person.name}</p>
<p
style="margin: 0; font-size: var(--arche-font-size-xs); color: var(--arche-color-text-muted)"
>
{person.role}
</p>
</div>
{/if}
</li>
{/each}
</ul>
</div>
<Button variant="secondary" size="sm" onclick={reload}>Volver a cargar</Button>
</div> Sobre las capas
Los bloques son velos translúcidos: se ven un paso más claros sobre una Card y dentro de una superposición.
<script lang="ts">
import { Card, Skeleton } from '@archeblack/ui';
</script>
<!-- Sobre cualquier capa, el bloque queda un paso más claro que lo que tiene debajo. -->
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--arche-spacing-4); width: 100%"
aria-hidden="true"
>
<Card>
<div style="display: grid; gap: var(--arche-spacing-2)">
<Skeleton shape="rect" height="4rem" />
<div>
<Skeleton width="80%" />
<Skeleton width="50%" />
</div>
</div>
</Card>
<div
style="padding: var(--arche-spacing-4); background: var(--arche-color-surface-overlay); border: 1px solid var(--arche-color-border); border-radius: var(--arche-radius-lg); display: grid; gap: var(--arche-spacing-2)"
>
<Skeleton shape="rect" height="4rem" />
<div>
<Skeleton width="80%" />
<Skeleton width="50%" />
</div>
</div>
</div> Props
| Prop | Descripción |
|---|---|
shape? 'text' | 'rect' | 'circle' Por defecto 'text' | text: una línea del texto actual (una barra de 0,75 rem centrada en el alto de línea). rect: una caja. circle: un avatar. |
size? 'sm' | 'md' | 'lg' Por defecto 'md' | Solo con circle: 24, 32 o 40 px, las tallas de Avatar. |
width? string | Ancho en CSS (70%, 12rem). Por defecto, todo el ancho en text y rect, y el de la talla en circle. |
height? string | Alto en CSS, para rect (por defecto 4 rem) y circle. En text no se usa: el alto sigue a la línea (cambia el font-size si la línea es más chica). |
class? ClassValue | Clases del producto; se suman a arche-skeleton. |
...rest HTMLAttributes<HTMLDivElement> | Van al bloque: id, style y data-*. No lleva contenido ni rol. |
Accesibilidad
- Cada bloque es decorativo (
aria-hidden="true"): un lector de pantalla no tiene nada que leer en una forma vacía. - Guía para el contenedor: mientras carga, pon
aria-busy="true"en el elemento que se va a llenar (la lista, la tabla, la región) y quítalo cuando llegue el contenido: los lectores que lo soportan esperan a que termine para leer los cambios. Como el soporte dearia-busyes desparejo, suma un texto en una regiónstatusque ya esté en la página («Cargando el equipo»), visible u oculto a la vista, o un Spinner conhideLabel. Es lo que hace el ejemplo «Una lista que carga». - Si la carga falla, reemplaza los bloques por un Empty state con
variant="danger"y una acción para reintentar. - Con movimiento reducido, el brillo deja de desplazarse y el bloque queda quieto, en un solo tono.
- En colores forzados, los fondos desaparecen: el bloque se pinta en
GrayText, quieto, para que se vea que ahí falta contenido.
Qué evitar
- Bloques que no se parecen al contenido que llega (tres líneas donde llega una tabla). La misma forma y el mismo tamaño: así la página no salta cuando llega el contenido.
- Un Skeleton para una espera de menos de medio segundo. Nada: el parpadeo de los bloques molesta más que la espera.
- Bloques sin ningún aviso para el lector de pantalla.
aria-busyen el contenedor y un texto en una regiónstatus. - Un Skeleton para una tarea larga cuyo avance se conoce (una subida, una importación). Un Progress.