Zoeken...  ⌘K GitHub

Skeleton UI Elements

Laad-placeholder met shimmer animatie. 5 preset varianten: text, card, avatar, image, custom.

/skeleton
src/components/ui/Skeleton.astro
---
/**
 * Skeleton
 * Loading placeholder. Shimmer animatie, puur CSS.
 * Variant: text (meerdere regels), card, avatar, image, custom.
 */
interface Props {
  variant?: 'text' | 'card' | 'avatar' | 'image' | 'custom';
  lines?: number;       /** voor text variant */
  width?: string;
  height?: string;
  circle?: boolean;     /** voor avatar */
  size?: 'sm' | 'md' | 'lg';
}

const {
  variant = 'text',
  lines = 3,
  width,
  height,
  circle = false,
  size = 'md',
} = Astro.props;
---

{variant === 'text' && (
  <div class:list={['sk-text', `sk-text--${size}`]} data-component="skeleton">
    {Array.from({ length: lines }).map((_, i) => (
      <div
        class="sk sk--text"
        style={i === lines - 1 && lines > 1 ? 'width:65%' : ''}
      ></div>
    ))}
  </div>
)}

{variant === 'card' && (
  <div class:list={['sk-card', `sk-card--${size}`]} data-component="skeleton">
    <div class="sk sk--image" style="height:160px"></div>
    <div class="sk-card__body">
      <div class="sk sk--text" style="width:40%;height:12px;margin-bottom:.5rem"></div>
      <div class="sk sk--text"></div>
      <div class="sk sk--text" style="width:80%"></div>
      <div class="sk sk--text" style="width:60%"></div>
    </div>
  </div>
)}

{variant === 'avatar' && (
  <div class="sk-avatar-row" data-component="skeleton">
    <div class:list={['sk sk--avatar', { 'sk--circle': true }]} style={width ? `width:${width};height:${height ?? width}` : ''}></div>
    <div class="sk-avatar-text">
      <div class="sk sk--text" style="width:120px"></div>
      <div class="sk sk--text" style="width:80px"></div>
    </div>
  </div>
)}

{variant === 'image' && (
  <div
    class="sk sk--image"
    data-component="skeleton"
    style={`width:${width ?? '100%'};height:${height ?? '200px'}`}
  ></div>
)}

{variant === 'custom' && (
  <div
    class="sk"
    data-component="skeleton"
    style={`width:${width ?? '100%'};height:${height ?? '20px'};border-radius:${circle ? '50%' : ''}`}
  ></div>
)}

<style>
  /* Base shimmer */
  .sk {
    background: linear-gradient(
      90deg,
      rgba(0,0,0,0.06) 25%,
      rgba(0,0,0,0.1) 50%,
      rgba(0,0,0,0.06) 75%
    );
    background-size: 400% 100%;
    border-radius: var(--radius, 0.5rem);
    animation: sk-shimmer 1.6s ease-in-out infinite;
  }

  @keyframes sk-shimmer {
    from { background-position: 100% 0; }
    to   { background-position: -100% 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .sk { animation: none; background: rgba(0,0,0,0.07); }
  }

  /* Text lines */
  .sk--text {
    height: 14px;
    width: 100%;
    border-radius: 4px;
  }

  .sk-text { display: flex; flex-direction: column; }
  .sk-text--sm { gap: 0.5rem; }
  .sk-text--md { gap: 0.625rem; }
  .sk-text--lg { gap: 0.75rem; }

  .sk-text--sm .sk--text { height: 12px; }
  .sk-text--md .sk--text { height: 14px; }
  .sk-text--lg .sk--text { height: 18px; }

  /* Image */
  .sk--image { width: 100%; border-radius: var(--radius, 0.5rem); }

  /* Avatar */
  .sk--avatar { width: 40px; height: 40px; flex-shrink: 0; }
  .sk--circle { border-radius: 50%; }

  .sk-avatar-row { display: flex; align-items: center; gap: 0.75rem; }
  .sk-avatar-text { display: flex; flex-direction: column; gap: 0.5rem; flex: 1; }

  /* Card */
  .sk-card {
    border: 1px solid rgba(0,0,0,0.07);
    border-radius: var(--radius, 0.5rem);
    overflow: hidden;
    background: var(--color-bg, #fff);
  }

  .sk-card__body {
    padding: 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .sk-card--sm .sk-card__body { padding: 0.75rem; }
  .sk-card--lg .sk-card__body { padding: 1.5rem; }
</style>

Props

Prop Type Default Beschrijving
variant 'text' | 'card' | 'avatar' | 'image' | 'custom' 'text' Preset layout variant
lines number 3 Aantal tekst regels (variant=text)
width string Breedte (variant=custom)
height string Hoogte (variant=custom of image)
rounded boolean false Volledig afgerond

* = verplicht