Teams need an agreed goal that stays consistent across dashboards and alerts.
Updated September 10, 2026
Problem
People cannot judge a metric consistently when its target is hidden, outdated, or copied differently across dashboards.
Solution
Store each target with its metric definition, owner, and review date. Let panels and alerts reference the same target rather than keeping separate copies.
Show the target beside the value and make its reasoning available. Define the time window and what happens when it is missed.
Where the target allows a measured amount of failure, show how much of that allowance remains and how quickly it is being used. Keep a history of target changes so past results remain understandable.
Use when
A team has agreed on a measurable goal that several views or alerts need to use consistently.
Don’t use when
The number is aspirational or nobody owns it. A target nobody is accountable for teaches viewers that targets on this dashboard are decorative, and that lesson transfers to the ones that matter.
Trade-offs
Targets can become outdated or encourage people to improve the number without improving the underlying outcome. Shared targets need permissions and version history. One target can also hide problems elsewhere in a distribution.
Checklist
Who owns this target, and do they know?
When was it last reviewed, and when is it next due?
Is the metric definition it applies to unambiguous and reachable?
Is the target stored once and referenced, or retyped per panel?
Does the display show distance to target, not only current value?
Is there a budget or rate framing, or only a pass/fail?
What happens when it is missed—does anything at all change?
Could this target be met while the underlying goal is not?
Does the alert that fires use the same number the panel draws?
Is the history of the target visible, so a change is not silent?
Compare
Honeycomb treats SLOs as objects with budget burn as the primary display, which is the fullest expression of the pattern and the one that answers “should I act now” rather than only “am I over”. Grafana has no target concept: goals live as per-panel thresholds, so the same objective is retyped across dashboards and drifts independently in each. Datadog offers SLOs as first-class entities with their own status and history, so a target has a page rather than a field. Looker and BI tools generally push targets into the modelling layer, which makes them consistent everywhere and makes changing one an engineering task rather than a product decision.
Related
Target and progress displays progress toward the goal. Threshold line draws a boundary on a chart. Alert rule uses the condition for notification.
Explain this metric defines what is measured. KPI tile can show the value beside its target.
Wireframe — the pattern's anatomy, not any one product's version of it
Implementation
This is the dashboard knowing what good is. A target turns a number into a judgement, which means the target needs an owner and a date on it or nobody can say whether missing it matters.
MetricTarget.tsxTarget is a type with owner, review date and definition as required fields. TargetCard shows it; ErrorBudget derives the burn from it.
import { Card } from "@/components/ui/card";import { Progress } from "@/components/ui/progress";/** * A target is a commitment with provenance, and the provenance is the type. * Owner and review date are required because a target with no name cannot * be revised, only ignored, and an unreviewed one is wrong within a year and * still colouring panels. Panels take a Target; they never retype the number. */export type Target = { name: string; /** The objective, as a fraction: 0.999 for 99.9%. */ objective: number; /** What the objective is a fraction of: "requests", "checkouts". */ of: string; /** How the indicator is computed, so the reasoning is reachable from the panel. */ measuredAs: string; owner: string; reviewDue: Date; /** What draws or fires on this target. Stored once, referenced from here. */ referencedBy: string[];};const DAY = 86_400_000;const pct = (f: number) => `${+(f * 100).toFixed(2)}%`;function review(due: Date, now: Date) { const days = Math.ceil((due.getTime() - now.getTime()) / DAY); if (days < 0) return { text: `overdue by ${-days} days`, cls: "text-status-critical" }; const text = days >= 14 ? `due in ${Math.round(days / 7)} weeks` : `due in ${days} days`; return { text, cls: days <= 30 ? "text-status-warn" : "text-card-foreground" };}export function TargetCard({ target, now, onOpen }: { target: Target; now: Date; onOpen?: (t: Target) => void }) { const r = review(target.reviewDue, now); return ( <Card className="p-4"> <p className="border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground">{target.name}</p> <dl className="mt-3 grid grid-cols-[84px_1fr] gap-x-3 gap-y-2.5 text-xs"> <dt className="text-muted-foreground">objective</dt><dd className="m-0 tabular-nums text-card-foreground">{pct(target.objective)} of {target.of}</dd> <dt className="text-muted-foreground">measured as</dt><dd className="m-0 text-card-foreground">{target.measuredAs}</dd> <dt className="text-muted-foreground">owner</dt><dd className="m-0 text-card-foreground">{target.owner}</dd> <dt className="text-muted-foreground">reviewed</dt><dd className={`m-0 ${r.cls}`}>{r.text}</dd> </dl> <p className="mt-3 border-t pt-2 text-[11px] text-muted-foreground">used by</p> <ul className="mt-1 flex flex-wrap gap-1.5 text-[11px]"> {target.referencedBy.map((ref) => ( <li key={ref}> <button type="button" onClick={onOpen && (() => onOpen(target))} className="rounded-[2px] border px-2 py-0.5 text-card-foreground">{ref}</button> </li> ))} </ul> </Card> );}/** * The budget is what makes the target usable on a Tuesday. It converts the * pass/fail into a rate: how much of the permitted failure is spent, against * how much of the period has elapsed. */export function budget(target: Target, period: { start: Date; end: Date }, errors: number, total: number, now: Date) { const allowed = (1 - target.objective) * total; const left = Math.max(0, 1 - errors / allowed); const elapsed = (now.getTime() - period.start.getTime()) / (period.end.getTime() - period.start.getTime()); const daysToGo = Math.ceil((period.end.getTime() - now.getTime()) / DAY); return { left, expectedLeft: 1 - elapsed, daysToGo, fast: left < 1 - elapsed };}export function ErrorBudget({ target, period, errors, total, now }: { target: Target; /** The period the budget is spread over, usually the quarter. */ period: { start: Date; end: Date }; errors: number; total: number; now: Date;}) { const b = budget(target, period, errors, total, now); return ( <Card className="bg-muted p-4"> <p className="border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground">Error budget</p> <p className="mt-3 text-3xl font-medium tabular-nums text-card-foreground">{Math.round(b.left * 100)}%</p> <p className="text-[11px] tabular-nums text-muted-foreground">left, {b.daysToGo} days to go</p> <div className="relative mt-3"> <Progress value={b.left * 100} aria-label="error budget left" className="h-3 rounded-[2px] bg-background [&>div]:bg-chart-1" /> {/* Where the budget would be if it were spent evenly over the period. */} <span aria-hidden="true" className="absolute -top-1 h-5 w-px bg-status-warn" style={{ left: `${b.expectedLeft * 100}%` }} /> </div> <p className={`mt-2 text-[11px] ${b.fast ? "text-status-warn" : "text-muted-foreground"}`}> {b.fast ? "burning faster than the quarter allows" : "on pace for the quarter"} </p> </Card> );}
demo.tsxHow it is called: one target object, the card that names it and the budget computed from it against the quarter.
import { ErrorBudget, TargetCard, type Target } from "./MetricTarget";/** * One target, stored once, drawn twice: the card that names it and the * budget derived from it. Fifty days into the quarter, 62% of the permitted * failures are spent, so the budget is burning faster than the period allows. */const NOW = new Date("2026-08-20T09:00:00Z");const Q3 = { start: new Date("2026-07-01T00:00:00Z"), end: new Date("2026-09-30T00:00:00Z") };const AVAILABILITY: Target = { name: "Checkout availability", objective: 0.999, of: "requests", measuredAs: "non-5xx / total, 28d", owner: "payments", reviewDue: new Date("2026-09-10T00:00:00Z"), referencedBy: ["availability panel", "the wallboard tile", "the paging rule"],};export default function Demo() { return ( <div className="grid grid-cols-[1.4fr_1fr] gap-5"> <TargetCard target={AVAILABILITY} now={NOW} onOpen={(t) => console.log("open", t.name)} /> <ErrorBudget target={AVAILABILITY} period={Q3} errors={25_544} total={41_200_000} now={NOW} /> </div> );}
What it renders. Identical markup in both panes, with only the token values changing.
Screenshots are grouped by product and include the capture date.
Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.
Grafana SLO / SLO Overview September 10, 2026Grafana Play (signed out; no version string exposed)dense · dark · desktop-web
The overview lists twenty-eight objectives with an indicator, remaining budget, and sparkline for each. The budget column combines large negative percentages with positive values, but gives little explanation of how to read them. No data uses the same red as breached objectives, making missing information harder to distinguish from a failed target. The sparklines also use different scales, so their shapes cannot be compared directly across rows.
Metric targetsThe budget column adds context to each objective’s result.
Target and progressLarge negative budget values need a clear explanation of the calculation.