Empty
A calm blank state with icon, message, and optional CTA. Zero results and first-run screens.
Variants: defaultoutlinefilled (default: default)
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Zero results with recovery CTA:
```tsx import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, } from '@/components/atoms/Empty';
<Empty variant="outline"> <EmptyHeader> <EmptyMedia variant="icon">{/* icon */}</EmptyMedia> <EmptyTitle>No projects</EmptyTitle> <EmptyDescription>Create one to get started.</EmptyDescription> </EmptyHeader> <EmptyContent><button type="button">New project</button></EmptyContent> </Empty> ```
## Accesibilidad
- Title should explain the empty state; put recovery actions in `EmptyContent`. - Decorative media can be `aria-hidden` when the title already carries the meaning.
## Cuándo no usar
- Loading placeholders → `Skeleton` / `Spinner`. - Error failures with retry of a failed request → error banner/toast + retry, not a first-run empty.
## Criterio de uso
- Usa Empty cuando el estado es válido pero todavía no hay contenido: primer uso, cero resultados o una colección que quedó vacía. - Explica qué está vacío y ofrece una acción de recuperación o creación cuando exista; el CTA debe estar en `EmptyContent`. - Distingue “no hay datos” de “no pudimos cargar”: el segundo caso necesita error y retry, no un mensaje neutro.
## Gotchas
- El título debe nombrar el estado, no sólo decir “sin resultados”; la descripción puede explicar el siguiente paso. - La media decorativa no debe competir con el mensaje ni repetirlo para tecnologías asistivas.
Uso
import {
Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
} from '@/components/atoms/Empty';
<Empty variant="outline">
<EmptyHeader>
<EmptyMedia variant="icon">{/* icon */}</EmptyMedia>
<EmptyTitle>No projects</EmptyTitle>
<EmptyDescription>Create one to get started.</EmptyDescription>
</EmptyHeader>
<EmptyContent><button type="button">New project</button></EmptyContent>
</Empty>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| variant | select | default | `default`, `outline`, `filled` | Surface style of the empty state container. | default for page empties; outline inside cards; filled for soft callouts. Default: default. |
Gotchas
- react
Compose EmptyHeader/Media/Title/Description/Content. Media has its own variant (default|icon).
- a11y
Title should explain the empty state; put recovery actions in EmptyContent.
Anatomía CSS
<div class="empty"> <span class="empty__content"></span> <span class="empty__description"></span> <span class="empty__header"></span> <span class="empty__media"></span> <span class="empty__media--icon"></span> <span class="empty__title"></span> </div>
| Clase | Propósito |
|---|---|
empty | root |
empty--filled | modifier |
empty--outline | modifier |
empty__content | element |
empty__description | element |
empty__header | element |
empty__media | element |
empty__media--icon | element |
empty__title | element |
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 |
|---|---|---|---|
| outline | border | 1px | stroke.hairline |
| filled | bg | #fafafa | background |
| all | fg | #525252 | muted.foreground |
| all | bg | #f5f5f5 | muted |
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).
/* -------------------------------------------------------------------------
Empty
Empty state placeholder. Centered layout with media, title,
description, and action slot.
Variants: outline (border), filled (bg)
Parts: .empty, .empty__header, .empty__media, .empty__title,
.empty__description, .empty__content
------------------------------------------------------------------------- */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
width: 100%;
padding: 48px 24px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
text-align: center;
}
/* ---- Variants ---- */
.empty--outline {
border: 1px dashed #e5e5e5;
border-radius: 12px;
}
.empty--filled {
background-color: #f5f5f5;
border-radius: 12px;
}
/* ---- Header ---- */
.empty__header {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
max-width: 420px;
}
/* ---- Media ---- */
.empty__media {
display: flex;
align-items: center;
justify-content: center;
color: #525252;
}
.empty__media--icon {
width: 48px;
height: 48px;
border-radius: 12px;
background-color: #f5f5f5;
padding: 12px;
}
.empty--filled .empty__media--icon {
background-color: #fafafa;
}
.empty__media svg {
width: 100%;
height: 100%;
}
/* ---- Title ---- */
.empty__title {
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: #0a0a0a;
margin: 0;
}
/* ---- Description ---- */
.empty__description {
font-size: 12.8px;
line-height: 1.45;
color: #525252;
margin: 0;
}
/* ---- Content (actions slot) ---- */
.empty__content {
display: flex;
align-items: center;
gap: 12px;
}
Codigo fuente
import { type ReactNode } from 'react'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } /* ---- Root ---- */ export type EmptyProps = { variant?: 'default' | 'outline' | 'filled'; children: ReactNode; className?: string; }; export function Empty({ variant = 'default', children, className }: EmptyProps) { return ( <div className={cn( 'empty', variant !== 'default' && `empty--${variant}`, className, )} > {children} </div> ); } /* ---- Header ---- */ export function EmptyHeader({ children, className }: { children: ReactNode; className?: string }) { return <div className={cn('empty__header', className)}>{children}</div>; } /* ---- Media ---- */ export type EmptyMediaProps = { variant?: 'default' | 'icon'; children: ReactNode; className?: string; }; export function EmptyMedia({ variant = 'default', children, className }: EmptyMediaProps) { return ( <div className={cn('empty__media', variant === 'icon' && 'empty__media--icon', className)}> {children} </div> ); } /* ---- Title ---- */ export function EmptyTitle({ children, className }: { children: ReactNode; className?: string }) { return <h3 className={cn('empty__title', className)}>{children}</h3>; } /* ---- Description ---- */ export function EmptyDescription({ children, className }: { children: ReactNode; className?: string }) { return <p className={cn('empty__description', className)}>{children}</p>; } /* ---- Content (actions) ---- */ export function EmptyContent({ children, className }: { children: ReactNode; className?: string }) { return <div className={cn('empty__content', className)}>{children}</div>; }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Empty) 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)
selectoron.empty--filled .empty__media--icon— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.empty__media svg— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
Tras pegar: Create component → nombre Atom / Empty → Publish.