A bar that fills as something advances, or animates continuously when there is no figure to report. It can be turned upright, and it can carry its own percentage inside it.
value is how far along it is, against a max that says what counts as finished. Anything outside the range is brought back into it.
vue
40%40%
value = 40, max = 100
7 of 12 files, max = 12
58%58%
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VProgressLinear, VTypography } from 'vectis-ui'
const value = ref(40)
const step = (delta: number) => (value.value += delta)
</script>
<template>
<div class="column">
<VProgressLinear :value="value" thickness="10" label="Upload" show-value />
<div class="row">
<VButton variant="outline" tone="neutral" size="sm" @click="step(-25)">-25</VButton>
<VButton variant="outline" tone="neutral" size="sm" @click="step(25)">+25</VButton>
<!-- Anything outside the range is brought back into it, so a value of 130 or -10
needs no clamping of your own. -->
<VTypography variant="body-sm" tone="muted">value = {{ value }}, max = 100</VTypography>
</div>
<!-- `max` says what counts as finished. The other end is always zero. -->
<div class="labelled">
<VTypography variant="caption" tone="muted">7 of 12 files, max = 12</VTypography>
<VProgressLinear :value="7" :max="12" thickness="10" label="Files" show-value />
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
max-inline-size: 28rem;
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-3);
}
.labelled {
display: grid;
gap: var(--vectis-space-2);
}
</style>
Indeterminate
indeterminate is for a wait that cannot be measured: the bar animates continuously and the value is ignored. Under reduced motion it is slowed rather than stopped.
vue
For a server that reports no percentage. Under reduced motion the bar is slowed rather than stopped: a motionless loader no longer says that anything is happening, which is the one thing it exists to say.
<script setup lang="ts">
import { VProgressLinear, VTypography } from 'vectis-ui'
</script>
<template>
<div class="column">
<!-- One bar crosses the track, flush with each edge at the extremes, so the loop is
invisible and the track is never empty. The value is ignored: there is nothing
to report. -->
<VProgressLinear indeterminate label="Waiting for the server" />
<VProgressLinear indeterminate tone="neutral" thickness="8" label="Loading" />
<VTypography variant="body-sm" tone="muted">
For a server that reports no percentage. Under reduced motion the bar is slowed rather than
stopped: a motionless loader no longer says that anything is happening, which is the one thing
it exists to say.
</VTypography>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
max-inline-size: 28rem;
}
</style>
Tones
tone says what the progress means, as a colour. There are five rather than a button's three, a bar reporting a state rather than starting an action.
color replaces the tone, as a hex value, a CSS name or an oklch(). The track's own shade is derived from it against the theme.
vue
A hex value
A CSS colour name
An oklch() colour
<script setup lang="ts">
import { VProgressLinear, VTypography } from 'vectis-ui'
const COLORS = [
{ value: '#7c3aed', caption: 'A hex value' },
{ value: 'teal', caption: 'A CSS colour name' },
{ value: 'oklch(0.72 0.19 45)', caption: 'An oklch() colour' },
]
</script>
<template>
<div class="column">
<div v-for="colour in COLORS" :key="colour.value" class="row">
<VTypography variant="caption" tone="muted" class="name">{{ colour.caption }}</VTypography>
<!-- The track's own shade is derived from the colour against the theme, so both
follow: one value to set, and it stays right in dark mode. -->
<VProgressLinear :value="60" :color="colour.value" :label="`Upload, ${colour.caption}`" />
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-4);
max-inline-size: 28rem;
}
.row {
display: grid;
grid-template-columns: 9rem 1fr;
align-items: center;
gap: var(--vectis-space-4);
}
.name {
text-align: end;
}
</style>
Thickness
thickness is always in pixels, whether given as a number or a numeric string, and 4px unless you say otherwise. There is no length prop to go with it: the bar takes the width of whatever holds it.
vue
2px
4px
8px
16px
<script setup lang="ts">
import { VProgressLinear, VTypography } from 'vectis-ui'
const THICKNESSES = [2, 4, 8, 16]
</script>
<template>
<div class="column">
<!-- Always pixels, whether a number or a numeric string. The bar takes the width of
whatever holds it, so its length is the container's business and not a prop. -->
<div v-for="thickness in THICKNESSES" :key="thickness" class="row">
<VTypography variant="caption" tone="muted" class="name">{{ thickness }}px</VTypography>
<VProgressLinear :value="60" :thickness="thickness" :label="`Upload, ${thickness}px`" />
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-4);
max-inline-size: 28rem;
}
.row {
display: grid;
grid-template-columns: 4rem 1fr;
align-items: center;
gap: var(--vectis-space-4);
}
.name {
text-align: end;
}
</style>
Shape
shape says whether the ends of the bar are rounded or cut square. It shows on a thick bar and is all but invisible on the default 4px one.
showValue writes the percentage inside the bar and valuePosition says where along it that text sits. The default slot replaces the figure and receives the value, the max and the percentage. The content is rendered twice, once over the track and once over the fill, so it has to be free of side effects.
vue
valuePosition start
55%55%
valuePosition center
55%55%
valuePosition end
55%55%
A slot of your own
7 of 12 files7 of 12 files
The bar is 4px by default, so writing inside it means giving it a thickness that can hold a line of text.
<script setup lang="ts">
import { VProgressLinear, VTypography, type ProgressLinearValuePosition } from 'vectis-ui'
const POSITIONS: ProgressLinearValuePosition[] = ['start', 'center', 'end']
</script>
<template>
<div class="column">
<!-- The text is rendered TWICE: once over the empty track, once over the fill in a
contrasting colour, the second copy clipped at the fill's edge. Whatever the
slot renders must therefore be free of side effects. -->
<div v-for="position in POSITIONS" :key="position" class="labelled">
<VTypography variant="caption" tone="muted">valuePosition {{ position }}</VTypography>
<VProgressLinear
:value="55"
thickness="22"
show-value
:value-position="position"
:label="`Upload, ${position}`"
/>
</div>
<!-- The slot replaces the percentage and receives the value, the max and the
percentage worked out from them. -->
<div class="labelled">
<VTypography variant="caption" tone="muted">A slot of your own</VTypography>
<VProgressLinear :value="7" :max="12" thickness="22" label="Files uploaded">
<template #default="{ value, max }">{{ value }} of {{ max }} files</template>
</VProgressLinear>
</div>
<VTypography variant="body-sm" tone="muted">
The bar is 4px by default, so writing inside it means giving it a thickness that can hold a
line of text.
</VTypography>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
max-inline-size: 28rem;
}
.labelled {
display: grid;
gap: var(--vectis-space-2);
}
</style>
Orientation
orientation set to vertical fills the bar from the bottom up, in a right-to-left page as well. Its length is a token, 10rem by default, which a height of your own replaces. The text copies stay horizontal.
vue
25%
60%
90%
indeterminate
<script setup lang="ts">
import { VProgressLinear, VTypography } from 'vectis-ui'
</script>
<template>
<div class="row">
<!-- Upright, the bar fills from the BOTTOM up: zero is anchored to the bottom edge,
so a vertical bar reads the way a gauge does. It takes the height of whatever
holds it, the way the horizontal one takes the width. -->
<div v-for="value in [25, 60, 90]" :key="value" class="gauge">
<VProgressLinear
:value="value"
orientation="vertical"
thickness="14"
:label="`Tank, ${value} percent`"
/>
<VTypography variant="caption" tone="muted">{{ value }}%</VTypography>
</div>
<div class="gauge">
<VProgressLinear indeterminate orientation="vertical" thickness="14" label="Filling" />
<VTypography variant="caption" tone="muted">indeterminate</VTypography>
</div>
</div>
</template>
<style scoped>
.row {
display: flex;
align-items: end;
gap: var(--vectis-space-6);
}
.gauge {
display: grid;
justify-items: center;
gap: var(--vectis-space-2);
/* The bar takes the height it is given, exactly as the horizontal one takes a width. */
block-size: 9rem;
grid-template-rows: 1fr auto;
}
</style>
API
Props
Prop
Type
Default
value
number
0
How far along it is. Anything outside the range is brought back into it.
label
string
none
What is progressing, in words, for screen readers. It draws nothing on screen and falls back to the design system dictionary; an aria-label or aria-labelledby of your own takes precedence over it.
max
number
100
What counts as finished. The other end is always zero.
indeterminate
boolean
false
Says that the progress cannot be measured: the bar animates continuously and the value is ignored. It is what to use while waiting for a server that reports no percentage.
A colour of your own, as hex, a CSS name or oklch(), which replaces the tone. The track's own shade is derived from it against the theme, so it follows both.
thickness
number | string
none
How thick the bar is, always in pixels: 12 and '12' both give 12px. It is 4px by default, so showing text inside the bar needs an explicit thickness.
shape
ProgressLinearShape'rounded' | 'square'
'rounded'
Whether the ends of the bar are rounded or square.
showValue
boolean
false
Writes the percentage inside the bar. It is ignored while the progress is unmeasurable, there being no figure to write.
What to write inside the bar instead of the percentage. It is rendered twice, once over the empty track and once over the filled part in a contrasting colour, each copy cut at the fill's edge, so whatever it renders must be free of side effects.
Types
The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.
export interface ProgressLinearSlotProps {
value: number
max: number
percent: number
}