- Timeline
- Using the data adapter
- Basic REST example
- Handling the AbortSignal
- Error handling
- The CareTimelineEvent object
- Tags
- Event types and colors
- Cross-system match confidence
- Mismatch highlighting
- Expanding event details
- Flagging a result inside the panel
- Expand or collapse all rows
- Filtering events
- Date range
- Filtering while events are still streaming
- ResizeObserver diagnostics
- Printing
- Print context
- Color and black-and-white output
- Filtered prints
- Refreshing data
- Best practices
- Importing
- Properties
- Events
- Methods
- Custom Properties
- CSS Parts
- Dependencies
Care Timeline
<ahi-care-timeline> | AhiCareTimeline
Renders a virtualized, streaming EHR event timeline with built-in filtering, sorting, and expandable detail panels.
<!-- Inspect the templates/care-timeline/index.html file to see the actual source code for this example -->
<iframe
loading="lazy"
style="visibility:hidden;min-height:600px;" onload="this.style.visibility='visible';"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
title="Care timeline template demo"
src="../../templates/care-timeline/">
</iframe>
The timeline displays a chronological stream of clinical events — encounters, lab results, medications, procedures, and more — for a single patient. It supports cursor-based incremental data loading, off-thread sorting and filtering, and DOM virtualization for large datasets.
Using the data adapter
The timeline does not accept events directly as a property. Instead, you provide a
CareTimelineDataAdapter that the component calls to fetch pages of events. This decouples the
component from any specific backend — REST, GraphQL, FHIR, or static mock data all work the same way.
interface CareTimelineDataAdapter {
fetchPage(input: {
cursor: unknown | null; // null on the first call
pageSize?: number; // defaults to the component's page-size attribute
signal?: AbortSignal; // abort in-flight requests when this fires
}): Promise<{
events: CareTimelineEvent[];
nextCursor: unknown | null; // null signals the last page
}>;
}
The component calls fetchPage repeatedly, passing the nextCursor returned by the
previous call, until nextCursor is null. Events stream into the UI as each page
resolves — the user sees results immediately without waiting for all pages to load.
Basic REST example
const adapter = {
async fetchPage({ cursor, pageSize = 50, signal }) {
const params = new URLSearchParams({ limit: String(pageSize) });
if (cursor) params.set('after', String(cursor));
const res = await fetch(`/api/patients/123/events?${params}`, { signal });
if (!res.ok) throw new Error(`Failed to load events: ${res.statusText}`);
const { events, nextCursor } = await res.json();
return { events, nextCursor };
}
};
document.querySelector('ahi-care-timeline').adapter = adapter;
Handling the AbortSignal
Each fetchPage call receives an AbortSignal. The signal fires when the component
unmounts or when the adapter is replaced. Pass it to fetch (or your HTTP client) so in-flight
requests are cancelled automatically:
const res = await fetch(url, { signal });
If you’re using a library that doesn’t accept a signal natively, check signal.aborted after
each async step and throw early:
await someLibraryCall();
if (signal?.aborted) return { events: [], nextCursor: null };
Error handling
Throw from fetchPage to display an error in the timeline. The component catches the error and
normalizes it into a CareError — the thrown error’s message becomes the
user-facing text and its stacktrace (when available) is captured into details — then renders it
in the error region below the filter bar. Errors are non-blocking: events that loaded before the failure
stay visible, and the adapter error clears whenever a new load attempt begins (a refresh(), an
adapter change, or the stream resuming as the user scrolls).
const res = await fetch(url, { signal });
if (!res.ok) throw new Error('Unable to load patient events. Please try again.');
Here, the adapter loads one page successfully and then throws on the second page — the events from the first page stay visible below the error:
<ahi-care-timeline id="timeline-error-demo" style="height: 360px;"></ahi-care-timeline>
<script>
document.querySelector('#timeline-error-demo').adapter = {
fetchPage({ cursor }) {
if (cursor === 1) {
return Promise.reject(new Error('Unable to load patient events. Please try again.'));
}
return Promise.resolve({
events: [
{ id: 'evt-1', start: '2025-03-01', type: 'encounter', title: 'ED Visit', description: 'Chest pain, ruled out MI' },
{ id: 'evt-2', start: '2025-02-14', type: 'lab-results', title: 'CBC Panel', description: 'Within normal limits' }
],
nextCursor: 1
});
}
};
</script>
Surfacing errors from outside the adapter
If you fetch supplementary data outside the adapter — or one of several upstream sources fails while the
rest succeed — pass those failures to the errors property and they render in the same error
region as adapter errors. Each entry is a CareError:
interface CareError {
/** User-facing message describing the failure. */
message: string;
/**
* Optional technical diagnostic (e.g. a stacktrace) revealed through a
* per-error disclosure that is collapsed by default. Always rendered as
* plain text. When omitted, the error renders as a message-only row with
* no disclosure control.
*/
details?: string;
}
const timeline = document.querySelector('ahi-care-timeline');
timeline.errors = [
{ message: 'Immunization records could not be loaded.' },
{ message: 'External lab source timed out.', details: error.stack }
];
Entries with details render a “Show details” toggle that expands to the raw text in a
scrollable monospace block. Each assignment to errors replaces the full list — the component
does not accumulate entries across calls, so if you have multiple independent sources of consumer-supplied
errors, merge them into one array yourself before assigning. Set errors to [] (or
null) to remove consumer-supplied entries; an adapter error is managed by the component itself
and clears when a new load attempt begins. The error region is exposed as the errors CSS part.
The demo below sets one error without details and one with — the second renders a “Show
details” toggle. Both appear alongside the successfully loaded events:
<ahi-care-timeline id="timeline-consumer-errors-demo" style="height: 360px;"></ahi-care-timeline>
<script>
const consumerErrorsTimeline = document.querySelector('#timeline-consumer-errors-demo');
consumerErrorsTimeline.adapter = {
fetchPage() {
return Promise.resolve({
events: [
{ id: 'evt-1', start: '2025-03-01', type: 'encounter', title: 'ED Visit', description: 'Chest pain, ruled out MI' },
{ id: 'evt-2', start: '2025-02-14', type: 'lab-results', title: 'CBC Panel', description: 'Within normal limits' }
],
nextCursor: null
});
}
};
consumerErrorsTimeline.errors = [
{ message: 'Immunization records could not be loaded.' },
{
message: 'External lab source timed out.',
details: 'Error: ETIMEDOUT\n at fetchLabs (labs-client.js:42:11)\n at async loadSupplementary (page.js:9:3)'
}
];
</script>
Set the adapter property via JavaScript, not as an HTML attribute. If your script runs before
the browser has upgraded the <ahi-care-timeline> element (i.e. before the custom
element definition has loaded), setting the property on the DOM node still works — the component reads it
during connectedCallback and begins loading automatically.
The CareTimelineEvent object
Each object returned in the events array must conform to the
CareTimelineEvent interface. Only id, start, type, and
title are required; all other fields are optional.
interface CareTimelineEvent {
/** Stable unique identifier used for deduplication across pages. */
id: string;
/**
* Event start as an ISO 8601 string — either date-only (`'2025-06-27'`) or
* datetime with offset (`'2025-06-27T21:19:00+00:00'`). Time is displayed
* when present; date-only values render without a time.
*/
start: string;
/**
* Optional end date or datetime in the same ISO 8601 format as `start`.
* When present, the end is shown below the start time in the date column.
* If the end falls on the same calendar day as `start`, only the end time
* is shown. If it falls on a different day, the full end date and time are shown.
*/
end?: string;
/**
* Semantic event category in kebab-case (e.g. 'encounter', 'lab-results',
* 'medication'). Any value is accepted — the timeline generates a filter
* button for each distinct type seen in the data, with dot colors drawn
* from the `typeColors` property.
*/
type: string;
/** Primary label shown in bold at the top of the card. */
title: string;
/** Secondary text shown below the title. */
description?: string;
/**
* Care source where the event occurred, in kebab-case (e.g. 'hospital',
* 'outpatient-clinic'). Any value is accepted — the timeline generates a
* source filter button for each distinct value seen in the data.
*/
source?: string;
/**
* Clinical participants associated with this event. Each entry may include
* an opaque `id`, a human-readable `name`, and an optional `role` label
* (e.g. 'Prescriber', 'Ordered by', 'Attending', 'Asserter').
*/
participants?: { id?: string; name: string; role?: string }[];
/** Opaque facility identifier (not displayed). */
facilityId?: string;
/** Human-readable facility name shown right-aligned in the card footer. */
facilityName?: string;
/** Colored pill labels rendered at the bottom of the card. */
tags?: Array<{
label: string;
/**
* Background color as any valid CSS color value (e.g. `'#d00001'`, `'hsl(0 100% 41%)'`).
* The component automatically selects black or white text using the APCA algorithm (WCAG 3 draft).
* Omit for a neutral default pill.
*/
color?: string;
}>;
/** Structured content shown in the expandable detail panel. */
detail?: {
/** Key–value pairs rendered as a definition list. */
fields: Array<{
label: string;
value: string;
/** Highlight color for the value; the indicator glyph inherits it. */
valueColor?: string;
/** Subdued suffix rendered after the value (e.g. `'mg/dL'`). */
valueSuffix?: string;
/** Flag glyph rendered after the value — typically `'↑'` or `'↓'`. */
indicator?: string;
/** Short label rendered after the value (e.g. `'Critical'`). */
annotation?: string;
/** Color applied to the annotation text. */
annotationColor?: string;
}>;
/**
* Optional link rendered at the bottom of the panel.
* Must be an http/https URL or a root-relative path — `javascript:` URIs are rejected.
*/
link?: { label: string; href: string };
};
/**
* Cross-system patient match confidence level. When set, a colored left border is rendered
* on the card so clinicians can quickly assess data provenance.
* - `'high'` — strong match (e.g. exact MRN).
* - `'moderate'` — probable match (e.g. demographic match).
* - `'low'` — weak match; treat with caution.
*/
matchConfidence?: 'high' | 'moderate' | 'low';
/**
* Source patient identity from the originating system. When set, the patient's
* demographics are displayed on the card so clinicians can visually verify the
* cross-system match before acting on the data. Fields that differ from the
* component's `patientIdentity` property are highlighted in yellow.
*/
sourcePatient?: {
name: string;
dob: string;
mrn: string;
/** Last 4 digits only — rendered as `•••-••-XXXX`. */
ssnLast4?: string;
sex?: string;
/**
* Optional link to any resource in the originating system — a patient chart,
* a C-CDA document, an external web application, etc.
*/
link?: { label: string; href: string };
};
}
Tags
The tags array renders colored pill labels at the bottom of each event card. Use them to
surface clinically meaningful status — abnormal results, active medications, critical flags, etc. — rather
than encoding meaning in the type field.
Set color to any valid CSS color value. The component uses the APCA algorithm (WCAG 3 draft) to
automatically render the tag text in black or white, whichever produces higher perceptual contrast against
the background. Omit color for a neutral default pill.
tags: [
{ label: 'Critical', color: '#a70002' /* --ahi-color-red-800 */ }, // → white text
{ label: 'Normal', color: '#007c3d' /* --ahi-color-green-700 */ }, // → white text
{ label: 'Pending' } // → neutral default
];
Event types and colors
Each event card displays the type value as a text badge (e.g. lab-results → “Lab
results”) in the top-left of the card. The badge uses the type’s assigned color as a subtle background tint,
so color reinforces the label rather than being the sole indicator — important for color-blind users.
Set typeColors to an object mapping each kebab-case type value to a CSS color string. Types not
present in the map render without a color tint.
document.querySelector('ahi-care-timeline').typeColors = {
encounter: '#0a77bc', // --ahi-color-data-1
'lab-results': '#d16c00', // --ahi-color-data-2
medication: '#00964c', // --ahi-color-data-3
procedure: '#8a55a1', // --ahi-color-data-4
imaging: '#007d8a' // --ahi-color-data-6
};
Filter buttons for both types and sources appear dynamically as new values are seen — there is no hardcoded list. Type filter pills use the same color-tinted style as the card badges for visual consistency.
Cross-system match confidence
When events are sourced from an external or federated system, set matchConfidence and
sourcePatient to give clinicians the context they need to assess data provenance before acting
on it.
matchConfidence renders a colored left border and a confidence icon with a tooltip on the card:
| Value | Meaning | Icon |
|---|---|---|
'high' |
Strong match — e.g. exact MRN | Check square |
'moderate' |
Probable match — e.g. demographic match | Alert triangle |
'low' |
Weak match; treat with caution | Alert circle |
The icon is color-coded and includes a tooltip (e.g. “High match confidence”) so the confidence level is communicated to color-blind users and screen reader users without relying on color alone.
A Match confidence button in the filter bar opens a legend that describes each level.
sourcePatient displays the patient’s demographics from the originating system directly on the
card — name, DOB (with calculated age), sex, masked SSN last 4, and MRN — in that order. An optional
link renders after the MRN as a → link, useful for navigating to the source chart,
a C-CDA document, or an external application.
{
id: 'evt-999',
start: '2025-03-01',
type: 'encounter',
title: 'ED Visit',
matchConfidence: 'moderate',
sourcePatient: {
name: 'Jane Doe',
dob: '04/12/1980',
sex: 'Female',
ssnLast4: '6789',
mrn: 'EXT-00042',
link: { label: 'View C-CDA document', href: '/records/external/ccda/EXT-00042' }
}
}
Both matchConfidence and sourcePatient are optional and independent — you can set
either or both.
Mismatch highlighting
Set the patientIdentity property on the component to the known identity of the patient being
viewed. Each sourcePatient field is then compared against the corresponding value, and any
field that does not match exactly is highlighted in yellow so clinicians can immediately spot discrepancies
— a different MRN, a name variation, or a mismatched SSN.
document.querySelector('ahi-care-timeline').patientIdentity = {
name: 'Jane Doe',
dob: '04/12/1980',
sex: 'Female',
ssnLast4: '6789',
mrn: 'EXT-00042'
};
SSN comparison only highlights when both patientIdentity.ssnLast4 and
sourcePatient.ssnLast4 are set. Sex comparison only highlights when both are set.
Expanding event details
Events that include a detail object display a chevron button in the top-right corner of the
card. Clicking the chevron expands or collapses the detail panel and fires
ahi-care-timeline-event-expand when expanding. Clicking the card body selects the event without
toggling the detail panel.
Events without a detail object have no chevron and are not expandable.
Flagging a result inside the panel
A detail field is plain text by default. When a value needs to stand out — an out-of-range lab result, a
critical value — add valueColor, valueSuffix, indicator,
annotation and annotationColor to that field. These are the same property names
CareSummaryGroupRow uses in
<ahi-care-summary>, so one adapter can feed both components.
The timeline never decides which results are abnormal. Pass a resolved CSS color and let your
application own the interpretation — the mapping from Observation.interpretation (or your own
thresholds) to a color and a glyph belongs in the consuming app, not in the design system.
Color is never the only signal: the value carries the color, the indicator glyph inherits it, and the panel
adds screen-reader-only text marking the value as flagged. Use annotation when the specific
interpretation matters to a screen-reader user — the component can’t infer what an arrow means, but your
text can say it.
<ahi-care-timeline id="timeline-flagged-demo" style="height: 340px;" expand-all></ahi-care-timeline>
<script>
document.querySelector('#timeline-flagged-demo').adapter = {
fetchPage() {
return Promise.resolve({
events: [
{
id: 'cmp-1',
start: '2025-03-04T09:12:00-05:00',
type: 'lab-results',
title: 'Comprehensive Metabolic Panel',
tags: [{ label: 'Abnormal', color: '#a70002' /* --ahi-color-red-800 */ }],
detail: {
fields: [
{ label: 'Sodium', value: '139', valueSuffix: 'mmol/L' },
{ label: 'Potassium', value: '4.1', valueSuffix: 'mmol/L' },
{
label: 'Alkaline Phosphatase',
value: '48',
valueSuffix: 'U/L',
indicator: '↓',
valueColor: '#a70002' /* --ahi-color-red-800 */,
annotation: 'Below reference range',
annotationColor: '#a70002' /* --ahi-color-red-800 */
},
{ label: 'Albumin', value: '4.2', valueSuffix: 'g/dL' }
]
}
}
],
nextCursor: null
});
}
};
</script>
Expand or collapse all rows
The filter bar includes an Expand all / Collapse all toggle (to the left of the event
count) that flips the whole timeline between compact and detailed layouts in one action. Only events with a
detail object are affected. Because the timeline streams in pages, this is a live mode: once
“Expand all” is active, events that stream in afterward render expanded automatically. Individually
collapsing or expanding a single card still works and overrides the bulk mode for that one row until the
mode is toggled again. Bulk expansion is silent — it does not fire the per-card
ahi-care-timeline-event-expand event.
By default the chosen mode is remembered per-device via localStorage, so reopening a timeline
(or loading a new patient) restores the clinician’s last preference. If localStorage is
unavailable (private mode, blocked), the control still works for the session — it just won’t be remembered.
To persist the preference server-side instead, use the controllable expand-all attribute (or
the expandAll property) together with the
ahi-care-timeline-expand-all-change event:
<!-- Hydrate from a server-stored preference; the host value wins over localStorage. -->
<ahi-care-timeline expand-all></ahi-care-timeline>
timelineEl.addEventListener('ahi-care-timeline-expand-all-change', e => {
// e.detail.expanded is a boolean (no PHI) — safe to store server-side.
saveUserPreference('careTimeline.expandAll', e.detail.expanded);
});
The event fires only on user-initiated toggles. Setting expand-all /
expandAll programmatically (e.g. when hydrating from the server) applies silently and does not
re-emit the event, so there is no feedback loop. Resolution order when the timeline first renders is:
host-provided expand-all > localStorage value > default (collapsed).
Filtering events
The filter bar is built in — no wiring required. It is the timeline’s own header, so the controls and the results they narrow share one surface:
-
A search field matching
title,type,descriptionand participant names. - A single Filters control opening a panel with every criterion in it — date range first, then event type, then source — with a badge counting how many are active. The panel is anchored to the trigger’s left edge, so it doesn’t move under the pointer when the badge appears or disappears.
- A chip row beneath, listing each active filter with a remove button and a Clear all. It is hidden entirely when nothing is filtering.
The chip row is the point of the arrangement: a closed panel would otherwise hide why a clinician is looking at a short timeline. Whatever is narrowing the list is always stated in the open.
Every filter composes as AND, and each change is reported as
ahi-care-timeline-filter-change carrying the resulting CareTimelineFilters:
| Field | Type | Description |
|---|---|---|
types |
string[] |
Only events whose type is in the list. Empty or absent means all types. |
sources |
string[] |
Only events whose source is in the list. Empty or absent means all sources. |
search |
string |
Case-insensitive match against title, type, description and
participant names.
|
dateFrom |
string |
Earliest event day, as ISO YYYY-MM-DD. Inclusive. Absent means no
lower bound.
|
dateTo |
string |
Latest event day, as ISO YYYY-MM-DD. Inclusive. Absent means no upper
bound.
|
timelineEl.addEventListener('ahi-care-timeline-filter-change', e => {
// e.g. { types: ['lab-results'], dateFrom: '2026-02-01', dateTo: '2026-03-31' }
console.log(e.detail.filters);
});
Date range
The date-range section is a segmented control with five modes: 30 days, 6 months, 12 months, All time, and Custom.
A preset owns its bounds. It resolves to a closed range ending today, and a caption under the control states
what that range came out as — Feb 17, 2026 – Aug 17, 2026 — so a range is never applied without
showing its ends. All time clears both bounds and captions itself as “No date limit” — scoped
to dates on purpose, since a type, source or search filter may still be narrowing the view. It is the date
section’s off state, not the panel’s.
Custom is where the user supplies the bounds. Choosing it replaces the caption with From and To fields, seeded from whatever the outgoing preset resolved to, so a custom range starts from the window already on screen rather than from empty fields. The two are deliberately exclusive: showing editable fields next to a preset that owns its own bounds invites the question of which one is in charge.
Setting one bound alone leaves the range open-ended: dateFrom alone means “on or after”,
dateTo alone means “on or before”. Both bounds include events on the boundary day itself. Each
field constrains the other via min/max so the date picker cannot produce a
backwards range; if one is typed anyway, the timeline says so — in the panel and in the chip row — and
leaves the range unapplied rather than emptying itself.
The selected mode is held on its own rather than inferred from the bounds, because the two are not equivalent: a custom range that happens to coincide with the last 30 days is still Custom, and its fields have to stay on screen.
An event’s day is taken from the leading YYYY-MM-DD of its start, which is the
same value the timeline groups its day headers by — so a range boundary always agrees with the day headers,
whether start is a date-only value or carries a time and offset.
Because presets end today, a preset hides any event dated in the future — a scheduled appointment, for
instance. The caption is what makes that safe to ship: the upper bound is stated rather than implied, so a
clinician can see the window they are looking through. Use All time (or a Custom range with
no upper bound) to include future-dated events.
Filtering while events are still streaming
Filtering happens client-side, in the timeline’s Web Worker, over the pages fetched so far. Filters therefore need no round-trip and no adapter support — but while a long history is still streaming, any filter can legitimately match fewer events than exist. A date range over the older end of a long history is the clearest case, though a type, source or search filter is affected identically. The timeline handles this honestly rather than silently:
- While any filter is active and the stream is unfinished, the chip row says more matching events may appear. Search is included even though it gets no chip of its own — the row then exists purely to carry that caveat.
- The “No events match the current filters” empty state is withheld until the stream finishes, so an incomplete result is never presented as a settled one.
- Events from later pages that match appear on their own; the user never re-applies the filter.
- With no filter active there is no caveat and no chip row: nothing is being hidden, and the header count already reports that loading is in progress.
Clearing a filter never triggers a refetch — the full event set is already in the worker.
ResizeObserver diagnostics
The timeline uses ResizeObserver internally for virtualization. Browsers may emit benign
"ResizeObserver loop completed with undelivered notifications" or
"ResizeObserver loop limit exceeded" diagnostics as a side effect. The W3C CSS Working Group
has
classified these as benign and proposed downgrading them to warnings.
If your application has a global error handler that surfaces these as visible dialogs or alerts, suppress them at the application level:
window.addEventListener(
'error',
e => {
if (
e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
e.message === 'ResizeObserver loop limit exceeded'
) {
e.stopImmediatePropagation();
}
},
true
);
Printing
Call timelineEl.print() to open the browser print dialog with a formatted clinical document.
The output respects whatever filter and search state the user has applied — to print the full history, clear
filters first.
print() is a no-op if the timeline has not finished loading. Render the print button as
disabled and enable it once loading completes. Use the loaded property for a
synchronous check in case the component has already finished loading before your listener is attached:
const timeline = document.querySelector('ahi-care-timeline');
const printBtn = document.querySelector('#print-btn'); // rendered with disabled attribute
if (timeline.loaded) {
printBtn.disabled = false;
} else {
timeline.addEventListener(
'ahi-care-timeline-load',
() => {
printBtn.disabled = false;
},
{ once: true }
);
}
printBtn.addEventListener('click', () => {
timeline.print({
patientName: 'Margaret A. Thompson',
dob: '03/14/1951',
mrn: '0042-B',
room: '4W-12',
facility: 'Heart & Vascular Center',
admissionDate: 'Nov 7, 2024',
attendingPhysician: 'Dr. James Park, MD',
printedBy: 'Dr. Sarah Chen, MD',
footerNote: 'Generated from Azalea Health EHR — for clinical use only'
});
});
Print context
All fields of the CareTimelinePrintContext object are optional. Include only what is relevant
for your use case.
| Field | Type | Description |
|---|---|---|
patientName |
string |
Patient’s full name — shown prominently at the top of the document. |
dob |
string |
Date of birth in any display format (e.g. '03/14/1951'). |
mrn |
string |
Medical record number. |
facility |
string |
Facility or department name (e.g. 'Heart & Vascular Center'). |
room |
string |
Patient room or bed identifier (e.g. '4W-12'). |
admissionDate |
string |
Admission or encounter date in any display format. |
attendingPhysician |
string |
Attending or referring physician name. |
printedBy |
string |
Name of the clinician triggering the print — shown in the running footer. |
printedAt |
string |
Override the “Printed on” timestamp. Defaults to the current date and time. |
footerNote |
string |
Optional note on the left side of every page footer (e.g. a disclaimer). |
Color and black-and-white output
The print document renders in full color when the printer supports it — event type badges use their assigned
typeColors tint, tag pills use their color values, and confidence badges are
colored green, amber, or red. Every signal is also carried by text and symbols, so the document remains
fully legible when printed in black-and-white or viewed by users with color vision deficiencies.
Filtered prints
The printed document reflects the timeline’s current filter and search state. If the user has filtered to a
specific event type, searched for a term, or narrowed to a date range, only matching events are included. A
Filtered view notice is printed in the overview band when any filter is active, listing the
applied filters — including the date range, spelled out as Feb 1, 2026 – Mar 31, 2026 (or
On or after … for an open-ended bound) — so the recipient knows the document is not a complete
history.
Note the distinction in the overview band: Range always reports the span of the events actually printed, while Date range appears only when a range filter is active and reports the bounds the user asked for.
Refreshing data
Call timelineEl.refresh() to reset all state and re-fetch events from the beginning using the
current adapter. This is equivalent to re-assigning the adapter property but without replacing
the adapter object.
document.querySelector('#refresh-btn').addEventListener('click', () => {
document.querySelector('ahi-care-timeline').refresh();
});
refresh() is a no-op when no adapter is set. Active filters and search are reset alongside the
event data so the refreshed stream starts in an unfiltered state.
Best practices
Timeline should:
- Be used to display a longitudinal clinical history for a single patient.
-
Always provide a
CareTimelineDataAdapter— even for static or mock data — to keep the data-loading contract consistent. - Prefer cursor-based pagination over offset-based to avoid duplicate or missing events when data changes while the user scrolls.
-
Honour the
AbortSignalpassed tofetchPageso in-flight requests are cancelled when the component unmounts or new filters are applied.
Timeline should NOT:
- Be used for non-clinical or non-chronological data — use
ahi-tablefor tabular data. - Pre-load all events into memory for large datasets; let the adapter stream pages incrementally.
-
Use the
typefield to communicate status or outcomes to the user — it drives the card type badge, the connector dot color (viatypeColors), and generates a filter button. Usetagsfor labels that carry meaning (e.g.Critical,Abnormal,Active).
Importing
To import this component using a script tag:
<script type="module" src="/path/to/azalea-kit-ui/dist/components/care-timeline/care-timeline.js"></script>
To import this component using a bundler:
import '@azaleahealth/azalea-kit-ui/dist/components/care-timeline/care-timeline.js';
Properties
| Name | Description | Type | Default | |
|---|---|---|---|---|
expandAll
expand-all
|
Expand-all mode. When true, every event with detail content renders expanded —
including events that stream in later — and the toggle control shows “Collapse all”; when
false (default) rows are compact. This is the controllable surface for hosts that
persist the clinician’s preference server-side: set the expand-all attribute (or this
property, in any order relative to connection) from a stored value and it takes precedence over the
component’s per-device localStorage fallback. Setting it programmatically applies
silently — only a user toggle emits ahi-care-timeline-expand-all-change. Individual
per-card expand/collapse still works and overrides the mode for that row.
|
boolean
|
- | |
adapter
|
The data adapter that supplies paginated events. Setting this property resets all state and starts a fresh streaming load. |
CareTimelineDataAdapter | null
|
null
|
|
errors
|
Consumer-supplied errors to display in the component’s error region — use this to surface failures
that occurred while fetching data outside the adapter (e.g. one of several upstream sources failed).
Each entry’s
message is shown to the user; when details is present it is revealed
through a collapsed-by-default disclosure. Errors thrown by the adapter are displayed in the same
region automatically. Setting [] or null
removes consumer-supplied entries only.
|
CareError[] | null
|
null
|
|
pageSize
page-size
|
Number of events requested per fetchPage call. Defaults to 50. |
number
|
50
|
|
typeColors
|
Explicit mapping of event type values to CSS color strings. Each key is a kebab-case type value
(e.g. 'encounter', 'lab-results'); the value is any valid CSS color. Types
not present in the map render without a color.
|
Record<string, string>
|
{}
|
|
patientIdentity
|
The known identity of the patient being viewed. When set, each field in an event’s
sourcePatient is compared against the corresponding field here. Any field that does not
match exactly is highlighted so clinicians can quickly spot discrepancies (e.g. a different MRN or a
name variation).
|
{ name: string; dob: string; mrn: string; ssnLast4?: string; sex?: string; } | null
|
null
|
|
flow
|
When set, the component renders in flow mode — it expands to its natural height and the page (or any ancestor) handles scrolling. Virtualization still works via window-level scroll. When absent (default), the component manages its own internal scroll container and fills the height of its parent. |
boolean
|
false
|
|
loaded
|
true once the adapter has finished streaming every page and the worker has applied the
final batch — i.e. all events are present in the timeline. Becomes false again whenever
a refresh or adapter change kicks off a new stream. Parent applications should use this (or listen
for the ahi-care-timeline-load event) to gate UI like a print button — calling
print() before this is true is a no-op.
|
boolean
|
- | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about properties and attributes.
Events
| Name | Description | Event Detail |
|---|---|---|
ahi-care-timeline-event-expand |
Emitted when the user expands an event card to see details. detail.event is the
expanded CareTimelineEvent:
{ id, start, end?, type, title, description?, source?, participants?, facilityId?, facilityName?,
tags?, detail?, matchConfidence?, sourcePatient? }. Note: sourcePatient carries patient identifiers (name, dob, mrn, and optionally
ssnLast4/sex) — treat this event’s payload as PHI. Fired only for single-card expansions; bulk
expand-all/collapse-all is silent.
|
AhiCareTimelineEventExpandEvent
|
ahi-care-timeline-expand-all-change |
Emitted when the user toggles the expand-all / collapse-all control. detail.expanded is
a boolean (true = expand all, false = collapse all) and carries no PHI, so
a host may persist it server-side. Fired only for user-initiated toggles — setting the
expand-all property/attribute programmatically does not re-emit it.
|
AhiCareTimelineExpandAllChangeEvent
|
ahi-care-timeline-filter-change |
Emitted when the active filter pills change. detail.filters is a
CareTimelineFilters object:
{ types?: string[], sources?: string[], search?: string }.
|
AhiCareTimelineFilterChangeEvent
|
ahi-care-timeline-load |
Emitted once the adapter finishes streaming every page and the worker has applied the final batch.
detail.totalCount is the total number of loaded events. Listen for this to enable
print/export controls that need the full event list.
|
AhiCareTimelineLoadEvent
|
Learn more about events.
Methods
| Name | Description | Arguments |
|---|---|---|
refresh() |
Resets all state and re-fetches events from the beginning using the current adapter. No-op if no adapter is set. | - |
print() |
Opens the browser print dialog for a formatted timeline document. The printed document includes
every event currently visible in the timeline — i.e. it respects whatever filter and search state
the user has applied. To print the entire history, clear filters before calling
print(). Refuses to print if the timeline has not finished loading; parent applications
should listen for the ahi-care-timeline-load event (or read the
loaded property) before exposing a print control. The output never relies on color
alone — type, confidence, source, tags, and source-patient discrepancies are all carried by text
labels and symbols so the document remains legible printed in black-and-white or read by users with
color vision deficiencies.
|
context: CareTimelinePrintContext
|
Learn more about methods.
Custom Properties
| Name | Description | Default |
|---|---|---|
--date-width |
Width of the date column. | 4.5rem |
--connector-width |
Width of the connector column. | 2.5rem |
Learn more about customizing CSS custom properties.
CSS Parts
| Name | Description |
|---|---|
base |
Deprecated. Use the care-timeline part instead. |
care-timeline |
The component’s outer wrapper. |
filters |
The filter pill bar at the top. |
errors |
The error region listing adapter and consumer-supplied data-fetch errors. |
scroll |
The scrollable event list. |
detail |
The expanded detail panel of an event card. |
detail-field |
A single key–value row within the detail panel. |
detail-label |
The label (dt) of a detail row. |
detail-value |
The value (dd) of a detail row, including any suffix, indicator and annotation. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<ahi-button><ahi-button-group><ahi-icon><ahi-icon-button><ahi-input><ahi-popover><ahi-popup><ahi-radio-button><ahi-radio-group><ahi-skeleton-body-text><ahi-spinner><ahi-tag><ahi-tooltip>