Appearance
@vibe-labs/design-vue-progress
Vue 3 progress indicators for the Vibe Design System. Linear bars, radial rings, auto-color thresholds, shimmer effects, and ETA estimation.
Installation
ts
import { VibeProgressBar, VibeProgressRing } from "@vibe-labs/design-vue-progress";Requires the CSS layer from @vibe-labs/design-components-progress. The package also ships side-effect CSS (shimmer, inside labels, ring spin) imported automatically from progress-extras.css.
Components
VibeProgressBar
Linear progress bar with label positions, shimmer, stripes, and auto-color.
Usage
vue
<!-- Basic -->
<VibeProgressBar :value="65" />
<!-- With title and above label -->
<VibeProgressBar :value="65" title="Upload" label="above" />
<!-- Inside label -->
<VibeProgressBar :value="80" label="inside" size="lg" />
<!-- Right label -->
<VibeProgressBar :value="42" label="right" />
<!-- Custom label format -->
<VibeProgressBar :value="750" :max="1000" label="above" :format-label="(v, min, max) => `${v} / ${max} MB`" />
<!-- Sizes -->
<VibeProgressBar :value="50" size="xs" />
<VibeProgressBar :value="50" size="sm" />
<VibeProgressBar :value="50" size="md" />
<VibeProgressBar :value="50" size="lg" />
<!-- Color variants -->
<VibeProgressBar :value="80" variant="success" />
<VibeProgressBar :value="50" variant="warning" />
<VibeProgressBar :value="20" variant="danger" />
<!-- Auto-color (changes color based on value) -->
<VibeProgressBar :value="progress" variant="auto" />
<!-- Custom auto thresholds -->
<VibeProgressBar
:value="progress"
variant="auto"
:auto-thresholds="[
{ max: 25, color: 'var(--progress-fill-danger)' },
{ max: 75, color: 'var(--progress-fill-warning)' },
{ max: 100, color: 'var(--progress-fill-success)' },
]"
/>
<!-- Custom color -->
<VibeProgressBar :value="65" color="#7c3aed" />
<!-- Indeterminate -->
<VibeProgressBar indeterminate />
<!-- Striped -->
<VibeProgressBar :value="70" striped />
<!-- Animated stripes -->
<VibeProgressBar :value="70" striped animated />
<!-- Shimmer effect -->
<VibeProgressBar :value="70" shimmer />
<!-- Custom min/max -->
<VibeProgressBar :value="3" :min="0" :max="10" label="above" />
<!-- Chromeless (for composing segmented bars) -->
<VibeProgressBar :value="40" chromeless />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Current value |
min | number | 0 | Minimum |
max | number | 100 | Maximum |
size | ProgressSize | "md" | xs · sm · md · lg |
variant | ProgressVariant | "accent" | accent · success · warning · danger · auto |
color | string | — | Custom CSS color (overrides variant) |
autoThresholds | AutoColorThreshold[] | danger→warning→success | Thresholds for variant="auto" |
label | ProgressLabelPosition | "none" | above · inside · right · none |
labelText | string | — | Custom label text |
formatLabel | (value, min, max, percentage) => string | — | Label format function |
title | string | — | Field title above the bar |
indeterminate | boolean | false | Loading state |
striped | boolean | false | Striped pattern |
animated | boolean | false | Animate stripes (requires striped) |
shimmer | boolean | false | Shimmer/liquid leading edge |
chromeless | boolean | false | No border-radius or background |
ariaLabel | string | "Progress" | Accessible label |
VibeProgressRing
Radial/circular progress indicator with centre label.
Usage
vue
<!-- Basic -->
<VibeProgressRing :value="72" />
<!-- Sizes -->
<VibeProgressRing :value="72" size="sm" />
<VibeProgressRing :value="72" size="lg" />
<VibeProgressRing :value="72" size="xl" />
<!-- Custom stroke width -->
<VibeProgressRing :value="72" :stroke-width="4" />
<!-- Auto-color -->
<VibeProgressRing :value="progress" variant="auto" />
<!-- Custom color -->
<VibeProgressRing :value="72" color="#7c3aed" />
<!-- Custom label -->
<VibeProgressRing :value="3" :max="10" :format-label="(v, min, max) => `${v}/${max}`" />
<!-- Label slot -->
<VibeProgressRing :value="72">
<template #default="{ percentage, formatted }">
<strong>{{ formatted }}</strong>
</template>
</VibeProgressRing>
<!-- No label -->
<VibeProgressRing :value="72" :show-label="false" />
<!-- Indeterminate spinner -->
<VibeProgressRing indeterminate />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Current value |
min | number | 0 | Minimum |
max | number | 100 | Maximum |
size | ProgressRadialSize | "md" | sm · md · lg · xl |
variant | ProgressVariant | "accent" | accent · success · warning · danger · auto |
color | string | — | Custom CSS color |
autoThresholds | AutoColorThreshold[] | — | Thresholds for auto variant |
strokeWidth | number | — | Stroke width in px |
showLabel | boolean | true | Show centre label |
labelText | string | — | Custom label text |
formatLabel | (value, min, max, percentage) => string | — | Label format function |
indeterminate | boolean | false | Spinner mode |
ariaLabel | string | "Progress" | Accessible label |
Scoped Slot
| Property | Type | Description |
|---|---|---|
value | number | Clamped value |
percentage | number | Percentage 0–100 |
formatted | string | Formatted label string |
Composables
useProgress(options)
Shared computation for percentage, clamping, and auto-color resolution. Used internally by both components.
ts
import { useProgress } from "@vibe-labs/design-vue-progress";
const { percentage, clampedValue, resolvedColor } = useProgress({
value: toRef(props, "value"),
min: toRef(props, "min"),
max: toRef(props, "max"),
variant: toRef(props, "variant"),
color: toRef(props, "color"),
autoThresholds: toRef(props, "autoThresholds"),
});useProgressTimer(options)
ETA estimation for long-running operations. Tracks progress samples over time, computes a smoothed rate, and estimates time remaining.
ts
import { useProgressTimer } from "@vibe-labs/design-vue-progress";
const { percentage } = useProgress({ value, min, max });
const { state, reset } = useProgressTimer({ percentage });
// state.value.etaFormatted → "~2m 30s"
// state.value.rate → 0.65 (% per second)
// state.value.completionTime → Date
// state.value.reliable → true (enough samples)Options
| Option | Type | Default | Description |
|---|---|---|---|
percentage | Ref<number> | required | Reactive percentage (0–100) |
sampleSize | number | 10 | Samples to keep for smoothing |
minSamples | number | 3 | Minimum samples before showing estimate |
State
| Property | Type | Description |
|---|---|---|
eta | number | null | Estimated seconds remaining |
etaFormatted | string | null | Formatted ETA (e.g. "~2m 30s") |
rate | number | null | Progress rate (% per second) |
reliable | boolean | Enough samples collected |
completionTime | Date | null | Predicted completion time |
Auto-Color Thresholds
The variant="auto" mode changes the bar/ring color based on the current percentage. Default thresholds:
| Range | Color |
|---|---|
| 0–33% | var(--progress-fill-danger) (red) |
| 34–66% | var(--progress-fill-warning) (yellow) |
| 67–100% | var(--progress-fill-success) (green) |
Override with the autoThresholds prop for custom breakpoints.
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-progress | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations. Note: sideEffects: ["*.css"] is set in package.json to ensure the extras CSS is not tree-shaken.
Usage Guide
Setup
ts
import { VibeProgressBar, VibeProgressRing } from "@vibe-labs/design-vue-progress";
import "@vibe-labs/design-components-progress";VibeProgressBar — Practical Examples
File Upload Progress
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeProgressBar } from "@vibe-labs/design-vue-progress";
const uploadProgress = ref(0);
const uploadName = ref("");
async function uploadFile(file: File) {
uploadName.value = file.name;
uploadProgress.value = 0;
const formData = new FormData();
formData.append("file", file);
// Simulated progress — replace with XHR onprogress
const interval = setInterval(() => {
if (uploadProgress.value < 95) uploadProgress.value += 5;
}, 200);
await fetch("/api/upload", { method: "POST", body: formData });
clearInterval(interval);
uploadProgress.value = 100;
}
</script>
<template>
<div>
<input type="file" @change="(e) => uploadFile((e.target as HTMLInputElement).files![0])" />
<VibeProgressBar
:value="uploadProgress"
:title="uploadName"
label="above"
variant="auto"
shimmer
/>
</div>
</template>Segmented Multi-Step Progress
vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { VibeProgressBar } from "@vibe-labs/design-vue-progress";
const steps = ["Validate", "Process", "Finalise"];
const currentStep = ref(0);
const stepProgress = ref(0); // 0–100 within current step
const totalPercent = computed(() =>
((currentStep.value / steps.length) * 100) + (stepProgress.value / steps.length)
);
</script>
<template>
<VibeProgressBar
:value="totalPercent"
label="above"
:format-label="(v) => `Step ${currentStep + 1} of ${steps.length}: ${steps[currentStep]}`"
variant="accent"
striped
animated
/>
</template>Storage Quota Indicator
vue
<script setup lang="ts">
import { VibeProgressBar } from "@vibe-labs/design-vue-progress";
const usedBytes = 7_500_000_000;
const totalBytes = 10_000_000_000;
function formatBytes(b: number) {
return `${(b / 1e9).toFixed(1)} GB`;
}
</script>
<template>
<VibeProgressBar
:value="usedBytes"
:max="totalBytes"
label="above"
variant="auto"
:format-label="(v, _min, max) => `${formatBytes(v)} of ${formatBytes(max)} used`"
:auto-thresholds="[
{ max: 60, color: 'var(--progress-fill-success)' },
{ max: 85, color: 'var(--progress-fill-warning)' },
{ max: 100, color: 'var(--progress-fill-danger)' },
]"
/>
</template>VibeProgressRing — Practical Examples
Dashboard Metric Ring
vue
<script setup lang="ts">
import { VibeProgressRing } from "@vibe-labs/design-vue-progress";
const cpuUsage = 72;
</script>
<template>
<VibeProgressRing
:value="cpuUsage"
size="xl"
variant="auto"
>
<template #default="{ percentage }">
<div class="ring-label">
<span class="ring-label__value">{{ Math.round(percentage) }}%</span>
<span class="ring-label__unit">CPU</span>
</div>
</template>
</VibeProgressRing>
</template>Loading Indicator Ring
vue
<template>
<div v-if="loading" class="loading-center">
<VibeProgressRing indeterminate size="lg" aria-label="Loading content" />
</div>
</template>Composables — Usage Example
vue
<script setup lang="ts">
import { ref } from "vue";
import { useProgress, useProgressTimer } from "@vibe-labs/design-vue-progress";
import { VibeProgressBar } from "@vibe-labs/design-vue-progress";
const value = ref(0);
const min = ref(0);
const max = ref(100);
const { percentage } = useProgress({ value, min, max });
const { state } = useProgressTimer({ percentage });
// Simulate progress
setInterval(() => { if (value.value < 100) value.value += 1; }, 300);
</script>
<template>
<VibeProgressBar :value="value" :max="max" label="above" shimmer />
<p v-if="state.reliable">ETA: {{ state.etaFormatted }}</p>
</template>Common Patterns
Indeterminate While Loading, Determinate on Progress
vue
<template>
<VibeProgressBar
:value="progress"
:indeterminate="progress === 0"
label="above"
variant="accent"
/>
</template>Ring with Fraction Label
vue
<template>
<VibeProgressRing
:value="completed"
:max="total"
:format-label="(v, _min, max) => `${v}/${max}`"
size="lg"
/>
</template>