Visual primitive
Progress
Progress familyAn accessible progress bar for uploads, imports, background jobs, and completion meters.
Playground
Linear progress
Progress playground
Use the linear meter for page-level loading, imports, uploads, and background work with enough horizontal space for a label.
<script setup>
import { reactive } from 'vue';
import { DomProgress } from '@getdom/studio/vue';
const data = reactive({
"value": 68,
"max": 100,
"label": "Progress",
"tone": "primary",
"size": "md",
"showValue": true,
"showLabel": true,
"indeterminate": false
});
</script>
<template>
<DomProgress
v-bind="data"
/>
</template>Playground
Segmented progress
Segmented progress playground
An accessible stacked progress bar for workflows that divide a changing total across multiple states.
- 2 completed
- 1 processing
- 1 queued
<script setup>
import { reactive } from 'vue';
import { DomSegmentedProgress } from '@getdom/studio/vue';
const data = reactive({
"segments": [
{
"key": "complete",
"label": "completed",
"value": 2,
"tone": "success",
"color": ""
},
{
"key": "processing",
"label": "processing",
"value": 1,
"tone": "warning",
"color": "var(--warning)"
},
{
"key": "queued",
"label": "queued",
"value": 1,
"tone": "neutral",
"color": ""
}
],
"value": 2,
"max": null,
"label": "pages processed",
"tone": "warning",
"size": "lg",
"showSummary": true,
"showPercentage": true,
"showLegend": true,
"showZeroSegments": false
});
</script>
<template>
<DomSegmentedProgress
v-bind="data"
/>
</template>Workflow
Crawl progress with a changing total
Move pages between queued, processing, completed, and failed states. Discovering more pages increases the total and recalculates every segment automatically.
- 18 completed
- 3 processing
- 21 queued
- 2 failed
<script setup>
import { computed, reactive } from 'vue';
import { DomButton, DomSegmentedProgress } from '../../../lib/vue';
const crawl = reactive({
complete: 18,
processing: 3,
queued: 21,
failed: 2,
});
const segments = computed(getSegments);
const processed = computed(getProcessedCount);
const summaryTone = computed(getSummaryTone);
/**
* Builds the ordered crawl-state segments from the current lifecycle counts.
*
* @returns {Array<Record<string, unknown>>} Crawl progress segments.
*/
function getSegments() {
return [
{ key: 'complete', label: 'completed', value: crawl.complete, tone: 'success' },
{ key: 'processing', label: 'processing', value: crawl.processing, tone: 'warning' },
{ key: 'queued', label: 'queued', value: crawl.queued, tone: 'neutral' },
{ key: 'failed', label: 'failed', value: crawl.failed, tone: 'danger' },
];
}
/**
* Counts terminal crawl states as processed pages.
*
* @returns {number} Complete and failed page count.
*/
function getProcessedCount() {
return crawl.complete + crawl.failed;
}
/**
* Resolves the summary tone from the most important active crawl state.
*
* @returns {'warning'|'danger'|'success'|'neutral'} Summary tone.
*/
function getSummaryTone() {
if (crawl.processing > 0) return 'warning';
if (crawl.queued > 0) return 'neutral';
if (crawl.failed > 0) return 'danger';
return 'success';
}
/**
* Adds newly discovered pages to the queued segment and dynamic total.
*
* @returns {void}
*/
function discoverPages() {
crawl.queued += 3;
}
/**
* Moves the next queued page into active processing.
*
* @returns {void}
*/
function startNextPage() {
if (crawl.queued < 1) return;
crawl.queued -= 1;
crawl.processing += 1;
}
/**
* Moves one active page into the completed state.
*
* @returns {void}
*/
function completePage() {
if (crawl.processing < 1) return;
crawl.processing -= 1;
crawl.complete += 1;
}
/**
* Moves one active page into the failed state.
*
* @returns {void}
*/
function failPage() {
if (crawl.processing < 1) return;
crawl.processing -= 1;
crawl.failed += 1;
}
</script>
<template>
<div class="w-full max-w-5xl space-y-5">
<DomSegmentedProgress
:segments="segments"
:value="processed"
:tone="summaryTone"
label="pages processed"
size="lg"
/>
<div class="flex flex-wrap gap-2 border-t border-border pt-4">
<DomButton size="sm" variant="secondary" @click="discoverPages">Discover 3 pages</DomButton>
<DomButton size="sm" variant="secondary" :disabled="crawl.queued < 1" @click="startNextPage">Start next page</DomButton>
<DomButton size="sm" variant="secondary" :disabled="crawl.processing < 1" @click="completePage">Complete page</DomButton>
<DomButton size="sm" variant="danger" :disabled="crawl.processing < 1" @click="failPage">Fail page</DomButton>
</div>
</div>
</template>
Playground
Radial progress
Radial progress playground
A compact circular progress meter for uploads, media processing, sync status, and inline background jobs.
<script setup>
import { reactive } from 'vue';
import { DomRadialProgress } from '@getdom/studio/vue';
const data = reactive({
"value": 64,
"max": 100,
"label": "Progress",
"tone": "success",
"size": "md",
"showValue": true
});
</script>
<template>
<DomRadialProgress
v-bind="data"
/>
</template>Composition
Unknown-duration loading
Combine the spinner's separate track and arc colors with an indeterminate progress sweep when work has started but no total is available.
Reading getdom.studio...
<script setup>
import { DomProgress, DomSpinner } from '../../../lib/vue';
</script>
<template>
<div class="w-full max-w-xl rounded-2xl border border-border bg-canvas p-6 shadow-sm">
<div class="mb-4 flex items-center gap-3">
<DomSpinner
label="Reading getdom.studio"
size="md"
background="color-mix(in oklab, var(--warning) 20%, transparent)"
foreground="var(--warning)"
/>
<p class="text-sm font-semibold text-muted-fg">Reading getdom.studio...</p>
</div>
<DomProgress
label="Reading getdom.studio"
:show-label="false"
indeterminate
size="md"
tone="warning"
/>
</div>
</template>
Demo
Status progress bars
Use DomProgress for clear task progress, including indeterminate work that has started but has not reported a value yet.
<script setup>
import { DomProgress } from '../../../lib/vue';
const jobs = [
{
label: 'Uploading images',
value: 72,
tone: 'success',
size: 'md',
},
{
label: 'Encoding video',
value: 42,
tone: 'primary',
size: 'lg',
},
{
label: 'Waiting for import',
value: null,
tone: 'neutral',
size: 'sm',
indeterminate: true,
},
];
</script>
<template>
<div class="w-full max-w-xl space-y-5">
<DomProgress
v-for="job in jobs"
:key="job.label"
:label="job.label"
:value="job.value"
:tone="job.tone"
:size="job.size"
:indeterminate="job.indeterminate"
show-value
/>
</div>
</template>
Demo
Radial meters
Use DomRadialProgress when the progress state needs to sit inside a thumbnail, list row, toolbar, or compact status area.
Image upload
Document sync
Processing warning
Compact transfer
<script setup>
import { DomRadialProgress } from '../../../lib/vue';
const meters = [
{
label: 'Image upload',
value: 86,
tone: 'success',
size: 'lg',
},
{
label: 'Document sync',
value: 52,
tone: 'primary',
size: 'md',
},
{
label: 'Processing warning',
value: 31,
tone: 'warning',
size: 'md',
},
{
label: 'Compact transfer',
value: 68,
tone: 'neutral',
size: 'sm',
showValue: false,
},
];
</script>
<template>
<div class="flex flex-wrap items-center justify-center gap-6">
<div
v-for="meter in meters"
:key="meter.label"
class="flex min-w-28 flex-col items-center gap-3 text-center"
>
<DomRadialProgress
:label="meter.label"
:value="meter.value"
:tone="meter.tone"
:size="meter.size"
:show-value="meter.showValue !== false"
/>
<p class="text-xs font-medium text-muted-fg">{{ meter.label }}</p>
</div>
</div>
</template>
Composition
Upload overlays
Radial progress works well over media previews because it keeps the upload state readable without resizing the item layout.
homepage-hero.jpg
image/jpeg - 78%
brand-guidelines.pdf
application/pdf - 44%
product-gallery.png
image/png - 91%
<script setup>
import { DomRadialProgress } from '../../../lib/vue';
const uploads = [
{
name: 'homepage-hero.jpg',
type: 'image/jpeg',
progress: 78,
tone: 'success',
},
{
name: 'brand-guidelines.pdf',
type: 'application/pdf',
progress: 44,
tone: 'primary',
},
{
name: 'product-gallery.png',
type: 'image/png',
progress: 91,
tone: 'success',
},
];
</script>
<template>
<div class="grid w-full gap-4 sm:grid-cols-3">
<article
v-for="upload in uploads"
:key="upload.name"
class="overflow-hidden rounded-lg border border-border bg-secondary/35 p-3"
>
<div class="relative grid aspect-[4/3] place-items-center overflow-hidden rounded-md bg-canvas/60">
<div class="grid size-14 place-items-center rounded-2xl bg-secondary text-muted-fg ring-1 ring-border">
<svg viewBox="0 0 24 24" class="size-8" fill="none" aria-hidden="true">
<path d="M7 3.75h7l3 3v13.5H7V3.75Zm7 0v3h3M9.5 12.5h5M9.5 15.5h3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<div class="absolute inset-0 grid place-items-center bg-canvas/50 backdrop-blur-[1px]">
<DomRadialProgress
:label="`${upload.name} upload progress`"
:value="upload.progress"
:tone="upload.tone"
size="lg"
/>
</div>
</div>
<div class="mt-3 min-w-0 text-center">
<p class="truncate text-sm font-semibold text-canvas-fg">{{ upload.name }}</p>
<p class="mt-1 text-xs text-muted-fg">{{ upload.type }} - {{ upload.progress }}%</p>
</div>
</article>
</div>
</template>
Linear reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
value | number | number | — | Current progress value. Leave empty for indeterminate progress. |
max | number | number | 100 | Maximum progress value. |
label | string | string | 'Progress' | Accessible progress label. |
tone | 'primary' | 'success' | 'warning' | 'danger' | 'neutral' | string | 'primary' | Filled bar tone. |
size | 'sm' | 'md' | 'lg' | string | 'md' | Track height. |
showValue | boolean | boolean | false | Show a percentage label beside the title. |
showLabel | boolean | boolean | true | Show the label above the bar. The label remains accessible when visually hidden. |
indeterminate | boolean | boolean | false | Show a sweeping loading bar when the total is unknown. |
Auto-generated from Progress.props and inline _edit hints.
Segmented reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
segmentsts | array | Array< | [] | Ordered progress states. Each segment accepts key, label, value, a semantic tone, and an optional CSS colour override. |
value | number | number | 0 | Completed or otherwise processed item count used by the summary and accessibility value. |
max | number | number | — | Maximum item count. Defaults to the sum of all segments and expands when segment values grow. |
label | string | string | 'items processed' | Visible summary label and accessible progress label. |
tone | 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | string | 'primary' | Semantic tone used by the percentage summary. |
size | 'sm' | 'md' | 'lg' | string | 'md' | Segmented track height. |
showSummary | boolean | boolean | true | Show the processed count, total, label, and optional percentage. |
showPercentage | boolean | boolean | true | Show the processed percentage in the summary. |
showLegend | boolean | boolean | true | Show segment counts and labels below the track. |
showZeroSegments | boolean | boolean | false | Keep zero-value states visible in the legend. |
Auto-generated from Segmented progress.props and inline _edit hints.
Radial reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
value | number | number | — | Current progress value. Leave empty for an empty meter. |
max | number | number | 100 | Maximum progress value. |
label | string | string | 'Progress' | Accessible progress label. |
tone | 'primary' | 'success' | 'warning' | 'danger' | 'neutral' | string | 'primary' | Progress stroke tone. |
size | 'sm' | 'md' | 'lg' | string | 'md' | Rendered diameter. Use a preset or a pixel number. |
showValue | boolean | boolean | true | Show the percentage inside the circle. |
Auto-generated from Radial progress.props and inline _edit hints.