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
dose-tracker · base base · style base-lyraOpen under the product themeInstallation
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-sheetinstead. - You need to tell the reader what to do next. That is advice, and this component asserts only facts.Use
care-cardinstead. - You are showing a single reading or figure rather than a list of dose events.Use
metric-tileinstead.
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
DoseTrackerThe root, an unordered list on neither colour axis, with an optional aria-label naming the log.Controlled byentriesDoseTracker.EntryOne logged dose, a list item holding the name, the marker, the time and an optional note.Controlled bystateDoseTracker.MarkerThe state told twice: an aria-hidden lucide shape and the word, in neutral chrome, with no data-status.Controlled bystateDoseTracker.NameThe medicine's name, in the product's own words. A placeholder in every example, never a real drug.Controlled bynameDoseTracker.TimeWhen the dose event sits, rendered through RelativeTime, which keeps the exact date in its datetime attribute.Controlled bytime
- 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.
- DoseTracker.Entry 1..ndata-slot="dose-tracker-entry", a li.
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.
dose-tracker-a-days-doses · base base · style base-lyraOpen under the product themeA 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.
dose-tracker-a-missed-dose · base base · style base-lyraOpen under the product themeContent 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-hiddenbeside a visible word (1.1.1), and a realul/licarries 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
pxtype size.
What a reader should still know.
- The list's accessible name is offered only through
label, as anaria-label. Every example supplies one, but a persistent visible label is a recommended enhancement. - No
aria-liveregion 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.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the whole component | Nothing is focusable. The tracker is a record to read, not a control. |
| Shift+Tab | Skips the whole component | Same in reverse. Never in the focus order. |
| Arrow keys | Nothing | Nothing to select, move or expand. The entries are static content. |
| 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 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.
Related
- 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.
ScaleInput
A rating scale drawn from the points and end words the product supplies, for a self-report the product reads, shipping no instrument of its own.
Questionnaire
The styled form shell that groups a product's own questions and shows how far through them a reader is, while scoring and interpreting nothing clinical.