Progress
Visually represents progress or completion of a task. Updates dynamically to give users clear feedback on status.
Playground
<Progress
:value="60"
label="Upload"
hint
/>Animated
The continuous bar transitions its fill, so values arriving on a timer read as one smooth movement rather than a series of jumps.
<script setup lang="ts">
import { ref } from 'vue'
import { useIntervalFn } from '@vueuse/core'
import { Progress, Button } from 'frappe-ui'
const value = ref(0)
// A poll or upload reports progress in coarse steps. The fill transitions
// between them, so the bar reads as continuous motion instead of jumping.
// The tick matches the fill's own transition duration, so each step is still
// moving when the next arrives — that hand-off is the point of the demo.
const { pause, resume, isActive } = useIntervalFn(
() => {
value.value += 10
if (value.value >= 100) pause()
},
660,
{ immediate: false },
)
function start() {
value.value = 0
resume()
}
</script>
<template>
<div class="grid w-full max-w-[400px] gap-4">
<Progress :value="value" label="Uploading" :hint="true" size="md" />
<div class="flex gap-2">
<Button variant="solid" :disabled="isActive" @click="start">
{{ value === 100 ? 'Restart' : 'Start' }}
</Button>
<Button :disabled="!isActive" @click="pause()">Pause</Button>
</div>
</div>
</template>Storage quota
A consumption meter. The numbers sit below the bar instead of in the hint slot, because "700 GB of 2 TB" carries more than a percentage and shares the row with an action.
<script setup lang="ts">
// A quota meter can pair calculated progress with richer usage details below.
import { computed } from 'vue'
import { Button, Progress } from 'frappe-ui'
const usedGB = 700
const totalGB = 2048
const percent = computed(() => Math.round((usedGB / totalGB) * 100))
const usageLabel = computed(
() => `${formatSize(usedGB)} of ${formatSize(totalGB)}`,
)
function formatSize(gigabytes: number) {
if (gigabytes >= 1024) return `${gigabytes / 1024} TB`
return `${gigabytes} GB`
}
</script>
<template>
<div class="w-full max-w-[240px]">
<Progress :value="percent" label="Storage" size="md" />
<div class="mt-3.5 flex items-center gap-2">
<span
class="lucide-cloud size-4 shrink-0 text-ink-gray-5"
aria-hidden="true"
/>
<span class="text-base text-ink-gray-5">{{ usageLabel }}</span>
<Button class="ml-auto" variant="ghost" size="sm">Manage</Button>
</div>
</div>
</template>Onboarding checklist
The value is derived from how many steps are done, so finishing a step moves the bar. Completing items is the most common source of a progress value.
<script setup lang="ts">
// Checklist completion drives progress while the next unfinished task stays in focus.
import { computed, ref } from 'vue'
import { Button, Progress } from 'frappe-ui'
const steps = ref([
{
title: 'Upload file or folder',
description: 'Add content to start organizing your workspace.',
action: 'Upload',
done: true,
},
{
title: 'Share a file or folder',
description: "Share content with someone you'd like to collaborate with.",
action: 'Share',
done: false,
},
{
title: 'Create a shared folder',
description: 'Keep files your team uses together in one place.',
action: 'Create',
done: false,
},
])
const expanded = ref<number | null>(1)
const percent = computed(() => {
const doneCount = steps.value.filter((step) => step.done).length
return Math.round((doneCount / steps.value.length) * 100)
})
function toggleStep(index: number) {
expanded.value = expanded.value === index ? null : index
}
function completeStep(index: number) {
steps.value[index].done = true
const followingStep = steps.value.findIndex(
(step, stepIndex) => stepIndex > index && !step.done,
)
expanded.value =
followingStep === -1
? steps.value.findIndex((step) => !step.done)
: followingStep
if (expanded.value === -1) {
expanded.value = null
}
}
</script>
<template>
<div class="w-full max-w-[320px] rounded-7 bg-surface-base p-2.5 shadow-lg">
<Progress
:value="percent"
label="Get Started with Frappe Drive"
size="md"
/>
<div class="mt-6 flex flex-col gap-1">
<div
v-for="(step, index) in steps"
:key="step.title"
class="rounded-6 p-2"
:class="{ 'bg-surface-gray-1': expanded === index }"
>
<button
type="button"
class="flex w-full items-center gap-2 text-left"
:aria-expanded="expanded === index"
@click="toggleStep(index)"
>
<span
class="size-4 shrink-0"
:class="step.done ? 'lucide-circle-check' : 'lucide-circle'"
aria-hidden="true"
/>
<span class="flex-1 text-base text-ink-gray-8">
{{ step.title }}
</span>
<span
class="size-4 shrink-0"
:class="
expanded === index ? 'lucide-chevron-up' : 'lucide-chevron-down'
"
aria-hidden="true"
/>
</button>
<div v-if="expanded === index" class="mt-1 px-6">
<p class="text-p-sm text-ink-gray-6">
{{ step.description }}
</p>
<Button
class="mt-2"
variant="solid"
size="sm"
@click="completeStep(index)"
>
{{ step.action }}
</Button>
</div>
</div>
</div>
</div>
</template>Multi-step form
intervals turns the bar into a step indicator — one segment per step, filled up to the current one. This is what the interval variant is for.
<script setup lang="ts">
// This story demonstrates segmented progress as a replayable step indicator.
import { computed, reactive, ref } from 'vue'
import { Button, FormControl, Progress } from 'frappe-ui'
const steps = ['Payment mode', 'Billing address', 'Review'] as const
const step = ref(0)
const form = reactive({
cardNumber: '',
cardholderName: '',
city: '',
postalCode: '',
})
const percent = computed(() => ((step.value + 1) / steps.length) * 100)
function continueForm() {
step.value = step.value === steps.length - 1 ? 0 : step.value + 1
}
</script>
<template>
<div class="w-full max-w-[400px] rounded-7 border border-outline-gray-1 p-4">
<Progress
:value="percent"
:label="steps[step]"
:intervals="true"
:interval-count="steps.length"
>
<template #hint>
<span class="text-base text-ink-gray-5">
Step {{ step + 1 }} of {{ steps.length }}
</span>
</template>
</Progress>
<div v-if="step === 0" class="mt-6 flex flex-col gap-4">
<FormControl
v-model="form.cardNumber"
label="Card number"
placeholder="1234 5678 9012 3456"
/>
<FormControl
v-model="form.cardholderName"
label="Name on card"
placeholder="Full name"
/>
</div>
<div v-else-if="step === 1" class="mt-6 flex flex-col gap-4">
<FormControl v-model="form.city" label="City" placeholder="City" />
<FormControl
v-model="form.postalCode"
label="Postal code"
placeholder="Postal code"
/>
</div>
<p v-else class="mt-6 text-p-sm text-ink-gray-7">
Confirm your details to authorize the subscription charge.
</p>
<div class="mt-6 flex justify-end gap-2">
<Button :disabled="step === 0" @click="step -= 1">Back</Button>
<Button variant="solid" @click="continueForm">
{{ step === steps.length - 1 ? 'Confirm' : 'Save & Continue' }}
</Button>
</div>
</div>
</template>Sizes
<script setup lang="ts">
import { Progress } from 'frappe-ui'
</script>
<template>
<div class="grid w-full max-w-[400px] gap-5">
<Progress :value="50" size="sm" label="sm" />
<Progress :value="50" size="md" label="md" />
<Progress :value="50" size="lg" label="lg" />
<Progress :value="50" size="xl" label="xl" />
</div>
</template>API Reference
Show types
export interface ProgressProps {
/** Current progress value */
value: number
/** Size of the progress bar: "sm" | "md" | "lg" | "xl" */
size?: 'sm' | 'md' | 'lg' | 'xl'
/** Optional text label displayed on the progress bar */
label?: string
/** Whether to show a hint/tooltip for the progress value */
hint?: boolean
/** Whether to show interval markers on the progress bar */
intervals?: boolean
/** Number of intervals to display if `intervals` is true */
intervalCount?: number
}Current progress value
Size of the progress bar: "sm" | "md" | "lg" | "xl"
Optional text label displayed on the progress bar
Whether to show a hint/tooltip for the progress value
Whether to show interval markers on the progress bar
Number of intervals to display if `intervals` is true
| Slot | Payload |
|---|---|
hint | — Custom content for the hint area (usually displays the progress value). If not provided, defaults to showing `props.value` followed by `%`. |
Custom content for the hint area (usually displays the progress value). If not provided, defaults to showing `props.value` followed by `%`.