Customization

Customization works at two levels: classes on .timeline that affect the whole component, and classes or attributes on individual .event divs. Built-in modifier classes handle common cases; CSS variable hooks let you go further with your own classes.

Timeline classes

Class Effect
.tl-dashed Dashed line
.tl-dotted Dotted line
.tl-square Square dots
.tl-diamond Diamond dots
.tl-none No dots
.tl-compact Tighter spacing and smaller text
.tl-spacious Looser spacing and larger text
.tl-label-banner Label as colored full-width pill banner
.tl-label-badge Label as a compact colored rectangular badge
.tl-dot-center Vertically center the dot on each event (vertical layouts)
.tl-label-panel Label as a full-height colored left panel on .tl-card events
.tl-label-opposite Move labels to the opposite side of the center line (.vertical-alt only)
.tl-pill Pill-shaped events curving toward the center line (.vertical-alt only)

Event classes and attributes

Class / attribute Effect
.tl-card Card background and border on the event
data-dot="★" Character inside the dot

Note: Labels set via data-label use white-space: nowrap and will not wrap. Very long label strings can overflow their container on narrow viewports — keep labels short or use the .tl-label-banner / .tl-label-badge variants which are also nowrap but visually contained.

CSS variables

All visual properties are exposed as CSS custom properties on .timeline. Override them inline on a single timeline or document-wide in a stylesheet.

Property Default Controls
--tl-color-accent (unset) Per-event shorthand: sets dot border and label/badge color at once
--tl-color-line #222222 Timeline line color
--tl-color-dot #222222 Dot border color (overridden by --tl-color-accent when set)
--tl-color-dot-bg #ffffff Dot background color
--tl-color-label #222222 Label text color (overridden by --tl-color-accent when set)
--tl-color-content currentColor Event content text color
--tl-color-card-bg #ffffff Card background color (.tl-card)
--tl-dot-size 0.875rem Dot diameter
--tl-dot-icon-color currentColor Icon/character color inside data-dot dots
--tl-line-width 3px Line thickness
--tl-label-size 0.8em Label font size
--tl-content-size 1em Content font size
--tl-gap 2rem Space between events
--tl-event-width 320px Fixed event width in pan modes
--tl-snake-radius 1.875rem Corner radius of snake path turns
--tl-empty-height 3rem Extra space added below the label of empty events in vertical layouts
--tl-panel-width 90px Width of the label panel when using .tl-label-panel

Inline override (single timeline):

::: {.timeline .vertical style="--tl-color-dot: #e74c3c; --tl-gap: 3rem;"}
...
:::

Document-wide override (in styles.scss or a <style> block):

.timeline {
  --tl-color-dot: #e74c3c;
}

Timeline modifier classes

These classes go on the .timeline div and change its overall appearance.

Line style

.tl-dashed and .tl-dotted change the line between events.

Project Started — Initial concept.

First Release — Version 1.0 launched.

Major Update — Core engine rewrite.

::: {.timeline .vertical .tl-dashed}
...
:::
CSS source
/* Horizontal layouts */
.timeline.tl-dashed:not(.vertical):not(.vertical-right):not(.vertical-alt):not(.snake)::before {
  background: repeating-linear-gradient(
    90deg,
    var(--tl-color-line) 0px, var(--tl-color-line) 8px,
    transparent 8px, transparent 16px
  );
}

/* Vertical layouts */
.timeline.vertical.tl-dashed::before,
.timeline.vertical-right.tl-dashed::before,
.timeline.vertical-alt.tl-dashed::before {
  background: repeating-linear-gradient(
    180deg,
    var(--tl-color-line) 0px, var(--tl-color-line) 8px,
    transparent 8px, transparent 16px
  );
}

/* Snake layout */
.timeline.snake.tl-dashed .event:not(:last-child) { border-bottom-style: dashed; }
.timeline.snake.tl-dashed .event:nth-child(odd)   { border-right-style:  dashed; }
.timeline.snake.tl-dashed .event:nth-child(even)  { border-left-style:   dashed; }

Project Started — Initial concept.

First Release — Version 1.0 launched.

Major Update — Core engine rewrite.

::: {.timeline .vertical .tl-dotted}
...
:::
CSS source
/* Horizontal layouts */
.timeline.tl-dotted:not(.vertical):not(.vertical-right):not(.vertical-alt):not(.snake)::before {
  background: repeating-linear-gradient(
    90deg,
    var(--tl-color-line) 0px, var(--tl-color-line) 4px,
    transparent 4px, transparent 10px
  );
}

/* Vertical layouts */
.timeline.vertical.tl-dotted::before,
.timeline.vertical-right.tl-dotted::before,
.timeline.vertical-alt.tl-dotted::before {
  background: repeating-linear-gradient(
    180deg,
    var(--tl-color-line) 0px, var(--tl-color-line) 4px,
    transparent 4px, transparent 10px
  );
}

/* Snake layout */
.timeline.snake.tl-dotted .event:not(:last-child) { border-bottom-style: dotted; }
.timeline.snake.tl-dotted .event:nth-child(odd)   { border-right-style:  dotted; }
.timeline.snake.tl-dotted .event:nth-child(even)  { border-left-style:   dotted; }

Dot shape

.tl-square, .tl-diamond, and .tl-none change the shape of the event markers.

Default round dot.

.tl-square

.tl-diamond

.tl-none — no dot rendered.

::: {.timeline .tl-square}   <!-- square dots -->
::: {.timeline .tl-diamond}  <!-- rotated square -->
::: {.timeline .tl-none}     <!-- no dots -->
CSS source
.timeline.tl-square .event::after {
  border-radius: 0;
}

/* Diamond: horizontal layout needs translateX preserved */
.timeline.tl-diamond .event::after,
.timeline.horizontal.tl-diamond .event::after {
  border-radius: 0;
  transform: translateX(-50%) rotate(45deg);
}

/* Diamond: vertical layouts have no translateX */
.timeline.vertical.tl-diamond .event::after,
.timeline.vertical-right.tl-diamond .event::after,
.timeline.vertical-alt.tl-diamond .event::after,
.timeline.snake.tl-diamond .event::after {
  border-radius: 0;
  transform: rotate(45deg);
}

.timeline.tl-none .event::after {
  display: none;
}

Density

.tl-compact and .tl-spacious adjust spacing, dot size, and font sizes as a unit.

Project Started — Initial concept.

First Release — Version 1.0.

Major Update — Engine rewrite.

::: {.timeline .vertical .tl-compact}

Project Started — Initial concept.

First Release — Version 1.0.

Major Update — Engine rewrite.

::: {.timeline .vertical .tl-spacious}
CSS source
.timeline.tl-compact {
  --tl-gap:          1rem;
  --tl-dot-size:     0.625rem;
  --tl-label-size:   0.7em;
  --tl-content-size: 0.85em;
}

.timeline.tl-spacious {
  --tl-gap:          3.5rem;
  --tl-dot-size:     1.125rem;
  --tl-label-size:   0.9em;
  --tl-content-size: 1.1em;
}

Dot centering

.tl-dot-center vertically centers the dot on each event. In vertical layouts the dot defaults to the top of the event, which works well for short single-line events but looks unanchored on taller cards. Add .tl-dot-center to pin the dot to the middle of the event height.

Project Started

Initial concept and planning phase. The team gathers to define scope and set milestones.

First Release

Version 1.0 ships to early users.

Major Update

Core engine rewrite.

::: {.timeline .vertical .tl-dot-center}
...
:::

Works with .vertical, .vertical-right, and .vertical-alt. Diamond dots keep their rotation when combined with .tl-diamond.

CSS source
.timeline.vertical.tl-dot-center .event::after,
.timeline.vertical-right.tl-dot-center .event::after {
  top: 50%;
  transform: translateY(-50%);
}

.timeline.vertical-alt.tl-dot-center .event:nth-child(odd)::after,
.timeline.vertical-alt.tl-dot-center .event:nth-child(even)::after {
  top: 50%;
  transform: translateY(-50%);
}

/* Preserve diamond rotation when centering */
.timeline.vertical.tl-diamond.tl-dot-center .event::after,
.timeline.vertical-right.tl-diamond.tl-dot-center .event::after,
.timeline.vertical-alt.tl-diamond.tl-dot-center .event:nth-child(odd)::after,
.timeline.vertical-alt.tl-diamond.tl-dot-center .event:nth-child(even)::after {
  transform: translateY(-50%) rotate(45deg);
}

Label banner

.tl-label-banner turns the label into a colored pill banner. By default it uses --tl-color-label, giving a uniform banner across all events.

Project Started — Initial concept and planning phase.

First Release — Launched version 1.0 to early users.

Major Update — Rewrote core engine for performance.

::: {.timeline .vertical .tl-label-banner}
...
:::

Setting --tl-color-label and --tl-color-dot per-event gives each entry its own accent color.

Founding The company is incorporated and the founding team assembles.

First Product Version 1.0 ships to early customers.

Series B Raised $20M to expand into new markets.

Acquisition Acquired by a strategic partner.

::: {.timeline .vertical-alt .tl-label-banner}
::: {.event data-label="2002" style="--tl-color-label: #41516C; --tl-color-dot: #41516C;"}
**Founding**
:::
::: {.event data-label="2007" style="--tl-color-label: #FBCA3E; --tl-color-dot: #FBCA3E;"}
**First Product**
:::
...
:::

Works with all vertical layouts and snake.

CSS source
.timeline.tl-label-banner .event::before {
  color: white;
  background: var(--tl-color-accent, var(--tl-color-label));
  padding: 0.4em 1em;
  margin-bottom: 0.5rem;
  border-radius: 2em;
  white-space: nowrap;
}

Label badge

.tl-label-badge turns the label into a compact colored rectangular badge, sized to the text. Unlike .tl-label-banner (which stretches full-width), the badge only wraps the label. The dot color automatically follows --tl-color-label.

Founding — The company is incorporated.

Series B — Raised $20M to expand.

Launch — Product ships to all users.

::: {.timeline .vertical .tl-label-badge}
::: {.event data-label="2020" style="--tl-color-label: #41516C;"}
**Founding** — The company is incorporated.
:::
::: {.event data-label="2024" style="--tl-color-label: #E24A68;"}
**Series B** — Raised $20M to expand.
:::
:::

Use --tl-color-accent as a shorthand to set both the badge color and dot border at once (see Accent color below).

CSS source
.timeline.tl-label-badge {
  --tl-color-dot: var(--tl-color-label);
}

.timeline.tl-label-badge .event::before {
  display: inline-block;
  color: white;
  background: var(--tl-color-accent, var(--tl-color-label));
  padding: 0.25em 0.6em;
  margin-bottom: 0.5rem;
  border-radius: 4px;
  font-size: calc(var(--tl-label-size) * 0.9);
  white-space: nowrap;
}

Dot icons

Add a data-dot attribute to any .event to place a character inside its dot.

Version 1.0 ships.

First thousand users.

Infrastructure overhaul.

::: {.event data-label="Launched" data-dot="★"}
::: {.event data-label="Milestone" data-dot="1"}
::: {.event data-label="Deploy" data-dot="▲"}

Works with any single character or symbol. The dot automatically enlarges to 2rem when data-dot is set. Override --tl-dot-size on the event or timeline if you need a different size.

Note: The icon is sized at 60% of --tl-dot-size with overflow: hidden. Multi-character strings will be clipped — keep data-dot values to a single character or emoji.

Use --tl-dot-icon-color to control the icon/text color inside the dot independently of the dot border or the event’s text color. This is particularly useful when the dot has a solid colored background:

Infrastructure overhaul complete.

Product ships to all users.

::: {.timeline .vertical style="--tl-dot-icon-color: white;"}
::: {.event data-label="Deploy" data-dot="▲" style="--tl-color-accent: #2563eb; --tl-color-dot-bg: #2563eb;"}
Infrastructure overhaul complete.
:::
:::
CSS source
.timeline .event[data-dot] {
  --tl-dot-size: 2rem;
}

.timeline .event[data-dot]::after {
  content: attr(data-dot);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--tl-dot-size) * 0.6);
  line-height: 1;
  overflow: hidden;
  color: var(--tl-dot-icon-color);
}

Accent color

--tl-color-accent is a per-event shorthand that sets both the dot border color and the label/badge color at once. It applies directly in the CSS of those elements, so it works reliably without needing to set --tl-color-dot and --tl-color-label separately.

One property sets both dot and badge color.

Works on any vertical layout with .tl-label-badge.

Use --tl-color-dot-bg separately for solid dots.

::: {.timeline .vertical .tl-label-badge}
::: {.event data-label="Primary" data-dot="●" style="--tl-color-accent: #1d8cf8; --tl-color-dot-bg: #1d8cf8;"}
One property sets both dot and badge color.
:::
:::

--tl-color-accent works with any label modifier (.tl-label-banner, .tl-label-badge, .tl-label-panel) and with the plain label color. It does not set --tl-color-dot-bg — set that separately if you want a solid-filled dot.


Event card

Add .tl-card to any .event to give its content a card appearance with a white background and shadow.

Project Started

Initial concept and planning phase. The team gathers to define scope and set milestones.

First Release

Version 1.0 ships to early users. Feedback collection begins immediately.

::: {.event data-label="2020" .tl-card}
Content here.
:::
CSS source
.timeline .event.tl-card {
  background: var(--tl-color-card-bg);
  border-radius: 10px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06);
  padding: 0.875rem 1rem;
}

/* Vertical layouts use padding-bottom for the gap; card overrides that
   and uses margin-bottom instead so the card is visually bounded */
.timeline.vertical .event.tl-card,
.timeline.vertical-right .event.tl-card,
.timeline.vertical-alt .event.tl-card {
  padding-bottom: 0.875rem;
  margin-bottom: var(--tl-gap);
}

Label panel

.tl-label-panel turns the label into a full-height colored side panel on .tl-card events. It uses CSS Grid to place the label in a fixed-width column on the left, with the event content filling the remaining width. Only applies in .vertical layouts.

The panel color follows --tl-color-label, and the dot color defaults to match it — so setting one variable per event controls the whole accent.

Birthday

Lorem ipsum dolor sit amet consectetur adipisicing elit.

Lunch

Lorem ipsum dolor sit amet consectetur adipisicing elit.

Exercise

Lorem ipsum dolor sit amet consectetur adipisicing elit.

::: {.timeline .vertical .tl-label-panel .tl-dot-center style="--tl-color-line: #d1d5db; --tl-dot-size: 1.25rem;"}
::: {.event data-label="Aug 2019" .tl-card style="--tl-color-label: #9251ac;"}
**Birthday**

Lorem ipsum dolor sit amet consectetur adipisicing elit.
:::
...
:::

Control the panel width with --tl-panel-width (default 90px):

::: {.timeline .vertical .tl-label-panel style="--tl-panel-width: 120px;"}
CSS source
.timeline.vertical.tl-label-panel {
  --tl-color-dot: var(--tl-color-label);
}

.timeline.vertical.tl-label-panel .event.tl-card {
  display: grid;
  grid-template-columns: var(--tl-panel-width) 1fr;
  padding: 0;
  padding-bottom: 0;
}

.timeline.vertical.tl-label-panel .event.tl-card::before {
  position: static;
  display: flex;
  align-items: center;
  justify-content: center;
  grid-column: 1;
  grid-row: 1 / span 20;
  background: var(--tl-color-label);
  color: #fff;
  margin-bottom: 0;
  padding: 1rem 0.5rem;
  border-radius: 10px 0 0 10px;
}

.timeline.vertical.tl-label-panel .event.tl-card > * {
  margin: 0;
  padding: 0.4rem 1rem;
}

.timeline.vertical.tl-label-panel .event.tl-card > *:first-child { padding-top: 0.75rem; }
.timeline.vertical.tl-label-panel .event.tl-card > *:last-child  { padding-bottom: 0.75rem; }

Label opposite

.tl-label-opposite moves each event’s label to the opposite side of the center line in .vertical-alt layouts. Rather than appearing above the card content in normal flow, the label floats beside the center line on the other side from its card. Pair with .tl-dot-center for consistent vertical alignment between the label and dot.

Project Started

Initial concept and planning phase.

First Release

Version 1.0 ships to early users.

Major Update

Core engine rewrite.

::: {.timeline .vertical-alt .tl-label-opposite .tl-dot-center}
::: {.event data-label="2020" .tl-card}
**Project Started**
:::
::: {.event data-label="2021" .tl-card}
**First Release**
:::
:::

Only applies to .vertical-alt. The label is positioned with left: calc(100% + 30px) / right: calc(100% + 30px), anchoring it just past the center line regardless of card width.

CSS source
.timeline.vertical-alt.tl-label-opposite .event::before {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  margin-bottom: 0;
  white-space: nowrap;
}

.timeline.vertical-alt.tl-label-opposite .event:nth-child(odd)::before {
  left: calc(100% + 30px + var(--tl-dot-size) * 3);
  right: auto;
  text-align: left;
}

.timeline.vertical-alt.tl-label-opposite .event:nth-child(even)::before {
  right: calc(100% + 30px + var(--tl-dot-size) * 3);
  left: auto;
  text-align: right;
}

Pill shape

.tl-pill gives each .vertical-alt event a pill shape by rounding its inward-facing edge so the card curves toward the center line. Combine with .tl-label-opposite and .tl-dot-center for the classic alternating pill timeline look.

Project Started

Initial concept and planning phase.

First Release

Version 1.0 ships to early users.

Major Update

Core engine rewrite.

::: {.timeline .vertical-alt .tl-pill .tl-label-opposite .tl-dot-center}
::: {.event data-label="2020" .tl-card}
**Project Started**
:::
::: {.event data-label="2021" .tl-card}
**First Release**
:::
:::

Only applies to .vertical-alt. Left-side events (odd) get border-radius: 0 500px 500px 0; right-side events (even) get border-radius: 500px 0 0 500px.

CSS source
.timeline.vertical-alt.tl-pill .event:nth-child(odd) {
  border-radius: 0 500px 500px 0;
}

.timeline.vertical-alt.tl-pill .event:nth-child(even) {
  border-radius: 500px 0 0 500px;
}

User-defined classes

For anything not covered by the built-in modifier classes, write your own CSS class and apply it to the event. The stable selectors to target are:

.timeline { }              /* the container */
.timeline .event { }       /* any event */
.timeline .event::after {  /* the dot */
  --tl-color-dot: ...;
  --tl-color-dot-bg: ...;
}
.timeline .event::before { /* the label */
  --tl-color-label: ...;
}

Example: custom event color

Scope defined and team assembled.

Public launch — product ships to all users.

First patch released based on feedback.

.event.launch {
  --tl-color-dot:   #2563eb;
  --tl-color-label: #2563eb;
}
::: {.event data-label="Launch" .launch}
**Public launch** — product ships to all users.
:::

Example: highlighted event

Beta released.

Public launch

First patch.

.event.highlight::before {
  font-size: 1em;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.event.highlight::after {
  --tl-color-dot:    #dc2626;
  --tl-color-dot-bg: #dc2626;
  width: calc(var(--tl-dot-size) * 1.4);
  height: calc(var(--tl-dot-size) * 1.4);
}
::: {.event data-label="Jun" .highlight}
**Public launch**
:::

Technique: speech-bubble arrows on cards

To add a speech-bubble arrow to .tl-card events pointing toward the center line, use p:first-child::after. This is a deliberate workaround: .event::before and .event::after are already owned by the framework for the label and dot, so the arrow must live on a child element instead.

The arrow is a zero-size CSS triangle made entirely from borders. position: relative on the <p> lets the absolute-positioned ::after escape the text flow and sit at the card edge.

Project Started

Initial concept and planning phase.

First Release

Version 1.0 ships to early users.

Major Update

Core engine rewrite.

/* Anchor the first paragraph so the arrow can be positioned */
.my-timeline .event.tl-card > p:first-child {
  position: relative;
}

/* Arrow on left-side cards (odd) pointing right toward center */
.my-timeline.vertical-alt .event:nth-child(odd).tl-card > p:first-child::after {
  content: '';
  position: absolute;
  right: calc(-1rem - 14px); /* 1rem = card padding-right; 14px ≈ arrow width */
  top: 50%;
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
  border-left: 10px solid var(--tl-color-card-bg, #ffffff);
}

/* Arrow on right-side cards (even) pointing left toward center */
.my-timeline.vertical-alt .event:nth-child(even).tl-card > p:first-child::after {
  content: '';
  position: absolute;
  left: calc(-1rem - 14px);
  top: 50%;
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
  border-right: 10px solid var(--tl-color-card-bg, #ffffff);
}

The offset calc(-1rem - 14px) is derived from the card’s padding-right (1rem) plus the arrow’s border width (~14px), placing the arrow’s base flush with the card edge. Use var(--tl-color-card-bg, #ffffff) for the border color so the arrow automatically matches if you change the card background.

DOM assumption: the arrow appears on the first <p> child of the card. If your first child element is something other than a paragraph — a heading, a div, or no content at all — the arrow will either be missing or misplaced. Adjust the selector (h2:first-child, > *:first-child, etc.) to match your actual content structure.

See Flag Timeline and Material Timeline for full worked examples.


brand.yml integration

If your document uses a Quarto brand file, you can reference brand colors through SCSS in a custom stylesheet:

/* styles.scss */
.event.primary-event {
  --tl-color-dot:   #{$brand-primary};
  --tl-color-label: #{$brand-primary};
}

Accessibility

The extension renders as plain <div> elements with no ARIA roles or landmark markup added. Timelines are treated as visual content, not interactive components.

Recommendations:

  • Wrap the timeline in a <figure> with a <figcaption> to give screen readers a label:

    <figure aria-label="Project history">
    ::: {.timeline .vertical}
    ...
    :::
    <figcaption>Project history from 2020 to 2024.</figcaption>
    </figure>
  • Use descriptive content inside each .event — the data-label value is rendered as visual text via a CSS ::before pseudo-element and is not read as a heading by screen readers. If the label is meaningful for navigation, repeat it as a visible heading inside the event content.

  • Fragment timelines (RevealJS) rely on Reveal.js keyboard navigation. No additional keyboard handling is added by this extension.