SourceCitation
Where a piece of health information came from, in plain words, with an optional link to the fuller citation and an optional date it was last checked.
Preview
source-citation · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/source-citation
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 { SourceCitation } from "@/components/ui/source-citation"<SourceCitation
source="Estimated by your watch from movement and heart rate."
more={{ label: "Read how this is worked out", href: "/how-it-works" }}
checkedOn="2026-03-14"
locale="en-GB"
/>When to use it
Use it when
- Beside a health value whose provenance a reader needs to weigh it: a wearable estimate, a home device reading, a typed figure.
- To attribute and link a manufacturer's accuracy document or a study you have read, rather than restating its claim as your own.
- Wherever a number's origin bounds what may be said about it, so origin travels with the number where it is read.
Do not use it when
- The message is a standing statement about the whole product, rather than the source of one value.Use
disclaimer-noteinstead. - You are saying when a reading was taken or synced, rather than where it came from.Use
relative-timeinstead. - You are explaining what a term means, rather than where a value came from.Use
terminstead. - The note is information about the content with no source to attribute.Use
calloutinstead.
Clinical meaning
Asserts. Where a health value came from, in the product's own plain words, with an optional checked date and a link to the fuller source.
Never read as. A guarantee that the source is right, current, or checked by anybody. checkedOn is the date the product recorded. A value old enough to doubt is wrapped in RelativeTime beside it.
Colour axis. Neither: no status prop and no category prop, and a class from either axis raises a development warning. See The two colour axes.
Thresholds. None. No reference range, no threshold, no verdict, and no clinical number passes through.
Who owns the words. The product. Data provenance and device accuracy sets ten rules, and this component carries two. Rule 2 puts provenance in plain words at the value. Rule 6 attributes a manufacturer's claim rather than restating it. The other eight stay the caller's, and it renders a wrong string as readily as a right one. opsinjs ships no source, no date, no DOI and no study. See Evidence and references.
Anatomy
SourceCitationThe root. Quiet and outside both colour axes: no fill, no radius, no role.Controlled byclassNameSourceCitation.SourceThe product's plain words for where the value came from. Given nothing, it prints one line saying so.Controlled bysourceSourceCitation.CheckedThe checked date, one line down in quieter ink. The component prefixes it; a time element carries the machine date.Controlled bycheckedOnSourceCitation.LinkThe fuller citation. Label and destination arrive together, on its own line, keeping the full target size.Controlled bymore
Examples
A device estimate naming its own method
The provenance names the device and the class of the figure, so it reads as an estimate. more points at the fuller accuracy document.
source-citation-a-device-estimate · base base · style base-lyraOpen under the product themeWhen nobody said where it came from
With no source, the citation prints an admission rather than rendering empty. Markup you wrap a source in is taken at its word.
source-citation-no-source-supplied · base base · style base-lyraOpen under the product themeContent guidelines
Name the concrete thing rather than the brand's marketing, and say the class of the figure: an estimate is called an estimate. Name the link for where it goes, since a screen reader may list it alone. A vague label raises a development warning in English only.
Do
"Estimated by your watch from movement and heart rate." Names the device, the inputs and the class of the figure.
Don’t
"Powered by clinically validated technology." Names no device and no method.
Accessibility
This component was audited against WCAG 2.2 AA in two passes, one over the source and one over the rendered output. The audit is author-run, not independent, and clinical review is still pending. Both passes were clean: no axe violations, target sizes met, focus visible, and no loss at 320px reflow.
- No
pxtype size. Both lines are set attext-opsin-footnote, a rem from the generated ramp. - No raw colour literal and no ramp step. The source ink is
--foregroundand the date ink is--muted-foreground. morecarries the label and the href together, so a link cannot be added without a label. An external href adds an off-screen "opens in a new tab".- The link is not inside a sentence, so it re-adds
--opsin-target-minimumon both axes. See Target size and motor. - It reflows at 200% text: no fixed height,
min-w-0withwrap-break-word, so a long device name breaks.
Residual, left for you to weigh.
- The source line, the last-checked time and the fuller-citation link render as loose siblings with no programmatic group binding them into one citation. Reading order keeps the association, so this is not an AA failure.
- The "Last checked on" chrome is always English while the date follows the caller's
locale; when they differ, the localized date carries nolangseam yet, per ADR 0005.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves to the link, when there is one | The citation is text, not a tab stop. The link is its only focusable element. |
| Shift+Tab | Moves back out of the link | Reverse order matches visual order. The link is last. |
| Enter | Follows the link | Only when a link is present. An ordinary anchor, so Enter and not Space. |
| Space | Scrolls the page, as it does anywhere else | Nothing here captures Space or activates on it. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| 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 |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from SourceCitationProps in registry/bases/base/source-citation.tsx.
source is typed optional but required by the contract, so the missing state can render. checkedOn is a strict YYYY-MM-DD string or epoch milliseconds, both pinned to UTC. A value that is not a real calendar date is refused, not rendered as a plausible wrong one.
Related
- DisclaimerNote: the standing statement about the whole product, not one value.
- RelativeTime: when a reading was taken or synced, not where it came from.
- Callout: information about the content with no source to attribute.
- Term: what a word means, rather than where a value came from.
DisclaimerNote
The short standing note saying this is information, not medical advice. It is placed by rule, so it is always in the same place and never in the way.
LogSheet
A small sheet that slides up so you can record something in a few seconds without leaving the screen you were on. One reading, one dose, one note.