Building Responsive Loading Placeholders with MUI Skeleton in React Dashboards
When developing data-driven React dashboards, handling the loading state is crucial for delivering a polished user experience. Blank screens or generic spinners can make your application feel unresponsive and unprofessional. This is where MUI's Skeleton component comes in—it allows you to create elegant, animated loading placeholders that mimic the actual content structure, providing users with a preview of what's coming.
In this comprehensive guide, I'll walk you through using MUI Skeleton to create sophisticated loading states for your dashboard components. By the end, you'll understand how to implement skeleton loaders that match your content layout, customize their appearance, and integrate them seamlessly into your React application flow.
Learning Objectives
After reading this article, you'll be able to:
- Understand the purpose and benefits of skeleton loading patterns in modern web applications
- Implement basic and complex skeleton loaders using MUI's Skeleton component
- Create responsive dashboard loading states that match your content structure
- Customize skeletons with different variants, animations, and styling options
- Implement advanced loading state patterns using composition techniques
- Apply best practices for performance and accessibility with skeleton loaders
Understanding MUI Skeleton Component
The Skeleton component from Material UI provides a way to display a placeholder preview of your content before the data is loaded. Instead of showing a traditional spinner or loading indicator, skeletons mimic the shape and structure of your content, creating a smoother perceived loading experience.
MUI Skeletons are particularly effective in dashboards where multiple data components load simultaneously. They reduce the perception of waiting time and prevent layout shifts when content appears, which significantly improves the user experience.
Core Functionality and Props
The Skeleton component is highly customizable through its props, allowing you to create placeholders that closely match your actual content. Let's explore the key props that control its behavior:
Prop | Type | Default | Description |
---|---|---|---|
animation | 'pulse' | 'wave' | false | 'pulse' | The animation effect applied to the skeleton |
children | node | - | Optional children to render when not in loading state |
height | number | string | - | Height of the skeleton |
variant | 'text' | 'rectangular' | 'circular' | 'rounded' | 'text' | The shape variant of the skeleton |
width | number | string | - | Width of the skeleton |
sx | object | - | The system prop for custom styling |
Understanding these props is essential for creating skeletons that accurately represent your content. For example, using the variant
prop, you can create text lines, rectangular areas for cards or images, and circular shapes for avatars or profile pictures.
Skeleton Variants
MUI Skeleton offers several variants to match different UI elements:
-
Text variant: The default option, creates a text-like skeleton with rounded edges, perfect for mimicking paragraphs and text content.
-
Rectangular variant: Creates a sharp-edged rectangular shape, ideal for cards, images, or content blocks.
-
Circular variant: Creates a perfect circle, suitable for avatars, icons, or circular UI elements.
-
Rounded variant: Creates a rectangular shape with rounded corners, useful for buttons or rounded containers.
Each variant helps you match the skeleton's appearance to the actual UI element it's replacing during loading.
Animation Options
Animations make skeleton loaders more engaging and indicate that content is loading. MUI offers two animation types:
-
Pulse: The default animation that gradually changes opacity to create a pulsing effect.
-
Wave: A left-to-right wave animation that sweeps across the skeleton.
You can also disable animations entirely by setting animation={false}
, which might be preferable for performance reasons or to accommodate users with motion sensitivity.
Setting Up Your Project
Before diving into implementation, let's set up a React project with Material UI. If you already have a project, you can skip to the next section.
Creating a New React Project
First, let's create a new React application using Create React App:
npx create-react-app mui-skeleton-dashboard
cd mui-skeleton-dashboard
Installing Material UI
Next, install Material UI and its dependencies:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Project Structure
For our dashboard example, we'll create a simple structure:
src/
├── components/
│ ├── Dashboard.jsx
│ ├── StatCard.jsx
│ ├── DataTable.jsx
│ ├── ChartSection.jsx
│ └── ProfileSection.jsx
├── App.js
└── index.js
Now that our project is set up, let's start implementing skeleton loaders for our dashboard components.
Basic Skeleton Implementation
Let's begin with a simple implementation to understand how the Skeleton component works in practice.
Creating a Basic Text Skeleton
The most common use case is replacing text content. Here's how to create a basic text skeleton:
import React from 'react';
import { Skeleton, Box } from '@mui/material';
const BasicTextSkeleton = () => {
return (
<Box sx={{ width: '100%', maxWidth: 500 }}>
<Skeleton />
<Skeleton />
<Skeleton width="60%" />
</Box>
);
};
export default BasicTextSkeleton;
In this example, I'm creating three skeleton lines to mimic a paragraph of text. The third line is shorter (60% width), which creates a more natural text-like appearance. By default, the Skeleton component uses the 'text' variant and has a height that matches the line height of typography.
Creating a Card Skeleton
For dashboard cards, we need a more complex structure. Let's create a skeleton for a statistics card:
import React from 'react';
import { Skeleton, Card, CardContent, Box } from '@mui/material';
const StatCardSkeleton = () => {
return (
<Card sx={{ minWidth: 275, margin: 2 }}>
<CardContent>
<Skeleton variant="text" sx={{ fontSize: '1.5rem', width: '60%' }} />
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="text" sx={{ ml: 1, width: '40%' }} />
</Box>
<Skeleton variant="rectangular" height={60} sx={{ mt: 2 }} />
</CardContent>
</Card>
);
};
export default StatCardSkeleton;
This skeleton represents a card with a title, an icon with a label, and a rectangular area for data visualization. Notice how I'm using different variants to match the structure of the actual content.
Building a Complete Dashboard Skeleton
Now, let's implement a comprehensive skeleton loading state for an entire dashboard. We'll create placeholder components for each section of our dashboard.
Dashboard Layout Component
First, let's create our main Dashboard component that will manage the loading state:
import React, { useState, useEffect } from 'react';
import { Container, Grid, Box } from '@mui/material';
import StatCard from './StatCard';
import DataTable from './DataTable';
import ChartSection from './ChartSection';
import ProfileSection from './ProfileSection';
const Dashboard = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate API call
const fetchData = async () => {
try {
// In a real app, you would fetch your data here
await new Promise(resolve => setTimeout(resolve, 3000)); // 3 second delay
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setLoading(false);
}
};
fetchData();
}, []);
return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Grid container spacing={3}>
{/* Stat Cards Row */}
<Grid item xs={12} container spacing={3}>
<Grid item xs={12} sm={6} md={3}>
<StatCard
loading={loading}
title="Total Users"
value="2,573"
trend="+12%"
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<StatCard
loading={loading}
title="Revenue"
value="$45,678"
trend="+23%"
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<StatCard
loading={loading}
title="Tasks"
value="156"
trend="-8%"
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<StatCard
loading={loading}
title="Conversion"
value="28%"
trend="+4%"
/>
</Grid>
</Grid>
{/* Chart Section */}
<Grid item xs={12} md={8}>
<ChartSection loading={loading} />
</Grid>
{/* Profile Section */}
<Grid item xs={12} md={4}>
<ProfileSection loading={loading} />
</Grid>
{/* Data Table */}
<Grid item xs={12}>
<DataTable loading={loading} />
</Grid>
</Grid>
</Container>
);
};
export default Dashboard;
In this component, I've set up a loading state that simulates an API call with a 3-second delay. The loading state is passed to each child component, which will render either the actual content or a skeleton based on this prop.
Stat Card Component with Skeleton
Now, let's implement the StatCard component with a skeleton loading state:
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Skeleton
} from '@mui/material';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
const StatCard = ({ loading, title, value, trend }) => {
const isTrendPositive = trend && trend.includes('+');
if (loading) {
return (
<Card sx={{ height: '100%' }}>
<CardContent>
<Skeleton variant="text" width="60%" height={32} />
<Skeleton variant="text" width="40%" height={48} sx={{ my: 1 }} />
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Skeleton variant="circular" width={24} height={24} />
<Skeleton variant="text" width={60} sx={{ ml: 1 }} />
</Box>
</CardContent>
</Card>
);
}
return (
<Card sx={{ height: '100%' }}>
<CardContent>
<Typography color="textSecondary" gutterBottom>
{title}
</Typography>
<Typography variant="h4" component="div" sx={{ my: 1 }}>
{value}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{isTrendPositive ? (
<TrendingUpIcon color="success" fontSize="small" />
) : (
<TrendingDownIcon color="error" fontSize="small" />
)}
<Typography
variant="body2"
sx={{
ml: 1,
color: isTrendPositive ? 'success.main' : 'error.main'
}}
>
{trend}
</Typography>
</Box>
</CardContent>
</Card>
);
};
export default StatCard;
This component renders either a skeleton or the actual content based on the loading prop. The skeleton mimics the structure of the card, with placeholders for the title, value, and trend indicator.
Chart Section with Skeleton
For the chart section, we need a more complex skeleton structure:
import React from 'react';
import {
Paper,
Typography,
Box,
Skeleton
} from '@mui/material';
// In a real application, you would import a chart library
// import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
const ChartSection = ({ loading }) => {
if (loading) {
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column', height: 340 }}>
<Skeleton variant="text" width="50%" height={32} />
<Skeleton variant="text" width="30%" height={20} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" height={250} animation="wave" />
</Paper>
);
}
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column', height: 340 }}>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Monthly Revenue
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Last 6 months performance
</Typography>
<Box sx={{ height: 250, width: '100%' }}>
{/* In a real app, you would render your chart here */}
<Typography variant="body1" color="textSecondary" align="center" sx={{ mt: 10 }}>
Chart would be rendered here
</Typography>
</Box>
</Paper>
);
};
export default ChartSection;
In this component, I'm using a rectangular skeleton to represent the chart area. For a real application, you would integrate a charting library like Recharts or Chart.js.
Data Table with Skeleton
For the data table section, we need to create skeletons for both the table header and rows:
import React from 'react';
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
Skeleton,
Box
} from '@mui/material';
const DataTable = ({ loading }) => {
// Generate an array of 5 items for skeleton rows
const skeletonRows = Array.from(new Array(5));
if (loading) {
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
<Skeleton variant="text" width="40%" height={32} sx={{ mb: 2 }} />
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell align="right"><Skeleton variant="text" /></TableCell>
</TableRow>
</TableHead>
<TableBody>
{skeletonRows.map((_, index) => (
<TableRow key={index}>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell><Skeleton variant="text" /></TableCell>
<TableCell align="right"><Skeleton variant="text" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
);
}
// Sample data for the table
const rows = [
{ id: 1, date: '2023-05-01', customer: 'John Doe', product: 'Premium Plan', amount: '$99.99' },
{ id: 2, date: '2023-05-02', customer: 'Jane Smith', product: 'Basic Plan', amount: '$49.99' },
{ id: 3, date: '2023-05-03', customer: 'Bob Johnson', product: 'Enterprise Plan', amount: '$199.99' },
{ id: 4, date: '2023-05-04', customer: 'Alice Brown', product: 'Premium Plan', amount: '$99.99' },
{ id: 5, date: '2023-05-05', customer: 'Charlie Davis', product: 'Basic Plan', amount: '$49.99' },
];
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Recent Transactions
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Product</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.date}</TableCell>
<TableCell>{row.customer}</TableCell>
<TableCell>{row.product}</TableCell>
<TableCell>Completed</TableCell>
<TableCell align="right">{row.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
);
};
export default DataTable;
This component creates a skeleton for the table header and five rows of data. I'm using Array.from(new Array(5))
to generate an array of undefined values that I can map over to create the skeleton rows.
Profile Section with Skeleton
Finally, let's create a profile section with avatar and information:
import React from 'react';
import {
Paper,
Typography,
Avatar,
Box,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
Skeleton
} from '@mui/material';
import EmailIcon from '@mui/icons-material/Email';
import PhoneIcon from '@mui/icons-material/Phone';
import LocationOnIcon from '@mui/icons-material/LocationOn';
const ProfileSection = ({ loading }) => {
if (loading) {
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column', height: 340 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 3 }}>
<Skeleton variant="circular" width={80} height={80} />
<Skeleton variant="text" width={120} height={32} sx={{ mt: 1 }} />
<Skeleton variant="text" width={160} height={24} />
</Box>
<Divider />
<List>
{[1, 2, 3].map((item) => (
<ListItem key={item}>
<ListItemIcon>
<Skeleton variant="circular" width={24} height={24} />
</ListItemIcon>
<ListItemText
primary={<Skeleton variant="text" width={120} />}
secondary={<Skeleton variant="text" width={160} />}
/>
</ListItem>
))}
</List>
</Paper>
);
}
return (
<Paper sx={{ p: 2, display: 'flex', flexDirection: 'column', height: 340 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 3 }}>
<Avatar
sx={{ width: 80, height: 80, mb: 1 }}
alt="User Profile"
src="/static/images/avatar/1.jpg"
/>
<Typography variant="h6">Jane Doe</Typography>
<Typography variant="body2" color="textSecondary">
Product Manager
</Typography>
</Box>
<Divider />
<List>
<ListItem>
<ListItemIcon>
<EmailIcon />
</ListItemIcon>
<ListItemText
primary="Email"
secondary="jane.doe@example.com"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<PhoneIcon />
</ListItemIcon>
<ListItemText
primary="Phone"
secondary="+1 (555) 123-4567"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<LocationOnIcon />
</ListItemIcon>
<ListItemText
primary="Location"
secondary="San Francisco, CA"
/>
</ListItem>
</List>
</Paper>
);
};
export default ProfileSection;
This component creates a skeleton for a profile section with an avatar, name, title, and contact information. The skeleton mimics the structure of the actual content, including circular skeletons for the avatar and icons.
Advanced Skeleton Techniques
Now that we've covered the basics, let's explore some advanced techniques for using MUI Skeleton in your dashboard.
Creating Custom Skeleton Layouts
Sometimes, you need to create more complex skeleton layouts that don't match the built-in variants. You can compose multiple Skeleton components to create custom layouts:
import React from 'react';
import { Box, Skeleton, Card, CardContent, Grid } from '@mui/material';
const ComplexCardSkeleton = () => {
return (
<Card>
<CardContent>
<Grid container spacing={2}>
<Grid item xs={4}>
<Skeleton
variant="rectangular"
height={140}
sx={{ borderRadius: 1 }}
/>
</Grid>
<Grid item xs={8}>
<Skeleton variant="text" height={32} width="80%" />
<Skeleton variant="text" height={20} width="60%" sx={{ mt: 1 }} />
<Box sx={{ mt: 2 }}>
{[1, 2, 3].map((item) => (
<Skeleton
key={item}
variant="text"
height={16}
width={item === 3 ? '75%' : '100%'}
sx={{ mt: 1 }}
/>
))}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
<Skeleton variant="rectangular" width={80} height={32} sx={{ borderRadius: 1 }} />
<Skeleton variant="rectangular" width={80} height={32} sx={{ borderRadius: 1 }} />
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
);
};
export default ComplexCardSkeleton;
This example creates a complex card skeleton with an image placeholder, title, description, text lines, and buttons. By composing multiple Skeleton components with different variants and sizes, you can create highly customized loading states.
Responsive Skeletons
To ensure your skeletons look good on all screen sizes, you can make them responsive using MUI's responsive styling:
import React from 'react';
import { Box, Skeleton, useTheme, useMediaQuery } from '@mui/material';
const ResponsiveSkeleton = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 2 }}>
<Skeleton
variant="rectangular"
width={isMobile ? '100%' : 200}
height={isMobile ? 120 : 200}
/>
<Box sx={{ flex: 1 }}>
<Skeleton variant="text" height={32} />
<Skeleton variant="text" height={20} width="80%" sx={{ mt: 1 }} />
<Box sx={{ mt: 2, display: isMobile ? 'none' : 'block' }}>
{[1, 2, 3].map((item) => (
<Skeleton
key={item}
variant="text"
height={16}
width={item === 3 ? '75%' : '100%'}
sx={{ mt: 1 }}
/>
))}
</Box>
<Box sx={{ mt: 2, display: isMobile ? 'block' : 'none' }}>
<Skeleton variant="text" height={16} width="100%" sx={{ mt: 1 }} />
<Skeleton variant="text" height={16} width="75%" sx={{ mt: 1 }} />
</Box>
</Box>
</Box>
</Box>
);
};
export default ResponsiveSkeleton;
This component creates a responsive skeleton that changes its layout based on the screen size. On mobile devices, it stacks elements vertically and shows fewer text lines, while on larger screens, it uses a horizontal layout with more content.
Conditional Rendering with Children
The Skeleton component accepts children, which allows you to create conditional rendering patterns:
import React, { useState, useEffect } from 'react';
import { Skeleton, Card, CardContent, Typography, Avatar, Box } from '@mui/material';
const UserCard = ({ userId }) => {
const [loading, setLoading] = useState(true);
const [userData, setUserData] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
setUserData({
name: 'John Doe',
role: 'Software Engineer',
avatar: '/path/to/avatar.jpg'
});
setLoading(false);
} catch (error) {
console.error('Error fetching user:', error);
setLoading(false);
}
};
fetchUser();
}, [userId]);
return (
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Skeleton variant="circular" width={60} height={60} animation="wave">
{!loading && (
<Avatar
src={userData?.avatar}
alt={userData?.name}
sx={{ width: 60, height: 60 }}
/>
)}
</Skeleton>
<Box sx={{ ml: 2 }}>
<Skeleton variant="text" width={150} height={32}>
{!loading && (
<Typography variant="h6">{userData?.name}</Typography>
)}
</Skeleton>
<Skeleton variant="text" width={100} height={20}>
{!loading && (
<Typography variant="body2" color="textSecondary">
{userData?.role}
</Typography>
)}
</Skeleton>
</Box>
</Box>
</CardContent>
</Card>
);
};
export default UserCard;
In this example, I'm using the children prop to render the actual content inside the Skeleton component when loading is complete. This approach ensures that the skeleton and actual content have the same dimensions, preventing layout shifts when content loads.
Customizing Skeleton Appearance with Theming
You can customize the appearance of all Skeleton components in your application using MUI's theming system:
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import Dashboard from './components/Dashboard';
const theme = createTheme({
components: {
MuiSkeleton: {
styleOverrides: {
root: {
backgroundColor: 'rgba(210, 210, 210, 0.2)',
'&::after': {
background: 'linear-gradient(90deg, transparent, rgba(210, 210, 210, 0.4), transparent)',
},
},
rectangular: {
borderRadius: 4,
},
circular: {
borderRadius: '50%',
},
},
},
},
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Dashboard />
</ThemeProvider>
);
}
export default App;
This example customizes the appearance of all Skeleton components in the application by modifying their background color, animation gradient, and border radius through the theme. This approach ensures consistent styling across your application.
Performance Optimization
When using skeletons in your dashboard, it's important to consider performance implications, especially if you have many skeleton components rendering simultaneously.
Reducing Skeleton Animation Impact
Skeleton animations can impact performance, especially on low-end devices. Here are some strategies to optimize performance:
import React from 'react';
import { Box, Skeleton, useMediaQuery, useTheme } from '@mui/material';
const OptimizedSkeletons = () => {
const theme = useTheme();
const isLowPowerMode = useMediaQuery('(prefers-reduced-motion: reduce)');
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// Disable animations on mobile or when reduced motion is preferred
const animationValue = (isLowPowerMode || isMobile) ? false : 'pulse';
// Reduce the number of skeleton elements on mobile
const skeletonCount = isMobile ? 3 : 5;
return (
<Box>
{Array.from(new Array(skeletonCount)).map((_, index) => (
<Skeleton
key={index}
animation={animationValue}
height={24}
sx={{ my: 1 }}
/>
))}
</Box>
);
};
export default OptimizedSkeletons;
This component optimizes skeleton performance by:
- Disabling animations on mobile devices or when the user has requested reduced motion
- Reducing the number of skeleton elements on smaller screens
- Using simpler animations (pulse instead of wave) for better performance
Lazy Loading Skeletons
For complex dashboards with many sections, you can lazy load both the actual components and their skeletons:
import React, { lazy, Suspense, useState, useEffect } from 'react';
import { Box, CircularProgress } from '@mui/material';
// Lazy load the actual component
const ComplexDataVisualization = lazy(() =>
import('./ComplexDataVisualization')
);
// Lazy load the skeleton separately
const ComplexDataSkeleton = lazy(() =>
import('./ComplexDataSkeleton')
);
const LazyLoadedSection = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate data loading
const timer = setTimeout(() => {
setLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
return (
<Box>
{loading ? (
<Suspense fallback={<CircularProgress />}>
<ComplexDataSkeleton />
</Suspense>
) : (
<Suspense fallback={<CircularProgress />}>
<ComplexDataVisualization />
</Suspense>
)}
</Box>
);
};
export default LazyLoadedSection;
This approach lazy loads both the skeleton and the actual component, which can improve initial load performance for complex dashboards. The skeleton itself is only loaded when needed, reducing the initial bundle size.
Accessibility Considerations
Ensuring your skeleton loaders are accessible is crucial for providing a good user experience to all users, including those using assistive technologies.
Making Skeletons Accessible
Here are some key practices for making skeleton loaders accessible:
import React from 'react';
import { Skeleton, Box, Typography } from '@mui/material';
const AccessibleSkeleton = ({ loading, children }) => {
if (loading) {
return (
<Box
role="status"
aria-busy="true"
aria-live="polite"
>
<Typography variant="srOnly">
Loading content, please wait.
</Typography>
<Skeleton variant="text" height={40} />
<Skeleton variant="text" />
<Skeleton variant="text" />
</Box>
);
}
return children;
};
export default AccessibleSkeleton;
This component enhances accessibility by:
- Using appropriate ARIA attributes (
role="status"
,aria-busy="true"
,aria-live="polite"
) - Adding a screen reader-only message explaining that content is loading
- Maintaining the same structure as the actual content
Respecting User Preferences
It's important to respect user preferences for animations and motion:
import React from 'react';
import { Skeleton, Box, useMediaQuery } from '@mui/material';
const MotionSensitiveSkeleton = () => {
// Check if the user has requested reduced motion
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
// Disable animation if reduced motion is preferred
const animation = prefersReducedMotion ? false : 'pulse';
return (
<Box>
<Skeleton variant="text" animation={animation} />
<Skeleton variant="text" animation={animation} />
<Skeleton variant="rectangular" height={200} animation={animation} />
</Box>
);
};
export default MotionSensitiveSkeleton;
This component checks if the user has requested reduced motion through their operating system settings and disables skeleton animations accordingly, which is important for users with vestibular disorders or motion sensitivity.
Best Practices and Common Issues
Let's explore some best practices for using skeleton loaders effectively and address common issues you might encounter.
Best Practices
1. Match the Skeleton to Your Content
The most effective skeleton loaders closely match the structure and layout of your actual content:
import React from 'react';
import { Card, CardContent, CardActions, Skeleton, Box, Button } from '@mui/material';
const ProductCardSkeleton = () => {
return (
<Card>
{/* Image placeholder with correct aspect ratio */}
<Skeleton
variant="rectangular"
height={0}
sx={{
paddingTop: '56.25%', // 16:9 aspect ratio
width: '100%'
}}
/>
<CardContent>
{/* Title - larger and shorter than description */}
<Skeleton variant="text" height={28} width="80%" />
{/* Price - smaller and shorter */}
<Skeleton variant="text" height={24} width="40%" sx={{ mt: 1 }} />
{/* Description - multiple lines */}
<Box sx={{ mt: 2 }}>
<Skeleton variant="text" />
<Skeleton variant="text" />
<Skeleton variant="text" width="80%" />
</Box>
</CardContent>
<CardActions>
{/* Button placeholders */}
<Skeleton variant="rectangular" width={90} height={36} sx={{ borderRadius: 1 }} />
<Skeleton variant="rectangular" width={90} height={36} sx={{ borderRadius: 1, ml: 1 }} />
</CardActions>
</Card>
);
};
export default ProductCardSkeleton;
This example creates a skeleton that accurately reflects the structure of a product card, including the image aspect ratio, title, price, description, and buttons.
2. Use Consistent Loading Times
Ensure your skeletons are visible for a reasonable amount of time—not too short or too long:
import React, { useState, useEffect } from 'react';
import { Box, Typography, Skeleton } from '@mui/material';
const LoadingContent = ({ children }) => {
const [loading, setLoading] = useState(true);
const [content, setContent] = useState(null);
useEffect(() => {
// Store the content to avoid flash of loading state
setContent(children);
// Set minimum loading time (300ms) to avoid flicker
const minLoadingTime = 300;
const startTime = Date.now();
const timer = setTimeout(() => {
const elapsedTime = Date.now() - startTime;
if (elapsedTime < minLoadingTime) {
// If data loaded too quickly, wait until minimum time has passed
setTimeout(() => {
setLoading(false);
}, minLoadingTime - elapsedTime);
} else {
setLoading(false);
}
}, 2000); // Simulated data loading time
return () => clearTimeout(timer);
}, [children]);
if (loading) {
return (
<Box>
<Skeleton variant="text" height={32} />
<Skeleton variant="text" />
<Skeleton variant="text" />
</Box>
);
}
return content;
};
const ExampleUsage = () => {
return (
<LoadingContent>
<Box>
<Typography variant="h5">Content Title</Typography>
<Typography variant="body1">
This is the actual content that will be displayed after loading.
</Typography>
<Typography variant="body1">
More content here...
</Typography>
</Box>
</LoadingContent>
);
};
export default ExampleUsage;
This component ensures a consistent loading experience by:
- Setting a minimum loading time to prevent flickering for fast-loading content
- Caching the actual content to avoid re-rendering
- Using a simulated loading time for demonstration purposes (in a real app, this would be your API call)
Common Issues and Solutions
1. Layout Shifts When Content Loads
A common issue with skeleton loaders is layout shifts when the actual content replaces the skeleton:
import React, { useState, useEffect } from 'react';
import { Card, CardContent, Typography, Skeleton, Box } from '@mui/material';
// Problem: Layout shift when content loads
const ProblemExample = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => setLoading(false), 2000);
}, []);
return (
<Card>
<CardContent>
{loading ? (
<Skeleton variant="text" height={24} />
) : (
<Typography variant="h5">
This title might be a different height than the skeleton
</Typography>
)}
{loading ? (
<Box sx={{ mt: 2 }}>
<Skeleton variant="text" />
<Skeleton variant="text" />
</Box>
) : (
<Typography variant="body1" sx={{ mt: 2 }}>
The content might be longer or shorter than what the skeleton suggested,
causing a layout shift when it loads.
</Typography>
)}
</CardContent>
</Card>
);
};
// Solution: Maintain consistent dimensions
const SolutionExample = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => setLoading(false), 2000);
}, []);
return (
<Card>
<CardContent>
<Box sx={{ height: 32 }}>
{loading ? (
<Skeleton variant="text" height={24} />
) : (
<Typography variant="h5">
This title has a consistent container height
</Typography>
)}
</Box>
<Box sx={{ mt: 2, height: 80 }}>
{loading ? (
<>
<Skeleton variant="text" />
<Skeleton variant="text" />
</>
) : (
<Typography variant="body1">
By wrapping in a Box with fixed height, we prevent layout shifts
when the content loads, regardless of content length.
</Typography>
)}
</Box>
</CardContent>
</Card>
);
};
export default SolutionExample;
The solution involves:
- Wrapping both the skeleton and content in containers with consistent heights
- Ensuring that the skeleton dimensions closely match the actual content
- Using fixed dimensions for critical layout elements
2. Performance Issues with Many Skeletons
When rendering many skeletons, you might encounter performance issues:
import React, { useState, useEffect, memo } from 'react';
import { Box, Grid, Skeleton } from '@mui/material';
// Memoized skeleton item to prevent unnecessary re-renders
const MemoizedSkeletonItem = memo(({ animation }) => (
<Box sx={{ p: 1 }}>
<Skeleton variant="rectangular" height={200} animation={animation} />
<Skeleton variant="text" height={24} sx={{ mt: 1 }} animation={animation} />
<Skeleton variant="text" width="60%" animation={animation} />
</Box>
));
const PerformantSkeletonGrid = ({ itemCount = 12 }) => {
const [loading, setLoading] = useState(true);
// Reduce animation complexity for many items
const animation = itemCount > 20 ? false : 'pulse';
useEffect(() => {
setTimeout(() => setLoading(false), 2000);
}, []);
if (loading) {
return (
<Grid container spacing={2}>
{Array.from(new Array(itemCount)).map((_, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<MemoizedSkeletonItem animation={animation} />
</Grid>
))}
</Grid>
);
}
return (
<Grid container spacing={2}>
{/* Actual content would go here */}
<Grid item xs={12}>
<Box sx={{ p: 2, textAlign: 'center' }}>
Content loaded
</Box>
</Grid>
</Grid>
);
};
export default PerformantSkeletonGrid;
This example improves performance by:
- Memoizing individual skeleton items to prevent unnecessary re-renders
- Disabling animations when rendering many items
- Using simpler animation types for better performance
- Limiting the number of skeleton items displayed
Wrapping Up
Skeleton loaders are a powerful UI pattern for improving perceived performance and user experience in React dashboards. MUI's Skeleton component provides a flexible and customizable solution for implementing this pattern, allowing you to create loading states that closely match your actual content.
In this guide, we've explored how to implement basic and complex skeleton loaders, optimize their performance, ensure accessibility, and address common issues. By applying these techniques, you can create polished, responsive, and user-friendly loading experiences for your dashboard applications.
Remember that the best skeleton loaders are those that accurately reflect the structure of your content, maintain consistent dimensions to prevent layout shifts, and respect user preferences for animations and motion. With these principles in mind, you can create loading states that enhance rather than detract from your application's user experience.