How to Build a Bootstrap 5 Timeline Component

How to Build a Bootstrap 5 Timeline Component

Key Takeaways

  • A Bootstrap 5 timeline is built from a custom CSS connector line combined with Bootstrap’s spacing and flex utilities, not a built-in component.
  • Semantic HTML matters: use <ul> or an ordered list so screen readers and search engines understand the sequence.
  • The connector line is drawn with a CSS ::before pseudo-element on the list container, not with borders on individual items.
  • Bootstrap’s CSS custom properties (for example, --cnvs-themecolor in Canvas) make it trivial to theme the timeline dot colour without touching compiled CSS.
  • Horizontal timelines look appealing on desktop but break badly on small screens; a vertical layout is almost always the right default.
  • Scroll-triggered animation can be layered on top with Intersection Observer and a small utility class, keeping the JS minimal.

Why Bootstrap 5 Has No Built-In Timeline

Bootstrap 5.3 ships components for carousels, accordions, modals, offcanvases, and more, but a timeline is conspicuously absent. The Bootstrap team deliberately keeps the component library focused on interaction patterns rather than purely presentational layouts. That is actually good news: you are not fighting a framework opinionated about dot size or connector colour. You own every pixel. The trade-off is roughly 40 to 60 lines of CSS written by hand, which this guide covers in full.

If you are already using the Canvas HTML Template, Canvas ships several pre-built timeline block variations inside its content blocks library. The technique below mirrors how those blocks are constructed, so the two approaches are fully compatible.

How to Build a Bootstrap 5 Timeline Component, abstract concept illustration

Semantic HTML Foundation

Start with an ordered or unordered list. An ordered list (<ol>) is semantically correct when the sequence matters (a project history, for example). Use <ul> for non-ordered milestones. Each <li> becomes one timeline entry.

<ol class="bs-timeline">
  <li class="bs-timeline__item">
    <div class="bs-timeline__dot"></div>
    <div class="bs-timeline__content">
      <span class="bs-timeline__date text-muted small">January 2022</span>
      <h3 class="h5 mt-1">Company Founded</h3>
      <p>Opened our first office and onboarded three founding clients.</p>
    </div>
  </li>
  <li class="bs-timeline__item">
    <div class="bs-timeline__dot"></div>
    <div class="bs-timeline__content">
      <span class="bs-timeline__date text-muted small">June 2023</span>
      <h3 class="h5 mt-1">Series A Funding</h3>
      <p>Raised $4 million to expand the engineering team.</p>
    </div>
  </li>
  <li class="bs-timeline__item">
    <div class="bs-timeline__dot"></div>
    <div class="bs-timeline__content">
      <span class="bs-timeline__date text-muted small">March 2025</span>
      <h3 class="h5 mt-1">Product v2 Launch</h3>
      <p>Shipped a fully redesigned platform to 12,000 active users.</p>
    </div>
  </li>
</ol>

Strip the browser’s default list styles immediately. You will add custom spacing through Bootstrap utilities and your own CSS class, so the default padding and bullet markers will only create conflicts.

Core CSS: The Connector Line and Dot

The vertical line connecting each entry is drawn once on the container using a ::before pseudo-element positioned absolutely. Individual dots sit on each .bs-timeline__item. This approach means you never have to worry about the last item’s line running past the final dot, because you target only the items that are not the last child.

<style>
/ Reset list defaults /
.bs-timeline {
  list-style: none;
  padding-left: 0;
  margin: 0;
  position: relative;
}

/ Vertical connector drawn on the container /
.bs-timeline::before {
  content: "";
  position: absolute;
  left: 18px;            / aligns with the dot centre /
  top: 0;
  bottom: 0;
  width: 2px;
  background-color: #dee2e6; / Bootstrap gray-300 /
}

/ Each row /
.bs-timeline__item {
  position: relative;
  display: flex;
  gap: 1.25rem;
  padding-bottom: 2rem;
}

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

/ The dot /
.bs-timeline__dot {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background-color: var(--bs-primary);
  border: 3px solid #fff;
  box-shadow: 0 0 0 2px var(--bs-primary);
  position: relative;
  z-index: 1;            / sits above the connector line /
}

/ Content block /
.bs-timeline__content {
  flex: 1;
  padding-top: 4px;      / optically aligns text with dot centre /
}
</style>

If you are working inside a Canvas project and want the dot to respond to the active theme colour automatically, replace var(--bs-primary) with var(--cnvs-themecolor). The rest of the rule stays identical. For more on theming with CSS custom properties versus SCSS, see our post on SCSS vs CSS Variables for Theming Bootstrap 5.

timeline component html, abstract technical diagram

Adding Icons Inside the Dot

A plain coloured circle works, but an icon inside the dot improves scannability considerably. Bootstrap 5 ships no icon font by default. Your practical options are: Bootstrap Icons (via CDN), a Feather icon sprite, or an inline SVG. Bootstrap Icons is the most common pairing.

<!-- Bootstrap Icons CDN -->
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">

<!-- Updated dot markup -->
<div class="bs-timeline__dot d-flex align-items-center justify-content-center">
  <i class="bi bi-flag-fill text-white" style="font-size: 0.85rem;"></i>
</div>

The d-flex align-items-center justify-content-center Bootstrap utilities centre the icon inside the dot without any additional CSS. This is where Bootstrap’s utility-first approach genuinely saves time. For a full reference of the utilities available to you, the Complete Bootstrap 5 Utility Class Reference for 2026 is a useful companion.

Responsive Behaviour and Alternating Layout

The single-column vertical timeline works on every screen size without modification. An alternating layout, where entries switch left and right of the central line, only makes sense on viewports wider than roughly 768 px. Below that breakpoint it must collapse back to a single column, otherwise the content boxes become too narrow to read.

<style>
/ Alternating layout, medium screens and above /
@media (min-width: 768px) {

  .bs-timeline--alternate::before {
    left: 50%;
    transform: translateX(-50%);
  }

  .bs-timeline--alternate .bs-timeline__item {
    width: 50%;
    align-self: flex-start;
  }

  / Even items sit on the right side /
  .bs-timeline--alternate .bs-timeline__item:nth-child(even) {
    margin-left: 50%;
    flex-direction: row;
  }

  / Odd items sit on the left, content before dot /
  .bs-timeline--alternate .bs-timeline__item:nth-child(odd) {
    flex-direction: row-reverse;
    text-align: right;
  }

  .bs-timeline--alternate .bs-timeline__item:nth-child(odd)
    .bs-timeline__dot {
    order: -1;
  }
}
</style>

<!-- Apply the modifier class to the list -->
<ol class="bs-timeline bs-timeline--alternate">
  <!-- items unchanged -->
</ol>

Be honest about when the alternating pattern is appropriate. A company-history section with four to eight entries benefits from the visual rhythm. A changelog with 30 entries will be exhausting to scan in the alternating format; use the single-column version instead.

Scroll-Triggered Animation with Intersection Observer

Animation should enhance comprehension, not distract from it. A simple fade-in-and-rise effect as each entry enters the viewport is sufficient. Keep staggered delays below 150 ms per item, or the component will feel sluggish on long timelines.

<style>
/ Initial hidden state /
.bs-timeline__item {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

/ Visible state added by JS /
.bs-timeline__item.is-visible {
  opacity: 1;
  transform: translateY(0);
}
</style>

<script>
(function () {
  const items = document.querySelectorAll('.bs-timeline__item');
  if (!items.length) return;

  const observer = new IntersectionObserver(
    function (entries) {
      entries.forEach(function (entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add('is-visible');
          observer.unobserve(entry.target); // animate once only
        }
      });
    },
    { threshold: 0.15 }
  );

  items.forEach(function (item) {
    observer.observe(item);
  });
}());
</script>

One important caveat: set the initial opacity and transform in CSS, not in JavaScript. If the script is blocked or slow to load, users still see the content rather than a blank page. This is the same principle behind Canvas’s own animation utilities in functions.bundle.js, which register Intersection Observer handlers during DOM-ready rather than inline.

If you want countdown or counter elements adjacent to your timeline (common in company-history sections that display “years in business” or “clients served” figures), see our post on Canvas Countdown and Counter Blocks for Launches and Stats for compatible block patterns.

Accessibility and Common Mistakes

Several issues appear repeatedly when developers build custom timeline components.

  • Using <div> soup instead of a list: Non-list markup breaks the announced item count in screen readers. Always use <ol> or <ul>.
  • Relying on colour alone to convey meaning: If you use different dot colours to represent categories (completed, pending, cancelled), add a visually hidden text label inside the dot as well.
  • Forgetting prefers-reduced-motion: Wrap your animation CSS in a media query so users who have requested reduced motion do not see any transitions.
  • Overflow issues on mobile: The connector ::before element must be inside an ancestor with position: relative and no overflow: hidden set on any wrapping Bootstrap column.
  • Hard-coded colours: Use Bootstrap CSS custom properties (--bs-primary, --bs-gray-300) rather than hex values so dark mode adaptations require no extra work. For a thorough walkthrough of dark mode implementation, see How to Add Dark Mode to a Bootstrap 5 HTML Template the Right Way.
<style>
@media (prefers-reduced-motion: reduce) {
  .bs-timeline__item {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
</style>

Frequently Asked Questions

No. Bootstrap 5.3 does not ship a timeline component. You build it from Bootstrap’s spacing utilities, flex helpers, and your own CSS for the connector line and dot. The code in this guide gives you a production-ready starting point without any additional dependencies.

Draw the line on the container with ::before and set bottom: 0 on it, then remove padding-bottom from the last <li> using the :last-child pseudo-class. Because the line is drawn to the bottom of the container (not of the last item), it naturally ends where the last item ends when padding-bottom: 0 is applied.

Yes, but with caution. A horizontal timeline requires a fixed-height horizontal scroll container on mobile or a responsive collapse to vertical at small breakpoints. The alternating vertical layout described above is almost always a better default. Reserve horizontal timelines for full-width hero sections where you control the viewport context, such as a step-by-step onboarding flow displayed only to authenticated desktop users.

Override the Bootstrap CSS custom property at the :root level or on a parent wrapper. Setting --bs-primary: #e63946; on the .bs-timeline element will change the dot colour for that component only, without affecting the rest of the page. Inside Canvas projects, use --cnvs-themecolor instead, which cascades through all Canvas components automatically.

The component itself has negligible rendering cost regardless of entry count. The Intersection Observer pattern described above scales to hundreds of items without issue. Performance concerns arise from the media attached to each entry (images, video thumbnails) rather than from the timeline structure. Lazy-load any images inside timeline cards using the native loading="lazy" attribute and follow the guidance in our post on How to Optimise Bootstrap 5 Images for Core Web Vitals.

Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.

If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.

Skip the setup and build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog