opsinjs
ComponentsHealth input

DoseTracker

A record of the medicine doses a product has already logged, drawing each as a fact with a word and a shape, never computing adherence or judging a missed dose.

Preview

Theme
Density
Text
Status
dose-tracker · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/dose-tracker

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 { DoseTracker } from "@/components/ui/dose-tracker"
<DoseTracker
  label="Today's doses"
  now={new Date().toISOString()}
  entries={[
    { name: "Morning tablet", time: "2026-03-14T08:00:00+00:00", state: "taken" },
    { name: "Afternoon tablet", time: "2026-03-14T15:00:00+00:00", state: "missed" },
  ]}
/>

When to use it

Use it when

  • A record of medicine doses the product has already logged, taken and missed together, read as history.
  • A day's doses as plain facts, each state a word and a shape beside its time, nothing computed across rows.
  • Anywhere the product owns the schedule and the drug data and needs only the log drawn.

Do not use it when

  • You are logging a general daily entry, such as a symptom or a note, not a dose with a state.Use log-sheet instead.
  • You need to tell the reader what to do next. That is advice, and this component asserts only facts.Use care-card instead.
  • You are showing a single reading or figure rather than a list of dose events.Use metric-tile instead.

Clinical meaning

Asserts. Exactly the log the product handed it: these medicines, at these times, in these states. A "Missed" marker says the product recorded that dose as missed, and nothing further.

Never read as. Advice, a diagnosis, an adherence score or an instruction. A "Missed" row names a fact, never a fault and never a next step. It computes no percentage, because that would be a clinical figure nobody authored. Clinical interaction guidelines draws the line between recording a fact and prescribing a response.

Colour axis. Neither. A dose state is a fact, not a level of urgency, so no marker carries data-status or data-category. Each marker is neutral chrome with a word and a distinct shape, so the four states survive greyscale. Painting a missed dose the red that means "act now" is exactly the axis mixing The two colour axes forbids.

Thresholds. The consuming product, always. DoseTracker ships no schedule, no drug data, no dose, no threshold and no default text. Each would be a clinical claim Regulatory context says a presentation layer cannot make.

Time. Each entry's time and now go to RelativeTime unmodified, so it owns what a stale or future instant renders as. Uncertainty and staleness is the rule it keeps.

Anatomy

  1. DoseTrackerThe root, an unordered list on neither colour axis, with an optional aria-label naming the log.Controlled by entries
  2. DoseTracker.EntryOne logged dose, a list item holding the name, the marker, the time and an optional note.Controlled by state
  3. DoseTracker.MarkerThe state told twice: an aria-hidden lucide shape and the word, in neutral chrome, with no data-status.Controlled by state
  4. DoseTracker.NameThe medicine's name, in the product's own words. A placeholder in every example, never a real drug.Controlled by name
  5. DoseTracker.TimeWhen the dose event sits, rendered through RelativeTime, which keeps the exact date in its datetime attribute.Controlled by time
  • DoseTracker 1data-slot="dose-tracker", a ul.
    • DoseTracker.Entry 1..ndata-slot="dose-tracker-entry", a li.
      • DoseTracker.Name 1data-slot="dose-tracker-name". An optional note sits beneath it, with no slot.
      • DoseTracker.Marker 0..1data-slot="dose-tracker-marker". Absent for an unrecognised state.
      • DoseTracker.Time 1data-slot="dose-tracker-time". Wraps a RelativeTime.

Examples

A day's doses

A day's medicines as the product logged them, some taken, one missed, one skipped, driven by each entry's state. In greyscale the states are still told apart by their words and shapes.

Theme
Density
Text
Status
dose-tracker-a-days-doses · base base · style base-lyraOpen under the product theme

A missed dose

One state="missed" entry, shown as a fact and left there: a word, a shape, a time, no red, no next step and no adherence figure.

Theme
Density
Text
Status
dose-tracker-a-missed-dose · base base · style base-lyraOpen under the product theme

Content guidelines

Write each medicine name in the words the reader already knows it by. Keep note short and factual, such as "with food" or who marked the dose. A note that tells the reader what to do is advice, and belongs in a CareCard. The banned words at Reference ranges apply to every name and note.

Do

A missed dose marked "Missed" in neutral chrome, with nothing else on the row.

Don’t

A missed dose in a red chip reading "Take now" under an "80% adherence" figure.

Accessibility

Audited against WCAG 2.2 AA, in a source pass and a rendered pass. This is an author-run audit, not an independent review, and clinical review is pending. The rendered pass on /view/base/base-lyra/component/dose-tracker was clean, and the source pass found no defect, so nothing was changed.

What the audit confirmed.

  • Each marker shape carries aria-hidden beside a visible word (1.1.1), and a real ul/li carries the list semantics (1.3.1).
  • No status colour, category colour or colour literal: each state is told by a word and a distinct lucide shape.
  • Icons are em-sized, so at 200% text they grow with the step and the times wrap rather than clip. No px type size.

What a reader should still know.

  • The list's accessible name is offered only through label, as an aria-label. Every example supplies one, but a persistent visible label is a recommended enhancement.
  • No aria-live region is announced: this static read-only log renders once and never updates in place, so 4.1.3 owes no status message.
  • With no interactive element, target size (2.5.8), focus visibility and keyboard operation do not apply.

Not measured. Every contrast pair of hairline and muted ink against the host ground, and the markers under forced-colors: active.

KeyActionNotes
TabSkips the whole componentNothing is focusable. The tracker is a record to read, not a control.
Shift+TabSkips the whole componentSame in reverse. Never in the focus order.
Arrow keysNothingNothing to select, move or expand. The entries are static content.
PairThemeAPCA LcWCAG 2.2Floor
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

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

API reference

Prop

Type

Generated from DoseTrackerProps in registry/bases/base/dose-tracker.tsx.

An empty entries array renders nothing and warns in development. A state outside the four renders its row with no marker and warns, never approximated into a shape. now is required: read the clock once per screen and pass the same instant to every timestamp. label is the list's aria-label.

  • LogSheet records the general daily entries the product defines. A symptom, a meal or a mood is not a dose.
  • CareCard is where advice about a dose belongs, with its guidance and author explicit.
  • TimelineEntry marks one event on a timeline, where DoseTracker is a day's doses read together.

On this page