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
enum TimelineVariant { rail, sections, rows }| Variant | What it is | Use 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 |
sections | Each 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 |
rows | No 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
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.
| Rail | Sections | Rows | |
|---|---|---|---|
TaskActivityItem | card beside its node | card beside its node | plain row led by the same marker |
MilestoneActivityItem | light line with a small dot | tinted card with a node | plain row led by the same marker |
CustomActivityItem | your content beside its node | the same | a row |
CustomActivityItem(emphasis: TimelineEmphasis.system) | your content beside a small dot in iconColor | your content in a card tinted with iconColor | a 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.
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.
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.
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:
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:
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.
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.
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),
)| Style | Wrapper | Use |
|---|---|---|
inline | none | Default: the content lives inside a parent timeline row |
card | flat bordered card | Standalone use as a tappable list item |
compact | none | Narrow 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.
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.
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:
| Type | Use |
|---|---|
Timeline | Main widget; scrollable or embedded |
TimelineVariant | rail, sections or rows |
TimelineItem / TimelineHeader / TimelineCustom | An event, a label for the entries after it, and any widget between events; each takes a key |
TimelineEmphasis | standard or system; each variant draws a system event its own way |
TimelineAnchor | Names the title of an event's content: the text its marker sits beside |
TimelineOrder | Oldest first, newest first, or the caller's order |
TimelineGrouping | Date headers with custom text or a custom label widget, and a clock |
DotIndicator / IconIndicator / PendingIndicator / CustomIndicator | Built-in markers |
TimelineThemeData | Spine colour and thickness, column widths (spineColumnWidth, iconColumnWidth, leadingColumnWidth), spacing |
Cross-links
- Inputs —
RemarksTextfor comment bodies - Theming —
successOf,criticalOf,warningOf,neutralOfresolvers - Field Formatting —
FieldFormatter.instancefor timestamps