Agent skill
suspense-loading
Suspense and loading state patterns for this Next.js application. Covers loading.tsx files, skeleton components, INP optimization, and streaming SSR. Use this skill when implementing loading states or optimizing perceived performance.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/suspense-loading
SKILL.md
Suspense & Loading States Skill
Patterns for implementing loading states and Suspense boundaries in this Next.js 15 application.
Architecture Overview
LOADING STATE ARCHITECTURE:
Route-Level Loading (loading.tsx):
├── app/dashboard/(main)/loading.tsx # Dashboard home skeleton
├── app/dashboard/settings/loading.tsx # Settings overview skeleton
├── app/dashboard/settings/*/loading.tsx # Setting-specific skeletons
├── app/dashboard/features/loading.tsx # Features placeholder skeleton
└── app/dashboard/(main)/[entity]/ # Entity list skeleton (existing)
Skeleton Components (core/components/ui/):
├── skeleton.tsx # Base Skeleton + SkeletonContainer + SkeletonText
├── skeleton-dashboard.tsx # SkeletonDashboardHome, SkeletonStatsGrid, etc.
├── skeleton-settings.tsx # SkeletonSettingsOverview, SkeletonProfileForm, etc.
├── skeleton-features.tsx # SkeletonFeaturePlaceholder, SkeletonAnalyticsPage
├── skeleton-list.tsx # SkeletonEntityList, SkeletonTable
├── skeleton-form.tsx # SkeletonEntityForm, SkeletonFormCard
└── skeleton-detail.tsx # SkeletonEntityDetail, SkeletonDetailCard
INP Optimizations
The skeleton system includes optimizations for Interaction to Next Paint (INP):
1. CSS Containment
/* Isolates layout/paint calculations */
.skeleton-contained {
contain: content;
}
/* Container-level containment */
.skeleton-container {
contain: layout style;
}
2. Content Visibility
/* Skips rendering of off-screen skeleton items */
.skeleton-container > * {
content-visibility: auto;
contain-intrinsic-size: auto 100px;
}
3. GPU-Accelerated Animations
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-skeleton-pulse {
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
will-change: opacity;
}
4. Reduced Motion Support
@media (prefers-reduced-motion: reduce) {
.animate-skeleton-pulse {
animation: none;
}
}
Component Patterns
Base Skeleton
import { Skeleton, SkeletonContainer, SkeletonText } from '@nextsparkjs/core/components/ui/skeleton'
// Simple skeleton
<Skeleton className="h-4 w-32" />
// Text skeleton with multiple lines
<SkeletonText lines={3} />
// Container for multiple skeletons (enables content-visibility)
<SkeletonContainer className="space-y-4">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</SkeletonContainer>
Loading.tsx Pattern
// app/dashboard/settings/profile/loading.tsx
import { SkeletonProfileForm } from '@nextsparkjs/core/components/ui/skeleton-settings'
export default function ProfileLoading() {
return <SkeletonProfileForm />
}
Suspense Boundary in Component
import { Suspense } from 'react'
import { SkeletonStatsGrid } from '@nextsparkjs/core/components/ui/skeleton-dashboard'
export default function DashboardPage() {
return (
<div>
{/* Header (instant) */}
<h1>Dashboard</h1>
{/* Stats with Suspense boundary (streamed) */}
<Suspense fallback={<SkeletonStatsGrid />}>
<DashboardStats />
</Suspense>
{/* Activity with Suspense boundary (streamed) */}
<Suspense fallback={<SkeletonActivity />}>
<RecentActivity />
</Suspense>
</div>
)
}
Available Skeleton Components
Dashboard
| Component | Description |
|---|---|
SkeletonDashboardHome |
Complete dashboard home page skeleton |
SkeletonStatsGrid |
4-card stats grid |
SkeletonStatCard |
Single stat card |
SkeletonQuickActions |
Quick actions section |
SkeletonActivity |
Activity feed |
Settings
| Component | Description |
|---|---|
SkeletonSettingsOverview |
Settings navigation cards |
SkeletonProfileForm |
Profile edit form |
SkeletonBillingPage |
Billing/plans page |
SkeletonPasswordPage |
Password change form |
SkeletonSecurityPage |
Sessions list |
SkeletonApiKeysPage |
API keys list |
SkeletonNotificationsPage |
Notification settings |
SkeletonTeamsPage |
Teams list |
SkeletonInvoicesPage |
Invoices table |
SkeletonPlansPage |
Plans grid |
Features
| Component | Description |
|---|---|
SkeletonFeaturePlaceholder |
Feature gate placeholder |
SkeletonAnalyticsPage |
Analytics dashboard |
SkeletonWebhooksPage |
Webhooks configuration |
SkeletonAutomationPage |
Automation list |
Entity Pages
| Component | Description |
|---|---|
SkeletonEntityList |
Complete entity list page |
SkeletonEntityForm |
Entity create/edit form |
SkeletonEntityDetail |
Entity detail view |
Best Practices
1. Match Skeleton to Content
Skeletons should closely match the final content layout:
// Good - matches actual content structure
<div className="max-w-4xl space-y-6">
<header>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-5 w-64" />
</header>
{/* ... */}
</div>
// Bad - generic box that doesn't match
<Skeleton className="h-[500px] w-full" />
2. Use Route-Level Loading
Prefer loading.tsx files over component-level loading states:
app/dashboard/settings/profile/
├── page.tsx # Actual content
└── loading.tsx # Skeleton shown during load
3. Wrap Multiple Skeletons
Use SkeletonContainer for lists to enable content-visibility:
// Good - enables off-screen optimization
<SkeletonContainer className="divide-y">
{Array.from({ length: 10 }).map((_, i) => (
<SkeletonListItem key={i} />
))}
</SkeletonContainer>
// Less optimal - no content-visibility
<div className="divide-y">
{Array.from({ length: 10 }).map((_, i) => (
<SkeletonListItem key={i} />
))}
</div>
4. Consider Inheritance
Next.js inherits loading.tsx from parent routes. You only need specific loading files for pages with different layouts:
app/dashboard/settings/
├── loading.tsx # Covers /settings and simple subroutes
├── profile/
│ └── loading.tsx # Different layout, needs own skeleton
├── billing/
│ └── loading.tsx # Different layout, needs own skeleton
├── password/ # Uses parent loading.tsx
├── security/ # Uses parent loading.tsx
└── notifications/ # Uses parent loading.tsx
Testing Loading States
// Cypress test for loading states
it('should show skeleton while content loads', () => {
// Simulate slow network
cy.intercept('GET', '/api/**', (req) => {
req.on('response', (res) => res.setDelay(500))
})
cy.visit('/dashboard/settings/profile')
// Skeleton should appear briefly
// Content should replace skeleton
cy.get('[data-cy="settings-profile-container"]', { timeout: 10000 })
.should('be.visible')
})
Related Skills
react-patterns- Server/Client components, Suspense basicsshadcn-components- UI component librarytanstack-query- Data fetching with loading states
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?