Reference
Progress
Progress indicators, often referred to as spinners, convey either an indefinite wait period or show the duration of a task.
Progress indicators provide users with updates on the status of ongoing tasks, such as application loading, form submission, or data saving.
- Determinate indicators illustrate the expected duration of a process.
- Indeterminate indicators depict an unspecified waiting period.
The components' animations leverage CSS to the fullest to ensure functionality even before JavaScript is fully loaded.
Circular
Circular indeterminate
import * as React from 'react'; import CircularProgress from '@mui/material/CircularProgress'; import Box from '@mui/material/Box'; export default function CircularIndeterminate() { return ( <Box sx={{ display: 'flex' }}> <CircularProgress /> </Box> ); }
Circular color
import * as React from 'react'; import Stack from '@mui/material/Stack'; import CircularProgress from '@mui/material/CircularProgress'; export default function CircularColor() { return ( <Stack sx={{ color: 'grey.500' }} spacing={2} direction="row"> <CircularProgress color="secondary" /> <CircularProgress color="success" /> <CircularProgress color="inherit" /> </Stack> ); }
Circular size
import * as React from 'react'; import Stack from '@mui/material/Stack'; import CircularProgress from '@mui/material/CircularProgress'; export default function CircularSize() { return ( <Stack spacing={2} direction="row" alignItems="center"> <CircularProgress size="30px" /> <CircularProgress size={40} /> <CircularProgress size="3rem" /> </Stack> ); }
Circular determinate
import * as React from 'react'; import Stack from '@mui/material/Stack'; import CircularProgress from '@mui/material/CircularProgress'; export default function CircularDeterminate() { const [progress, setProgress] = React.useState(0); React.useEffect(() => { const timer = setInterval(() => { setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10)); }, 800); return () => { clearInterval(timer); }; }, []); return ( <Stack spacing={2} direction="row"> <CircularProgress variant="determinate" value={25} /> <CircularProgress variant="determinate" value={50} /> <CircularProgress variant="determinate" value={75} /> <CircularProgress variant="determinate" value={100} /> <CircularProgress variant="determinate" value={progress} /> </Stack> ); }
Interactive integration
import * as React from 'react'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import { green } from '@mui/material/colors'; import Button from '@mui/material/Button'; import Fab from '@mui/material/Fab'; import CheckIcon from '@mui/icons-material/Check'; import SaveIcon from '@mui/icons-material/Save'; export default function CircularIntegration() { const [loading, setLoading] = React.useState(false); const [success, setSuccess] = React.useState(false); const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined); const buttonSx = { ...(success && { bgcolor: green[500], '&:hover': { bgcolor: green[700], }, }), }; React.useEffect(() => { return () => { clearTimeout(timer.current); }; }, []); const handleButtonClick = () => { if (!loading) { setSuccess(false); setLoading(true); timer.current = setTimeout(() => { setSuccess(true); setLoading(false); }, 2000); } }; return ( <Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ m: 1, position: 'relative' }}> <Fab aria-label="save" color="primary" sx={buttonSx} onClick={handleButtonClick} > {success ? <CheckIcon /> : <SaveIcon />} </Fab> {loading && ( <CircularProgress size={68} sx={{ color: green[500], position: 'absolute', top: -6, left: -6, zIndex: 1, }} /> )} </Box> <Box sx={{ m: 1, position: 'relative' }}> <Button variant="contained" sx={buttonSx} disabled={loading} onClick={handleButtonClick} > Accept terms </Button> {loading && ( <CircularProgress size={24} sx={{ color: green[500], position: 'absolute', top: '50%', left: '50%', marginTop: '-12px', marginLeft: '-12px', }} /> )} </Box> </Box> ); }
Circular with label
10%
import * as React from 'react'; import CircularProgress, { CircularProgressProps, } from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; function CircularProgressWithLabel( props: CircularProgressProps & { value: number }, ) { return ( <Box sx={{ position: 'relative', display: 'inline-flex' }}> <CircularProgress variant="determinate" {...props} /> <Box sx={{ top: 0, left: 0, bottom: 0, right: 0, position: 'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > <Typography variant="caption" component="div" sx={{ color: 'text.secondary' }} >{`${Math.round(props.value)}%`}</Typography> </Box> </Box> ); } export default function CircularWithValueLabel() { const [progress, setProgress] = React.useState(10); React.useEffect(() => { const timer = setInterval(() => { setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10)); }, 800); return () => { clearInterval(timer); }; }, []); return <CircularProgressWithLabel value={progress} />; }
Linear
Linear indeterminate
import * as React from 'react'; import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; export default function LinearIndeterminate() { return ( <Box sx={{ width: '100%' }}> <LinearProgress /> </Box> ); }
Linear color
import * as React from 'react'; import Stack from '@mui/material/Stack'; import LinearProgress from '@mui/material/LinearProgress'; export default function LinearColor() { return ( <Stack sx={{ width: '100%', color: 'grey.500' }} spacing={2}> <LinearProgress color="secondary" /> <LinearProgress color="success" /> <LinearProgress color="inherit" /> </Stack> ); }
Linear determinate
import * as React from 'react'; import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; export default function LinearDeterminate() { const [progress, setProgress] = React.useState(0); React.useEffect(() => { const timer = setInterval(() => { setProgress((oldProgress) => { if (oldProgress === 100) { return 0; } const diff = Math.random() * 10; return Math.min(oldProgress + diff, 100); }); }, 500); return () => { clearInterval(timer); }; }, []); return ( <Box sx={{ width: '100%' }}> <LinearProgress variant="determinate" value={progress} /> </Box> ); }
Linear buffer
import * as React from 'react'; import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; export default function LinearBuffer() { const [progress, setProgress] = React.useState(0); const [buffer, setBuffer] = React.useState(10); const progressRef = React.useRef(() => {}); React.useEffect(() => { progressRef.current = () => { if (progress === 100) { setProgress(0); setBuffer(10); } else { setProgress(progress + 1); if (buffer < 100 && progress % 5 === 0) { const newBuffer = buffer + 1 + Math.random() * 10; setBuffer(newBuffer > 100 ? 100 : newBuffer); } } }; }); React.useEffect(() => { const timer = setInterval(() => { progressRef.current(); }, 100); return () => { clearInterval(timer); }; }, []); return ( <Box sx={{ width: '100%' }}> <LinearProgress variant="buffer" value={progress} valueBuffer={buffer} /> </Box> ); }
Linear with label
10%
import * as React from 'react'; import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) { return ( <Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ width: '100%', mr: 1 }}> <LinearProgress variant="determinate" {...props} /> </Box> <Box sx={{ minWidth: 35 }}> <Typography variant="body2" sx={{ color: 'text.secondary' }} >{`${Math.round(props.value)}%`}</Typography> </Box> </Box> ); } export default function LinearWithValueLabel() { const [progress, setProgress] = React.useState(10); React.useEffect(() => { const timer = setInterval(() => { setProgress((prevProgress) => (prevProgress >= 100 ? 10 : prevProgress + 10)); }, 800); return () => { clearInterval(timer); }; }, []); return ( <Box sx={{ width: '100%' }}> <LinearProgressWithLabel value={progress} /> </Box> ); }
Non-standard ranges
Progress components accept values between 0 and 100, simplifying accessibility for screen-reader users with these as default minimum and maximum values. However, if your data source uses a different range, you can transform those values to fit the 0–100 scale as shown below:
// MIN = Minimum expected value // MAX = Maximum expected value // Method to normalize the values (MIN / MAX could be integrated) const normalise = (value) => ((value - MIN) * 100) / (MAX - MIN); // Example component that utilizes the `normalise` function at the point of render. function Progress(props) { return ( <React.Fragment> <CircularProgress variant="determinate" value={normalise(props.value)} /> <LinearProgress variant="determinate" value={normalise(props.value)} /> </React.Fragment> ); }
Customization
Below are examples of tailoring the component. Further details can be found in the overrides documentation page.
import * as React from 'react'; import { styled } from '@mui/material/styles'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import CircularProgress, { circularProgressClasses, CircularProgressProps, } from '@mui/material/CircularProgress'; import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({ height: 10, borderRadius: 5, [`&.${linearProgressClasses.colorPrimary}`]: { backgroundColor: theme.vars.palette.grey[200], ...theme.applyStyles('dark', { backgroundColor: theme.vars.palette.grey[800], }), }, [`& .${linearProgressClasses.bar}`]: { borderRadius: 5, backgroundColor: '#1a90ff', ...theme.applyStyles('dark', { backgroundColor: '#308fe8', }), }, })); // Inspired by the former Facebook spinners. function FacebookCircularProgress(props: CircularProgressProps) { return ( <Box sx={{ position: 'relative' }}> <CircularProgress variant="determinate" sx={(theme) => ({ color: theme.vars.palette.grey[200], ...theme.applyStyles('dark', { color: theme.vars.palette.grey[800], }), })} size={40} thickness={4} {...props} value={100} /> <CircularProgress variant="indeterminate" disableShrink sx={(theme) => ({ color: '#1a90ff', animationDuration: '550ms', position: 'absolute', left: 0, [`& .${circularProgressClasses.circle}`]: { strokeLinecap: 'round', }, ...theme.applyStyles('dark', { color: '#308fe8', }), })} size={40} thickness={4} {...props} /> </Box> ); } // From https://github.com/mui/material-ui/issues/9496#issuecomment-959408221 function GradientCircularProgress() { return ( <React.Fragment> <svg width={0} height={0}> <defs> <linearGradient id="my_gradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" stopColor="#e01cd5" /> <stop offset="100%" stopColor="#1CB5E0" /> </linearGradient> </defs> </svg> <CircularProgress sx={{ 'svg circle': { stroke: 'url(#my_gradient)' } }} /> </React.Fragment> ); } export default function CustomizedProgressBars() { return ( <Stack spacing={2} sx={{ flexGrow: 1 }}> <FacebookCircularProgress /> <GradientCircularProgress /> <br /> <BorderLinearProgress variant="determinate" value={50} /> </Stack> ); }
Delaying appearance
There are three critical thresholds to understand regarding response times. The ripple effect in the
ButtonBase
component creates an immediate sense of responsiveness. Typically, no additional feedback is needed for delays between 0.1 and 1.0 seconds. Beyond 1.0 second, displaying a loader helps maintain the user’s thought flow.import * as React from 'react'; import Box from '@mui/material/Box'; import Fade from '@mui/material/Fade'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; export default function DelayingAppearance() { const [loading, setLoading] = React.useState(false); const [query, setQuery] = React.useState('idle'); const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined); React.useEffect( () => () => { clearTimeout(timerRef.current); }, [], ); const handleClickLoading = () => { setLoading((prevLoading) => !prevLoading); }; const handleClickQuery = () => { if (timerRef.current) { clearTimeout(timerRef.current); } if (query !== 'idle') { setQuery('idle'); return; } setQuery('progress'); timerRef.current = setTimeout(() => { setQuery('success'); }, 2000); }; return ( <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box sx={{ height: 40 }}> <Fade in={loading} style={{ transitionDelay: loading ? '800ms' : '0ms', }} unmountOnExit > <CircularProgress /> </Fade> </Box> <Button onClick={handleClickLoading} sx={{ m: 2 }}> {loading ? 'Stop loading' : 'Loading'} </Button> <Box sx={{ height: 40 }}> {query === 'success' ? ( <Typography>Success!</Typography> ) : ( <Fade in={query === 'progress'} style={{ transitionDelay: query === 'progress' ? '800ms' : '0ms', }} unmountOnExit > <CircularProgress /> </Fade> )} </Box> <Button onClick={handleClickQuery} sx={{ m: 2 }}> {query !== 'idle' ? 'Reset' : 'Simulate a load'} </Button> </Box> ); }
Limitations
High CPU load
During intense processing, you may notice the loss of stroke dash animations or inconsistent
CircularProgress
ring widths. To avoid blocking the main rendering thread, consider running demanding operations in a web worker or processing them in batches.When this isn’t feasible, using the
disableShrink
prop can help address the issue. Refer to this issue for more details.import * as React from 'react'; import CircularProgress from '@mui/material/CircularProgress'; export default function CircularUnderLoad() { return <CircularProgress disableShrink />; }
High frequency updates
The
LinearProgress
component employs a CSS transform property transition for smooth value updates, with a default duration of 200ms. If a parent component updates the value
prop too rapidly, there will be at least a 200ms delay before the progress bar fully reflects the change.For scenarios requiring 30 or more re-renders per second, we suggest disabling the transition as shown below:
.MuiLinearProgress-bar { transition: none; }