# Timeline

Things that happened, one after another: each event a dot for its kind, what happened, when, and who or what did it, joined by a line.

Block: `smbk-timeline`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|

Parts: `smbk-timeline__item`, `smbk-timeline__rail`, `smbk-timeline__marker`, `smbk-timeline__dot`, `smbk-timeline__line`, `smbk-timeline__body`, `smbk-timeline__disclosure`, `smbk-timeline__summary`, `smbk-timeline__top`, `smbk-timeline__title`, `smbk-timeline__time`, `smbk-timeline__caret`, `smbk-timeline__meta`, `smbk-timeline__detail`, `smbk-timeline__more`, `smbk-timeline__hidden`

## Markup

```html
<ol class="smbk-timeline" role="list" aria-label="Project activity">
  <li class="smbk-timeline__item smbk-timeline__item--tone-success">
    <span class="smbk-timeline__rail" aria-hidden="true">
      <span class="smbk-timeline__marker">
        <span class="smbk-timeline__dot"></span>
      </span>
      <span class="smbk-timeline__line"></span>
    </span>
    <div class="smbk-timeline__body">
      <div class="smbk-timeline__top">
        <p class="smbk-timeline__title"><span class="smbk-timeline__hidden">Success: </span>Homepage published</p>
        <span class="smbk-timeline__time">15:28</span>
      </div>
      <p class="smbk-timeline__meta">Website rebuild · by Ana Silva</p>
    </div>
  </li>
  <li class="smbk-timeline__item smbk-timeline__item--tone-info">
    <span class="smbk-timeline__rail" aria-hidden="true">
      <span class="smbk-timeline__marker">
        <span class="smbk-timeline__dot"></span>
      </span>
      <span class="smbk-timeline__line"></span>
    </span>
    <div class="smbk-timeline__body">
      <div class="smbk-timeline__top">
        <p class="smbk-timeline__title"><span class="smbk-timeline__hidden">Information: </span>Brand review approved</p>
        <span class="smbk-timeline__time">14:02</span>
      </div>
      <p class="smbk-timeline__meta">Website rebuild · by Ana Silva</p>
    </div>
  </li>
  <li class="smbk-timeline__item smbk-timeline__item--tone-default">
    <span class="smbk-timeline__rail" aria-hidden="true">
      <span class="smbk-timeline__marker">
        <span class="smbk-timeline__dot"></span>
      </span>
      <span class="smbk-timeline__line"></span>
    </span>
    <div class="smbk-timeline__body">
      <div class="smbk-timeline__top">
        <p class="smbk-timeline__title">Project started</p>
        <span class="smbk-timeline__time">09:15</span>
      </div>
      <p class="smbk-timeline__meta">Website rebuild · by Ana Silva</p>
    </div>
  </li>
</ol>
```

## React

```tsx
import { Timeline, TimelineItem } from './design-system/components/timeline/timeline.js';

<Timeline aria-label="Project activity">
  <TimelineItem tone="success" title="Homepage published" time="15:28" meta="Website rebuild · by Ana Silva" />
  <TimelineItem tone="info" title="Brand review approved" time="14:02" meta="Website rebuild · by Ana Silva" />
  <TimelineItem title="Project started" time="09:15" meta="Website rebuild · by Ana Silva" />
</Timeline>
```

## Stylesheet

```css
/* smbk-timeline — Timeline. Figma "Timeline" 7607:118483 and "Timeline item" 7580:1607.
 * Events one under another. Each has a rail, a 16 marker with a dot for its kind and a line down to the next
 * event, beside its body: what happened and when on one line, who or what did it under that, and an optional
 * detail in code type on a sunken box. The last event has no line below it. The dot's kind is also said in
 * words, for screen readers; the words a reader sees say it too. An event can hold more that it shows in place
 * when pressed: its body is then a native details element, what happened, when, who and the detail its summary
 * with a caret after the time, and the more under them (see the unfolding rules at the end). */

.smbk-timeline {
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-timeline) {
  position: relative;
}

.smbk-timeline__item {
  /* The plain kind (default); each feedback kind sets these again below (the tone recipe). */
  --smbk-tone-surface: var(--smbk-color-surface-sunken);
  --smbk-tone-icon: var(--smbk-color-border-strong);
  display: flex;
  gap: var(--smbk-space-inline-md);
  min-width: 0;
}

/* In forced colors the neutral dot takes the palette's text color (the high contrast block at the end); a feedback
   kind's dot keeps its own color, set below. */
@media (forced-colors: active) {
  .smbk-timeline__item {
    --smbk-tone-icon: CanvasText;
  }
}

.smbk-timeline__rail {
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: center;
  gap: var(--smbk-space-4);
  width: var(--smbk-size-mark-sm);
  padding-top: var(--smbk-space-2);
}

.smbk-timeline__marker {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--smbk-size-mark-sm);
  height: var(--smbk-size-mark-sm);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-tone-surface);
}

.smbk-timeline__dot {
  width: var(--smbk-size-8);
  height: var(--smbk-size-8);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-tone-icon);
}

.smbk-timeline__line {
  flex: 1 1 auto;
  width: var(--smbk-border-width-emphasis);
  background: var(--smbk-color-border-subtle);
}

.smbk-timeline__body {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: var(--smbk-space-2);
  min-width: 0;
  padding-bottom: var(--smbk-space-stack-lg);
}

/* The last event: no line below it, and no room kept for the next. */
.smbk-timeline__item:last-child .smbk-timeline__line {
  display: none;
}

.smbk-timeline__item:last-child .smbk-timeline__body {
  padding-bottom: 0;
}

.smbk-timeline__top {
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-md);
  min-width: 0;
}

.smbk-timeline__title {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-body-sm-medium-font-family);
  font-size: var(--smbk-typography-body-sm-medium-font-size);
  font-weight: var(--smbk-typography-body-sm-medium-font-weight);
  line-height: var(--smbk-typography-body-sm-medium-line-height);
  letter-spacing: var(--smbk-typography-body-sm-medium-letter-spacing);
  text-transform: var(--smbk-typography-body-sm-medium-text-transform);
  overflow-wrap: anywhere;
}

.smbk-timeline__time,
.smbk-timeline__meta {
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
  text-transform: var(--smbk-typography-body-xs-text-transform);
}

.smbk-timeline__time {
  flex: none;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.smbk-timeline__meta {
  margin: 0;
  overflow-wrap: anywhere;
}

.smbk-timeline__detail {
  margin: 0;
  padding: var(--smbk-space-inset-xs) var(--smbk-space-inset-sm);
  border-radius: var(--smbk-radius-control-sm);
  background: var(--smbk-color-surface-sunken);
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-code-sm-font-family);
  font-size: var(--smbk-typography-code-sm-font-size);
  font-weight: var(--smbk-typography-code-sm-font-weight);
  line-height: var(--smbk-typography-code-sm-line-height);
  letter-spacing: var(--smbk-typography-code-sm-letter-spacing);
  text-transform: var(--smbk-typography-code-sm-text-transform);
  overflow-wrap: anywhere;
}

/* ---- an event that unfolds to more ----
   The summary lays out what happened, when, who and the detail as the body does, so the event looks the same closed;
   a caret after the time, in the small icon size the title's text takes, centred on the title's first line, turns
   when it is open. Pointing at the summary darkens the caret, and keyboard focus rings the summary. The more is Body
   SM in the secondary text color, a small stack step under the summary, its paragraphs a small stack step apart. */

.smbk-timeline__summary {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-2);
  min-width: 0;
  border-radius: var(--smbk-radius-control-sm);
  list-style: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.smbk-timeline__summary::-webkit-details-marker {
  display: none;
}

/* On a touch screen a summary shorter than the comfortable target takes an invisible press area out to it; the room
   under each event keeps it clear of the next. */
@media (pointer: coarse) {
  .smbk-timeline__summary {
    position: relative;
  }

  .smbk-timeline__summary::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }
}

.smbk-timeline__summary:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

.smbk-timeline__caret {
  flex: none;
  margin-top: calc((var(--smbk-typography-body-sm-medium-font-size) * var(--smbk-typography-body-sm-medium-line-height) - var(--smbk-size-icon-sm)) / 2);
  color: var(--smbk-color-icon-subtle);
  font-size: var(--smbk-size-icon-sm);
  transition: transform var(--smbk-motion-expand-duration) var(--smbk-motion-expand-easing);
}

@media (hover: hover) {
  .smbk-timeline__summary:hover .smbk-timeline__caret {
    color: var(--smbk-color-icon-default);
  }
}

/* An event the React component is closing (data-closing) is still open until its motion ends; its caret turns back
   as the motion starts. */
.smbk-timeline__disclosure[open]:not([data-closing]) > .smbk-timeline__summary .smbk-timeline__caret {
  transform: rotate(180deg);
}

.smbk-timeline__more {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-sm);
  padding-top: var(--smbk-space-stack-sm);
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  text-transform: var(--smbk-typography-body-sm-text-transform);
  overflow-wrap: anywhere;
}

/* Paragraphs and lists in the more are spaced by its gap, not by their own margins. */
.smbk-timeline__more > * {
  margin: 0;
}

/* ---- motion: the more unfolds with the expand purpose and folds back the same way, as an accordion's item does.
   The body's height grows from the summary to its own while the more fades in from the purpose's slight blur, and the
   caret turns with it. The React component moves it, the same in every browser, and marks the disclosure data-motion
   once it runs. While it moves (data-moving) it is clipped, with room for the summary's focus ring, and the foot of
   what shows fades out over a spacing step, or over what is still hidden when that is less. Markup written by hand is
   moved by the stylesheet alone where the browser can animate to a natural height, without the fading foot, and opens
   at once elsewhere. Under reduced motion the height lands at once and the fade and the blur still play. ---- */

/* How much of the more shows while the body moves, from its top: the React component moves it with the height. */
@property --smbk-timeline-reveal {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 100%;
}

.smbk-timeline__disclosure[data-moving] {
  overflow: clip;
  overflow-clip-margin: calc(var(--smbk-focus-ring-width) + var(--smbk-focus-ring-offset));
}

/* The mask reads only how opaque its paint is, so the text color stands in for "shown". */
.smbk-timeline__disclosure[data-moving] > .smbk-timeline__more {
  mask-image: linear-gradient(currentcolor calc(var(--smbk-timeline-reveal) - min(var(--smbk-space-16), 100% - var(--smbk-timeline-reveal))), transparent var(--smbk-timeline-reveal));
}

@supports (interpolate-size: allow-keywords) {
  .smbk-timeline__disclosure:not([data-motion]) {
    interpolate-size: allow-keywords;
  }

  .smbk-timeline__disclosure:not([data-motion])::details-content {
    block-size: 0;
    opacity: 0;
    filter: blur(var(--smbk-motion-expand-blur));
    overflow: clip;
    overflow-clip-margin: calc(var(--smbk-focus-ring-width) + var(--smbk-focus-ring-offset));
    transition-property: block-size, opacity, filter, content-visibility;
    transition-duration: var(--smbk-motion-expand-duration);
    transition-timing-function: var(--smbk-motion-expand-easing);
    transition-behavior: allow-discrete;
  }

  .smbk-timeline__disclosure:not([data-motion])[open]::details-content {
    block-size: auto;
    opacity: 1;
    filter: none;
  }

  /* Under reduced motion the height steps: at once as it opens, and only once it has faded as it closes. */
  @media (prefers-reduced-motion: reduce) {
    .smbk-timeline__disclosure:not([data-motion])::details-content {
      transition-duration: 0s, var(--smbk-motion-expand-duration), var(--smbk-motion-expand-duration), var(--smbk-motion-expand-duration);
      transition-delay: var(--smbk-motion-expand-duration), 0s, 0s, 0s;
    }

    .smbk-timeline__disclosure:not([data-motion])[open]::details-content {
      transition-delay: 0s;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  .smbk-timeline__caret {
    transition: none;
  }
}

/* Words for screen readers only: the kind of event the dot shows. */
/* .smbk-timeline__hidden: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* ---- tone: the tone recipe, the marker in the kind's soft surface, the dot in its icon color ---- */

.smbk-timeline__item--tone-info { --smbk-tone-surface: var(--smbk-color-feedback-info-surface-subtle); --smbk-tone-icon: var(--smbk-color-feedback-info-icon); }
.smbk-timeline__item--tone-success { --smbk-tone-surface: var(--smbk-color-feedback-success-surface-subtle); --smbk-tone-icon: var(--smbk-color-feedback-success-icon); }
.smbk-timeline__item--tone-warning { --smbk-tone-surface: var(--smbk-color-feedback-warning-surface-subtle); --smbk-tone-icon: var(--smbk-color-feedback-warning-icon); }
.smbk-timeline__item--tone-danger { --smbk-tone-surface: var(--smbk-color-feedback-danger-surface-subtle); --smbk-tone-icon: var(--smbk-color-feedback-danger-icon); }

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills, so the dot and the line would vanish. The line and the
   neutral dot take the palette's text color (the dot's is set beside the item rule, so a feedback kind's dot keeps
   its own color). Nothing here applies outside that mode. */

@media (forced-colors: active) {
  .smbk-timeline__dot {
    forced-color-adjust: none;
  }

  .smbk-timeline__line {
    forced-color-adjust: none;
    background: CanvasText;
  }

  .smbk-timeline__detail {
    outline: var(--smbk-border-width-hairline) solid CanvasText;
    outline-offset: calc(-1 * var(--smbk-border-width-hairline));
  }
}
```

## Tokens it reads

```json
{
  "color": {
    "surface": {
      "sunken": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.5}}",
        "$description": "Recessed wells: fields, tracks, code. See-through, so a well always shows one step below what it sits on: the page, a card or a panel."
      }
    },
    "border": {
      "strong": {
        "$type": "color",
        "$value": "{{color.grey.400}}",
        "$description": "Stronger edges and separators."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "icon": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Quiet icons."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Icons."
      }
    },
    "feedback": {
      "info": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.blue.200}}",
          "$description": "Info messages: pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.blue.600}}",
          "$description": "The info tone’s own icon (an info sign), so the tone is never shown by colour alone."
        }
      },
      "success": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.green.100}}",
          "$description": "Success messages: pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.green.600}}",
          "$description": "The success tone’s own icon (a check), so the tone is never shown by colour alone."
        }
      },
      "warning": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.orange.200}}",
          "$description": "Warning messages: pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.orange.600}}",
          "$description": "The warning tone’s own icon (a warning sign), so the tone is never shown by colour alone."
        }
      },
      "danger": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.red.200}}",
          "$description": "Danger messages: pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "space": {
    "2": {
      "$type": "dimension",
      "$value": "2px",
      "$description": "Raw space step, 2 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "4": {
      "$type": "dimension",
      "$value": "4px",
      "$description": "Raw space step, 4 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "16": {
      "$type": "dimension",
      "$value": "16px",
      "$description": "Raw space step, 16 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      }
    },
    "stack": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Gap between things stacked one above another, large."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things stacked one above another, small."
      }
    },
    "inset": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Padding inside a component, small."
      }
    }
  },
  "size": {
    "8": {
      "$type": "dimension",
      "$value": "8px",
      "$description": "Raw size step, 8 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "mark": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Small fixed marks inside components (dots, bars, keys), small."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    },
    "icon": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    }
  },
  "radius": {
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    },
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "border-width": {
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    },
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "typography": {
    "body-sm-medium": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 500,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Body SM one weight step bolder, for the current item, so the current item is not shown by colour alone."
    },
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "code-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist Mono, Geist Mono Fallback, ui-monospace, monospace",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Code SM: keys (Kbd) and short code in a line of text. Geist Mono."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "motion": {
    "expand": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.cubic-out}}"
      },
      "blur": {
        "$type": "dimension",
        "$value": "2px",
        "$description": "How blurred the content is as it starts to open, and as it finishes closing."
      }
    }
  }
}
```

## Purpose

Shows things that happened, one after another, so the reader can follow a history: a project's activity, the steps of a deploy, an order on its way. Each event has a dot for its kind, what happened, when, and who or what did it. It differs from the stepper, which shows where the reader is in steps still to come, and from the list, whose rows are not tied to a moment.

## Anatomy

- The block, an ordered list of events.
- The item part, a list item for each event. Its tone is a modifier of the part: default, info, success, warning or danger.
- The rail part: the marker part, a 16 circle in the kind's soft color holding the dot part, and the line part, down to the next event. The last event has no line.
- The body part: the top part holding the title part, what happened, and the time part, when; then the meta part, who or what did it; then the detail part, an optional line in code type on a sunken box.
- An event that unfolds: the body holds the disclosure part, a `details` element whose summary part holds the top, meta and detail parts, with the caret part after the time, and the more part, what it shows when open.
- The hidden part: the event's kind in words for screen readers, before its title.

## When to use

- A record of what happened to one thing, newest first or oldest first.
- The stages a piece of work went through, with when each happened.
- A short activity panel on a dashboard.

## When not to use

- Steps the reader is still working through. Use the [stepper](stepper.md).
- Items that are not tied to a moment, or that the reader acts on. Use the [list](list.md).
- Messages or log lines in the hundreds. Use a [log](log.md) or a [table](table.md) that can be searched.

## Behavior

The events keep the order the page gives them; the timeline does not sort. The title wraps beside its time, which stays at the end of the line. The detail wraps inside its box. Nothing moves and nothing takes focus, except an event that unfolds.

An event can hold more than its title, time, meta line and detail (`more`), such as the whole entry of a log, shown in place when the reader presses the event. The event's title, time, meta line and detail are then one row that opens and closes it, with a caret after the time that turns when it is open; the more unfolds under the row with the expand motion, as an accordion's item does, in Body SM and the secondary text color, its paragraphs a small step apart. It starts closed unless `defaultExpanded` is set, or the page holds it with `expanded` and `onExpandedChange`. Under reduced motion the height lands at once and the fade still plays. Written by hand, the body holds a `details` element with the disclosure class, its `summary` with the summary class holding spans with the top, title, time, meta and detail classes, and the more after it.

## Accessibility
### What it does for you

- The events are an ordered list.
- The rail is decoration and hidden from screen readers.
- The kind of event is said in words before the title, such as "Warning: Two images failed to upload".
- With a machine-readable date, the time is a `time` element carrying it.
- It takes no focus of its own; a link in a title or a detail is a Tab stop.
- An event that unfolds is a native disclosure: its row is a Tab stop, named by the kind, title, time, meta line and detail, says whether it is expanded, and opens and closes with Enter and Space, with no script.

### What you must do

- Name the list with `aria-label` when the page has more than one.
- Say the kind in the title's own words too ("Deploy failed", not "Build 481"), so a reader who cannot tell the dots' colors apart still knows it.
- In markup written by hand, add the kind's words in the hidden part.
- Keep links and buttons out of an unfolding event's title, meta line and detail: they sit inside the row that opens it. Put them in the more.

### How to test

- With a screen reader, the events are read as an ordered list in the order given, each with its kind before its title.
- Tab through the page: focus skips the timeline itself and lands only on a link in a title or a detail.
- With text at 200% and the page 320 pixels wide, the title wraps beside its time, and the detail wraps inside its box.
- Tab to an event that unfolds and press Enter or Space: the more shows under it and a screen reader hears it expanded; press again and it closes.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the row of each event that unfolds, and to a link in a title, a detail or an open more. |
| Enter or Space | On an event's row, shows its more, or hides it when shown. |

## Usage rules

### Do

- Say what happened in the title, in a few words.
- Keep one order through the whole timeline, newest first or oldest first.
- Give the time in the reader's own form, and a machine-readable date beside it.

### Don't

- Leave the dot's color alone to say that something failed.
- Mix events of different things in one timeline without saying in the meta line which each is about.
