vwf .

Developer Docs

Quick reference guides with props tables, usage examples, and best practices for rapid development

Components/Docs/Skeleton

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.

PropTypeDefaultDescription
variantstring'text'Type of skeleton: 'text', 'title', 'avatar', 'button', 'card', 'image', 'custom'
widthstringvariant defaultCustom width (Tailwind class like "w-full" or CSS value like "200px")
heightstringvariant defaultCustom height (Tailwind class like "h-4" or CSS value like "50px")
roundedbooleanfalseMakes skeleton fully rounded (rounded-full)
animatebooleantrueEnable shimmer animation
linesnumber1Number of lines for text variant
classstring''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