Component · Empty States

Empty States

Pantallas o secciones que el usuario ve cuando no hay datos legítimos que mostrar — portafolio recién creado, historial sin transacciones, búsqueda sin resultados. No es un skeleton (cargando) ni un error state (falló la carga). Es la primera impresión real del producto después del onboarding.

Estado legítimo de "sin datos" ≠ error · ≠ skeleton Tono sobrio · sin promesas
Anatomy

Estructura vertical

Cinco elementos centrados, todos opcionales menos título y descripción. La ilustración/ícono no es un héroe — es orientación. Sin color de marca, sin entusiasmo gráfico.

Tu portafolio está vacío

Compra tu primer criptoactivo para empezar.

1

Ícono · 48dp dentro de chip 96dp

Lucide outline, color --color-neutral-300 (light) / --color-neutral-600 (dark). Sin color de marca — el empty state orienta, no celebra. En empties legítimos de pantalla completa el chip se reemplaza por spot art (ver abajo).

2

Título · 16dp / 600

Body-large semibold. Una línea ideal, máx. dos. Directo y sin drama: "Tu portafolio está vacío", no "¡Tu historial te espera!".

3

Descripción · 13dp · muted

1–2 líneas, color --color-fg-muted. Explica por qué está vacío y qué puede hacer el usuario.

4

CTA primario · opcional

Botón rounded primary, 44dp de alto. La acción que llenaría esta sección. Si la acción no es obvia, no forzar un CTA.

5

CTA secundario · texto link

Alternativa sutil. Útil cuando hay un atajo opcional ("Limpiar filtros" en sin-resultados, "Comprar ETH" en el detalle de un activo).

6

Spacing vertical

Ícono → título: 16dp. Título → descripción: 8dp. Descripción → CTA: 24dp. Centrado vertical y horizontal en el área disponible.

Illustration system · decisión William 2026-07-18

Spot art de línea + dos tintas

Un glifo Lucide de 32 px en una pantalla de 844 no sostiene el espacio: los empty states legítimos de pantalla completa se sienten muertos. Se adopta un sistema de ilustración propio —outline + dos tintas, en la dirección que el README recomendó— para llenar ese espacio sin celebrar. El arte ilustra el objeto vacío (un recibo, una bandeja, una tarjeta, un sobre), nunca el resultado deseado. Sigue vigente la regla: la ilustración no es un héroe, es orientación; usa la paleta, pero no festeja.

Aún no tienes transacciones

Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí.

1

Trazo · 1.5px · primary-500

Terminaciones redondeadas, misma familia visual que Lucide para que no se lea como un cuerpo extraño. En dark el trazo sube a primary-300 por contraste.

2

Relleno plano · segunda tinta

primary-100 (light) / primary-500 @ ~30% (dark). Planos, sin degradados.

3

Apoyo neutro

Un elemento de apoyo (línea de superficie, llave) en neutral-300 / neutral-600. Máximo 3 tintas en total.

4

Caja 140×140

Óptica centrada, aire interno generoso. Reemplaza el chip Ø96 en los empties que llevan arte.

5

Sin volumen

Sin degradados, sin sombras, sin texturas, sin 3D, sin perspectiva. Estático: sin Lottie, sin GIF, sin loop (motion §1.5).

6

Sin figuras humanas

Sin mascotas, sin rostros. El arte es sereno, no festivo.

Trazo
primary-500 · 1.5px
Relleno
primary-100 / 500 @30% dark
Apoyo
neutral-300 / 600 dark
Dónde SÍ aplica
  • Empty states legítimos y de pantalla completa.El usuario aún no tiene datos y la pantalla entera está a su disposición: historial, PQRS, wallets, cuentas, notificaciones.
  • El arte ilustra el objeto vacío.Un recibo, una bandeja, una tarjeta con llave, una fachada, una campana en reposo.
Dónde NO aplica
  • Empties in-card / inline.Balance card, Recientes, sección de activo: el arte competiría con la tarjeta que lo contiene. Conservan ícono-en-chip Ø56.
  • Empties circunstanciales (sin resultados de filtro).Premiar con arte un callejón sin salida invita a quedarse. Conserva search-x en chip Ø96.
  • Estados de error.Conservan cloud-off en el chip de error. No son un vacío legítimo.
  • Estados deseables sin nada que llenar."Solo esta sesión" es positivo, no una carencia: no lleva arte.

Línea roja gráfica — no negociable

Cero caricatura cripto-bro: nada de cohetes, monedas con alas ni gráficas subiendo (screens-welcome). Elevado aquí a regla del sistema:

  • Prohibido representar crecimiento, rendimiento o ganancia: barras ascendentes, flechas al alza, líneas de tendencia subiendo, monedas apiladas, billetes, cofres, signos $ como motivo.
  • Prohibido el símbolo de dólar en cualquier pieza. MiCapi es COP.
  • El arte ilustra el objeto vacío, nunca el resultado deseado (ganancia, riqueza, éxito).

Ejemplo negativo concreto (rechazado 2026-07-18): la animación "Revenue" de LottieFiles —persona en un puf con laptop, ventana con gráfico de barras subiendo y moneda de US$—. Insinúa rentabilidad en la pantalla de un usuario que aún no ha operado y contradice el modelo COP no-wallet. Es exactamente lo que no debe producirse.

Cada pieza vive como SVG suelto en assets/illustrations/empty-*.svg (tintas por variable CSS / currentColor: un solo archivo sirve light y dark) e inline en cada preview. En Flutter, el slot illustration del widget EmptyState reemplaza al icon en las variantes de pantalla completa legítimas.

Context variants

Siete contextos

Cada variante tiene título y copy que respetan el tono. Las de pantalla completa legítimas (b · e · f · g) llevan spot art; las circunstanciales (c) y las inline (d) conservan ícono-en-chip. Todas a ancho mobile real (~328dp).

aPortafolio vacío
Tu portafolio está vacío

Compra tu primer criptoactivo para empezar.

DASHBOARD Dashboard de usuario nuevo, debajo del balance card vacío. Bitcoin como default — el más conocido, la entrada más intuitiva.
bHistorial vacío
Aún no tienes transacciones

Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí.

HISTORIAL Pestaña Historial sin operaciones. CTA primario "Hacer mi primera compra" — copy canónico de screens-historial.
cSin resultados de filtro
Nada con estos filtros

No hay operaciones que coincidan. Prueba con otro rango o limpia los filtros.

FILTROS Lista filtrada sin matches. CTA outlined porque la acción es reversible (no destructiva).
dSección de activo
Sin operaciones con ETH

Las operaciones que hagas con ETH aparecerán aquí.

INLINE Dentro de una sección del detalle de ETH. Variante compact — ícono 28dp, sin chip grande.
eSin notificaciones
No tienes notificaciones

Te avisaremos cuando haya algo importante sobre tu cuenta.

CONTENT-ONLY Pantalla de notificaciones vacía. Sin CTA — el usuario llega aquí esperando ver algo, no a actuar.
fSin wallets externas
No tienes wallets registradas

Registra una wallet externa para poder retirar tus criptoactivos.

FLUJO DE RETIRO Cuando el usuario intenta retirar cripto sin wallets registradas. CTA primario porque la acción es necesaria.
gSin cuenta bancaria
No tienes cuentas bancarias registradas

Registra una cuenta bancaria a tu nombre para recibir pesos colombianos.

FLUJO DE RETIRO "A tu nombre" refuerza la regla regulatoria de titular = remitente, sin tono legalista.
Size variants

Full-page vs. inline

Misma anatomía, dos escalas según el espacio disponible. La inline vive dentro de una sección; la full-page ocupa todo el body entre app bar y bottom nav.

FULL-PAGEOcupa todo el body

Centrado vertical y horizontal en el área entre top app bar y bottom nav. Para pantallas completas vacías: historial, notificaciones, PQRS. Con spot art cuando es un empty legítimo; con ícono 48dp en chip 96dp cuando no lleva arte (p. ej. sin resultados).

Aún no tienes transacciones

Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí.

Arte
Ilustración 140dp (outline + 2 tintas); sin arte → ícono 48dp en chip 96dp
Título
16dp / 600 · max 1–2 líneas
Descripción
13dp · max-width 280dp
Centrado
MainAxisAlignment.center dentro de Expanded

INLINEDentro de una sección

Más compacto: ícono pequeño, padding reducido. Centrado horizontal pero no vertical — va donde irían los datos.

Sin operaciones con ETH

Las operaciones que hagas con ETH aparecerán aquí.

Ícono
28dp dentro de chip 56dp
Título
14dp / 600
Descripción
12.5dp · max-width 240dp
CTA
Preferir es-link sobre botón — menos peso visual
Empty vs. error vs. loading

Tres estados, tres tratamientos

Confundirlos rompe la confianza. El mismo espacio puede verse vacío legítimamente, con error de red, o cargando — el tratamiento visual y de copy debe dejarlos sin ambigüedad.

EMPTY

Sin datos · estado legítimo

Aún no tienes transacciones

Cuando operes, tu historial aparecerá aquí.

CuándoNo hay datos porque el usuario aún no los generó
ÍconoOutline neutro · color muted
CTAAcción positiva ("Comprar Bitcoin")
TonoOrientación · invitación sutil
ERROR

Falló la carga

No pudimos cargar tus transacciones
Revisa tu conexión e intenta de nuevo.
CuándoLa petición al backend falló — red, timeout, 500
ÍconoColor error-bg · ícono neutro (no alarma)
CTA"Reintentar" outlined
TonoProblema técnico · solución inmediata
LOADING

Datos cargando

CuándoEsperando respuesta del backend
VisualShimmer 1500ms linear · ref. foundations-motion §3d
CTAN/A
TonoExpectativa · transición a real con crossfade 200ms
Copy

Reglas de tono

El copy de empty states es donde más fácil se desliza el tono de "neobank entusiasta". MiCapi no es eso. Las reglas son explícitas porque las trampas son comunes.

Sí

  • "Aún no tienes transacciones."Directo. Sin drama. Estado de hecho.
  • "Compra tu primer criptoactivo para empezar."Invitación con acción concreta — no promesa.
  • "Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí."Explica el "por qué está vacío" sin culpa.
  • CTA: "Comprar Bitcoin"Describe la acción, no el resultado.
  • "Registra una cuenta bancaria a tu nombre…"Refuerza la regla regulatoria de manera natural.

No

  • "¡Tu historial te está esperando!"Drama vacío. Antropomorfiza un estado de UI.
  • "¡No te pierdas la oportunidad!"Urgencia falsa. Insinúa pérdida.
  • "Empieza a hacer crecer tu dinero"Promesa de rentabilidad. Prohibido por compliance — los criptoactivos son volátiles.
  • CTA: "¡Empieza a invertir!"Resultado emocional, no acción descriptiva.
  • "Haz que tu plata trabaje para ti"Tropos de fintech agresivo. Fuera del DS de MiCapi.
Dark mode

Claro y oscuro

El ícono y los textos cambian de color. El CTA primario mantiene --color-primary-500 en ambos modos. Sin cambios estructurales.

LightDEFAULT
Tu portafolio está vacío

Compra tu primer criptoactivo.

Aún no tienes transacciones

Cuando operes, tu historial aparecerá aquí.

DarkDARK MODE
Tu portafolio está vacío

Compra tu primer criptoactivo.

Aún no tienes transacciones

Cuando operes, tu historial aparecerá aquí.

In context

Dashboard vacío + Historial vacío

Dos pantallas reales. En el dashboard, el balance card vacío arriba (variante del DS) + empty state portafolio debajo. En el historial, full-page con bottom nav presente.

DASHBOARD · USUARIO NUEVO
9:41
Valor estimado en COP
$0COP
Tu portafolio está vacío

Compra tu primer criptoactivo para empezar.

Inicio
Mercado
Historial
Perfil
HISTORIAL · FULL-PAGE
9:41
Historial
Aún no tienes transacciones

Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí.

Inicio
Mercado
Historial
Perfil

Observaciones

1

El balance card vacío mantiene su forma (variante del DS). El empty state vive debajo, no lo reemplaza.

2

El bottom nav siempre presente, incluso con la pantalla vacía. El usuario sigue pudiendo navegar — la sección vacía no lo atrapa.

3

Historial usa el CTA primario "Hacer mi primera compra" — copy canónico de screens-historial, verbatim en la app. Es la única acción de la pantalla vacía.

4

"Compra tu primer Bitcoin" en el dashboard porque BTC es el activo más conocido. En el detalle de un activo específico, el CTA cambia ("Comprar ETH" en ETH detail).

5

El usuario que llega aquí acaba de completar KYC. Es el primer momento real con el producto — el tono define si el producto se siente sobrio y profesional, o vacío y frío.

Implementation

Notas de implementación Flutter

Widget reutilizable

Widget canónico

Crear EmptyState con parámetros: icon, title, description, primaryAction, secondaryAction, size: EmptyStateSize.fullPage | .inline.

Centrado full-page

Center(child: Column(mainAxisSize: MainAxisSize.min, ...)) dentro de Expanded. El Column con min agrupa los hijos al centro.

Ícono

Icon(LucideIcons.wallet, size: 48, color: colorScheme.outline). El chip detrás es un Container de 96dp con shape: BoxShape.circle.

Estado vacío vs cargando

Switch/case sobre el estado del provider: loading → Skeleton, error → ErrorState, data.isEmpty → EmptyState, default → la lista.

// Componente reutilizable
class EmptyState extends StatelessWidget {
  final IconData icon;
  final String title;
  final String description;
  final Widget? primaryAction;
  final Widget? secondaryAction;
  final EmptyStateSize size;

  const EmptyState({...});

  Widget build(BuildContext context) {
    final isFullPage = size == EmptyStateSize.fullPage;
    final iconSize = isFullPage ? 48.0 : 28.0;
    final chipSize = isFullPage ? 96.0 : 56.0;

    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 24, vertical: isFullPage ? 32 : 16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Container(
            width: chipSize, height: chipSize,
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceVariant,
              shape: BoxShape.circle,
            ),
            child: Icon(icon, size: iconSize, color: theme.colorScheme.outline),
          ),
          const SizedBox(height: 16),
          Text(title, style: theme.textTheme.titleMedium),
          const SizedBox(height: 8),
          Text(description, style: theme.textTheme.bodyMedium, textAlign: TextAlign.center),
          if (primaryAction != null) ...[
            const SizedBox(height: 24),
            primaryAction!,
          ],
          if (secondaryAction != null) ...[
            const SizedBox(height: 4),
            secondaryAction!,
          ],
        ],
      ),
    );
  }
}

// Builder pattern para distinguir estados:
switch (state) {
  Loading() => TxListSkeleton(),
  Error(error: final e) => ErrorStateView(onRetry: refresh),
  Loaded(items: final items) when items.isEmpty => EmptyState(
    icon: LucideIcons.receipt,
    title: 'Aún no tienes transacciones',
    description: 'Cuando compres, vendas o intercambies cripto, todo tu historial aparecerá aquí.',
    primaryAction: FilledButton(onPressed: ..., child: Text('Hacer mi primera compra')),
  ),
  Loaded(items: final items) => TxList(items: items),
};