Progress indicators inform users about the status of ongoing processes, such as loading an app, submitting a form, or saving updates. They communicate an app’s state and indicate available actions, such as whether users can navigate away from the current screen.
All progressbar components require an aria-label or aria-labelledby
for accessibility. The label will probably be "Loading" for most use cases,
but could also be things like:
id of a Dialog titleid of a Buttonid of a heading elementCircular progress indicators display progress by animating an indicator along an invisible circular track in a clockwise direction. They can be applied directly to a surface, such as a button or card.
The default behavior for a circular progress bar is to be centered within the container element and spin indefinitely.
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { type ReactElement } from "react";
export default function SimpleCircularProgress(): ReactElement {
return <CircularProgress aria-label="Example" />;
}
If the progress state is quantifiable in a range from 0% - 100%, provide that
value to the CircularProgress component to create a determinate progress bar.
The progress bar will grow as the value expands to show the current state to the
user.
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { loop } from "@react-md/core/utils/loop";
import { type ReactElement, useEffect, useState } from "react";
export default function DeterminateCircularProgress(): ReactElement {
const progress = useProgress();
return (
<>
<CircularProgress aria-label="Example" value={10} />
<CircularProgress aria-label="Example" value={30} />
<CircularProgress aria-label="Example" value={70} />
<CircularProgress aria-label="Example" value={progress} />
</>
);
}
function useProgress(): number {
const [progress, setProgress] = useState(0);
useEffect(() => {
const interval = globalThis.setInterval(() => {
setProgress(
(prev) =>
loop({
min: 0,
max: 10,
value: prev / 10,
increment: true,
}) * 10,
);
}, 1000);
return () => {
globalThis.clearInterval(interval);
};
}, []);
return progress;
}
The CircularProgress also supports a dense size:
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { type ReactElement } from "react";
export default function CircularProgressSizes(): ReactElement {
return <CircularProgress aria-label="Example" dense />;
}
The CircularProgress bar supports all the different theme colors and the current text color.
import { Box } from "@react-md/core/box/Box";
import { Switch } from "@react-md/core/form/Switch";
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { type ReactElement, useState } from "react";
export default function CircularProgressTheme(): ReactElement {
const [checked, setChecked] = useState(false);
const value = checked ? undefined : 30;
return (
<>
<CircularProgress aria-label="Example" value={value} theme="primary" />
<CircularProgress aria-label="Example" value={value} theme="secondary" />
<CircularProgress aria-label="Example" value={value} theme="warning" />
<CircularProgress aria-label="Example" value={value} theme="success" />
<CircularProgress aria-label="Example" value={value} theme="error" />
<CircularProgress
aria-label="Example"
value={checked ? undefined : 30}
theme="current-color"
/>
<Box disablePadding fullWidth>
<Switch
label="Run"
checked={checked}
onChange={(event) => {
setChecked(event.currentTarget.checked);
}}
/>
</Box>
</>
);
}
When running CPU intensive tasks, the CircularProgress animation might appear
sluggish because it animates using a rotate and a scaling stroke-dashoffset. It
is recommended to move CPU intensive tasks to a
Web Worker
when possible, but the animation can be simplified to only a rotation by
enabling the disableShrink prop.
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { type ReactElement } from "react";
export default function PerformanceConcerns(): ReactElement {
return <CircularProgress aria-label="Example" disableShrink />;
}
Linear progress indicators support both determinate and indeterminate operations.
import { LinearProgress } from "@react-md/core/progress/LinearProgress";
import { type ReactElement } from "react";
export default function SimpleLinearProgress(): ReactElement {
return <LinearProgress aria-label="Example" />;
}
The LinearProgress bar kind of supports all the different theme colors and
the current text color. The colors will probably need to be modified to be more
visible based on the background color.
import { Box } from "@react-md/core/box/Box";
import { Switch } from "@react-md/core/form/Switch";
import { LinearProgress } from "@react-md/core/progress/LinearProgress";
import { type ReactElement, useState } from "react";
export default function LinearProgressTheme(): ReactElement {
const [checked, setChecked] = useState(false);
const value = checked ? undefined : 30;
return (
<>
<LinearProgress aria-label="Example" value={value} theme="primary" />
<LinearProgress aria-label="Example" value={value} theme="secondary" />
<LinearProgress aria-label="Example" value={value} theme="warning" />
<LinearProgress aria-label="Example" value={value} theme="success" />
<LinearProgress aria-label="Example" value={value} theme="error" />
<LinearProgress
aria-label="Example"
value={checked ? undefined : 30}
theme="current-color"
/>
<Box disablePadding fullWidth>
<Switch
label="Run"
checked={checked}
onChange={(event) => {
setChecked(event.currentTarget.checked);
}}
/>
</Box>
</>
);
}
The determinate circular and linear progress bars animate to the next value after 200ms
which might cause a delay if the value updates more quickly than that. In those cases,
enable the disableTransition prop.
"use client";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { Switch } from "@react-md/core/form/Switch";
import { CircularProgress } from "@react-md/core/progress/CircularProgress";
import { LinearProgress } from "@react-md/core/progress/LinearProgress";
import { type ReactElement, useEffect, useState } from "react";
export default function DisableDeterminateTransition(): ReactElement {
const { progress, toggle, restart, running } = useProgress();
return (
<Box stacked align="start" fullWidth>
<CircularProgress
aria-label="Example"
value={progress}
disableTransition
/>
<LinearProgress aria-label="Example" value={progress} disableTransition />
<Switch label="Run" checked={running} onChange={toggle} />
<Button onClick={restart}>Restart</Button>
</Box>
);
}
const UPDATE_INTERVAL = 10;
interface ProgressControls {
toggle: () => void;
restart: () => void;
running: boolean;
progress: number;
}
function useProgress(): ProgressControls {
const [state, setState] = useState({
running: false,
progress: 0,
});
const { running, progress } = state;
useEffect(() => {
if (!running) {
return;
}
const timeout = globalThis.setTimeout(() => {
const nextProgress = Math.min(100, progress + 0.1);
setState({
running: progress !== nextProgress && progress !== 100,
progress: nextProgress,
});
}, UPDATE_INTERVAL);
return () => {
globalThis.clearTimeout(timeout);
};
}, [progress, running]);
return {
toggle: () => {
setState((prev) => ({ ...prev, running: !prev.running }));
},
restart: () => {
setState({ running: false, progress: 0 });
},
running,
progress,
};
}
Check out the Async Button to see how circular and linear progress bars can be rendered within buttons.