Skip to content

Timeline ​

ActivityTimeline shows an activity feed, an audit log, a version history or a workflow run. It maps a sealed ActivityItem hierarchy onto the Timeline of the vyuh_timelines package, so callers think in tasks and milestones and pick one of three variants for how they are drawn.

Live examples ​

Open the full Timeline example.

Import package:vyuh_timelines/vyuh_timelines.dart.

Variants ​

dart
enum TimelineVariant { rail, sections, rows }
VariantWhat it isUse it for
rail (default)One unbroken spine from the first event to the last. A date is a chip on the spine. Every event has a node on it. A system event is a light line with a small dot.Record history, workflow activity, activity logs
sectionsEach day is a group under a plain date label. The spine runs only from that day's first node to its last. A system event is a tinted card on the same indent as the others, with a node.A history people read day by day
rowsNo spine. Each day is one bordered list, and each event is a row led by its status marker: the same marker a Rail has as its node.A bordered, list-like presentation with no spine: narrow side panels, and lists that sit among other bordered lists. Not a denser layout: a row is as tall as its marker and padding need, taller than a Rail's pitch.

The spine is always at the start edge. A timeline sizes by its width and grows downwards.

Quick example ​

dart
ActivityTimeline(
  variant: TimelineVariant.rail,
  order: TimelineOrder.newestFirst,
  showDateGroups: true,
  items: [
    MilestoneActivityItem(
      title: 'Submitted for review',
      milestoneType: MilestoneType.start,
      timestamp: lot.submittedAt,
      outcome: ActivityOutcome.primary,
      formattedActorName: lot.submittedBy,
    ),
    TaskActivityItem(
      title: 'Approved',
      outcome: ActivityOutcome.success,
      timestamp: review.approvedAt,
      senderName: review.approver,
      comments: RemarksText(text: review.comment),
    ),
    TaskActivityItem(
      title: 'Revision requested',
      outcome: ActivityOutcome.revision,
      timestamp: review2.requestedAt,
      senderName: review2.approver,
      version: 2,
      isPending: true,
    ),
    MilestoneActivityItem(
      title: 'Effective',
      milestoneType: MilestoneType.terminal,
      outcome: ActivityOutcome.success,
      timestamp: lot.effectiveAt,
    ),
  ],
)

ActivityItem (sealed) ​

Three kinds, picked by meaning. Each variant draws them its own way.

RailSectionsRows
TaskActivityItemcard beside its nodecard beside its nodeplain row led by the same marker
MilestoneActivityItemlight line with a small dottinted card with a nodeplain row led by the same marker
CustomActivityItemyour content beside its nodethe samea row
CustomActivityItem(emphasis: TimelineEmphasis.system)your content beside a small dot in iconColoryour content in a card tinted with iconColora row like any other

Every item takes an optional key; see Leading controls and keys.

TaskActivityItem ​

What a person did: a submission, an approval, a rejection, a revision.

dart
TaskActivityItem(
  title: 'Approved',
  timestamp: review.approvedAt,
  outcome: ActivityOutcome.success,
  subtitle: 'After 1 round of revisions.',
  senderName: review.approver,
  senderWidget: EntityLink<User>(id: review.approverId),  // optional, wins over senderName
  formattedTimestamp: '2 hours ago',
  comments: RemarksText(text: review.comment),
  customContent: ApprovalDetails(review: review),
  version: 2,
  isPending: false,
)

A pending task has a pulsing beacon (PendingIndicator) as its marker. A task without a timestamp has not happened yet, so it counts as the newest. The version badge and the pending beacon sit beside the title while the title keeps half the row; on a narrow panel or at a large text scale they move to a line of their own under it.

MilestoneActivityItem ​

What the system did: a workflow start, a checkpoint, a terminal state.

dart
MilestoneActivityItem(
  title: 'Effective',
  milestoneType: MilestoneType.terminal,    // start / checkpoint / terminal
  outcome: ActivityOutcome.success,
  timestamp: lot.effectiveAt,
  subtitle: 'Released to production',
  errorInfo: const MilestoneErrorInfo(
    type: 'WorkflowError',
    message: 'Approval expired',
    isRetryable: true,
  ),
)

When errorInfo is set, an info button appears beside the milestone's title. It opens a dialog with the type, the message, whether a retry is possible, and any details. On a Rail a failed milestone is the same light line as any other, with its dot, icon and text in the error colour. In Sections and Rows its title is in the error colour too.

CustomActivityItem ​

Any content with a configurable indicator.

dart
CustomActivityItem(
  key: ValueKey(version.id),
  title: 'Version 2 published',
  timestamp: version.publishedAt,
  icon: FluentIcons.tag_24_regular,
  iconColor: Theme.of(context).colorScheme.primary,
  leading: CdxCheckbox(value: selected, onChanged: onSelected),
  content: VersionCard(version: version),
)

Indicator resolution: indicator (your own widget), then icon with iconColor, then a small primary dot. A leading control sits before the indicator: [ ] ● Version 2.

Set emphasis: TimelineEmphasis.system on a custom item that is a system event. It is then drawn like a milestone, with iconColor as its tone.

Where the marker sits ​

The marker of an event (its node, or its status marker in Rows) and its leading control sit on the first line of the event's text, at any width and any text scale. A title that wraps, or a detail that opens under it, leaves them beside the first line. A marker taller than that line grows the row downward.

Any text counts as the first: the initials in an avatar, the label of a chip, an overline, and text that is invisible but keeps its room. Icons are passed over. The package's own content (ActivityEntryContent, the task and milestone content) names its title for you. In content of your own that begins with other text, wrap the title in TimelineAnchor:

dart
CustomActivityItem(
  title: 'Version 2',
  content: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    mainAxisSize: MainAxisSize.min,
    children: [
      const StatusBadge.neutral(label: 'Draft'),
      const TimelineAnchor(child: Text('Version 2')),
    ],
  ),
)

One limit. An anchor (or first text) inside a fixed-size box that comes after anything with a baseline — an icon included — and changes from inside that box, or moves inside it, stays stale until the row's next layout.

Leading controls and keys ​

A timeline keeps its rows by position. A leading control must be controlled (its value passed in each time the timeline builds), or its item must have a key. Without either, a control that keeps its own state shows that state on another item once a newer one is inserted above it. Keys are unique in a timeline; two with the same key are reported in debug builds.

Markers and their columns ​

IconIndicator, DotIndicator, PendingIndicator and CustomIndicator are drawn the same in every variant: a row in Rows is led by the 32 px ringed disc a Rail has as its node.

A marker must fit its column: TimelineThemeData.spineColumnWidth in Rail and Sections, TimelineThemeData.iconColumnWidth in Rows; both default to 32. A wider marker of your own is reported in debug builds as "A timeline marker is wider than its column", naming both fields. Pass a wider column:

dart
ActivityTimeline(
  variant: TimelineVariant.rows,
  theme: const TimelineThemeData(iconColumnWidth: 40),
  items: items,
)

TimelineThemeData.leadingColumnWidth does the same for a large leading control; its default is CdxConfig.controlHeight.

ActivityOutcome ​

Drives the icon and the colour through the CDX status palette.

dart
enum ActivityOutcome {
  success,    // checkmark, success palette
  failure,    // dismiss, critical palette
  revision,   // edit, warning palette
  warning,    // warning triangle, warning palette
  info,       // info circle, info (= primary) palette
  primary,    // send icon, primary
  neutral,    // clipboard task, neutral palette
}

ActivityEntryContent ​

A content widget for use inside CustomActivityItem or any other timeline-like card: a title, who and when, a subtitle and your own content.

dart
ActivityEntryContent(
  title: 'Updated price',
  titleColor: null,                        // a state colour, such as the error colour
  subtitle: 'Was $99.99, now $89.99',
  formattedTimestamp: 'Apr 18, 2026 14:30',
  performerName: 'Jane Doe',
  performerWidget: EntityLink<User>(id: 'user-123'),
  style: ActivityEntryStyle.inline,        // inline / card / compact
  onTap: () => navigator.openVersion(version),
)
StyleWrapperUse
inlinenoneDefault: the content lives inside a parent timeline row
cardflat bordered cardStandalone use as a tappable list item
compactnoneNarrow surfaces: who and when are joined as actor · timestamp

A titleTrailing annotation follows the title on its line, and goes under the title when the row is too narrow for both.

Date grouping ​

When showDateGroups is true (the default), items are grouped by local calendar day and each day gets a header ("Today", "Yesterday", or d MMM yyyy). The variant draws it: an outlined chip on the spine in Rail, a plain label in Sections and Rows. Pass dateFormatter to change the label.

clock tells the moment that "Today" is judged from. It is asked once for each day every time the timeline builds, and defaults to the device clock. No timer runs: a timeline that is open across midnight keeps its labels until it is rebuilt.

The spine's colour ​

The spine is the theme's outlineVariant. (Before the variants it was outline at 30% opacity.) In a theme where outlineVariant is close to the page, the line is faint: in the Blueprint light theme its contrast against the page is only 1.17:1 to 1.28:1. Set a stronger colour there.

colorScheme.outline is often not enough: in the Blueprint light theme it reaches only 1.91:1 to 2.08:1. A line should have 3:1 against what it is drawn on. There, onSurfaceVariant at 70% opacity does: 3.05:1 on the page and 3.20:1 on white.

dart
ActivityTimeline(
  theme: TimelineThemeData(
    spineColor: Theme.of(context).colorScheme.onSurfaceVariant.withValues(
      alpha: 0.7,
    ),
  ),
  items: items,
)

A translucent spine has no darker joins: no part of the line is painted twice.

A date chip on a Rail has an outline as well as a fill, so it shows, and hides the line behind it, on any surface.

The Timeline underneath ​

Use Timeline when the feed is not an activity log, or when you want full control over indicators and content.

dart
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:material_ui/material_ui.dart';
import 'package:vyuh_timelines/vyuh_timelines.dart';

Timeline(
  variant: TimelineVariant.rail,
  order: TimelineOrder.newestFirst,
  grouping: const TimelineGrouping(),
  entries: [
    TimelineItem(
      indicator: IconIndicator(
        icon: FluentIcons.checkmark_circle_24_regular,
        color: Theme.of(context).colorScheme.primary,
      ),
      content: const Text('Submitted for review'),
      timestamp: submittedAt,
    ),
    TimelineItem(
      emphasis: TimelineEmphasis.system,
      indicator: DotIndicator(color: Theme.of(context).colorScheme.outline),
      content: const Text('Workflow started'),
      timestamp: startedAt,
    ),
  ],
)

Public building blocks:

TypeUse
TimelineMain widget; scrollable or embedded
TimelineVariantrail, sections or rows
TimelineItem / TimelineHeader / TimelineCustomAn event, a label for the entries after it, and any widget between events; each takes a key
TimelineEmphasisstandard or system; each variant draws a system event its own way
TimelineAnchorNames the title of an event's content: the text its marker sits beside
TimelineOrderOldest first, newest first, or the caller's order
TimelineGroupingDate headers with custom text or a custom label widget, and a clock
DotIndicator / IconIndicator / PendingIndicator / CustomIndicatorBuilt-in markers
TimelineThemeDataSpine colour and thickness, column widths (spineColumnWidth, iconColumnWidth, leadingColumnWidth), spacing
  • Inputs — RemarksText for comment bodies
  • Theming — successOf, criticalOf, warningOf, neutralOf resolvers
  • Field Formatting — FieldFormatter.instance for timestamps