Skip to main content
react-md

Progress

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:

  • An id of a Dialog title
  • An id of a Button
  • An id of a heading element
  • etc

Circular Progress

Circular 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" />;
}

Press Enter to start editing.

Determinate Circular Progress

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 = window.setInterval(() => {
      setProgress(
        (prev) =>
          loop({
            min: 0,
            max: 10,
            value: prev / 10,
            increment: true,
          }) * 10,
      );
    }, 1000);

    return () => {
      window.clearInterval(interval);
    };
  }, []);

  return progress;
}

Press Enter to start editing.

Circular Progress Sizes

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 />;
}

Press Enter to start editing.

Circular Progress Theme

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>
    </>
  );
}

Press Enter to start editing.

Performance Concerns

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 />;
}

Press Enter to start editing.

Linear Progress

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" />;
}

Press Enter to start editing.

Linear Progress Theme

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>
    </>
  );
}

Press Enter to start editing.

Disable Determinate Transition

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 = window.setTimeout(() => {
      const nextProgress = Math.min(100, progress + 0.1);
      setState({
        running: progress !== nextProgress && progress !== 100,
        progress: nextProgress,
      });
    }, UPDATE_INTERVAL);
    return () => {
      window.clearTimeout(timeout);
    };
  }, [progress, running]);

  return {
    toggle: () => {
      setState((prev) => ({ ...prev, running: !prev.running }));
    },
    restart: () => {
      setState({ running: false, progress: 0 });
    },
    running,
    progress,
  };
}

Press Enter to start editing.

Within Buttons

Check out the Async Button to see how circular and linear progress bars can be rendered within buttons.