agentskills.codes
SK

skeleton-loading-system

Comprehensive guide for PharmaFlow AI's progressive skeleton loading architecture. Use when creating new pages, adding loading states, building skeleton components, or debugging loading UI issues. Covers the 4-tier loading hierarchy, design tokens, anti-patterns, and step-by-step checklists for addi

Install

mkdir -p .claude/skills/skeleton-loading-system && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15527" && unzip -o skill.zip -d .claude/skills/skeleton-loading-system && rm skill.zip

Installs to .claude/skills/skeleton-loading-system

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Comprehensive guide for PharmaFlow AI's progressive skeleton loading architecture. Use when creating new pages, adding loading states, building skeleton components, or debugging loading UI issues. Covers the 4-tier loading hierarchy, design tokens, anti-patterns, and step-by-step checklists for adding skeletons to new features.
329 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

Skeleton Loading System — Architecture & Implementation Guide

Philosophy: The UI should feel "already rendered" from the first frame. No spinners. No blank screens. No layout shift. Every pixel has a placeholder before data arrives.


1. Architecture Overview

PharmaFlow AI uses a 4-tier progressive loading hierarchy. Each tier handles a different lifecycle stage:

┌─────────────────────────────────────────────────────────────────────┐
│  TIER 0 — App Bootstrap (Auth + Onboarding Check)                  │
│  └─ Full-screen spinner on black bg (the ONLY spinner allowed)     │
│     File: App.tsx                                                  │
├─────────────────────────────────────────────────────────────────────┤
│  TIER 1 — Page Transition Skeleton (Blocking / Full-page)          │
│  └─ PageSkeletonRegistry → reads PAGE_REGISTRY[view].skeleton      │
│     Orchestrator: PageRouter.tsx                                   │
│     Registry: components/skeletons/PageSkeletonRegistry.tsx        │
├─────────────────────────────────────────────────────────────────────┤
│  TIER 2 — Section-Level Skeletons (Non-blocking / Progressive)     │
│  └─ Parent passes isLoading prop to children                       │
│     Children swap their content with imported skeleton components   │
├─────────────────────────────────────────────────────────────────────┤
│  TIER 3 — Inline Element Skeletons (Component-internal)            │
│  └─ Component handles loading internally via isLoading prop        │
│     No separate skeleton component, just conditional className     │
└─────────────────────────────────────────────────────────────────────┘

Decision Tree: Which Tier to Use?

Is it the initial app boot / auth check?
  → TIER 0 (spinner — already implemented, don't touch)

Is data loaded per-page and the ENTIRE page depends on it?
  → TIER 1 (register a skeleton in PAGE_REGISTRY)

Does only a SECTION of the page depend on async data?
  → TIER 2 (create skeleton components, import in section)

Is it a SINGLE element (card, value, badge) that loads?
  → TIER 3 (handle inline with isLoading prop + conditional classes)

2. Tier 1 — Page Transition Skeletons

How It Works

  1. App.tsx passes isLoading (from DataContext) to PageRouter
  2. PageRouter checks: is isLoading === true? Is the current view NOT in the exclusion list?
  3. If yes → renders <PageSkeletonRegistry view={view} />
  4. PageSkeletonRegistry looks up PAGE_REGISTRY[view].skeleton and renders it

The Exclusion List (PageRouter)

Some pages handle loading internally (Tier 2 pattern) and skip the blocking skeleton:

// These pages render their OWN progressive skeletons
if (
  isLoading &&
  view !== 'inventory' &&
  view !== 'customers' &&
  // ... other pages that handle loading internally
) {
  return <PageSkeletonRegistry view={view} />;
}

Rule: If a page handles its own loading, add it to this exclusion list.

PageConfig Type (pageRegistry.ts)

interface PageConfig {
  id: string;
  component: ComponentType<any>;
  skeleton?: ComponentType<any>;     // ← Tier 1 skeleton
  skeletonProps?: Record<string, any>; // ← Optional props for skeleton
  // ... other fields
}

Registration Example

// In config/pageRegistry.ts
'my-page': {
  id: 'my-page',
  component: MyPageComponent,
  skeleton: MyPageSkeleton,            // ← The skeleton component
  skeletonProps: { withTopBar: true },  // ← Props forwarded to skeleton
  permission: 'some.permission',
  layout: 'dashboard',
},

The PageSkeletonRegistry (Orchestrator)

// components/skeletons/PageSkeletonRegistry.tsx
export const PageSkeletonRegistry = ({ view }: { view: string }) => {
  const config = PAGE_REGISTRY[view];
  const Skeleton = config?.skeleton || (() => null); // Fallback to empty
  const skeletonProps = config?.skeletonProps || {};

  return (
    <div className='animate-fade-in transition-opacity duration-300'>
      <Skeleton {...skeletonProps} />
    </div>
  );
};

3. Tier 2 — Section-Level Skeletons

The Pattern

// Parent Page Component
const [isLoading, setIsLoading] = useState(true);

// 1. Fetch data
useEffect(() => { fetchData().finally(() => setIsLoading(false)) }, []);

// 2. Pass isLoading to ALL children (single source of truth)
return (
  <>
    <PageHeader ... />  {/* ← Always renders immediately */}
    <ChildSection isLoading={isLoading} data={data} />
    <AnotherSection isLoading={isLoading} data={data} />
  </>
);
// Child Component
import { ItemRowSkeleton } from '../skeletons/MyModuleSkeletons';

export const ListSection = ({ items, isLoading }) => {
  return (
    <div className={`p-5 rounded-3xl ${CARD_BASE}`}>
      <h3>Section Title</h3> {/* ← Header always visible */}
      
      <div>
        {isLoading ? (
          <>
            {[1, 2, 3, 4].map(i => <ItemRowSkeleton key={i} />)}
          </>
        ) : items.length === 0 ? (
          <EmptyState />
        ) : (
          items.map(item => <ItemRow key={item.id} item={item} />)
        )}
      </div>
    </div>
  );
};

Key Rules

  1. Single source of truth: isLoading state lives in the parent page, never in children
  2. Header always renders: The page shell (header, navigation tabs) never shows skeletons
  3. Ternary pattern: Always isLoading ? <Skeleton /> : isEmpty ? <Empty /> : <Content />
  4. Count matters: Render a realistic number of skeleton items (e.g., 4 rows, not 1 or 20)

4. Tier 3 — Inline Element Skeletons

The Pattern

No separate skeleton component. The component itself has conditional rendering based on isLoading:

// A component that handles its own skeleton internally
export const MetricCard = ({ title, value, icon, iconColor, isLoading }) => {
  return (
    <div className={`p-3 rounded-2xl ${CARD_BASE}`}>
      {/* Icon — swaps to gray box when loading */}
      <div className={`w-12 h-12 rounded-xl ${
        isLoading 
          ? 'bg-zinc-100 dark:bg-zinc-800 animate-pulse' 
          : `text-${iconColor}-600`
      }`}>
        {!isLoading && <span className="material-symbols-rounded">{icon}</span>}
      </div>

      {/* Title — always visible (static content) */}
      <p className="text-xs">{title}</p>
      
      {/* Value — swaps to gray bar when loading */}
      {isLoading ? (
        <div className="h-8 w-20 bg-zinc-100 dark:bg-zinc-800 rounded-lg animate-pulse" />
      ) : (
        <h4>{value}</h4>
      )}
    </div>
  );
};

Data Tables (Built-in Skeleton Rows)

Tables should generate their own skeleton rows based on column definitions:

{isLoading && rows.length === 0 ? (
  Array.from({ length: 8 }).map((_, i) => (
    <tr key={`skeleton-row-${i}`} className="animate-pulse border-b">
      {table.getAllColumns().map(col => (
        <td key={`skeleton-cell-${col.id}-${i}`} className="py-4 px-4">
          <div className="h-4 bg-zinc-100 dark:bg-zinc-800 rounded" />
        </td>
      ))}
    </tr>
  ))
)}

Rule: If a reusable common component supports isLoading, prefer using it over creating a separate skeleton.


5. Design Tokens & Visual Standards

Color Palette for Skeleton Blocks

VariantLight ModeDark ModeUsage
Primary blockbg-zinc-100dark:bg-zinc-800Icons, titles, main content areas
Secondary blockbg-zinc-50dark:bg-zinc-800/50Subtitles, badges, secondary info
Card containerbg-whitedark:bg-zinc-900Full card skeletons

Standard palette: Always use zinc variants for all skeletons. Do NOT mix gray/neutral colors.

Border & Rounding

/* Cards */
border border-zinc-200 dark:border-zinc-800 rounded-xl   /* Small cards */
border border-zinc-200 dark:border-zinc-800 rounded-3xl  /* Large card containers */

/* Elements inside cards */
rounded        /* Small text blocks */
rounded-lg     /* Buttons, inputs */
rounded-full   /* Badges, avatar circles */
rounded-xl     /* Icon containers */

Animation Classes

ClassEffectWhen to Use
animate-pulseOpacity fade in/out (shimmer)On every skeleton block — this is the core loading indicator
animate-fade-inOpacity 0→1 entranceOn skeleton containers — smooth appearance when switching views

Sizing Convention

Icon placeholder    → w-10 h-10  or  w-12 h-12 rounded-xl
Title placeholder   → h-4 w-24  or  h-4 w-32
Subtitle            → h-3 w-16  or  h-3 w-20
Value/number        → h-6 w-16  or  h-8 w-20
Badge               → h-4 w-16 rounded-full  or  h-6 w-20 rounded-full
Button              → h-8 flex-1 rounded-lg
Avatar              → w-8 h-8 rounded-full  (with border-2 border-white for stacked)
Progress bar        → h-2 w-full rounded-full

6. File Organization

Directory Structure

components/skeletons/
├── PageSkeletonRegistry.tsx          # Tier 1 orchestrator (DO NOT modify)
└── {Module}Skeletons.tsx             # Per-module skeleton components

Naming Convention

TypeNaming PatternExample
Full-page skeleton{PageName}SkeletonPOSSkeleton, InventorySkeleton
Section skeleton{SectionName}SkeletonItemRowSkeleton, ProgressBarSkeleton
Composite skeleton{Feature}{Type}SkeletonKPIGridSkeleton, StatsRowSkeleton
Skeleton file{Module}Skeletons.tsxSalesSkeletons.tsx, HRSkeletons.tsx

7. Step-by-Step: Adding Skeletons to a New Feature

Scenario A: New Full Page (Tier 1)

  1. Study the page layout — identify the major visual sections
  2. Create skeleton file in components/skeletons/{Module}Skeleton.tsx
  3. Mirror the layout exactly — same grid, same gaps, same dimensions
  4. Register in pageRegistry.ts:
    'my-page': {
      skeleton: MyPageSkeleton,
      // ..
    

Content truncated.

Search skills

Search the agent skills registry