GrProgressCircle
Shows progress as a ring or a gauge, with the value in the middle.
Overview
When to take it
- there is no room across the width — a dashboard tile, a cell, a metric card;
- the value matters more than the course — a number in the centre reads as a metric rather than as a wait;
- a “speedometer” gauge is needed —
shapebreaks the ring open when that is clearer; - the state has to be shown with a sign —
statusIconreplaces the number with a success or a failure on completion.
When to take something else
| Need | Take |
|---|---|
| There is enough room across the width | GrProgressBar |
| The share is unknown, the content is already there | GrLoading |
| A number without a gauge | GrStatistic |
| The shares of several parts of a whole | GrChartPie |
Variants every tone and view, rendered by the real component
tone \ shape | circle | dashboard |
|---|---|---|
primary | ||
neutral | ||
success | ||
warning | ||
danger | ||
info | ||
slate | ||
azure |
Sizes
xssmmdlg
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrProgressCircle :value="72" showValue />Examples 5 each with a live demo and its code
Quotas where a bar has no room
A ring fits a tile next to its label, and
show-valueputs the share inside — without the number the ring reads as decoration. The tone is the verdict: calm until 80 %,warningafter,dangernear the cap. With no visible heading on the ring,aria-labelnames it.Business planLimits reset in 12 days46%Storage23 of 50 GB85%Seats17 of 20 members98%API calls today98,120 of 100,000A cancel button in the centre, a verdict at the end
The default slot owns the centre — here a cancel button while the file uploads. It sits beside the
progressbarrole, not inside it, so it stays a button for the screen reader; the centre layer ignores the pointer, and the button turns it back on. At the endstatus-iconshows a tick, or a cross ondanger.Attachmentsscan-invoice-0418.pdf0.9 of 2.4 MB · 36%contract-northwind.pdf1.1 MB · uploadedreceipt-lunch.heicConnection lost at 52% — try againGauges on a server card
shape="dashboard"opens the ring at the bottom: a gauge reads as a measurement, not as a job heading for its end.format-valuespeaks the reader’s unit and becomesaria-valuetexttoo; the slot gets the clamped value and draws two lines;thicknessthins the stroke.api-eu-2c6g.xlarge · Frankfurt · last 5 minutesHigh memoryCPU · 4 cores38%Memory · of 16 GB13.0 GBDisk · /var/lib128of 200 GBUnknown first, then known
While the export is counting invoices its size is unknown, and
indeterminatespins the ring without announcing a value — a ring frozen at zero would read as a stalled job. Then the share fills, andstatus-iconturns a hundred per cent into a tick. Only the phase goes to the live region, not every percent.Export 214 invoicesSeptember 2026 · Excel with line items
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone |
| undefined | The colour tone of the arc. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The diameter of the ring by the scale of the package. |
ariaLabel | string | undefined | undefined | The label for a screen reader (mandatory if there is no visible heading beside it). |
value | number | undefined | 0 | The current value `0..100`; values out of range are clamped, and `NaN` becomes `0`. |
shape | "circle" | "dashboard" | undefined | undefined | A closed ring or an arc with a cut-out at the bottom. |
indeterminate | boolean | undefined | false | The progress is unknown: the arc runs around the circle, and no value is announced outside. |
thickness | number | undefined | undefined | The thickness of the stroke as a percentage of the diameter. Unset — by the `size` step. |
showValue | boolean | undefined | false | Show the value in the centre of the ring. The default slot is stronger. |
formatValue | ((value: number) => string) | undefined | undefined | A format of your own for the value. It governs both the label and `aria-valuetext`. |
statusIcon | boolean | undefined | false | On completion — a tick, and with `tone="danger"` — a cross instead of the value. |
trackless | boolean | undefined | undefined | Remove the track: on top of a picture the empty part of the ring is only noise. |
Slots
| Slot | Type | Description |
|---|---|---|
default | { value: number; } | The content of the centre instead of the value: an icon, two lines, a cancel button. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrProgressCircleProps, GrProgressCircleShape, GrArcGeometry } from '@feugene/granularity/components/GrProgressCircle'Filedist/types/components/GrProgressCircle/GrProgressCircle.vue.d.ts
interface GrProgressCircleProps {
value?: number;
indeterminate?: boolean;
shape?: GrProgressCircleShape;
showValue?: boolean;
formatValue?: (value: number) => string;Filedist/types/components/GrProgressCircle/geometry.d.ts
type GrProgressCircleShape = typeof GR_PROGRESS_CIRCLE_SHAPES[number];interface GrArcGeometry {
radius: number;
circumference: number;
arcLength: number;
trackDashArray: string;
valueDashArray: string;
rotation: number;
}Filedist/types/components/GrProgressCircle/grProgressCircleStyles.d.ts
type GrProgressCircleTone = GrTone;type GrProgressCircleSize = GrComponentSize;Filedist/types/components/GrProgressCircle/config.d.ts
const grProgressCircleConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrProgressCircle">;Filedist/types/components/GrProgressCircle/safelist.d.ts
const grProgressCircleSafelist: string[];Filedist/types/components/GrProgressCircle/defaults.d.ts
interface GrProgressCircleConfigurableProps {
size: GrProgressCircleSize;
tone: GrProgressCircleTone;
shape: GrProgressCircleShape;
trackless: boolean;
}Filedist/types/components/GrProgressCircle/index.d.ts
type GrProgressCircleInstance = ComponentExposed<typeof GrProgressCircleComponent>;Usage from the library prose
The centre lies beside the role rather than inside it
The markup is deliberately two-layered: role="progressbar" is carried by the SVG alone, and the
content of the centre is its absolutely positioned neighbour.
The reason is not cosmetic. A widget role declares its descendants presentational, and a “Cancel”
button inside the ring — the very first upload scenario — would turn out nested-interactive: a
screen reader would lose both the button and the indicator. The slot of the centre has to remain a
place where anything at all can be put, interactive elements included.
The layer of the centre itself does not catch the pointer (pointer-events: none) — otherwise it
would cover the ring; an interactive element inside switches the events back on for itself.
The value, the icon, the slot
The priority of the content of the centre: the slot → the outcome icon → the value.
showValueprints percentages, andformatValuesets a text of your own along witharia-valuetext;statusIconreplaces the number with a tick atvalue >= 100and with a cross attone="danger"— that is, only in terminal states;- the default slot receives the already clamped value and beats both variants. An empty slot (a
button under a
v-if, for instance) does not occupy the centre — what counts is the content, not the fact that a slot was passed.
The value fits into the centre from sm upwards; at xs it is either not shown or its type size
is reduced with the --gr-progress-circle-value-size token.
The shapes
circle is a closed ring, counted from twelve o’clock clockwise. dashboard is three quarters of
a circumference with the cut-out strictly at the bottom: room is freed under the value for a
caption, and the gauge reads as a speedometer. The value means the same thing in both shapes — the
share travelled — and only the length of the track changes.
Unknown progress
indeterminate sends the arc around the circle and announces no value: aria-valuenow is not
set and the label is not printed.
Under prefers-reduced-motion the component shows a closed neutral ring rather than stopping the
rotation. A stopped quarter of an arc would read as “25 % progress” — the same argument for which
GrProgressBar has a reduce branch of its own rather than the general clamp of animations.
Limits
bufferis not carried over from the bar: the “loaded ahead” layer reads on a gauge that is seen from left to right as a whole; on a ring two nested arcs look like one thick one;- the size is by the scale of the package only — the pixel escape hatch belongs to
GrAvatarandGrIconexactly; pointed adjustment goes through--gr-progress-circle-size, and the geometry of the arc is computed inviewBoxunits and does not depend on the diameter; - there is no gradient along the arc — it requires a
<linearGradient>with anidof its own in every instance, that is, one more source of hydration divergences; - there are no segments and no ticks — a ring with divisions is an indicator of steps rather than of progress.
Props in this example