PlTimeline
A sequence of steps, in the order they happen in. active says how far along it is, and the timeline works out what each step's bullet should be.
import { PlTimeline, PlTimelineItem } from 'plass-ui';
<PlTimeline active={2}>
<PlTimelineItem title="Ordered" meta="Mon 09:12" bullet="1" />
<PlTimelineItem title="Packed" meta="Mon 14:40" bullet="2" />
<PlTimelineItem title="Shipped" meta="Tue 07:05" bullet="3" />
</PlTimeline>;import 'package:plass_ui/plass_ui.dart';
const PlTimeline(
active: 2,
items: <PlTimelineItem>[
PlTimelineItem(title: Text('Ordered'), meta: Text('Mon 09:12'), bullet: Text('1')),
PlTimelineItem(title: Text('Packed'), meta: Text('Mon 14:40'), bullet: Text('2')),
PlTimelineItem(title: Text('Shipped'), meta: Text('Tue 07:05'), bullet: Text('3')),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| active | number | — | The index of the item being worked on now. Everything before it is complete, everything after still to come. An index rather than a value, because a timeline has no selection — nothing is chosen, and the only question is how far reality has got |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Bullet diameter and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The gradient in the bullets and the family the connectors take |
| densityshared | 'default' | 'compact' | 'default' | Spacing between items only. Never the type scale, never the bullet |
| orientationshared | PlassResponsive<'horizontal' | 'vertical'> | 'vertical' | Which way the sequence runs. vertical takes any number of steps with anything to say about each; horizontal is the stepper across the top of a checkout, honest only while every label is short |
| render | useRender.RenderProp | — | Renders something other than an ol |
| children | ReactNode | — | The PlTimelineItems |
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | List<PlTimelineItem> | — | The steps, as a list of descriptions rather than children — which step is complete is arithmetic on an index, and the last connector has to know it is the last |
| active | int? | — | The index of the item being worked on now. Everything before it is complete, everything after still to come. An index rather than a value, because a timeline has no selection — nothing is chosen, and the only question is how far reality has got |
| sizeshared | PlassSize | PlassSize.md | Bullet diameter and type scale |
| colorshared | PlassColor | PlassColor.primary | The gradient in the bullets and the family the connectors take |
| densityshared | PlassDensity | PlassDensity.standard | Spacing between items only. Never the type scale, never the bullet |
| orientationshared | PlassResponsive<PlassOrientation> | PlassOrientation.vertical | Which way the sequence runs. vertical takes any number of steps with anything to say about each; horizontal is the stepper across the top of a checkout, honest only while every label is short |
Every native <ol> attribute passes straight through. color is excluded from the pass-through because it is a Plass prop here.
The steps are items rather than children, and a PlTimelineItem is a description rather than a widget, the same call PlBreadcrumb makes, and Flutter's own idiom. Which step is complete is arithmetic on an index, and the last step's connector has to know it is the last; neither question can be asked of an opaque Widget.
There is no variant and no elevation: a timeline is a run of marks down the page, not a sheet laid on it. Put one inside a PlCard when it needs a surface.
PlTimelineItem
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | The heading of this step |
| meta | ReactNode | — | When it happened — a date, a duration, a name. Beside the title on a wide item, under it on a narrow one |
| bullet | ReactNode | — | What goes inside the bullet: a number, an icon, an avatar. Omit it and the bullet is a plain disc, which is what a step with nothing to say about itself should be |
| status | 'complete' | 'current' | 'upcoming' | — | Overrides what the timeline’s active would have computed for this item — a step that failed and stopped the sequence, a step that was skipped |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | Overrides the timeline’s colour family for this item alone |
| connector | 'solid' | 'dashed' | 'dotted' | 'none' | 'solid' | How the line to the next item is drawn. The last item’s line is never drawn anyway |
| children | ReactNode | — | The body of the step |
| Prop | Type | Default | Description |
|---|---|---|---|
| title | Widget? | — | The heading of this step |
| meta | Widget? | — | When it happened — a date, a duration, a name. Beside the title on a wide item, under it on a narrow one |
| bullet | Widget? | — | What goes inside the bullet: a number, an icon, an avatar. Omit it and the bullet is a plain disc, which is what a step with nothing to say about itself should be |
| status | PlTimelineStatus? | — | Overrides what the timeline’s active would have computed for this item — a step that failed and stopped the sequence, a step that was skipped |
| colorshared | PlassColor? | — | Overrides the timeline’s colour family for this item alone |
| connector | PlTimelineConnector | PlTimelineConnector.solid | How the line to the next item is drawn. The last item’s line is never drawn anyway |
| child | Widget? | — | The body of the step |
Every native <li> attribute passes straight through. size, density and orientation are inherited from the PlTimeline around it.
An item's index is not a property and cannot be. An item that had to be told where it was in the list would be an item every caller could put in the wrong place, and active would stop meaning anything. The timeline numbers its steps as it walks them.
What the shared axes (size color density orientation) mean across the library is in prop conventions.
Examples
active
An index rather than a value, because a timeline has no selection. Nothing here is chosen, and the only question is how far down the list reality has reached. Omit it and every item is upcoming; pass the item count to mark the whole sequence done.
status
Three states rather than two, because "the one you are on" is not the same claim as "done", and a sequence that cannot say which step is current is a list.
Each state is a different axis, never a different opacity: complete is the family's gradient, current is that gradient with a halo of the soft tint around it, and upcoming is a hairline ring on the page's own surface. A reader who cannot tell the colours apart still has a filled shape, a haloed shape and an empty one.
status on an item overrides what active computed for it, a step that failed and stopped the sequence, a step that was skipped.
connector
The line is drawn as one border edge rather than as a filled <div>, so dashed and dotted are the browser's own dashes and land on the device pixel grid the way every other edge in the library does.
Flutter's BorderSide has no dashes, so the line is painted: dashed and dotted are runs laid down by hand, at the same weight the solid one is drawn at. A dot is a round cap on a zero-length run, which is what makes it a circle rather than a short square.
It belongs to the item it leaves rather than the one it arrives at, which is what lets its colour say whether that step has been reached. The last item's line is never drawn. It would run off the end of the sequence into nothing.
orientation
vertical is the default and the one that takes an arbitrary number of steps with an arbitrary amount to say about each. horizontal is the stepper across the top of a checkout, and it is only honest while every label is short.
It is responsive, so a set can run one way on a phone and the other on a laptop. A server renders the xs entry and the browser corrects it on hydration.It is resolved against the window's width during build, so the first frame is already right. See breakpoints.
size
Accessibility
- It is an
<ol>for the reason it exists at all: the order is the content. A screen reader announcing "list of 5 items" over an unordered list would be describing something else. role="list"is written out because Tailwind's reset takes the markers off every<ol>, and Safari takes the list semantics off with them.- The current step carries
aria-current="step". That is the value for a sequence."page"is a trail of documents and"true"is one of a set of options. - The bullets and the connectors are
aria-hidden. The status is inaria-currentand in the text of each step, never in a shape alone. - There is no Base UI primitive under this. A timeline has no selection, no roving focus and no keyboard contract, and using a composite primitive would hand a record of events the semantics of a widget.
- The steps are read in order, which is what the sequence is. Each is its own node.
- The bullets and the connectors are excluded from semantics. A bullet drawn as a number says nothing a screen reader needs that the step's own title does not.
- Which is the consequence worth knowing: status does not reach a screen reader here. Flutter's semantics tree has no
currentfor a sequence, so a step that is complete and a step that is upcoming are announced the same way. Where the status matters, say it, in the step'smeta, or in its body. - A timeline has no selection, no roving focus and no keyboard contract, and it claims none.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
<PlTimelineItem> children | items, as descriptions | Which step is complete is arithmetic on an index, and the last connector has to know it is the last. Neither can be asked of a Widget. |
aria-current="step" | — | Flutter's semantics tree has no current. Say the status in the step's own text where it matters. |
<ol> and role="list" | a grouped semantics node | There are no markers to reset and no list semantics for a reset to take away. |
a border with dashed/dotted | a painted line | BorderSide has no dashes, so the runs are laid down by hand at the same weight. |
render | — | Flutter has no polymorphic element. |
children on a step | child | Flutter's name. |