---
title: "TimelineEntry"
description: "One dated event in a vertical history, with a neutral rail and marker, the time written the way people say it, and any clinical status shown as a nested pill."
url: "https://opsinjs.pensievelabs.org/components/timeline-entry"
source: "https://opsinjs.pensievelabs.org/components/timeline-entry.md"
section: "Components"
status: "shipped"
kind: "component"
category: "health-data-display"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["history item", "event list", "activity feed"]
governedBy: ["uncertainty-and-staleness", "trends-and-change"]
usedIn: ["daily-log-screen", "diabetes-medicines-app"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="timeline-entry"
  status="shipped"
  questions="[
  &#x22;No contrast pair has been measured. The neutral marker and the hairline connector are painted on whatever ground the host provides, and that pairing is unchecked.&#x22;,
  &#x22;No screen reader has met this component, so whether an entry reads well inside a product's own list has not been listened to by anybody.&#x22;,
  &#x22;No locale is forwarded to the composed RelativeTime, so the recorded time takes the runtime default and a server render can disagree with a browser.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="timeline-entry" />

## Installation [#installation]

<ComponentInstall name="timeline-entry" unbuilt="false" importPath="@/components/ui/timeline-entry" registryDependencies="[&#x22;relative-time&#x22;, &#x22;status-pill&#x22;]" />

## Usage [#usage]

```tsx
import { TimelineEntry } from "@/components/ui/timeline-entry"
```

```tsx
<ol>
  <TimelineEntry
    when="2026-03-14T08:12:00+00:00"
    now={new Date().toISOString()}
    title="Repeat prescription issued"
    status="watch"
    isLast
  >
    Issued by the example clinic.
  </TimelineEntry>
</ol>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Showing one recorded event in a history the product lays out: a prescription issued, a test booked, a note added.&#x22;,
  &#x22;Placing a single dated entry on a vertical rail, where the product owns the list around it.&#x22;,
  &#x22;A row that needs the recorded time in words, with an optional status beside the event.&#x22;,
]"
  avoid="[
  { case: &#x22;You want one component to own the whole history, its ordering and its data model.&#x22;, instead: &#x22;card&#x22; },
  { case: &#x22;You are charting one reading as it moves over time rather than listing distinct events.&#x22;, instead: &#x22;trend-sparkline&#x22; },
  { case: &#x22;You only need to say when a single thing happened, with no rail, marker or event around it.&#x22;, instead: &#x22;relative-time&#x22; },
  { case: &#x22;You are recording medicine doses taken and missed, which has its own shape.&#x22;, instead: &#x22;dose-tracker&#x22; },
  { case: &#x22;You need the reader to write down what happened rather than read a recorded event.&#x22;, instead: &#x22;log-sheet&#x22; },
]"
/>

## Clinical meaning [#clinical-meaning]

**Asserts.** That the named event was recorded at the given instant, and nothing
more. A gap between two entries is not evidence that nothing happened in it.

**Never read as.** A diagnosis, a verdict, or a guarantee that the history is
complete or current. The entry never triages, diagnoses or advises, and every
word in it is the product's.

**Colour axis.** Neither, on the entry. The rail and the marker are neutral
chrome. A supplied `status` is delegated to a nested StatusPill, which carries
the status axis with a word and a glyph. See
[The two colour axes](../health/two-colour-axes.mdx).

**Thresholds.** The product owns them all. The entry holds no threshold, no
staleness boundary and no event text.
[Uncertainty, staleness and missing data](../health/uncertainty-and-staleness.mdx)
covers why it never guesses whether a recorded time is too old to trust, and
[Trends and change](../health/trends-and-change.mdx) covers why one entry is one
event, not a direction.

## Anatomy [#anatomy]

<Anatomy
  name="timeline-entry"
  parts="[
  { name: &#x22;TimelineEntry&#x22;, describes: &#x22;The root, a single list item. It takes colour from neither axis.&#x22;, prop: &#x22;when&#x22; },
  { name: &#x22;TimelineEntry.Rail&#x22;, describes: &#x22;The neutral column holding the marker and, unless this is the last entry, the connector.&#x22;, prop: &#x22;isLast&#x22; },
  { name: &#x22;TimelineEntry.Marker&#x22;, describes: &#x22;The dot on the rail: a muted fill and a hairline. Never a status colour.&#x22; },
  { name: &#x22;TimelineEntry.Time&#x22;, describes: &#x22;The recorded time, drawn by RelativeTime with the event word fixed at recorded.&#x22;, prop: &#x22;now&#x22; },
  { name: &#x22;TimelineEntry.Title&#x22;, describes: &#x22;What happened, in the reader's words, with any status pill beside it.&#x22;, prop: &#x22;title&#x22; },
  { name: &#x22;StatusPill&#x22;, describes: &#x22;The level the product assigned, at size sm. The only part with an axis colour.&#x22;, prop: &#x22;status&#x22; },
  { name: &#x22;TimelineEntry.Body&#x22;, describes: &#x22;The product's own detail beneath the title, in muted text.&#x22;, prop: &#x22;children&#x22; },
]"
/>

<CompositionTree
  name="timeline-entry"
  tree="[
  {
    part: &#x22;TimelineEntry&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;A li element. Neither data-status nor data-category.&#x22;,
    children: [
      { part: &#x22;TimelineEntry.Rail&#x22;, cardinality: &#x22;1&#x22;, note: &#x22;aria-hidden.&#x22;, children: [
        { part: &#x22;TimelineEntry.Marker&#x22;, cardinality: &#x22;1&#x22; },
      ] },
      { part: &#x22;TimelineEntry.Time&#x22;, cardinality: &#x22;1&#x22;, note: &#x22;Wraps a RelativeTime.&#x22; },
      { part: &#x22;TimelineEntry.Title&#x22;, cardinality: &#x22;1&#x22;, children: [
        { part: &#x22;StatusPill&#x22;, cardinality: &#x22;0..1&#x22;, note: &#x22;Stamps data-status. Absent when no status was supplied.&#x22; },
      ] },
      { part: &#x22;TimelineEntry.Body&#x22;, cardinality: &#x22;0..1&#x22;, note: &#x22;Absent when no children are given.&#x22; },
    ],
  },
]"
/>

## Examples [#examples]

### A history on one rail [#a-history-on-one-rail]

Every row names and dates its event, the connector runs to the last entry and
stops, and one row carries a status the product assigned.

<ComponentPreview name="timeline-entry-in-a-history" kind="example" align="start" />

### The status rides the pill [#the-status-rides-the-pill]

Two entries alike but for `status`. The rail, the marker and the entry stay
neutral, and status changes only the pill beside the title.

<ComponentPreview name="timeline-entry-with-a-status" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Name the event in the title as something that happened: `Repeat prescription
issued`, `Blood test booked`. A bare noun leaves the reader guessing whether it
was done, due, or cancelled. Let any status stay on the pill.

<DoDont>
  <DoDont.Do>
    **"Repeat prescription issued"** with the recorded time beneath it and the status on a pill.
  </DoDont.Do>

  <DoDont.Dont>
    **The whole entry tinted amber** to flag the event. A status carried by colour alone dies in greyscale.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

**Audited against WCAG 2.2 AA in a source pass and a rendered pass.** The
rendered pass (axe, target size, focus visible, reflow at 320px) and the source
pass against the rubric were both clean. This audit is author run, not an
independent review.

* The rail and the marker carry `aria-hidden`, so a screen reader meets the
  time, the title, any status word and the body in reading order.
* The recorded time is the system RelativeTime, a semantic `time` element, so
  the exact instant stays in the accessibility tree when only the phrase shows.
* Nothing in the entry is focusable, so keyboard, focus and target size do not
  apply.

**Read this before you rely on it.**

* The marker and the connector are decorative chrome under `aria-hidden`, so
  they are exempt from Non-text Contrast (1.4.11). They use role tokens anyway.
* The root renders a bare `li`, so the product must supply the `ol` or `ul`
  around it for list semantics to hold. On Safari with VoiceOver a
  `list-style: none` list can drop its list role, a platform quirk shared by
  every list based component here rather than a fault in this one.

<KeyboardTable
  name="timeline-entry"
  rows="[
  { keys: &#x22;Tab&#x22;, action: &#x22;Skips the entry&#x22;, notes: &#x22;Nothing here is focusable, so a long history costs a keyboard user no stops.&#x22; },
  { keys: &#x22;Shift + Tab&#x22;, action: &#x22;Skips the entry&#x22;, notes: &#x22;Same in reverse. The time, the title and the body are reached by reading.&#x22; },
]"
/>

<ContrastReport component="timeline-entry" />

## API reference [#api-reference]

<PropsTable name="TimelineEntryProps" />

`when` is ISO 8601 with an offset, the contract RelativeTime enforces on its
`at`. `now` is required because this component never reads the clock, so read
the clock once per screen and pass one value to every entry. The event word is
fixed and no `locale` is forwarded, so a product needing either composes
RelativeTime directly.

## Related [#related]

* [RelativeTime](./relative-time.mdx) is the recorded time alone, with no event or rail around it.
* [Card](./card.mdx) is a bounded block around related content; an entry can sit inside one.
* [TrendSparkline](./trend-sparkline.mdx) is one reading moving over time, where this lists distinct events.
