Visual primitive

Progress

Progress family

An 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.

Progress68%
Playground.vuevue
vue
<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 / 4pages processed
50%
  • 2 completed
  • 1 processing
  • 1 queued
Playground.vuevue
vue
<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.

20 / 44pages processed
45%
  • 18 completed
  • 3 processing
  • 21 queued
  • 2 failed
vue
<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.

64%
Playground.vuevue
vue
<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

Reading getdom.studio...

vue
<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.

Uploading images72%
Encoding video42%
Waiting for import
vue
<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.

86%

Image upload

52%

Document sync

31%

Processing warning

Compact transfer

vue
<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.

78%

homepage-hero.jpg

image/jpeg - 78%

44%

brand-guidelines.pdf

application/pdf - 44%

91%

product-gallery.png

image/png - 91%

vue
<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

NameTypeTSDefaultDescription
valuenumbernumberCurrent progress value. Leave empty for indeterminate progress.
maxnumbernumber100Maximum progress value.
labelstringstring'Progress'Accessible progress label.
tone'primary' | 'success' | 'warning' | 'danger' | 'neutral'string'primary'Filled bar tone.
size'sm' | 'md' | 'lg'string'md'Track height.
showValuebooleanbooleanfalseShow a percentage label beside the title.
showLabelbooleanbooleantrueShow the label above the bar. The label remains accessible when visually hidden.
indeterminatebooleanbooleanfalseShow a sweeping loading bar when the total is unknown.

Auto-generated from Progress.props and inline _edit hints.

Segmented reference

Props

Control props

NameTypeTSDefaultDescription
segments
ts
[
	{
		key: "queued",
		label: "queued",
		value: 0,
		tone: "neutral",
		color: "var(--warning)",
	}
]
arrayArray<SegmentsItem
type SegmentsItem = {
	key?: string; // Key
	label?: string; // Label
	value?: number; // Value
	tone?: "neutral" | "primary" | "success" | "warning" | "danger"; // Tone
	color?: string; // Custom colour
};
>
[]Ordered progress states. Each segment accepts key, label, value, a semantic tone, and an optional CSS colour override.
valuenumbernumber0Completed or otherwise processed item count used by the summary and accessibility value.
maxnumbernumberMaximum item count. Defaults to the sum of all segments and expands when segment values grow.
labelstringstring'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.
showSummarybooleanbooleantrueShow the processed count, total, label, and optional percentage.
showPercentagebooleanbooleantrueShow the processed percentage in the summary.
showLegendbooleanbooleantrueShow segment counts and labels below the track.
showZeroSegmentsbooleanbooleanfalseKeep zero-value states visible in the legend.

Auto-generated from Segmented progress.props and inline _edit hints.

Radial reference

Props

Control props

NameTypeTSDefaultDescription
valuenumbernumberCurrent progress value. Leave empty for an empty meter.
maxnumbernumber100Maximum progress value.
labelstringstring'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.
showValuebooleanbooleantrueShow the percentage inside the circle.

Auto-generated from Radial progress.props and inline _edit hints.