opsinjs
ComponentsHealth data display

MetricTile

A compact readout of one measurement. It carries the number, its unit and how recent it is, and it is sized to sit in a grid with several others.

Preview

Theme
Density
Text
Status
metric-tile · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/metric-tile

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 { MetricTile } from "@/components/ui/metric-tile"
<MetricTile
  label="Example measurement"
  value={14}
  unit="steps"
  precision={0}
  measuredAt="2026-04-06T07:15:00+00:00"
  now={now}
  href="/results/example"
/>

When to use it

Use it when

  • A summary grid where several measurements are visible at once and the reader scans.
  • A measurement the reader already understands, such as their resting heart rate.
  • An entry point: a tile that opens the full result via href.

Do not use it when

  • The reader is seeing this measurement for the first time and needs it explained.Use result-card instead.
  • The value only makes sense against a reference range that will not fit.Use range-bar instead.
  • A whole surface has no reading yet. One empty cell in a grid is still a tile.Use empty-state instead.
  • You need to show how the value has moved. This draws no line.Use trend-sparkline instead.
  • The value is still loading and the grid would otherwise reflow.Use skeleton instead.

Clinical meaning

Asserts. With a reading, that this measurement had this value, in this unit, at this time, so measuredAt is required beside one. An absence is not a reading of zero.

Never read as. A current reading, unless it is one. Past the product's boundary the tile mutes and says so in words, as Uncertainty and staleness requires.

Colour axis. Category, on the icon and the label only. Status appears as an embedded StatusPill, never the tile's background, so no element carries both (The two colour axes).

Thresholds. None. staleAfterHours has no default, because opsinjs holds no boundary for any measurement. Omitted, there is no stale treatment. The tile derives no level: status is the product's, under Clinical status semantics. Pair attention or urgent with an href, because nothing here enforces it.

Provenance and masking. There is no provenance prop and no mask, so do not route a figure nobody measured through measuredAt. Data provenance and On-screen privacy name this component, and neither is met.

Anatomy

  1. MetricTileThe root: an <a> with href, a container without. Carries neither axis.Controlled by href
  2. SurfaceOne level down, the card rung supplies the material.
  3. MetricTile.HeaderIcon and label on one row. Not a heading element.
  4. MetricTile.IconThe product's glyph, category-tinted and aria-hidden, only when passed.Controlled by icon
  5. MetricTile.LabelWhat was measured, in the reader's words. Carries data-category, and mutes when stale.Controlled by label
  6. MetricTile.AffordanceA trailing aria-hidden chevron, only with href.Controlled by href
  7. MetricTile.ReadingOne Value at display size, the loudest thing here. An absence drops to muted headline.Controlled by value
  8. MetricTile.TimeOne RelativeTime: event word, exact date, staleness words. Dropped when value is null.Controlled by measuredAt
  9. StatusPillThe product's level, embedded last, after the time line, and absent when no status was assigned.Controlled by status

Examples

A summary grid

Every tint is category. Exactly one tile carries a status, as a pill with the word in it. The others say nothing about urgency.

Theme
Density
Text
Status
metric-tile-a-summary-grid · base base · style base-lyraOpen under the product theme

No boundary was supplied

Two readings, three hours and five weeks old, with no staleAfterHours. Only the printed date marks the older one.

Theme
Density
Text
Status
metric-tile-no-boundary-supplied · base base · style base-lyraOpen under the product theme

States

Data stateWhat MetricTile renders
ErrorA measuredAt the tile cannot locate beside a reading is refused, and nothing renders.
Emptyvalue={null} keeps the label, drops the time line, and is not zero.
StalePast staleAfterHours the parts mute and the time line adds may be out of date.

Content guidelines

Labels are two or three words in the reader's vocabulary: resting heart rate, not RHR. A caveat belongs on the surface the tile opens.

Do

"Resting heart rate", with the unit beside the number and the recency on the tile.

Don’t

"RHR" with a bare number: an unknown acronym, no unit and no date.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending. The rendered pass returned zero axe violations, no target-size hits, no focus-visible failure and no reflow at 320px; the source pass found no defect to fix.

Residual.

  • A linked tile can trip WCAG 2.5.3 Label in Name because Value speaks kilograms for a visible kg. The repair belongs in Value and is deferred; the steps demo does not trip.
  • Nothing enforces the JSDoc rule that attention and urgent pair with an href.
  • The event word names when a reading happened, not how it was obtained; there is no provenance prop, so an estimate and a lab result sound alike.

Beyond the audit. Contrast as the tile composes it, prefers-contrast: more, forced-colors, 200% text, print, and masking all stay unverified.

KeyActionNotes
TabMoves to the tile, when it has an hrefOne stop for the whole tile, none without an href.
Shift+TabMoves backwards to the tileDOM order is visual order.
EnterFollows the linkOnly when href is set.
SpaceScrolls the pageA link does not activate on Space.
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every part; icon, affordance and time only when renderedmetric-tile, metric-tile-header, metric-tile-icon, metric-tile-label, metric-tile-affordance, metric-tile-reading, metric-tile-time
data-categoryOn the icon and the label only, when a known category is passedsleep | heart | activity | nutrition | mind | labs
data-statusOn the embedded StatusPill only, when a status is set. The tile stamps nonesteady | watch | attention | urgent
data-opsinjs-valueOn the embedded Value, alwaysThe unrounded number, or an empty string with no reading

API reference

Prop

Type

Generated from MetricTileProps in registry/bases/base/metric-tile.tsx.

A measuredAt this component cannot locate beside a reading is refused: nothing renders, and development reports it once. label, value, measuredAt, now and precision are required with no default. value is number | null, so a compound reading takes two tiles. Read now once per screen and pass it to every tile.

  • ResultCard is the same data with room to explain itself.
  • Value is the formatting primitive inside the tile.
  • Card is a different component, not a bigger tile.
  • StatusPill is the level inside a tile, never its background.
  • TrendSparkline is not embedded, because a line needs a caption.

On this page