Skip to content

Progress ​

A horizontal bar showing how far along a task is — a file upload, a multi-step form, a loading process. The filled portion reflects the current value against a maximum, and changes animate smoothly so progress reads as motion rather than jumps.

Usage ​

Pass a value from 0 to 100. The bar fills to that percentage.

Values25%, 50%, 75%, and 100%.
<script setup lang="ts">
import { Progress } from "@dlbcodes/ui";
</script>

<template>
    <div class="flex w-full max-w-sm flex-col gap-4">
        <Progress :value="25" />
        <Progress :value="50" />
        <Progress :value="75" />
        <Progress :value="100" />
    </div>
</template>

Animated ​

Because the fill transitions, updating value over time animates the bar — handy for uploads or any task you poll.

AnimatedValue increasing over time.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { Progress, Button } from "@dlbcodes/ui";

const value = ref(10);
let timer: ReturnType<typeof setInterval> | undefined;

const start = (): void => {
    value.value = 0;
    clearInterval(timer);
    timer = setInterval(() => {
        value.value += 10;
        if (value.value >= 100) clearInterval(timer);
    }, 400);
};

onMounted(start);
onUnmounted(() => clearInterval(timer));
</script>

<template>
    <div class="flex w-full max-w-sm flex-col gap-3">
        <Progress :value="value" />
        <div class="flex items-center justify-between">
            <span class="text-sm text-text-secondary">{{ value }}%</span>
            <Button variant="outline" size="sm" @click="start">Restart</Button>
        </div>
    </div>
</template>

Custom maximum ​

Set max to measure against something other than 100 — useful for "step N of M" progress.

Custom max3 out of 5.
<script setup lang="ts">
import { Progress } from "@dlbcodes/ui";
</script>

<template>
    <div class="flex w-full max-w-sm flex-col gap-2">
        <Progress :value="3" :max="5" />
        <span class="text-sm text-text-secondary">Step 3 of 5</span>
    </div>
</template>

Indeterminate ​

When you don't know how far along the task is, omit value. The bar shows a segment sliding across the track instead of a fill.

vue
vue
<Progress label="Loading results" />

For users who prefer reduced motion, the sliding segment is replaced by a gentle pulse of the full bar.

Labelling ​

A progress bar needs an accessible name so screen readers can announce what is progressing, not just the percentage. Pass label with a short description:

vue
<Progress :value="86" label="Storage used" />

If a visible caption already names the bar, point at it with aria-labelledby instead, so the accessible name and the on-screen text stay in sync:

vue
<span id="upload-label">Uploading…</span>
<Progress :value="60" aria-labelledby="upload-label" />

Appearance ​

The track is the theme's --muted color and the fill is --brand, so a progress bar follows your theme, including dark mode. Both ends are fully rounded regardless of --radius. See Theming.

Props ​

PropTypeDefaultDescription
valuenumber—Current progress (0–max). Omit for an indeterminate (animated) bar. Clamped to range.
maxnumber100The value representing 100% complete.
labelstring—Accessible name for the bar, announced by screen readers (sets aria-label).
classstring—Classes merged onto the track — use for height or width.

Any other attribute (e.g. aria-labelledby) forwards to the progressbar element.

Accessibility ​

  • The bar has role="progressbar" with aria-valuemin, aria-valuemax, and aria-valuenow, so assistive tech announces the current progress.
  • Give every bar a name. A progressbar role without an accessible name is announced generically and is unusable on a screen reader. Pass label (or aria-labelledby to reference visible text). There is intentionally no default name — a generic one would hide the need for a meaningful one.
  • An indeterminate bar (no value) leaves out aria-valuenow, which is how ARIA tells assistive tech that the amount of progress is unknown.
  • Values are clamped to the 0–max range, so out-of-range input won't break the bar visually or in the ARIA values.