Zoeken...  ⌘K GitHub

EmptyState UI Elements

Lege staat component voor lege lijsten, zoekresultaten en errors. Icon, headline, sub en optionele CTA.

/empty-state
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