Ir al contenido

Componentes · Ola 3 · Estados

Empty state

Explica por qué no hay contenido y qué hacer para que lo haya. Un ícono, un título, una descripción y las acciones. La variante de error lleva el ícono de estado y la compacta entra en una tarjeta o una tabla.

import { EmptyState } from '@archeblack/ui';

Ejemplos

Sin contenido todavía

El vacío de una página: el ícono en su marco, la descripción y la acción que lo llena.

Todavía no hay proyectos

Un proyecto agrupa tus despliegues, dominios y variables de entorno. Crea el primero para publicar tu sitio.
Svelte
<script lang="ts">
	import { Button, EmptyState } from '@archeblack/ui';
	import { IconFolder, IconPlus } from '@archeblack/ui/icons';
</script>

<EmptyState icon={IconFolder} title="Todavía no hay proyectos">
	Un proyecto agrupa tus despliegues, dominios y variables de entorno. Crea el primero para publicar
	tu sitio.
	{#snippet actions()}
		<Button variant="primary" iconStart={IconPlus}>Crear proyecto</Button>
		<Button variant="ghost">Ver la guía</Button>
	{/snippet}
</EmptyState>

Sin resultados

Una búsqueda o un filtro que no encontró nada: la acción los deshace.

Sin resultados para «portal»

Prueba con otra palabra o quita algún filtro.
Svelte
<script lang="ts">
	import { Button, EmptyState } from '@archeblack/ui';
	import { IconSearch } from '@archeblack/ui/icons';
</script>

<EmptyState icon={IconSearch} title="Sin resultados para «portal»">
	Prueba con otra palabra o quita algún filtro.
	{#snippet actions()}
		<Button variant="secondary">Quitar los filtros</Button>
	{/snippet}
</EmptyState>

Error

variant="danger": el ícono de estado, nombrado, y la acción de reintentar.

No pudimos cargar los despliegues

El servidor no respondió a tiempo. Tus despliegues no cambiaron.
Svelte
<script lang="ts">
	import { Button, EmptyState } from '@archeblack/ui';
	import { IconRefresh } from '@archeblack/ui/icons';
</script>

<!-- El ícono de estado se nombra («Error») y se lee antes del título. -->
<EmptyState variant="danger" title="No pudimos cargar los despliegues">
	El servidor no respondió a tiempo. Tus despliegues no cambiaron.
	{#snippet actions()}
		<Button variant="secondary" iconStart={IconRefresh}>Reintentar</Button>
	{/snippet}
</EmptyState>

Compacto en una tarjeta

size="sm": sin marco, con el ícono md y menos aire.

Despliegues recientes

Sin despliegues esta semana

El próximo push a main despliega solo.
Svelte
<script lang="ts">
	import { Button, Card, EmptyState } from '@archeblack/ui';
	import { IconRocket } from '@archeblack/ui/icons';
</script>

<!-- size="sm": sin marco en el ícono y con menos aire, para una tarjeta o una celda de tabla. -->
<Card style="width: 100%; max-width: 26rem">
	{#snippet header()}
		<h4>Despliegues recientes</h4>
	{/snippet}
	<EmptyState size="sm" icon={IconRocket} title="Sin despliegues esta semana">
		El próximo push a main despliega solo.
		{#snippet actions()}
			<Button variant="ghost" size="sm">Desplegar ahora</Button>
		{/snippet}
	</EmptyState>
</Card>

Error compacto

La variante de error también en talla sm, sin descripción cuando el título alcanza.

Uso del mes

No pudimos cargar el uso

Svelte
<script lang="ts">
	import { Button, Card, EmptyState } from '@archeblack/ui';
</script>

<Card style="width: 100%; max-width: 26rem">
	{#snippet header()}
		<h4>Uso del mes</h4>
	{/snippet}
	<EmptyState size="sm" variant="danger" title="No pudimos cargar el uso">
		{#snippet actions()}
			<Button variant="ghost" size="sm">Reintentar</Button>
		{/snippet}
	</EmptyState>
</Card>

Anunciar el vacío

Una búsqueda que no encuentra nada: una región status que ya estaba montada lo anuncia; el vacío no lleva rol. Borrar la búsqueda devuelve el foco al campo.

Sin resultados para «portal web»

Sin resultados para «portal web»

Prueba con otra palabra.
Svelte
<script lang="ts">
	import { Button, EmptyState, Input } from '@archeblack/ui';
	import { IconSearch } from '@archeblack/ui/icons';

	const projects = ['Portal de clientes', 'Panel interno', 'Tienda', 'Documentación'];

	let query = $state('portal web');
	let search = $state<HTMLInputElement | null>(null);

	/** Al borrar, el botón desaparece con el vacío: el foco vuelve al campo, no se pierde. */
	function clear() {
		query = '';
		search?.focus();
	}

	const found = $derived(
		projects.filter((project) => project.toLowerCase().includes(query.trim().toLowerCase()))
	);
	/** Lo que dice la región status: vacía sin búsqueda, la cantidad o el vacío con búsqueda. */
	const announcement = $derived.by(() => {
		if (!query.trim()) return '';
		if (found.length === 0) return `Sin resultados para «${query.trim()}»`;
		return found.length === 1 ? '1 proyecto' : `${found.length} proyectos`;
	});
</script>

<div style="display: grid; gap: var(--arche-spacing-4); width: min(100%, 24rem)">
	<Input
		type="search"
		icon={IconSearch}
		bind:value={query}
		bind:ref={search}
		placeholder="Buscar proyectos"
		aria-label="Buscar proyectos"
	/>
	<!--
		La región status ya está montada (vacía) antes de que aparezca el vacío: los lectores de
		pantalla anuncian lo que cambia dentro de ella. El EmptyState entra y sale con {#if} y no
		lleva rol propio: montado junto con su rol, NVDA y JAWS no lo leerían.
	-->
	<p role="status" class="visually-hidden">{announcement}</p>
	{#if found.length > 0}
		<ul style="display: grid; gap: var(--arche-spacing-2); margin: 0; padding: 0; list-style: none">
			{#each found as project (project)}
				<li style="color: var(--arche-color-text-strong)">{project}</li>
			{/each}
		</ul>
	{:else}
		<EmptyState icon={IconSearch} size="sm" title="Sin resultados para «{query.trim()}»">
			Prueba con otra palabra.
			{#snippet actions()}
				<Button variant="secondary" size="sm" onclick={clear}>Borrar la búsqueda</Button>
			{/snippet}
		</EmptyState>
	{/if}
</div>

Props

Props de EmptyState
PropDescripción
title string Qué pasa, en una frase corta: «Todavía no hay proyectos», «No pudimos cargar los despliegues».
children? Snippet La descripción, en text-muted: por qué está vacío y qué se puede hacer.
actions? Snippet Acciones debajo de la descripción: uno o dos Button, centrados.
icon? IconGlyph Un ícono de @archeblack/ui/icons, dibujado con Icon (lg en md, md en sm). En neutral es opcional y decorativo; en danger reemplaza al de la variante (circle-x), no lo quita.
variant? 'neutral' | 'danger' Por defecto 'neutral'neutral: todavía no hay nada. danger: no se pudo cargar; el marco y el ícono toman el estado y el ícono se nombra.
statusLabel? string Por defecto 'Error'Solo en danger: el nombre del ícono de estado para el lector de pantalla, que se lee antes del título.
size? 'sm' | 'md' Por defecto 'md'md para una página o una sección, con el ícono en su marco. sm, la compacta, para una tarjeta, una celda de tabla o un menú.
headingLevel? 2 | 3 | 4 | 5 | 6 Con un nivel, el título es un encabezado (h2–h6); sin nivel, un párrafo. Úsalo cuando el vacío ocupa una sección entera.
class? ClassValue Clases del producto; se suman a arche-empty-state.
...rest HTMLAttributes<HTMLDivElement> Van a la raíz: id, style y data-*. Para anunciar el vacío no le pongas role: usa una región viva que ya esté en la página (ver «Accesibilidad»).

Accesibilidad

  • El título es un párrafo por defecto, porque el vacío suele vivir dentro de una sección que ya tiene encabezado. Si ocupa una sección entera, headingLevel lo vuelve un h2–h6 que aparece en la lista de encabezados.
  • En neutral el ícono es decorativo. En danger el ícono es el estado: se nombra con statusLabel («Error») y el lector lo dice antes del título. El estado nunca depende solo del color: lo dicen el ícono, su nombre y el título.
  • El componente no se anuncia solo. Si el vacío o el error aparecen después de una carga o una búsqueda, anúncialos con una región viva que ya esté montada en la página antes de que aparezcan: un role="status" vacío (visible u oculto a la vista) donde escribes un texto corto, «Sin resultados» o «No pudimos cargar los despliegues». Suele ser la misma región status que dijo «Cargando» (ver Skeleton). No pongas role="status" ni role="alert" en el propio EmptyState: se monta con {#if} junto con su rol, y NVDA y JAWS solo anuncian los cambios de una región que ya estaba en el árbol (lo que aprendió Alert con live). Es lo que hace el ejemplo «Anunciar el vacío».
  • Para un error que bloquea y no puede esperar, la región ya montada puede ser role="alert", o un Alert con live="assertive" junto al vacío: ese sí se monta vacío y escribe el texto después.
  • Las acciones son Button de Arche con su propio foco. Si el vacío aparece después de que la persona hizo algo (una búsqueda, un filtro), no muevas el foco: quedó en el control que usó. Si una acción hace desaparecer el vacío (y con él su botón), devuelve el foco a un lugar que siga en la página, como el campo de búsqueda.
  • El título va en text-strong y la descripción en text-muted, los dos en AA sobre todas las capas. En colores forzados, el marco del ícono se dibuja con su borde.

Qué evitar

  • Un vacío sin salida: «No hay datos» y nada más. Una descripción que diga por qué y una acción para salir (crear, quitar filtros, reintentar).
  • Mostrar el vacío mientras los datos todavía cargan. Un Skeleton o un Spinner hasta saber si hay algo.
  • Un error de carga con el aspecto de un vacío neutro («No hay despliegues» cuando falló la red). variant="danger", un título que diga que no se pudo cargar y la acción de reintentar.
  • role="status" o role="alert" en el EmptyState que aparece con {#if}: se monta con el texto y no se anuncia. Una región status que ya estaba en la página, vacía, donde escribes lo que pasó.
  • La talla md dentro de una fila de tabla o una tarjeta chica. size="sm", sin marco y con menos aire.
  • Ilustraciones grandes o varios íconos. Un solo ícono de Tabler que diga qué falta.