ResultCard
One test result, shown in full. What was measured, the number, where it sits against its range, what that means in plain English, and what to do next.
Preview
result-card · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/result-card
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { ResultCard } from "@/components/ui/result-card"
import { EXAMPLE_SOURCE } from "@/lib/opsinjs"<ResultCard
title="Example measurement"
value={14}
unit="mg/dL"
precision={0}
measuredAt="2026-03-14T08:12:00+00:00"
now="2026-03-14T11:12:00+00:00"
range={{ low: 10, high: 20, source: EXAMPLE_SOURCE }}
status="steady"
category="labs"
meaning="Two or three short sentences, in the second person, written by the product."
/>When to use it
Use it when
- A single result with room for its range, meaning and next step, behind authentication.
- A result seen for the first time, where the explanation matters as much as the number.
Do not use it when
- Several readings side by side in a dashboard grid.Use
metric-tileinstead. - The number has no range, no status and nothing to explain.Use
valueinstead. - You only need to show where a value sits within its range.Use
range-barinstead. - The main message is an instruction rather than a measurement.Use
care-cardinstead. - Something has changed and must interrupt the reader now.Use
alert-bannerinstead.
Clinical meaning
Asserts. A measurement was taken, at a time, in a unit. Where supplied, it also asserts a stated range and the level the product assigned. Every number goes through Value.
Never read as. A diagnosis, a summary of the reader's health, or a clinician's advice. A next step is the
product's, and who is speaking lives in the words you write in meaning and in
an action's label. A result that may distress needs
Delivering difficult results
around it.
Colour axis. Both, on different elements, as the two colour axes requires: status reaches only the StatusPill, category only the title, and no prop tints the surface.
Thresholds. The product's. No default range,
no status derivation and no
staleness boundary exist here.
staleAfterHours passes through to RelativeTime.
A reading you cannot vouch for must not drive attention or urgent.
Provenance and privacy. provenance is free text with no
class, so keeping an
estimated or self-reported value free of a status is your rule. There is no
masking affordance; On-screen privacy is
built around the card.
Anatomy
ResultCardAn article named by its title. Neutral surface; layout through className.ResultCard.HeaderTitle, time and level. On a phone the level wraps beneath the time.ResultCard.TitleA real heading at titleLevel. The only element the category axis touches.Controlled bycategoryResultCard.TimeWraps RelativeTime with the absolute date. Carries the staleness words past a supplied boundary.Controlled bystaleAfterHoursStatusPillThe only status-coloured element. Rendered only when the product assigned a level.Controlled bystatusResultCard.ReadingThe number at display size through Value, one per segment when compound.Controlled byvalueRangeBarDrawn only with a range, a unit and a single value. Given no status.Controlled byrangeResultCard.MeaningThe plain-English paragraph. When absent, the sentence saying so.Controlled bymeaningResultCard.ActionsAt most two. An href renders a Link, onSelect a Button; the recommended one is bordered.Controlled byactionsResultCard.FootnoteProvenance only, when supplied. No default disclaimer.Controlled byprovenance
Examples
A reading made of two numbers
segments carries a compound reading, each part a real Value.
The unit prints once, is spoken after every part, and no bar is drawn.
result-card-a-compound-reading · base base · style base-lyraOpen under the product themeThe smallest card, and the sentence it will not leave out
No range, so no bar; no status, so no pill; no provenance, so no footnote.
An absent meaning still renders as a sentence saying there is none.
result-card-no-explanation-supplied · base base · style base-lyraOpen under the product themeThe same result at two ages
Two cards differing only in measuredAt, with the same level, because age lowers
what a reading is worth and does not raise the alarm.
result-card-an-old-reading · base base · style base-lyraOpen under the product themeAccessibility
Audited against WCAG 2.2 AA. A source pass and a rendered browser pass were both clean: zero axe violations, no target below the 44px floor, no focus-visible gap. It found nothing to fix, is author-run rather than an independent review, and leaves clinical review pending.
What it confirmed.
- The two colour axes stay separate: category tint reaches only the title, clinical status only the StatusPill, which carries a word and icon.
- Units speak through Value after each part of a compound reading, the title is a real heading at the caller's level, and the card reflows at 320px and 200% without clipping.
Documented, by design.
- The root
<article>takesaria-labelfrom the title, notaria-labelledby. The string matches the visible heading and the region is non-interactive, so there is no name mismatch; a visible-heading reference is a future enhancement. classNamemerges onto the root, so a caller who passestruncatecan clip a reading. The component truncates nothing; the JSDoc warns of it.
Not checked by any gate. Print, forced-colors and prefers-contrast.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves to the first action, where the card has one | The card itself is not a tab stop. |
| Shift+Tab | Moves backwards through the same stops | Reverse order matches visual order. |
| Enter | Activates the focused action | Platform behaviour. Nothing is destructive. |
| Space | Activates a focused button action | On an onSelect action only. |
| Escape | Nothing | Not dismissible. Inside a Sheet or Dialog, Escape is theirs. |
| Arrow keys | Nothing | The bar is role="img", not a slider. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| activity ink on activity surface | light | 88.5 | 9.61:1 | Pass |
| activity ink on activity surface | dark | -83.1 | 12.58:1 | Pass |
| activity line on activity surface | light | 67.6 | 4.42:1 | Pass |
| activity line on activity surface | dark | -69.0 | 9.93:1 | Pass |
| activity accent on activity surface | light | 44.1 | 2.34:1 | Below floor |
| activity accent on activity surface | dark | -53.3 | 7.26:1 | Pass |
| activity accent on the page | light | 46.1 | 2.41:1 | Below floor |
| activity accent on the page | dark | -54.6 | 8.22:1 | Pass |
| activity ink on the page | light | 90.5 | 9.90:1 | Pass |
| activity ink on the page | dark | -84.4 | 14.24:1 | Pass |
| activity line on the band | light | 66.1 | 4.32:1 | Pass |
| activity line on the band | dark | -67.4 | 8.60:1 | Pass |
| heart ink on heart surface | light | 89.1 | 10.51:1 | Pass |
| heart ink on heart surface | dark | -80.5 | 12.55:1 | Pass |
| heart line on heart surface | light | 70.4 | 5.05:1 | Pass |
| heart line on heart surface | dark | -64.5 | 9.46:1 | Pass |
| heart accent on heart surface | light | 60.3 | 3.64:1 | Pass |
| heart accent on heart surface | dark | -48.2 | 6.68:1 | Pass |
| heart accent on the page | light | 63.2 | 3.81:1 | Pass |
| heart accent on the page | dark | -49.5 | 7.27:1 | Pass |
| heart ink on the page | light | 92.0 | 10.97:1 | Pass |
| heart ink on the page | dark | -81.8 | 13.65:1 | Pass |
| heart line on the band | light | 69.9 | 5.01:1 | Pass |
| heart line on the band | dark | -62.8 | 7.88:1 | Pass |
| labs ink on labs surface | light | 88.8 | 9.78:1 | Pass |
| labs ink on labs surface | dark | -83.1 | 12.69:1 | Pass |
| labs line on labs surface | light | 67.9 | 4.51:1 | Pass |
| labs line on labs surface | dark | -68.4 | 9.90:1 | Pass |
| labs accent on labs surface | light | 59.3 | 3.49:1 | Pass |
| labs accent on labs surface | dark | -52.7 | 7.21:1 | Pass |
| labs accent on the page | light | 61.5 | 3.60:1 | Pass |
| labs accent on the page | dark | -53.9 | 8.07:1 | Pass |
| labs ink on the page | light | 90.9 | 10.09:1 | Pass |
| labs ink on the page | dark | -84.3 | 14.22:1 | Pass |
| labs line on the band | light | 66.5 | 4.42:1 | Pass |
| labs line on the band | dark | -66.7 | 8.49:1 | Pass |
| mind ink on mind surface | light | 89.9 | 10.36:1 | Pass |
| mind ink on mind surface | dark | -81.3 | 12.56:1 | Pass |
| mind line on mind surface | light | 70.7 | 4.94:1 | Pass |
| mind line on mind surface | dark | -65.4 | 9.54:1 | Pass |
| mind accent on mind surface | light | 60.0 | 3.55:1 | Pass |
| mind accent on mind surface | dark | -49.1 | 6.77:1 | Pass |
| mind accent on the page | light | 62.8 | 3.70:1 | Pass |
| mind accent on the page | dark | -50.0 | 7.42:1 | Pass |
| mind ink on the page | light | 92.7 | 10.80:1 | Pass |
| mind ink on the page | dark | -82.2 | 13.77:1 | Pass |
| mind line on the band | light | 70.0 | 4.89:1 | Pass |
| mind line on the band | dark | -63.4 | 8.00:1 | Pass |
| nutrition ink on nutrition surface | light | 89.3 | 10.06:1 | Pass |
| nutrition ink on nutrition surface | dark | -81.9 | 12.55:1 | Pass |
| nutrition line on nutrition surface | light | 69.7 | 4.78:1 | Pass |
| nutrition line on nutrition surface | dark | -66.7 | 9.66:1 | Pass |
| nutrition accent on nutrition surface | light | 39.2 | 2.11:1 | Below floor |
| nutrition accent on nutrition surface | dark | -50.2 | 6.88:1 | Pass |
| nutrition accent on the page | light | 41.8 | 2.19:1 | Below floor |
| nutrition accent on the page | dark | -51.3 | 7.63:1 | Pass |
| nutrition ink on the page | light | 91.9 | 10.44:1 | Pass |
| nutrition ink on the page | dark | -82.9 | 13.91:1 | Pass |
| nutrition line on the band | light | 68.8 | 4.71:1 | Pass |
| nutrition line on the band | dark | -64.8 | 8.19:1 | Pass |
| sleep ink on sleep surface | light | 89.6 | 10.19:1 | Pass |
| sleep ink on sleep surface | dark | -81.9 | 12.60:1 | Pass |
| sleep line on sleep surface | light | 70.0 | 4.80:1 | Pass |
| sleep line on sleep surface | dark | -66.4 | 9.65:1 | Pass |
| sleep accent on sleep surface | light | 64.6 | 4.05:1 | Pass |
| sleep accent on sleep surface | dark | -50.4 | 6.94:1 | Pass |
| sleep accent on the page | light | 67.3 | 4.22:1 | Pass |
| sleep accent on the page | dark | -51.4 | 7.67:1 | Pass |
| sleep ink on the page | light | 92.3 | 10.60:1 | Pass |
| sleep ink on the page | dark | -82.9 | 13.92:1 | Pass |
| sleep line on the band | light | 69.1 | 4.75:1 | Pass |
| sleep line on the band | dark | -64.5 | 8.16:1 | Pass |
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| activity ink on the card material | light | 93.6 | 10.34:1 | Pass |
| activity ink on the card material | dark | -83.5 | 12.91:1 | Pass |
| heart ink on the card material | light | 95.1 | 11.47:1 | Pass |
| heart ink on the card material | dark | -80.9 | 12.38:1 | Pass |
| labs ink on the card material | light | 94.0 | 10.54:1 | Pass |
| labs ink on the card material | dark | -83.5 | 12.90:1 | Pass |
| mind ink on the card material | light | 95.7 | 11.28:1 | Pass |
| mind ink on the card material | dark | -81.4 | 12.49:1 | Pass |
| nutrition ink on the card material | light | 95.0 | 10.91:1 | Pass |
| nutrition ink on the card material | dark | -82.0 | 12.61:1 | Pass |
| sleep ink on the card material | light | 95.4 | 11.08:1 | Pass |
| sleep ink on the card material | dark | -82.1 | 12.62:1 | Pass |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On every part, always | result-card, result-card-header, -title, -time, -reading, -meaning, -actions, -footnote; result-card-action on a link action, button on an onSelect action |
| data-status | On the embedded StatusPill only, with a status | steady | watch | attention | urgent |
| data-category | On ResultCard.Title, for one of the six categories | sleep | heart | activity | nutrition | mind | labs |
| data-opsinjs-value | On every Value, including a drawn RangeBar's. De-duplicate before scraping. | The unrounded number, or an empty string with no reading |
API reference
Prop
Type
Generated from ResultCardProps in registry/bases/base/result-card.tsx.
now and precision are required: the card never reads the clock, so pass one
now to every card on a screen. value is number | null, never a string; a
pair is segments. A third action, a second recommended and a titleLevel
outside 2 to 6 each warn once and fall back.
Related
- MetricTile is the same data with no room to explain itself.
- Card is a neutral box with no clinical contract.
- CareCard is a card whose subject is an instruction, not a measurement.
- AlertBanner is for something that must interrupt.
- DisclaimerNote is where the not-medical-advice note lives.