Changelog Azalea Kit UI 101 Usage Customizing Utilities Form controls Laravel Vue FHIR Types Accessibility Icons Actions Button Button group Layout and structure Card Card section Care summary Care timeline Divider Empty state Footer Header Layout Stack Table Selection and input Calendar Checkbox Checkbox group Color picker Combobox Combobox option Input Input mask Memorable date Number input Option Phone input Radio Radio button Radio group Range Select SSN input Tag Textarea Toggle ZIP code input Images and icons Avatar Icon Icon button Logo Feedback indicators Badge Banner Progress bar Skeleton Skeleton body text Skeleton display text Skeleton thumbnail Spinner Typography Text Navigation Breadcrumb Breadcrumb item Dropdown Menu Menu item Menu label Navigation Nav group Nav item Nav toggle Pagination Tab Tab group Tab panel Top bar Tree Tree item User menu Overlays Dialog Popover Tooltip Utilities Collapsible FHIR address FHIR human name FHIR quantity FHIR questionnaire Format bytes Format date Format number Relative time Resize observer Split panel Tooltip Border Color Shadow Spacing Transition Typography Z-index Contributing Visual Tests Light Dark System

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>
Open in new window Unified Patient Clinical View Example

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, description and 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'
  });
});

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 AbortSignal passed to fetchPage so 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-table for tabular data.
  • Pre-load all events into memory for large datasets; let the adapter stream pages incrementally.
  • Use the type field to communicate status or outcomes to the user — it drives the card type badge, the connector dot color (via typeColors), and generates a filter button. Use tags for labels that carry meaning (e.g. Critical, Abnormal, Active).

Importing

Script Bundler

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>