Skeleton
ComponentsReactTailwindLoading placeholder with pulse animation. Composed variants: article card, profile header, stat card grid, table row. Reload toggle demonstrates loaded vs loading state.
Toggle to see real content. Skeletons match final shape so layout doesn't shift.
Card List (Article Feed)
Profile Header
Stats Grid
Table Rows
| Name | Status | Plan | |
|---|---|---|---|
1 import { cn } from "@/shared/utils/utils"; 2 3 // Primitive — shared/ui/skeleton.tsx 4 function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { 5 return <div className={cn("bg-muted animate-pulse rounded-md", className)} {...props} />; 6 } 7 8 export { Skeleton }; 9 10 // === Composed skeletons match the final shape === 11 12 // Card skeleton — for article feeds, blog lists 13 function ArticleSkeleton() { 14 return ( 15 <div className="bg-card space-y-3 rounded-lg border p-4"> 16 <div className="flex items-center gap-3"> 17 <Skeleton className="h-9 w-9 rounded-full" /> 18 <div className="space-y-1.5"> 19 <Skeleton className="h-3 w-24" /> 20 <Skeleton className="h-2.5 w-16" /> 21 </div> 22 </div> 23 <Skeleton className="h-4 w-3/4" /> 24 <div className="space-y-1.5"> 25 <Skeleton className="h-3 w-full" /> 26 <Skeleton className="h-3 w-5/6" /> 27 </div> 28 </div> 29 ); 30 } 31 32 // Profile header skeleton — for profile cards 33 function ProfileSkeleton() { 34 return ( 35 <div className="bg-card flex items-start gap-4 rounded-lg border p-5"> 36 <Skeleton className="h-16 w-16 shrink-0 rounded-full" /> 37 <div className="flex-1 space-y-2.5"> 38 <Skeleton className="h-4 w-40" /> 39 <Skeleton className="h-3 w-28" /> 40 <div className="flex gap-2 pt-1"> 41 <Skeleton className="h-7 w-20 rounded-md" /> 42 <Skeleton className="h-7 w-20 rounded-md" /> 43 </div> 44 </div> 45 </div> 46 ); 47 } 48 49 // Table row skeleton — for data tables 50 function TableRowSkeleton() { 51 return ( 52 <tr className="border-b"> 53 <td className="px-3 py-3"><Skeleton className="h-3 w-24" /></td> 54 <td className="px-3 py-3"><Skeleton className="h-3 w-32" /></td> 55 <td className="px-3 py-3"><Skeleton className="h-5 w-16 rounded-full" /></td> 56 <td className="px-3 py-3"><Skeleton className="ml-auto h-3 w-12" /></td> 57 </tr> 58 ); 59 } 60 61 // Stat card skeleton — for dashboards 62 function StatCardSkeleton() { 63 return ( 64 <div className="bg-card space-y-2 rounded-lg border p-4"> 65 <Skeleton className="h-3 w-20" /> 66 <Skeleton className="h-7 w-16" /> 67 <Skeleton className="h-2.5 w-24" /> 68 </div> 69 ); 70 } 71 72 // Usage — render skeleton while loading, real content when ready 73 export function ArticleFeed() { 74 const { data, isLoading } = useArticles(); 75 76 return ( 77 <div className="grid gap-3 md:grid-cols-3"> 78 {isLoading 79 ? Array.from({ length: 3 }).map((_, i) => <ArticleSkeleton key={i} />) 80 : data.map((a) => <ArticleCard key={a.id} article={a} />)} 81 </div> 82 ); 83 } 84 85 // Usage — swap a single profile skeleton for the loaded card 86 export function ProfileHeader() { 87 const { data, isLoading } = useProfile(); 88 89 return isLoading ? <ProfileSkeleton /> : <ProfileCard profile={data} />; 90 } 91 92 // Rule of thumb: skeleton dimensions should match real content dimensions 93 // (height, width, rounded shape) so the layout never shifts on load.