Skip to main content
Nexus Design System
GitHub

Progress

Progress shows how far along a task is, such as an upload or a multi-step form. Leave out value for an animated bar while the total is unknown. Always give it an aria-label or aria-labelledby.

'use client';

import { Progress } from '@/components/nxds/components/progress/progress';

export default function ProgressDefault() {
  return (
    <div className="nx:w-80">
      <Progress aria-label="Progress" value={60} />
    </div>
  );
}

Installation

npx nxds-cli@latest add progress

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { Progress } from '@/components/nxds/components/progress/progress';

Examples

Indeterminate

Leave out value to show a sweeping bar while the total is unknown.

'use client';

import { Progress } from '@/components/nxds/components/progress/progress';

export default function ProgressIndeterminate() {
  return (
    <div className="nx:w-80">
      <Progress aria-label="Loading" />
    </div>
  );
}

All variants

Fill the bar at 0, 25, 50, 75 and 100 percent.

0%
25%
50%
75%
100%
'use client';

import { Progress } from '@/components/nxds/components/progress/progress';

export default function ProgressAllVariants() {
  return (
    <div className="nx:w-80">
      <div className="nx:flex nx:flex-col nx:gap-6">
        {[0, 25, 50, 75, 100].map((pct) => (
          <div key={pct} className="nx:flex nx:flex-col nx:gap-2">
            <span className="nx:typography-label-default nx:tabular-nums nx:text-muted-foreground">
              {pct}%
            </span>
            <Progress value={pct} aria-label={`${pct}% complete`} />
          </div>
        ))}
      </div>
    </div>
  );
}

Props

Progress

Progress adds no props of its own; it accepts the props of the element or primitive it wraps.

Progress shows how far along a task is, such as an upload or a multi-step form. Leave out model-value for an animated bar while the total is unknown. Always give it an aria-label or aria-labelledby.

<script setup lang="ts">
import { Progress } from '@/components/progress';
</script>

<template>
  <Progress aria-label="Progress" :model-value="60" />
</template>

Installation

  1. Install the following dependencies:

    npm install @vueuse/core@^14.1.0 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/progress/Progress.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      ProgressIndicator,
      ProgressRoot,
      type ProgressRootEmits,
      type ProgressRootProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface ProgressProps extends ProgressRootProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<ProgressProps>(), {
      getValueText: (value: number | null | undefined, max: number) =>
        typeof value === 'number'
          ? `${Math.round((value / max) * 100)}%`
          : undefined,
      getValueLabel: () => undefined,
    });
    const emits = defineEmits<ProgressRootEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <ProgressRoot
        v-bind="forwarded"
        data-slot="progress"
        :class="
          cn(
            'nx:relative nx:h-2 nx:w-full nx:overflow-hidden nx:rounded-full nx:bg-control-background',
            props.class
          )
        "
      >
        <ProgressIndicator
          data-slot="progress-indicator"
          class="nx:h-full nx:w-full nx:bg-primary-background nx:transition-transform nx:duration-fast nx:data-[state=indeterminate]:w-1/3 nx:data-[state=indeterminate]:animate-progress-indeterminate"
          :style="
            modelValue == null
              ? undefined
              : { transform: `translateX(-${100 - modelValue}%)` }
          "
        />
      </ProgressRoot>
    </template>
    
    components/progress/index.ts
    export { default as Progress, type ProgressProps } from './Progress.vue';
    
    components/progress/progress.css
    /* Indeterminate sweep keyframe for the Progress indicator, registered the way
     * tw-animate-css registers its own (@theme inline so the named utility compiles
     * without emitting a :root var). Imported by src/index.css. */
    @theme inline {
      --animate-progress-indeterminate: nx-progress-indeterminate 1.5s ease-in-out
        infinite;
    
      @keyframes nx-progress-indeterminate {
        from {
          transform: translateX(-100%);
        }
        to {
          transform: translateX(300%);
        }
      }
    }
    
  3. Import the styles in your global stylesheet:

    /* app/globals.css */
    @import '../components/nxds/components/progress/progress.css';
    
  4. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Progress } from '@/components/progress';

Examples

Indeterminate

Leave out model-value to show a sweeping bar while the total is unknown.

<script setup lang="ts">
import { Progress } from '@/components/progress';
</script>

<template>
  <Progress aria-label="Loading" />
</template>

All variants

Fill the bar at 0, 25, 50, 75 and 100 percent.

<script setup lang="ts">
import { Progress } from '@/components/progress';
</script>

<template>
  <div class="nx:flex nx:flex-col nx:gap-6">
    <div
      v-for="pct in [0, 25, 50, 75, 100]"
      :key="pct"
      class="nx:flex nx:flex-col nx:gap-2"
    >
      <span
        class="nx:typography-label-default nx:tabular-nums nx:text-muted-foreground"
      >
        {{ pct }}%
      </span>
      <Progress :model-value="pct" :aria-label="pct + '% complete'" />
    </div>
  </div>
</template>

Props

Progress

Progress adds no props of its own; it accepts the props of the element or primitive it wraps.