Overview
The JobCardSkeleton component (exported from the Loading module) displays animated skeleton placeholders while job data is loading. It creates visual placeholders that match the structure of actual job cards.
Usage
import { JobCardSkeleton } from '@/components'
function JobsPage () {
const { jobs , isLoading } = useJobs ()
if ( isLoading ) {
return < JobCardSkeleton count = { 6 } />
}
return < JobListings jobs = { jobs } />
}
Props
The number of skeleton cards to display
Features
Customizable Count
The component renders the specified number of skeleton cards:
{ Array . from ({ length: count }). map (( _ , i ) => (
< div key = { i } className = { styles . skeletonCard } >
{ /* Skeleton content */ }
</ div >
))}
Structured Skeleton
Each skeleton card mirrors the structure of a real JobCard:
Info section : Title, subtitle, description text
Actions section : Link and button placeholders
< div className = { styles . skeletonCard } >
< div className = { styles . info } >
< div className = { ` ${ styles . skeleton } ${ styles . title } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . subtitle } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . text } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . textShort } ` } ></ div >
</ div >
< div className = { styles . actions } >
< div className = { ` ${ styles . skeleton } ${ styles . link } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . button } ` } ></ div >
</ div >
</ div >
Component Structure
Full Component
Basic Usage
With Loading State
import styles from './Loading.module.css'
export function JobCardSkeleton ({ count = 3 }) {
return (
< div className = { styles . skeletonContainer } >
{ Array . from ({ length: count }). map (( _ , i ) => (
< div key = { i } className = { styles . skeletonCard } >
< div className = { styles . info } >
< div className = { ` ${ styles . skeleton } ${ styles . title } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . subtitle } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . text } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . textShort } ` } ></ div >
</ div >
< div className = { styles . actions } >
< div className = { ` ${ styles . skeleton } ${ styles . link } ` } ></ div >
< div className = { ` ${ styles . skeleton } ${ styles . button } ` } ></ div >
</ div >
</ div >
)) }
</ div >
)
}
Styling
The component uses CSS modules (Loading.module.css) for styling:
Main Classes
styles.skeletonContainer - Container for all skeleton cards
styles.skeletonCard - Individual skeleton card wrapper
styles.info - Info section container
styles.actions - Actions section container
styles.skeleton - Base skeleton styling (animation, colors)
Element Classes
styles.title - Title placeholder dimensions
styles.subtitle - Subtitle placeholder dimensions
styles.text - Full-width text placeholder
styles.textShort - Shorter text placeholder
styles.link - Link button placeholder
styles.button - Action button placeholder
Usage Patterns
With React Query
import { useQuery } from '@tanstack/react-query'
import { JobCardSkeleton , JobListings } from '@/components'
function JobsPage () {
const { data , isLoading , error } = useQuery ({
queryKey: [ 'jobs' ],
queryFn: fetchJobs
})
if ( isLoading ) return < JobCardSkeleton count = { 6 } />
if ( error ) return < div > Error loading jobs </ div >
return < JobListings jobs = { data } />
}
With useState
import { useState , useEffect } from 'react'
import { JobCardSkeleton , JobListings } from '@/components'
function JobsPage () {
const [ jobs , setJobs ] = useState ([])
const [ isLoading , setIsLoading ] = useState ( true )
useEffect (() => {
fetchJobs ()
. then ( setJobs )
. finally (() => setIsLoading ( false ))
}, [])
if ( isLoading ) {
return < JobCardSkeleton count = { 10 } />
}
return < JobListings jobs = { jobs } />
}
With Suspense
import { Suspense } from 'react'
import { JobCardSkeleton } from '@/components'
import { JobsListAsync } from './JobsListAsync'
function JobsPage () {
return (
< Suspense fallback = { < JobCardSkeleton count = { 8 } /> } >
< JobsListAsync />
</ Suspense >
)
}
import { JobCardSkeleton , JobListings , Pagination } from '@/components'
import { useState , useEffect } from 'react'
function PaginatedJobs () {
const [ page , setPage ] = useState ( 1 )
const [ jobs , setJobs ] = useState ([])
const [ isLoading , setIsLoading ] = useState ( true )
useEffect (() => {
setIsLoading ( true )
fetchJobsPage ( page )
. then ( setJobs )
. finally (() => setIsLoading ( false ))
}, [ page ])
return (
<>
{ isLoading ? (
< JobCardSkeleton count = { 10 } />
) : (
< JobListings jobs = { jobs } />
) }
< Pagination
currentPage = { page }
totalPages = { 15 }
onPageChange = { setPage }
/>
</>
)
}
Animation
Typical skeleton screens use a shimmer animation. You would add this to Loading.module.css:
@keyframes shimmer {
0% {
background-position : -468 px 0 ;
}
100% {
background-position : 468 px 0 ;
}
}
.skeleton {
animation : shimmer 1.5 s infinite ;
background : linear-gradient (
to right ,
#f0f0f0 0 % ,
#e0e0e0 20 % ,
#f0f0f0 40 % ,
#f0f0f0 100 %
);
background-size : 800 px 104 px ;
}
Best Practices
Match Your Layout
Set the count prop to match the number of jobs you typically display:
// If you show 12 jobs per page
< JobCardSkeleton count = { 12 } />
// If you show a different amount based on screen size
const isMobile = useMediaQuery ( '(max-width: 768px)' )
< JobCardSkeleton count = { isMobile ? 6 : 12 } />
Consistent Dimensions
Ensure skeleton cards have the same dimensions as real JobCard components for a smooth transition.
Don’t Overuse
Only show skeletons for initial loading. For subsequent loads (like pagination), consider:
function PaginatedJobs () {
const [ page , setPage ] = useState ( 1 )
const { data , isLoading , isFetching } = useQuery ([ 'jobs' , page ])
if ( isLoading ) {
// First load - show skeleton
return < JobCardSkeleton count = { 10 } />
}
return (
<>
< div style = { { opacity: isFetching ? 0.5 : 1 } } >
< JobListings jobs = { data } />
</ div >
{ /* Show dim overlay or spinner instead of full skeleton */ }
</>
)
}
Accessibility
ARIA Attributes
Consider adding ARIA attributes for better accessibility:
export function JobCardSkeleton ({ count = 3 }) {
return (
< div
className = { styles . skeletonContainer }
role = "status"
aria-label = "Loading jobs"
>
{ /* Skeleton cards */ }
</ div >
)
}
Screen Reader Announcement
< div
className = { styles . skeletonContainer }
role = "status"
aria-live = "polite"
aria-busy = "true"
>
< span className = "sr-only" > Loading job listings... </ span >
{ /* Skeleton cards */ }
</ div >
Source Code
Location: src/components/Loading/Loading.jsx:3
JobListings Component that displays after loading
JobCard The component this skeleton represents
Optimize Re-renders
The skeleton component is simple and performant, but you can optimize further:
import { memo } from 'react'
export const JobCardSkeleton = memo ( function JobCardSkeleton ({ count = 3 }) {
// Component code
})
Lazy Loading
For very large lists, consider lazy loading:
import { JobCardSkeleton } from '@/components'
import { useInView } from 'react-intersection-observer'
function LazyJobList () {
const { ref , inView } = useInView ()
return (
<>
< JobListings jobs = { loadedJobs } />
< div ref = { ref } >
{ inView && < JobCardSkeleton count = { 3 } /> }
</ div >
</>
)
}