Skip to main content
cngx-src documentation

CngxTimelineItem

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

projects/common/timeline/timeline-item.component.ts

Import#

import { CngxTimelineItem } from '@cngx/common/timeline'

Description#

One event on the timeline: a marker, the rail segment below it, an optional timestamp, and the projected body.

A terminal composable unit, not a fragment of the organism. It ships its own token SET rules and defaults to the narrative raster, so dropping one into a hand-rolled layout renders a complete, density- correct item with no <cngx-timeline> above it. Inside the organism it inherits [data-mode] and re-rasters itself.

Two independent state channels. status is where the event sits in a history (done / active / upcoming / rejected) - editorial, set by the consumer. state is whether this row's own data is currently loading or failed. They compose: a done item whose state errored paints as rejected and announces the failure, without losing the editorial status once the retry succeeds.

What it announces. The marker and rail are aria-hidden, so colour is never the only channel - the status reaches assistive tech through a screen-reader-only line fed from CNGX_TIMELINE_CONFIG.labels.status, and a failed row through a visible inline error. Both are read as the row's own content, in DOM order, rather than through aria-describedby: the host is a plain element with no role, so a description pointed at its own descendants would resolve nowhere and duplicate the text for anyone browsing it. Neither element is rendered while it has nothing to say.

Standalone

<cngx-timeline-item status="done">
  <cngx-time cngxTimelineTime [date]="event.at" />
  <p>Deployment finished</p>
</cngx-timeline-item>

With its own async state

<cngx-timeline-item [state]="rowState" position="last">
  <p>{{ rowState.data()?.summary }}</p>
</cngx-timeline-item>

https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/standalone-atoms https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/custom-date-header-and-marker https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/marker-precedence https://cngxjs.github.io/cngx/examples/#/ui/timeline/layout/media-markers https://cngxjs.github.io/cngx/examples/#/ui/timeline/layout/placement-end

Metadata#

Host#

Relationships

Index#

Inputs#

item#unknown
input()

The event this row stands for. Only ever read as the $implicit of an app-wide *cngxTimelineMarkerTpl, so bind it when the app sets one and that template needs the payload; leave it off otherwise.

input()

Position in the run, forwarded to the rail. 'last' and 'only' stop the rail at this item so the timeline does not trail off into empty space. The organism sets it; standalone items default to 'middle'.

default 'middle'

This row's own async state, for a body that loads, mutates or fails independently of the list around it.

Optional by construction and tolerant of a bare state attribute (the empty string coerces to undefined), so the item never forces a binding a consumer may not have.

default
undefined, { transform: (value) => (typeof value === 'string' ? undefined : value) }
input()

Where the event sits in the history. Drives the marker and rail colours and the screen-reader status line. Purely editorial - it carries no flow logic, because a timeline is a record and a guided process is a stepper.

HostBindings#

BindingExpression
[attr.data-status]status()
[attr.data-failed]failed() ? "" : null
[attr.aria-busy]ariaBusy()

The --cngx-timeline-* custom-property family, shared by every artifact in @cngx/common/timeline and re-used by the <cngx-timeline> organism in @cngx/ui/timeline. A pure token surface - registrations plus :root chains, no structural rules and no @scope.

It lives with the atoms rather than with the organism on purpose: a <cngx-timeline-item> dropped into a hand-rolled layout has to render complete and density-correct on its own. A token family that only resolved under a Level-4 host would make the molecule a fragment instead of a terminal unit.

Where the spacing tokens are, and why not here

This file carries no -gap / -padding registration. A spacing token is registered in the same stylesheet that SETs it from --cngx-space-*, at that component's own host:

  • --cngx-timeline-gap, --cngx-timeline-row-gap, --cngx-timeline-content-padding - timeline-item.component.css
  • --cngx-timeline-group-gap - the organism's stylesheet

Registering them here and SETting them there would put a registration one file away from its derivation, where the two can drift silently: a registered initial-value defeats every use-site var(token, fallback), so a token that loses its SET rule freezes at the literal and a nested [data-density] never reaches it. Keeping the pair together makes that failure impossible to introduce by accident, and is what the repo's density guard checks per file.

The sizes that remain here are affordances, not compactness, and are deliberately outside the density scale. --cngx-timeline-rail-inset in particular derives from --cngx-timeline-marker-size: the rail has to meet the marker's centre, and a density swap that moved one without the other would detach the rail from the dot. That derivation is applied at the row host (timeline-item.component.css), the nearest common ancestor of the marker and the connector - custom properties inherit downwards only, so deriving it on either sibling would never reach the other.

Colour and dark mode

Every colour chains to a --cngx-color-* foundation token, which already carries its own light and dark values. Dark mode therefore needs no per-token redefinition here - overriding --cngx-color-primary or the active colour scheme re-themes the timeline for free. The literal in each chain's tail is the leaf default for consumers running without the foundation stylesheet.

  • --cngx-timeline-connector-color -> --cngx-color-border
  • --cngx-timeline-surface -> --cngx-color-surface
  • --cngx-timeline-text-color -> --cngx-color-text
  • --cngx-timeline-muted-color -> --cngx-color-text-muted
  • --cngx-timeline-done-color -> --cngx-color-success
  • --cngx-timeline-active-color -> --cngx-color-primary
  • --cngx-timeline-rejected-color -> --cngx-color-danger
  • --cngx-timeline-upcoming-color -> --cngx-color-text-muted

Default visuals for CngxTimelineItem - the two-column raster that puts the marker and its rail in one track and the item's content in the other.

Rasters

narrative is the default and is declared on the host itself, so an item outside any <cngx-timeline> still lays out completely. The timestamp sits above the body:

marker  time
rail    body

activity is an override, keyed off the [data-mode] attribute the organism sets on an ancestor. The timestamp trails the body on one line:

marker  body  time
rail    body  time

Placement

[data-row-side] says which side of the rail this row's body sits on - start is v1 (rail leading, body trailing), end mirrors both tracks and areas. The organism derives it per row from the loop index; a standalone row opts in by putting the attribute on any wrapper, exactly like [data-mode].

[data-placement='alternate'] centres the rail on symmetric 1fr auto 1fr tracks so the empty side can carry opposite content, and collapses back to the start raster below 30rem of the cngx-timeline container. The query lives here, next to the raster it overrides; the organism only names the container.

Combinations are selected as :is([data-x] *) / :not([data-x] *) on the row rather than as an ancestor chain: [data-mode] and [data-placement] are both organism host attributes, so [data-mode='...'] [data-placement='...'] matches nothing, while the row is a descendant of the element carrying either one no matter how a consumer distributes them. Every raster below is therefore settled by specificity, not by source order.

Orientation

[data-orientation='horizontal'] transposes every raster below: the marker and its rail share the axis row, the content stacks away from it, and [data-row-side] selects which side of the axis rather than which side of the column. The two axes are separate copies of the same ladder rather than steps in one, so horizontal combines with alternate instead of cancelling it.

activity has no horizontal variant: a scan-feed is a vertical shape. Those rows render the narrative axis.

Rail style

[data-rail='continuous'] stretches every non-last segment across the inter-row gap so the run reads as one line. Per segment rather than one line behind the whole band, because each segment carries its own status colour and a single line would flatten that channel.

Opposite content

[cngxTimelineOpposite] gets a third track, and only rows that project into it do: an unconditional auto track would collapse to zero width but still take a column-gap, shifting every row that predates the slot.

The gate is :has(> [cngxTimelineOpposite]), and both halves matter. The attribute, not the directive's host class, so it agrees with <ng-content select="[cngxTimelineOpposite]"> by construction - a consumer who projects the element without importing the directive still gets the track. The child combinator, because projection lands the node as a direct child of this host, and only a direct child can be a grid item at all: a descendant match would hand a row a track that nothing can occupy.

Density

The three spacing tokens are registered and SET here, in the one file that uses them, so the registration cannot outlive its derivation. A [data-density] swap on any ancestor re-scales all three.

--cngx-timeline-marker-size and --cngx-timeline-rail-inset are deliberately absent: they are affordance geometry and live with the atoms in timeline-tokens.css.

State

[data-failed] recolours the row's text and shows the inline error. The error and the screen-reader status line are rendered only while they have something to say, and are read as the row's own content - the host carries no role for an aria-describedby to resolve against.

Index#

Layout

--cngx-timeline-rail-inset#<length>
Default value 6px

Distance the rail is pulled back from the edge of the marker cell so it meets the marker's centre. Affordance geometry, deliberately outside the density scale - it tracks --cngx-timeline-marker-size instead, and a density swap that moved it would detach the rail from the dot.

--cngx-timeline-marker-size#<length>
Default value 12px

Diameter of the marker dot.

--cngx-timeline-marker-glyph-size#<percentage>
Default value 60%

Size of a bare svg glyph projected into the marker, as a share of the marker box. A percentage rather than a length so it tracks --cngx-timeline-marker-size on its own: em would resolve against the marker's pinned font-size, which is not the dot's size.

Photos are not covered by this - an img or picture fills the dot edge to edge, because a cropped portrait wants no inset and a glyph does.

--cngx-timeline-item-inline-size#<length>
Default value 192px

Width of one row on the horizontal axis. Ignored in the vertical default, where a row takes the container's width.

An affordance, not compactness, so it is deliberately outside the density scale. It exists because a horizontal run has no sensible content-derived width: flex would size every card to its longest word and the axis would read as a ragged queue. Override per instance for denser or wider cards.

--cngx-timeline-connector-width#<length>
Default value 2px

Thickness of the connector rail.

--cngx-timeline-marker-ring-width#<length>
Default value 3px

Width of the halo ring drawn around a marker in the active status.

--cngx-timeline-gap#<length>
Default value 8px

Column gap between the marker track and the content. SET from --cngx-space-sm.

--cngx-timeline-row-gap#<length>
Default value 4px

Row gap inside one item, between the timestamp and the body. SET from --cngx-space-xs.

--cngx-timeline-content-padding#<length>
Default value 4px

Padding around an item's projected body. SET from --cngx-space-xs.

Surface

--cngx-timeline-connector-color#<color>
Default value oklch(0.88 0.005 250)

Colour of the connector rail in its default (unstated) status.

See: [[--cngx-color-border]]

--cngx-timeline-surface#<color>
Default value oklch(1 0 0)

Background the marker is punched out of, so a coloured rail passing behind the dot does not show through it.

See: [[--cngx-color-surface]]

Typography

--cngx-timeline-marker-fg#<color>
Default value oklch(1 0 0)

Foreground colour of a glyph projected into the marker.

--cngx-timeline-meta-size#*
Default value 0.8125rem

Font size for secondary text across the family - timestamps, inline errors, date headers, the refreshing tail. Registered here rather than with either consumer, because the row and the organism both read it and a registration in one is out of scope for the other.

--cngx-timeline-text-color#<color>
Default value oklch(0.2 0.01 250)

Primary text colour inside an item.

See: [[--cngx-color-text]]

--cngx-timeline-muted-color#<color>
Default value oklch(0.5 0.01 250)

Secondary text colour - timestamps, group headers, the refreshing tail.

See: [[--cngx-color-text-muted]]

Variant / Done

--cngx-timeline-done-color#<color>
Default value oklch(0.6 0.15 145)

Marker and rail colour for the done status.

See: [[--cngx-color-success]]

Variant / Active

--cngx-timeline-active-color#<color>
Default value oklch(0.66 0.19 50)

Marker and rail colour for the active status.

See: [[--cngx-color-primary]]

Variant / Upcoming

--cngx-timeline-upcoming-color#<color>
Default value oklch(0.5 0.01 250)

Marker and rail colour for the upcoming status. Pairs with a dashed rail, so the state survives a colour-blind reader.

See: [[--cngx-color-text-muted]]

Variant / Rejected

--cngx-timeline-rejected-color#<color>
Default value oklch(0.6 0.18 25)

Marker and rail colour for the rejected status, and for an item whose own [state] failed.

See: [[--cngx-color-danger]]

Motion

--cngx-timeline-pulse-duration#<time>
Default value 1.6s

Period of the busy pulse on a marker. Set to 0s to stop it without touching the rest of the family.