Progress

Material Design 3 progress indicators with linear and circular variants

Progress

Progress indicators inform users about the status of ongoing processes, such as loading data, submitting a form, or saving updates. @duskmoon-dev/core provides both linear and circular progress indicators following Material Design 3 guidelines.

Basic Usage

Linear Progress (Determinate)

Linear Progress (Determinate)

Circular Progress (Determinate)

Circular Progress (Determinate)

Linear Progress

Determinate Progress

Use determinate progress when the loading time is known:

Determinate Progress

Indeterminate Progress

Use indeterminate progress when the loading time is unknown:

Indeterminate Progress

Progress with Label

Display the progress percentage:

Progress with Label

60%

Buffer Progress

For media players showing buffered content:

Buffer Progress

Circular Progress

Determinate Circular

Determinate Circular

Note: For a circle with radius 20, the circumference is 2πr = 125.6. To show 40% progress, set stroke-dashoffset to 125.6 * (1 - 0.4) = 75.36.

Indeterminate Circular

Indeterminate Circular

Circular with Percentage Label

Circular with Percentage Label

70%

Color Variants

Progress indicators support all theme colors:

Primary (Default)

Primary Color

Secondary

Secondary Color

Tertiary

Tertiary Color

Semantic Colors

Use semantic colors to indicate status:

Semantic Colors

Size Variants

Linear Progress Sizes

Linear Progress Sizes

Circular Progress Sizes

Circular Progress Sizes

Best Practices

When to Use

Linear Progress:

  • File uploads/downloads
  • Form submissions
  • Page loading
  • Multi-step processes

Circular Progress:

  • Loading content in a specific area
  • Button loading states
  • Refreshing data
  • Compact spaces

Determinate vs Indeterminate

Use Determinate When:

  • You can calculate the percentage of completion
  • Users need to know how long they’ll wait
  • The process has clear steps or milestones

Use Indeterminate When:

  • The loading time is unknown or variable
  • The process depends on external factors (network speed, server response)
  • Initial loading before progress can be calculated

Accessibility

  • Provide appropriate ARIA attributes for screen readers
  • Announce progress updates for long-running operations
  • Use semantic HTML where possible

Accessibility Examples

Visual Feedback

  • Always provide visual feedback for user actions
  • Use appropriate colors to indicate status (success, error, etc.)
  • Consider adding labels for clarity when showing specific percentages
  • Ensure sufficient contrast for accessibility

Framework Examples

React

interface ProgressProps {
  value?: number;
  max?: number;
  variant?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'error' | 'warning' | 'info';
  size?: 'sm' | 'md' | 'lg' | 'xl';
  indeterminate?: boolean;
  label?: boolean;
}

export function LinearProgress({
  value = 0,
  max = 100,
  variant = 'primary',
  size = 'md',
  indeterminate = false,
  label = false
}: ProgressProps) {
  const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
  const classes = [
    'progress',
    variant !== 'primary' && `progress-${variant}`,
    size !== 'md' && `progress-${size}`,
    indeterminate && 'progress-indeterminate'
  ].filter(Boolean).join(' ');

  const content = (
    <div className={classes}>
      <div
        className="progress-bar"
        style={!indeterminate ? { width: `${percentage}%` } : undefined}
      />
    </div>
  );

  if (label && !indeterminate) {
    return (
      <div className="progress-labeled">
        {content}
        <span className="progress-label">{Math.round(percentage)}%</span>
      </div>
    );
  }

  return content;
}

interface CircularProgressProps {
  value?: number;
  max?: number;
  size?: 'sm' | 'md' | 'lg' | 'xl';
  indeterminate?: boolean;
  label?: boolean;
}

export function CircularProgress({
  value = 0,
  max = 100,
  size = 'md',
  indeterminate = false,
  label = false
}: CircularProgressProps) {
  const sizes = { sm: 32, md: 48, lg: 64, xl: 96 };
  const radii = { sm: 12, md: 20, lg: 28, xl: 44 };

  const dimension = sizes[size];
  const radius = radii[size];
  const center = dimension / 2;
  const circumference = 2 * Math.PI * radius;
  const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
  const offset = circumference * (1 - percentage / 100);

  return (
    <div className={`progress-circular progress-circular-${size} ${indeterminate ? 'progress-circular-indeterminate' : ''}`}>
      <svg className="progress-circular-svg" width={dimension} height={dimension}>
        <circle className="progress-circular-track" cx={center} cy={center} r={radius} />
        <circle
          className="progress-circular-bar"
          cx={center}
          cy={center}
          r={radius}
          strokeDasharray={circumference}
          strokeDashoffset={!indeterminate ? offset : undefined}
        />
      </svg>
      {label && !indeterminate && (
        <span className="progress-circular-label">{Math.round(percentage)}%</span>
      )}
    </div>
  );
}

// Usage
<LinearProgress value={60} label />
<CircularProgress value={75} size="lg" label />
<LinearProgress indeterminate />

Vue

<template>
  <div v-if="label && !indeterminate" class="progress-labeled">
    <div :class="progressClasses">
      <div
        class="progress-bar"
        :style="!indeterminate ? { width: `${percentage}%` } : undefined"
      />
    </div>
    <span class="progress-label">{{ Math.round(percentage) }}%</span>
  </div>
  <div v-else :class="progressClasses">
    <div
      class="progress-bar"
      :style="!indeterminate ? { width: `${percentage}%` } : undefined"
    />
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  value: {
    type: Number,
    default: 0
  },
  max: {
    type: Number,
    default: 100
  },
  variant: {
    type: String,
    default: 'primary'
  },
  size: {
    type: String,
    default: 'md'
  },
  indeterminate: {
    type: Boolean,
    default: false
  },
  label: {
    type: Boolean,
    default: false
  }
});

const percentage = computed(() => {
  return Math.min(Math.max((props.value / props.max) * 100, 0), 100);
});

const progressClasses = computed(() => {
  return [
    'progress',
    props.variant !== 'primary' && `progress-${props.variant}`,
    props.size !== 'md' && `progress-${props.size}`,
    props.indeterminate && 'progress-indeterminate'
  ].filter(Boolean).join(' ');
});
</script>

<!-- Usage -->
<LinearProgress :value="60" label />
<LinearProgress indeterminate />

JavaScript

// Helper function to update progress
function updateProgress(element, value, max = 100) {
  const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
  const bar = element.querySelector('.progress-bar');
  if (bar) {
    bar.style.width = `${percentage}%`;
  }

  // Update label if present
  const label = element.parentElement?.querySelector('.progress-label');
  if (label) {
    label.textContent = `${Math.round(percentage)}%`;
  }

  // Update ARIA
  element.setAttribute('aria-valuenow', value);
}

// Helper for circular progress
function updateCircularProgress(element, value, max = 100) {
  const bar = element.querySelector('.progress-circular-bar');
  if (!bar) return;

  const radius = parseFloat(bar.getAttribute('r'));
  const circumference = 2 * Math.PI * radius;
  const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
  const offset = circumference * (1 - percentage / 100);

  bar.setAttribute('stroke-dasharray', circumference);
  bar.setAttribute('stroke-dashoffset', offset);

  // Update label if present
  const label = element.querySelector('.progress-circular-label');
  if (label) {
    label.textContent = `${Math.round(percentage)}%`;
  }
}

// Usage
const progress = document.querySelector('.progress');
updateProgress(progress, 75); // Set to 75%

const circularProgress = document.querySelector('.progress-circular');
updateCircularProgress(circularProgress, 60); // Set to 60%

API Reference

Linear Progress Classes

ClassDescription
.progressBase linear progress container (required)
.progress-barProgress bar element (required)
.progress-indeterminateIndeterminate animation
.progress-primaryPrimary color (default)
.progress-secondarySecondary color
.progress-tertiaryTertiary color
.progress-successSuccess semantic color
.progress-errorError semantic color
.progress-warningWarning semantic color
.progress-infoInfo semantic color
.progress-smSmall size (2px height)
.progress-mdMedium size (4px height, default)
.progress-lgLarge size (8px height)
.progress-xlExtra large size (12px height)
.progress-labeledContainer for progress with label
.progress-labelLabel element for percentage display
.progress-bufferBuffer progress container
.progress-buffer-barBuffer bar element

Circular Progress Classes

ClassDescription
.progress-circularBase circular progress container (required)
.progress-circular-svgSVG element (required)
.progress-circular-trackBackground circle track
.progress-circular-barProgress circle element
.progress-circular-indeterminateIndeterminate animation
.progress-circular-smSmall size (32px)
.progress-circular-mdMedium size (48px, default)
.progress-circular-lgLarge size (64px)
.progress-circular-xlExtra large size (96px)
.progress-circular-labelLabel element for percentage display

Circular Progress Calculations

For determinate circular progress, calculate the stroke-dashoffset:

const radius = 20; // or your circle's radius
const circumference = 2 * Math.PI * radius; // e.g., 125.6 for r=20
const percentage = 60; // your progress percentage
const offset = circumference * (1 - percentage / 100);

Circle radii by size:

  • sm: radius = 12, circumference ≈ 75.4
  • md: radius = 20, circumference ≈ 125.6
  • lg: radius = 28, circumference ≈ 175.84
  • xl: radius = 44, circumference ≈ 276.32
  • Button - For loading states
  • Spinner - Alternative loading indicator
  • Badge - Status indicators

See Also