Getting Started
Basic Usage
Learn the fundamentals of using Ghostly skeleton loaders.
Single component
The most basic usage — wrap any component with <Ghostly>:
import { Ghostly } from '@ghostly-ui/react'
function UserPage() {
const { data, isLoading } = useQuery('user')
return (
<Ghostly loading={isLoading}>
<UserProfile user={data} />
</Ghostly>
)
}When loading={true}, all text, images, and interactive elements inside become animated skeleton blocks. When loading={false}, your component renders normally.
Lists and grids
For lists that start empty during loading, use <GhostlyList>:
import { GhostlyList } from '@ghostly-ui/react'
function ShopPage() {
const { data: products, isLoading } = useQuery('products')
return (
<GhostlyList
loading={isLoading}
count={6}
item={<ProductCard />}
className="grid grid-cols-3 gap-4"
>
{products?.map(p => <ProductCard key={p.id} product={p} />)}
</GhostlyList>
)
}<GhostlyList> clones the item template count times during loading, then shows your real children when data arrives.
Choosing an animation
<Ghostly loading={true} animation="shimmer"> {/* Gradient sweep (default) */}
<Ghostly loading={true} animation="pulse"> {/* Opacity fade */}
<Ghostly loading={true} animation="wave"> {/* Staggered fade */}
<Ghostly loading={true} animation="none"> {/* Static blocks */}Excluding elements
Some elements should stay visible during loading. Add data-ghostly-ignore:
<Ghostly loading={isLoading}>
<div className="flex justify-between">
<div>
<h2>{data?.title ?? ''}</h2>
<p>{data?.description ?? ''}</p>
</div>
{/* This button stays visible during loading */}
<button data-ghostly-ignore className="btn-primary">
Share
</button>
</div>
</Ghostly>Writing skeleton-friendly components
Your components should handle undefined data gracefully with optional chaining. This is already good React practice.
// ✅ Good — handles undefined data
function ProductCard({ product }: { product?: Product }) {
return (
<div className="rounded-xl border p-4">
{product?.image ? (
<img src={product.image} alt={product.title} className="h-48 w-full" />
) : (
<div className="h-48 w-full" />
)}
<h3 className="text-lg font-bold">{product?.title ?? ''}</h3>
<p className="text-sm">{product?.price ?? ''}</p>
</div>
)
}Multiple loading sections
Different sections can have independent loading states:
function Dashboard() {
const stats = useQuery('stats')
const orders = useQuery('orders')
return (
<div className="space-y-8">
<Ghostly loading={stats.isLoading} animation="pulse">
<StatsRow stats={stats.data} />
</Ghostly>
<GhostlyList
loading={orders.isLoading}
count={5}
item={<OrderRow />}
animation="wave"
>
{orders.data?.map(o => <OrderRow key={o.id} order={o} />)}
</GhostlyList>
</div>
)
}