GrProgressCircle

  • core
  • feedback
  • API · 11 props

Shows progress as a ring or a gauge, with the value in the middle.

import { GrProgressCircle } from '@feugene/granularity/components/GrProgressCircle'

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 — shape breaks the ring open when that is clearer;
  • the state has to be shown with a sign — statusIcon replaces the number with a success or a failure on completion.

When to take something else

NeedTake
There is enough room across the widthGrProgressBar
The share is unknown, the content is already thereGrLoading
A number without a gaugeGrStatistic
The shares of several parts of a wholeGrChartPie

Variants every tone and view, rendered by the real component

Variants · GrProgressCircle
tone \ shapecircledashboard
primary
neutral
success
warning
danger
info
slate
azure

Sizes

  • xs
  • sm
  • md
  • lg

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

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-value puts the share inside — without the number the ring reads as decoration. The tone is the verdict: calm until 80 %, warning after, danger near the cap. With no visible heading on the ring, aria-label names it.

    Business planLimits reset in 12 days
    46%
    Storage23 of 50 GB
    85%
    Seats17 of 20 members
    98%
    API calls today98,120 of 100,000
  • A 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 progressbar role, 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 end status-icon shows a tick, or a cross on danger.

    Attachments
    scan-invoice-0418.pdf0.9 of 2.4 MB · 36%
    contract-northwind.pdf1.1 MB · uploaded
    receipt-lunch.heicConnection lost at 52% — try again
  • Gauges 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-value speaks the reader’s unit and becomes aria-valuetext too; the slot gets the clamped value and draws two lines; thickness thins the stroke.

    api-eu-2c6g.xlarge · Frankfurt · last 5 minutes
    High memory
    38%
    CPU · 4 cores
    13.0 GB
    Memory · of 16 GB
    128of 200 GB
    Disk · /var/lib
  • Unknown first, then known

    While the export is counting invoices its size is unknown, and indeterminate spins the ring without announcing a value — a ring frozen at zero would read as a stalled job. Then the share fills, and status-icon turns 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

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe colour tone of the arc.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe diameter of the ring by the scale of the package.
ariaLabelstring | undefinedundefinedThe label for a screen reader (mandatory if there is no visible heading beside it).
valuenumber | undefined0The current value `0..100`; values out of range are clamped, and `NaN` becomes `0`.
shape"circle" | "dashboard" | undefinedundefinedA closed ring or an arc with a cut-out at the bottom.
indeterminateboolean | undefinedfalseThe progress is unknown: the arc runs around the circle, and no value is announced outside.
thicknessnumber | undefinedundefinedThe thickness of the stroke as a percentage of the diameter. Unset — by the `size` step.
showValueboolean | undefinedfalseShow the value in the centre of the ring. The default slot is stronger.
formatValue((value: number) => string) | undefinedundefinedA format of your own for the value. It governs both the label and `aria-valuetext`.
statusIconboolean | undefinedfalseOn completion — a tick, and with `tone="danger"` — a cross instead of the value.
tracklessboolean | undefinedundefinedRemove the track: on top of a picture the empty part of the ring is only noise.

Slots

SlotTypeDescription
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
import type { GrProgressCircleProps, GrProgressCircleShape, GrArcGeometry } from '@feugene/granularity/components/GrProgressCircle'

Filedist/types/components/GrProgressCircle/GrProgressCircle.vue.d.ts

GrProgressCirclePropsinterface
interface GrProgressCircleProps {
    value?: number;
    indeterminate?: boolean;
    shape?: GrProgressCircleShape;
    showValue?: boolean;
    formatValue?: (value: number) => string;

Filedist/types/components/GrProgressCircle/geometry.d.ts

GrProgressCircleShapetype
type GrProgressCircleShape = typeof GR_PROGRESS_CIRCLE_SHAPES[number];
GrArcGeometryinterface
interface GrArcGeometry {
    radius: number;
    circumference: number;
    arcLength: number;
    trackDashArray: string;
    valueDashArray: string;
    rotation: number;
}

Filedist/types/components/GrProgressCircle/grProgressCircleStyles.d.ts

GrProgressCircleTonetype
type GrProgressCircleTone = GrTone;
GrProgressCircleSizetype
type GrProgressCircleSize = GrComponentSize;

Filedist/types/components/GrProgressCircle/config.d.ts

grProgressCircleConfigconst
const grProgressCircleConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrProgressCircle">;

Filedist/types/components/GrProgressCircle/safelist.d.ts

grProgressCircleSafelistconst
const grProgressCircleSafelist: string[];

Filedist/types/components/GrProgressCircle/defaults.d.ts

GrProgressCircleConfigurablePropsinterface
interface GrProgressCircleConfigurableProps {
    size: GrProgressCircleSize;
    tone: GrProgressCircleTone;
    shape: GrProgressCircleShape;
    trackless: boolean;
}

Filedist/types/components/GrProgressCircle/index.d.ts

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

  • showValue prints percentages, and formatValue sets a text of your own along with aria-valuetext;
  • statusIcon replaces the number with a tick at value >= 100 and with a cross at tone="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.

The tones are shared with the bar

The arc is coloured with the same variables as GrProgressBar (--gr-progress-bg, --gr-progress-success-bg and their neighbours): progress has one theming layer, and a consumer who has recoloured it expects the circle and the bar to agree. Every variable is taken with a fallback to a role of the theme — with a granular import of the circle alone the per-component theme of the bar is not connected, and without the fallback the arc would be left without colour.

Only the geometry belongs to the circle: --gr-progress-circle-size, --gr-progress-circle-track, --gr-progress-circle-cap, --gr-progress-circle-value-size.

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
  • buffer is 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 GrAvatar and GrIcon exactly; pointed adjustment goes through --gr-progress-circle-size, and the geometry of the arc is computed in viewBox units and does not depend on the diameter;
  • there is no gradient along the arc — it requires a <linearGradient> with an id of 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.