Developer Docs
Quick reference guides with props tables, usage examples, and best practices for rapid development
Skeleton
Loading placeholders with shimmer animation
Skeleton
Loading placeholder with shimmer animation that uses theme colors for consistent appearance. Provides 7 variants and 6 pre-configured components for common loading states.
Quick Example
🔧 Props
Skeleton component configuration options and pre-configured component variants.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | string | 'text' | Type of skeleton: 'text', 'title', 'avatar', 'button', 'card', 'image', 'custom' |
| width | string | variant default | Custom width (Tailwind class like "w-full" or CSS value like "200px") |
| height | string | variant default | Custom height (Tailwind class like "h-4" or CSS value like "50px") |
| rounded | boolean | false | Makes skeleton fully rounded (rounded-full) |
| animate | boolean | true | Enable shimmer animation |
| lines | number | 1 | Number of lines for text variant |
| class | string | '' | Additional CSS classes |
💻 Basic Usage
Simple skeleton variants for text, titles, and multiple line placeholders.
Text Skeleton
Skeleton({ variant: 'text' })Multiple Lines
Skeleton({ variant: 'text', lines: 3 })Title Skeleton
Skeleton({ variant: 'title' })🎨 All Variants
Seven built-in skeleton variants for different UI elements.
Avatar
Skeleton({ variant: 'avatar' })Button
Skeleton({ variant: 'button' })Card
Skeleton({ variant: 'card' })Image
Skeleton({ variant: 'image' })Custom Size
Skeleton({ variant: 'custom', width: 'w-64', height: 'h-20' })🧩 Pre-configured Components
VWF provides 6 pre-built skeleton components for common loading patterns.
CardSkeleton
import { CardSkeleton } from 'shared-components/ui/Skeleton.js';
CardSkeleton()
// or with options:
CardSkeleton({ showAvatar: false })ListItemSkeleton
import { ListItemSkeleton } from 'shared-components/ui/Skeleton.js';
ListItemSkeleton({ showIcon: true })ProfileSkeleton
import { ProfileSkeleton } from 'shared-components/ui/Skeleton.js';
ProfileSkeleton()ServiceCardSkeleton
import { ServiceCardSkeleton } from 'shared-components/ui/Skeleton.js';
ServiceCardSkeleton()TableRowSkeleton
import { TableRowSkeleton } from 'shared-components/ui/Skeleton.js';
TableRowSkeleton({ columns: 4 })FormSkeleton
import { FormSkeleton } from 'shared-components/ui/Skeleton.js';
FormSkeleton({ fields: 3 })✨ Animation Control
Toggle shimmer animation for different loading scenarios.
With Shimmer (Default)
Skeleton({ variant: 'card', animate: true })Static (No Animation)
Skeleton({ variant: 'card', animate: false })📏 Custom Sizing
Skeleton supports both Tailwind classes and CSS values for width and height.
Tailwind Classes
Skeleton({ variant: 'custom', width: 'w-32', height: 'h-4' })
Skeleton({ variant: 'custom', width: 'w-64', height: 'h-6' })
Skeleton({ variant: 'custom', width: 'w-full', height: 'h-8' })CSS Values
Skeleton({ variant: 'custom', width: '200px', height: '20px' })
Skeleton({ variant: 'custom', width: '300px', height: '30px' })
Skeleton({ variant: 'custom', width: '100%', height: '40px' })🔄 Loading State Patterns
Common patterns for implementing loading states with skeletons and VanJS reactive state.
Basic Loading State
const isLoading = van.state(true);
() => isLoading.val ?
Skeleton({ variant: 'text', lines: 3 }) :
div({ class: 'space-y-2' },
p('Content line 1'),
p('Content line 2'),
p('Content line 3')
)List Loading State
const items = van.state([]);
const isLoading = van.state(true);
() => isLoading.val ?
// Show skeleton list
div({ class: 'space-y-2' },
...Array.from({ length: 5 }, () =>
ListItemSkeleton({ showIcon: true })
)
) :
// Show actual list
div({ class: 'space-y-2' },
...items.val.map(item =>
ListItem({ title: item.title })
)
)Progressive Loading
const profile = van.state(null);
const posts = van.state(null);
div(
// Profile section
() => profile.val ?
ProfileCard(profile.val) :
ProfileSkeleton(),
// Posts section (loads after profile)
() => posts.val ?
posts.val.map(post => PostCard(post)) :
Array.from({ length: 3 }, () => CardSkeleton())
)✅ Best Practices
Follow these guidelines for optimal skeleton screen implementation:
✓ Do This
✓Match skeleton shape to actual content
✓Use pre-configured components when possible
✓Show skeletons for minimum 300ms (avoid flashing)
✓Use shimmer for long loads, static for quick loads
✓Keep skeleton count realistic (3-5 items)
✓Use CardSkeleton for card grids
✗ Avoid This
✗Show skeletons for cached/instant data
✗Mix skeleton shapes with mismatched content
✗Show dozens of skeletons (overwhelming)
✗Forget to set minimum display time
✗Use skeletons for error states
✗Animate skeletons on slow devices
⭐ Key Features
Comprehensive skeleton system with animations, pre-configured components, and flexible customization:
7 Built-in Variants
Text, title, avatar, button, card, image, and custom variants
Shimmer Animation
Hardware-accelerated CSS transform animation
6 Pre-configured Components
Card, list item, profile, service card, table row, and form skeletons
Flexible Sizing
Supports both Tailwind classes and CSS values
Multi-line Support
Text variant can show multiple lines with automatic sizing
Theme Integration
Uses theme colors (bg-mist) for consistent appearance
🎯 Common Use Cases
Real-world patterns showing how to implement skeleton screens for different content types:
1. Loading User Profile
const profile = van.state(null);
() => profile.val ?
div({ class: 'text-center' },
Avatar({ src: profile.val.avatar, size: 'xl' }),
h2(profile.val.name),
p(profile.val.bio)
) :
ProfileSkeleton()2. Loading Service Cards
const services = van.state([]);
const isLoading = van.state(true);
() => isLoading.val ?
div({ class: 'grid md:grid-cols-3 gap-4' },
...Array.from({ length: 6 }, () => ServiceCardSkeleton())
) :
div({ class: 'grid md:grid-cols-3 gap-4' },
...services.val.map(service => ServiceCard(service))
)3. Loading Table Data
const tableData = van.state([]);
const isLoading = van.state(true);
() => isLoading.val ?
div({ class: 'space-y-2' },
...Array.from({ length: 5 }, () =>
TableRowSkeleton({ columns: 4 })
)
) :
table(
...tableData.val.map(row =>
tr(
td(row.col1),
td(row.col2),
td(row.col3),
td(row.col4)
)
)
)4. Loading Form (Multi-step)
const formData = van.state(null);
() => !formData.val ?
FormSkeleton({ fields: 4 }) :
form(
Input({ label: 'Name', value: formData.val.name }),
Input({ label: 'Email', value: formData.val.email }),
TextArea({ label: 'Bio', value: formData.val.bio }),
Button({ variant: 'primary' }, 'Save')
)5. Infinite Scroll Loading
const items = van.state([]);
const isLoadingMore = van.state(false);
div(
{ class: 'space-y-4' },
// Existing items
...items.val.map(item => ItemCard(item)),
// Loading more indicator
() => isLoadingMore.val ?
div({ class: 'space-y-2' },
...Array.from({ length: 3 }, () => CardSkeleton())
) :
''
)⏱️ Timing Strategies
Proper timing prevents jarring flashes and improves perceived performance:
Minimum Display Time
const MIN_LOADING_TIME = 300; // ms
async function loadData() {
const startTime = Date.now();
isLoading.val = true;
const data = await fetchData();
// Ensure skeleton shows for at least 300ms
const elapsed = Date.now() - startTime;
if (elapsed < MIN_LOADING_TIME) {
await new Promise(resolve =>
setTimeout(resolve, MIN_LOADING_TIME - elapsed)
);
}
items.val = data;
isLoading.val = false;
}Staggered Loading
// Load critical content first
async function loadPage() {
// Show hero content immediately
heroLoading.val = false;
hero.val = getCachedHero();
// Then load main content
const mainData = await fetchMainContent();
mainLoading.val = false;
main.val = mainData;
// Finally load supplementary content
const extraData = await fetchExtraContent();
extraLoading.val = false;
extra.val = extraData;
}Caching Strategy
const cache = {};
async function loadWithCache(key) {
// If cached, show immediately (no skeleton)
if (cache[key]) {
isLoading.val = false;
items.val = cache[key];
return;
}
// Not cached - show skeleton
isLoading.val = true;
const data = await fetchData(key);
cache[key] = data;
items.val = data;
isLoading.val = false;
}Explore More Components
Browse other component categories