Skeleton UI Elements
Laad-placeholder met shimmer animatie. 5 preset varianten: text, card, avatar, image, custom.
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