Progress indicators
Progress indicators show the status of an ongoing process. Choose the shape that fits your layout. A linear indicator spans a width, a circular one fits a fixed square. Then choose the mode that matches what you know:
- Determinate when you know how far along the process is. Pass a
value. - Indeterminate when you do not. Pass
indeterminateand no value.
Determinate
value is a fraction between 0 and max, which defaults to 1. Drag the slider below to see how the active indicator, the gap and the stop
indicator change. Any progress above 0 shows the active indicator as at least a dot.
<script lang="ts">
import { CircularProgress, LinearProgress, Slider } from 'noph-ui'
let value = $state(0.6)
</script>
<div class="controls">
<Slider
min={0}
max={1}
labeled
step={0.01}
bind:value
aria-label="Progress value"
format={(v) => `${Math.round(v * 100)}%`}
/>
<div class="stack">
<LinearProgress aria-label="Download progress" {value} />
<LinearProgress aria-label="Download progress, wavy" wavy {value} />
</div>
<div class="row">
<CircularProgress aria-label="Download progress" {value} />
<CircularProgress aria-label="Download progress, wavy" wavy {value} />
</div>
</div>
<style>
.controls {
display: flex;
flex-direction: column;
gap: 1.75rem;
min-width: min(22rem, 70vw);
}
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.row {
display: flex;
align-items: center;
gap: 1.5rem;
}
</style>
Indeterminate
Use this when you cannot predict how long something takes. The indicator loops until you remove it or give it a value.
<script lang="ts">
import { CircularProgress, LinearProgress } from 'noph-ui'
</script>
<div class="controls">
<div class="stack">
<LinearProgress aria-label="Loading" indeterminate />
</div>
<CircularProgress aria-label="Loading" indeterminate />
</div>
<style>
.controls {
display: flex;
flex-direction: column;
gap: 1.75rem;
min-width: min(22rem, 70vw);
}
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
Wavy
The Material 3 expressive variant. Only the active indicator waves. The track stays smooth. The
wave moves one wavelength per second. A determinate LinearProgress flattens the wave at
10% and below and at 95% and above. Drag the slider above past those points to see it.
<script lang="ts">
import { CircularProgress, LinearProgress } from 'noph-ui'
</script>
<div class="controls">
<div class="stack">
<LinearProgress aria-label="Uploading, wavy" wavy value={0.6} />
<LinearProgress aria-label="Loading, wavy" wavy indeterminate />
</div>
<div class="row">
<CircularProgress aria-label="Uploading, wavy" wavy value={0.6} />
<CircularProgress aria-label="Loading, wavy" wavy indeterminate />
</div>
</div>
<style>
.controls {
display: flex;
flex-direction: column;
gap: 1.75rem;
min-width: min(22rem, 70vw);
}
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.row {
display: flex;
align-items: center;
gap: 1.5rem;
}
</style>
Buffer
Linear only. buffer shows how much has loaded ahead of the current position, like in a
streaming video. Everything after the buffer is drawn as dots.
<script lang="ts">
import { LinearProgress, Slider } from 'noph-ui'
let buffered = $state(0.85)
</script>
<div class="controls">
<Slider
labeled
min={0}
max={1}
step={0.01}
format={(v) => `${Math.round(v * 100)}%`}
bind:value={buffered}
aria-label="Buffer amount"
/>
<div class="stack">
<LinearProgress aria-label="Playback progress" value={0.4} buffer={buffered} />
</div>
</div>
<style>
.controls {
display: flex;
flex-direction: column;
gap: 1.75rem;
min-width: min(22rem, 70vw);
}
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
Without a track
Set track=false when the indicator sits on top of another component that already
provides a surface, such as a button's loading state. Button and IconButton do this when you pass loading.
<script lang="ts">
import { CircularProgress, LinearProgress } from 'noph-ui'
</script>
<CircularProgress aria-label="Loading, no track" indeterminate track={false} />
<div class="stack">
<LinearProgress aria-label="Loading, no track" indeterminate track={false} />
</div>
<style>
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
The 4dp stop indicator marks the end of a linear track. It stays when the track is hidden.
Material 3 allows removing it only where the indicator has at least 3:1 contrast with the surfaces
around it. In that case pass stopIndicator=false.
Four colors
fourColor cycles the indicator through primary and tertiary tones instead of a single colour.
Use it on indeterminate indicators.
<script lang="ts">
import { CircularProgress, LinearProgress } from 'noph-ui'
</script>
<CircularProgress aria-label="Loading, four colors" fourColor indeterminate />
<CircularProgress aria-label="Loading, four colors, wavy" fourColor wavy indeterminate />
<div class="stack">
<LinearProgress aria-label="Loading, four colors" fourColor indeterminate />
</div>
<style>
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
Accessibility
Both components render role="progressbar" and keep aria-valuenow and aria-valuemax in sync with value and max. Neither has text
of its own, so always pass an aria-label that says what is loading. Indeterminate
indicators leave out aria-valuenow. This tells assistive technology that the duration
is unknown.
<CircularProgress
indeterminate
aria-label="Loading search results"
/>wavy respects prefers-reduced-motion. With the preference set, the
indicator shows its plain shape without the wave. The spinner and the bar keep animating.
Right-to-left
LinearProgress follows the inherited dir. In a right-to-left context the
indicator grows from the right and the stop indicator moves to the left edge.
<script lang="ts">
import { LinearProgress } from 'noph-ui'
</script>
<div class="controls" dir="rtl">
<div class="stack">
<LinearProgress aria-label="Download progress, right to left" value={0.6} />
<LinearProgress aria-label="Loading, right to left" wavy indeterminate />
</div>
</div>
<style>
.controls {
display: flex;
flex-direction: column;
gap: 1.75rem;
min-width: min(22rem, 70vw);
}
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
Theming
Circular progress tokens
| Token | Default value |
|---|---|
--np-circular-progress-color | --np-color-primary |
--np-circular-progress-track-color | --np-color-secondary-container |
--np-circular-progress-active-indicator-width | 8.3333 |
--np-circular-progress-size | 3rem |
--np-circular-progress-active-indicator-width is a unitless percentage of the
indicator size, so the stroke scales with --np-circular-progress-size. The gap
between indicator and track stays the same at any stroke width: 4.8dp, and 4dp for the wavy variant.
Circular progress example
<script lang="ts">
import { CircularProgress } from 'noph-ui'
</script>
<CircularProgress
--np-circular-progress-color="var(--np-color-error)"
--np-circular-progress-track-color="var(--np-color-error-container)"
--np-circular-progress-active-indicator-width="20"
--np-circular-progress-size="5rem"
aria-label="Circular progress custom"
indeterminate
/>
Linear progress tokens
| Token | Default value |
|---|---|
--np-linear-progress-track-shape | --np-shape-corner-full |
--np-linear-progress-track-height | 0.25rem |
--np-linear-progress-active-indicator-height | 0.25rem |
--np-linear-progress-active-indicator-color | --np-color-primary |
--np-linear-progress-track-color | --np-color-secondary-container |
--np-linear-progress-track-gap | 0.25rem |
--np-linear-progress-wave-height | --np-linear-progress-active-indicator-height + 0.375rem |
--np-linear-progress-wave-height only applies to the wavy variant. It needs
a taller container to fit the wave. The wave keeps the Material 3 amplitude of 3dp and wavelength of
40dp at any thickness. A thicker indicator makes the container taller, so the wave is not clipped.
Linear progress example
<script lang="ts">
import { LinearProgress } from 'noph-ui'
</script>
<div class="stack">
<LinearProgress
--np-linear-progress-active-indicator-color="var(--np-color-error)"
--np-linear-progress-track-color="var(--np-color-error-container)"
--np-linear-progress-track-shape="0.5rem"
--np-linear-progress-track-height="0.5rem"
--np-linear-progress-active-indicator-height="0.5rem"
aria-label="Linear progress custom"
indeterminate
/>
</div>
<style>
.stack {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
</style>
API
Both components take the same attributes, apart from buffer, which is linear only.
Circular progress attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Progress to display, a fraction between 0 and max |
max | number | 1 | Maximum progress. |
indeterminate | boolean | false | Whether to show indeterminate progress, for tasks of unknown length. |
fourColor | boolean | false | Whether to use 4 colors in indeterminate mode instead of one. |
track | boolean | true | Whether to show the inactive track. Set to false for indicators on top of another
component, such as a button's loading state. |
wavy | boolean | false | Whether to draw the active indicator as a moving wave (M3 expressive). |
Linear progress attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Progress to display, a fraction between 0 and max |
max | number | 1 | Maximum progress. |
indeterminate | boolean | false | Whether to show indeterminate progress, for tasks of unknown length. |
fourColor | boolean | false | Whether to use 4 colors in indeterminate mode instead of one. |
track | boolean | true | Whether to show the inactive track. Set to false for indicators on top of another
component, such as a button's loading state. |
wavy | boolean | false | Whether to draw the active indicator as a moving wave (M3 expressive). |
buffer | number | 0 | Buffer amount, a fraction between 0 and max. A value of 0 or less hides the
buffer. |
stopIndicator | boolean | true | Shows the 4dp dot at the end of a determinate track. Only turn it off where the indicator has 3:1 contrast with its surroundings. |