Changelogs, resumes, project histories and product roadmaps keep asking for the same thing, which is dated events in order along a line. HTML and CSS can draw that without a JavaScript library.
The line can run vertically or horizontally, or it can alternate cards on both sides of a center line. Flexbox or grid places the cards, and pseudo-elements draw the line and the markers.
The markup is the easy part, since HTML already has the right pieces: an ordered list with a time element on every date. The time element has been Baseline Widely Available since April 2020 (WebDX web-features), so that structure works across current browsers.
What Is a CSS Timeline?
The term gets used for two unrelated things, and that causes some confusion.
Here it means the component described above, a dated list built with HTML and CSS instead of a timeline library. The other meaning is a CSS property. In the CSS Scroll-driven Animations Module from the W3C CSS Working Group, animation-timeline ties an animation to scroll position instead of elapsed time.
That property is a different subject. It matters here only as an optional layer for animating the finished component.
The component also isn’t a log of changes made to a stylesheet (that is a version-control job), and it isn’t a data widget with zoom, drag and thousands of events.
Pure CSS covers the layout, the line, the markers and hover transitions. Entrance effects come from keyframe animations, with no script attached.
Anything that touches data, such as loading events, filtering or zooming, still needs JavaScript.
What Markup Should a CSS Timeline Use?
Go with an ordered list (ol) with one li per event, and put a time element with a datetime attribute inside each item.
The sequence is the whole point of a timeline, and an ol tells assistive technology that the order carries meaning. A ul works only when the events have no real order.
The date goes in the time element, with datetime in ISO 8601 form such as 2026-05-22, so software reads it as a date and not as loose text. Under it sits the title, a heading level that fits the page outline (h3 in most articles), followed by a paragraph or two of description inside the same li.
Draw the line and the markers with pseudo-elements instead of extra elements. The HTML then holds only dates, titles and text.
MDN advises against putting real content in ::before, because screen readers do not reliably read it. Dates and titles stay in real HTML.
Customer.io’s Pluma design system follows the ordered-list pattern. Its Timeline component renders an ol by default, and it treats the marker as decorative, with an optional markerLabel for cases where the visible title needs extra context.
Skip most extra attributes. The first rule of ARIA is to leave it out when a native element already supplies the semantics, and an ol already exposes the list role.
Extra ARIA doesn’t always help, at least by raw error counts. WebAIM’s 2026 Million report found that home pages with ARIA averaged 59.1 detected errors, against 42 on pages without it, though those pages were also more complex.
One attribute does earn its place. An aria-label on the ol gives the list a name such as Release history.
Safari is the exception to the “native is enough” rule. A WebKit Bugzilla report (bug 304137) documents iOS VoiceOver reading a list with list-style: none as plain text, with no list announcement and no item count. The same report says macOS Safari still announces it.
Adding role=”list” to the ol restores the semantics on iOS. Validators flag the role as redundant, but keeping it is a common workaround.
Flexbox, Grid or Absolute Positioning for a Timeline Layout
Grid handles the two-sided version best. Flexbox is enough for single-sided and horizontal timelines, and absolute positioning belongs only to the line and the markers.
Each method does a different job in the component, and most good timelines combine two of them.
| Method | Best for | Strength | Failure mode |
|---|---|---|---|
| Flexbox | Single-sided and horizontal rows | Aligns marker and card on one axis | Alternating sides needs per-item overrides |
| Grid | Two-sided, centered line | Date and card share tracks | Needs explicit column placement per item |
| Absolute positioning | Line and markers | Pins a marker to an exact offset | Fixed offsets break when card heights vary |
Browser support doesn’t decide this one. Can I use puts global usage (full and partial support) at 97.46% for CSS Grid and 97.49% for flexbox (StatCounter data, September 2026).
With a grid layout, the center line sits in its own column and the cards fall into the columns on either side. Flexbox gets there with row-reverse on even items or auto margins, which means more per-item code.
Absolute positioning for the whole layout is the weakest option. Every card needs a hard-coded offset, and one longer description pushes the markers out of line.
If the line runs down the middle, use grid. If it hugs the left edge, flexbox is enough.
How to Build a Vertical CSS Timeline
Start with the list and work outward to the card.
- Set up the list: an ol with list-style none, position relative, and role=”list” for Safari.
- Give every li position relative and enough left padding for the line and the marker.
- Draw the line with ::before on the ol: absolute, top 0, bottom 0, a fixed width and a color from a variable.
- Draw the marker with ::before on each li: absolute, round with border-radius 50%, centered over the line.
- Style the card: padding, background, border or shadow, with the time element above the heading.
Draw the line and markers with pseudo-elements
Both parts are empty boxes. They need size, position and color, nothing else.
Every pseudo-element also needs a content property, even content: “”. MDN states that a ::before with no valid content value is not rendered.
- A border-left works for a plain line, while a linear-gradient lets it fade at the ends or change color per phase.
- A line drawn with ::after on the list paints after every item and covers the markers. Use ::before, or raise the marker z-index.
- Overflow hidden on the li or ol cuts off any marker that sits outside the box.
- A single line spans the whole list, so it runs past the last item. Draw one segment per li instead and remove it on the last child.
The gradient line is the fiddly one, and a CSS gradient generator shortens the trial and error. A plain border needs no tooling.
Alternate items left and right
Alternating is a placement problem more than a styling one. Move the line to the center of the list, then send odd items one way and even items the other.
With grid, use three columns (card, line, card) and put odd items in column 1 and even items in column 3. With flexbox, row-reverse on even items does the job. Either way the selectors are nth-child(odd) and nth-child(even).
Mirror the connector between card and marker on even items, or it points at the wrong side.
Theme it with custom properties
Most theming comes down to a few variables on the list. –timeline-line-color sets the line color and can be overridden per phase or event type. –timeline-line-width is the thickness, –timeline-marker-size is the dot diameter, and –timeline-gap controls the vertical space between items.
Derive the marker offset from the other two with calc(), as (line width minus marker size) divided by 2, so changing one variable never misaligns the marker.
Color-coded event types need only a modifier class that resets the color variable. Card shadows can be a variable too, and a CSS box shadow generator shortens the tuning.
How to Build a Horizontal CSS Timeline
Turn the ordered list into a flex row, wrap it in a container with overflow-x set to auto, and add scroll-snap so each item settles at the start edge.
The markup and pseudo-element rules from the vertical build still apply. What changes is the axis, and how the line is drawn.
- The wrapper carries overflow-x auto, so the list scrolls and the page does not.
- Items get flex: none with a fixed width, because shrinking cards wreck the spacing.
- The line is ::before on the list with width: max-content, so it spans every item and not just the visible edge.
- For snapping, put scroll-snap-type: x proximity on the wrapper and scroll-snap-align: start on each li.
Proximity is the safer default. Mandatory forces a snap position whenever scrolling stops (MDN), which gets in the way when a card is wider than the visible area.
Keyboard access needs one attribute. Before Chrome 132, a scroller with no focusable children could not take keyboard focus, so the wrapper needs tabindex=”0″ and an aria-label that names it.
The real limit is discoverability. Items past the right edge are invisible, so let the next card peek in at the edge to signal that the line continues.
How to Make a CSS Timeline Responsive
Collapse the layout to a single column below a breakpoint. The line moves to the left edge, every card sits on the same side, and the alternating offsets reset. A width-based media query applies the switch.
Cards go full width with padding that clears the line, and even items get the same placement as odd ones.
Write that single column as the base and add the two-sided layout at a min-width breakpoint. That is the mobile-first order, and phones get the simplest rules with nothing to undo.
Horizontal timelines need the opposite treatment: vertical below the breakpoint, horizontal above it. FlyonUI’s Tailwind-based timeline docs show it on one list that carries timeline-vertical, overflow-x-auto and lg:timeline-horizontal.
Sticky dates are the quiet breakage. position: sticky needs at least one inset value such as top, and it sticks to the nearest ancestor with a scrolling mechanism, even one that never scrolls (MDN). An overflow: hidden wrapper silently kills it.
Overflowing cards are the other usual suspect. A 1fr grid track stretches to fit long unbroken text, so use minmax(0, 1fr) to let the cards shrink.
How to Animate a CSS Timeline on Scroll Without JavaScript
Set animation-timeline to scroll() for a progress line that fills as the reader moves down the list, or to view() for item reveals as each card enters view. Leave animation-duration at auto, because scroll position drives progress, not the clock.
Chrome for Developers explains why this replaces scroll listeners. Scroll events arrive asynchronously and main-thread animations are prone to jank, while scroll-driven animations can run off the main thread.
Entrance reveals are the most common of the CSS animations on scroll, and a view timeline is built for them.
Scroll timelines and view timelines
Scroll timelines track how far a scroll container has scrolled, which is what you want on the line so it fills as the reader goes down the list. View timelines work per element instead, following its position inside the scroller. Put view() on each item and it fades in on arrival.
The animation shorthand resets animation-timeline, so declare animation-timeline after the shorthand, or the shorthand wipes it out. Add animation-range, such as entry 25% cover 50%, to finish a reveal early in the item’s pass.
Absolute positioning causes the one lookup trap. When the line’s positioned ancestor sits outside the scroller, scroll(nearest) skips the scroller, so a horizontal timeline needs a named timeline instead: scroll-timeline: –tl inline on the wrapper, animation-timeline: –tl on the line.
Browser support right now
Chrome and Edge have supported it since version 115 (Chrome for Developers, Can I use). Safari joined at version 26.0 (Can I use). Firefox is listed as not supported through version 160, with support first marked at 161 (Can I use).
Global usage for animation-timeline sits at 87.84% (Can I use, StatCounter data for September 2026).
Sources disagree on this. Some 2026 write-ups describe all three engines as consistent, put coverage above 95% of traffic, and place Safari support at version 18.2.
Can I use lists Safari 18.7 as unsupported and 26.0 as the first supported version. MDN’s list of experimental Firefox features still carries scroll-driven animations under the layout.css.scroll-driven-animations.enabled preference.
Treat Firefox as unsupported by default, and build the timeline so it reads correctly without the animation.
Fallbacks and reduced motion
Wrap every animation rule in @supports (animation-timeline: view()), and put the hidden start state inside the keyframes, never on the base rule. A base opacity of 0 leaves every card invisible in a browser that ignores the animation.
A short IntersectionObserver script that adds a class when an item enters view works as a fallback, and GSAP ScrollTrigger gives scrubbed effects with more control at the cost of a library. Static content is also a perfectly valid fallback for a timeline.
Chrome for Developers compares a view timeline to what IntersectionObserver tracks, which is why the script fallback maps one to one.
The W3C Understanding document for WCAG 2.2 SC 2.3.3, Animation from Interactions (Level AAA, updated September 2025), says motion animation triggered by interaction can be disabled unless it is essential to the functionality or the information being conveyed. It lists the prefers-reduced-motion query as a sufficient technique (C39).
Changes of color or opacity that leave size, shape and position alone fall outside its definition of motion animation, so fade-only reveals are the low-risk choice. Slides and scales belong inside an @media (prefers-reduced-motion: no-preference) block.
W3C names parallax scrolling as a common example of non-essential motion, and a timeline whose cards slide sideways on scroll sits in the same category.
When a CSS Timeline Is the Wrong Choice
Skip it when the sequence is data, when readers need to zoom or drag, or when the list grows past what anyone will scroll.
| Need | Better tool | Reason |
|---|---|---|
| Values plotted over time | Chart library (Recharts, visx) | The sequence is data, not a story |
| Zoom, drag or range selection | JavaScript timeline library (vis-timeline) | Needs interaction state and script |
| Large event sets with filters | Paginated or filtered list | A long scroll hides what matters |
| Reveals that must run in every engine | IntersectionObserver or GSAP ScrollTrigger | animation-timeline is not universal yet |
The pattern fits a short, fixed story, like a company history, a product roadmap or a release log.
Events live in the markup, so every edit goes through the template or the CMS. Reach for a library only when one of the needs above is real, because for a roadmap page the list and a few rules cost less to maintain than a dependency.
CSS Timeline Examples To Check Out
Pure CSS Timeline Card
See the Pen
pure css timeline card by Ajay (@hello-ajayrawat)
on CodePen.
Life Timeline

Grid Style Timeline

Horizontal Timeline Mockup V2

CSS Timeline Ching C Lee

Responsive Slider Timeline With Swiper

Left-to-Right Vertical Timeline

Timeline Scribble

CSS Timeline For Giggles

Timeline sequence V1

Swiping Horizontal Timeline

Bitcoin Timeline With Fixed Header

Responsive timeline V3

PS Group Timeline 2

Responsive History Timeline

Timeline UI

Project Timeline

Timeline Style Navigation

Comments And Feedback Timeline

CSS Timeline v2

Narrow Vertical Timeline

Animated SVG Travel Timeline

Timeline By Radhika

Responsive Grid Timeline

HR Timeline

CSS Timeline FAQ
Can I build a CSS timeline with Tailwind or Bootstrap?
Yes, with different amounts of help. Tailwind-based kits such as FlyonUI ship ready classes (timeline-start, timeline-middle, timeline-end) that place content around the line.
Bootstrap has no timeline component of its own, so a Bootstrap timeline is built from grid columns and custom CSS for the line.
How do I add icons to timeline markers?
Set the icon as a background-image or mask-image on the marker pseudo-element. That keeps the HTML free of extra elements.
Treat the icon as decoration, and keep the event meaning in the visible date and title.
How do I number timeline events with CSS counters?
Add counter-reset on the ol, counter-increment on each li, and print counter(name) in the marker’s content property.
A native ol already carries the order for screen readers, so treat the generated number as visual only.
How do I add a hover effect to timeline cards?
Transition box-shadow or border-color on :hover, and pair it with :focus-within so keyboard users get the same cue. Wrap it in @media (hover: hover) so touch screens do not keep a stuck hover state.
A set of CSS card hover effects offers ready patterns, though a shadow shift is enough for a timeline.
Keeping a CSS Timeline Maintainable
Editors should only ever touch the date, title and text inside each item. The list markup and the marker-offset custom properties stay locked in the template.
Structure goes first in the lockdown, because a broken ordered list loses order and count for screen readers, while a visual glitch at least shows. Lock the ol, li and time structure, generate datetime from one date field, and test the page with scroll animation off.
The trade-off is that one-off layouts are out. Each new event type needs a developer to add a template variant.
The scroll-animation fallback script stays only until stable Firefox ships animation-timeline (position verified October 2026).
Edits also change what screen readers announce, which makes web accessibility checks part of every publish.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026











