EmptyState UI Elements
Lege staat component voor lege lijsten, zoekresultaten en errors. Icon, headline, sub en optionele CTA.
src/components/ui/EmptyState.astro
---
/**
* EmptyState
* Lege staat component — voor lege lijsten, zoekresultaten, errors.
* Icon of illustratie, headline, sub, optionele CTA.
* 3 varianten: default, bordered, fullscreen.
*/
interface Props {
icon?: string;
headline: string;
sub?: string;
ctaLabel?: string;
ctaHref?: string;
secondaryLabel?: string;
secondaryHref?: string;
variant?: 'default' | 'bordered' | 'fullscreen';
size?: 'sm' | 'md' | 'lg';
}
const {
icon,
headline,
sub,
ctaLabel,
ctaHref = '#',
secondaryLabel,
secondaryHref = '#',
variant = 'default',
size = 'md',
} = Astro.props;
// Default icon if none provided
const defaultIcon = `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2z"/><path d="M16 3H8L4 7h16l-4-4z"/></svg>`;
---
<div
class:list={['es', `es--${variant}`, `es--${size}`]}
data-component="empty-state"
>
<!-- Slot for custom illustration -->
{Astro.slots.has('illustration') ? (
<div class="es__illustration">
<slot name="illustration" />
</div>
) : (
<div class="es__icon" set:html={icon ?? defaultIcon} />
)}
<div class="es__text">
<h3 class="es__headline">{headline}</h3>
{sub && <p class="es__sub">{sub}</p>}
</div>
{(ctaLabel || secondaryLabel) && (
<div class="es__actions">
{ctaLabel && (
<a href={ctaHref} class="es__cta">{ctaLabel}</a>
)}
{secondaryLabel && (
<a href={secondaryHref} class="es__secondary">{secondaryLabel}</a>
)}
</div>
)}
<!-- Optional extra slot for custom content -->
{Astro.slots.has('default') && (
<div class="es__extra">
<slot />
</div>
)}
</div>
<style>
.es {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 3rem 2rem;
gap: 1.25rem;
}
/* Variants */
.es--bordered {
border: 1.5px dashed rgba(0,0,0,0.12);
border-radius: var(--radius, 0.5rem);
background: rgba(0,0,0,0.01);
}
.es--fullscreen {
min-height: 60vh;
justify-content: center;
}
/* Sizes */
.es--sm { padding: 2rem 1.5rem; gap: 0.875rem; }
.es--lg { padding: 5rem 2.5rem; gap: 1.75rem; }
/* Icon */
.es__icon {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.04);
border-radius: var(--radius, 0.5rem);
color: var(--color-muted, #6b7280);
}
.es--sm .es__icon { width: 48px; height: 48px; }
.es--lg .es__icon { width: 80px; height: 80px; }
.es__illustration { max-width: 200px; }
/* Text */
.es__text { display: flex; flex-direction: column; gap: 0.5rem; max-width: 380px; }
.es__headline {
font-size: 1.0625rem;
font-weight: 700;
color: var(--color-primary, #0a0a0a);
letter-spacing: -0.02em;
margin: 0;
}
.es--sm .es__headline { font-size: 0.9375rem; }
.es--lg .es__headline { font-size: 1.25rem; }
.es__sub {
font-size: 0.9375rem;
line-height: 1.65;
color: var(--color-muted, #6b7280);
}
.es--sm .es__sub { font-size: 0.875rem; }
/* Actions */
.es__actions {
display: flex;
align-items: center;
gap: 0.875rem;
flex-wrap: wrap;
justify-content: center;
}
.es__cta {
display: inline-flex;
align-items: center;
background: var(--color-primary, #0a0a0a);
color: #fff;
padding: 0.625rem 1.5rem;
border-radius: var(--radius, 0.5rem);
font-size: 0.875rem;
font-weight: 700;
text-decoration: none;
transition: background 0.2s;
}
.es__cta:hover { background: var(--color-accent, #6366f1); }
.es__secondary {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-muted, #6b7280);
text-decoration: none;
transition: color 0.15s;
}
.es__secondary:hover { color: var(--color-primary, #0a0a0a); }
.es__extra { width: 100%; max-width: 480px; }
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
headline * | string | — | Hoofdtekst |
sub | string | — | Beschrijvende ondertekst |
icon | string | — | SVG HTML voor custom icoon (standaard: box icoon) |
ctaLabel | string | — | Primaire actie tekst |
ctaHref | string | '#' | Primaire actie link |
secondaryLabel | string | — | Secundaire actie tekst |
variant | 'default' | 'bordered' | 'fullscreen' | 'default' | Layout variant |
size | 'sm' | 'md' | 'lg' | 'md' | Grootte |
* = verplicht