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-labelusewhite-space: nowrapand 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-badgevariants 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 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-sizewithoverflow: hidden. Multi-character strings will be clipped — keepdata-dotvalues 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— thedata-labelvalue is rendered as visual text via a CSS::beforepseudo-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.