Score ring
ProA computed score out of a maximum ("84 out of 100", "Workspace health"), as a ring with the score in its centre. It is a figure, not a progressbar: a score isn't work done, so it has no progressbar role and no aria-valuenow, and it is never drawn with ProgressRing, which would read as "84 of 100 done".
A score says it is one, when and from what: computedAt and basis are required, and the caption reads "Computed 2 hours ago from a weighted mean of five subscores", with a "How it's computed" link to the subscores when detailsHref is given. A delta names the score it is compared with ("Up 6 points since last month"). bands name the range the score falls in with a word beside a dot. With a null score it says "Not computed" and why (missing), and the ring is a dashed track, never an empty ring that reads as zero.
A projected score (what the app expects if something happens) is an estimate, so it is drawn apart from the score: a thin dashed arc on its own inner track, never the score's solid arc, and the words "Projected 90 if the 2 chosen fixes land" under the caption, with its basis. It is shown only beside a computed score; with "Not computed" there is nothing to project from, and it is left out.
The arc runs clockwise from 12 o'clock in every language, as a dial does. In forced colours it is drawn in Highlight on a CanvasText track. Put a SubscoreCard with layout="tiles" beside it, computed from the same subscores.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/score-ring
Brings in: bidi, link, locale, motion, relative-time, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { ScoreRing } from "@/components/candor/score-ring"
import { SubscoreCard, type SubscoreTile } from "@/components/candor/subscore-card"
// One set of subscores feeds both: the ring's score is their mean, and the tiles show where it comes from.
const subscores: SubscoreTile[] = [
{ label: "Access and permissions", score: 74, max: 100, tone: { word: "Needs work", tone: "warning" }, findings: [{ severity: "serious", count: 1 }] },
{ label: "Maintenance debt", score: 84, max: 100, tone: { word: "Fair", tone: "neutral" }, findings: [{ severity: "warning", count: 1 }] },
{ label: "Automation upkeep", score: 84, max: 100, tone: { word: "Fair", tone: "neutral" }, findings: [{ severity: "warning", count: 1 }] },
{ label: "Unused configuration", score: 90, max: 100, tone: { word: "Good", tone: "success" }, findings: [{ severity: "warning", count: 1 }] },
{ label: "Documentation", score: 94, max: 100, tone: { word: "Good", tone: "success" }, findings: [{ severity: "info", count: 2 }] },
]
const score = Math.round(subscores.reduce((n, s) => n + (s.score ?? 0), 0) / subscores.length)
const twoHoursAgo = new Date(Date.now() - 2 * 3_600_000).toISOString()
export default function Example() {
return (
<div className="grid items-start gap-4 md:grid-cols-[16rem_1fr]">
<div className="rounded-xl border bg-card p-4">
<ScoreRing
score={score}
max={100}
label="Workspace health"
computedAt={twoHoursAgo}
basis="the mean of five subscores"
detailsHref="#subscores"
delta={{ value: 6, vs: "last month", good: "up" }}
// An estimate, drawn apart from the score: a dashed inner arc and "Projected 90 if …".
projected={{ score: 90, basis: "if the 2 chosen fixes land" }}
bands={[
{ from: 0, word: "Needs attention", tone: "destructive" },
{ from: 60, word: "Fair", tone: "warning" },
{ from: 80, word: "Healthy", tone: "success" },
]}
/>
</div>
<div id="subscores">
<SubscoreCard layout="tiles" title="Subscores" subscores={subscores} />
</div>
</div>
)
}<ScoreRing>
| Prop | Type | Default | Description |
|---|---|---|---|
| scorerequired | number | null | — | The score; `null` when it isn't computed (say why in `missing`). |
| maxrequired | number | — | The highest score possible: a full ring. |
| labelrequired | string | — | What is scored: "Workspace health". It names the figure. |
| computedAtrequired | string | null | — | When the score was computed (ISO date); `null` when that isn't recorded, and the caption says so. |
| basisrequired | string | — | What the score is computed from, in words that finish "Computed … from": "a weighted mean of five subscores". |
| detailsHref | string | undefined | — | Where the subscores or the report are, linked as "How it's computed". |
| delta | ScoreDelta | undefined | — | The change against a named earlier score. |
| bands | ScoreBand[] | undefined | — | Named ranges of the score; the one the score falls in is drawn as its word. |
| missing | string | undefined | — | Why the score isn't computed, shown under "Not computed" when `score` is null. |
| projected | ScoreProjection | undefined | — | A projected score with its basis: drawn as a dashed inner arc and "Projected {score} {basis}", only beside a computed score. |
| action | React.ReactNode | undefined | — | A control under the ring, such as a button that opens the report. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under scoreRing in <LocaleProvider strings={{ … }}>; the table is exported as scoreRingStrings.
| Key | English |
|---|---|
| outOf | out of {max} |
| notComputed | Not computed |
| computed | Computed {when} from {basis} |
| computedUnknown | Computed from {basis}; when isn’t recorded |
| howComputed | How it’s computed |
| up | { one: "Up {count} point since {vs}", other: "Up {count} points since {vs}" } |
| down | { one: "Down {count} point since {vs}", other: "Down {count} points since {vs}" } |
| same | No change since {vs} |
| projected | Projected {score} {basis} |
Motion
| Moment | Family | What it does |
|---|---|---|
| value | value, fade | When a new score arrives while the ring is on screen (a new `computedAt` or a new score), the arc and the number crossfade to the new value. A ring that mounts with its score draws it still. |
Set with --cd-motion-score-ring-<moment>-* in your brand file; the families are in the design reference.
Types
- ScoreDelta
- A change in the score against a named earlier one, as `StatCard`'s delta: the period is always said.
{ /** The change in points: positive is up. */ value: number; /** What it is compared with, in words that finish "since …": "last month", "the audit on 5 November". */ vs: string; /** Which direction is good news; it sets the colour, never the words. */ good: "up" | "down"; }- ScoreBand
- A range of scores named by a word: a score at or above `from` (and below the next band's `from`) falls in it.
{ /** The lowest score in the band. */ from: number; /** The band's word, printed beside its dot ("Healthy", "Needs work"). */ word: string; /** The dot's colour. */ tone: "success" | "warning" | "destructive" | "neutral"; }- ScoreProjection
- A score the app expects if something happens: an estimate, drawn apart from the score.
{ /** The projected score. */ score: number; /** What it rests on, in words that finish "Projected 90 …": "if the 2 chosen fixes land". */ basis: string; }