Bootstrap Timeline: 16 Examples, From Vertical to Roadmap

Search the Bootstrap docs for a timeline and you won't find one. The v5.3 documentation lists 24 components, and none of them is a timeline, so every Bootstrap timeline is custom code built from the grid, cards or a list group, utility classes and a little CSS.

Below are 16 Bootstrap timeline examples you can copy, followed by a guide to building, styling and fixing your own.

By Bogdan Sandu · Updated October 29, 2026

Every example runs on Bootstrap 5.3.8 from the jsDelivr CDN. Add these tags to your page, then paste an example's HTML, CSS and JS.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.2/font/bootstrap-icons.min.css" rel="stylesheet" integrity="sha384-nWN3KqaaVcbEsCjLSV+2jMjh62Iny/+mN3srM8TZeM8N6aj7eDl5wwYggjAJ4/sJ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

Vertical and alternating timelines

The classic layouts: an ordered list, a line drawn with a pseudo-element or border-start, and events in cards, grid columns or a list group. The first demo alternates sides from the md breakpoint and stacks to one side on phones.

Alternating company history

  • row g-0
  • col-md-6
  • offset-md-6
  • text-md-end
  • ::before line

Each li is a row with g-0, left events take col-md-6 with text-md-end and right events add offset-md-6. Below 768px the line and dots sit on the left; one min-width: 768px query moves the line to the center and flips each dot to the column edge facing it.

View code
<div class="btl-alt w-100">
  <h2 class="h4 mb-4 text-md-center">Ridgeline Cycles, from garage to factory</h2>
  <ol class="btl-alt-list list-unstyled mb-0" role="list">
    <li class="row g-0">
      <div class="col-md-6 btl-alt-col btl-alt-left">
        <div class="card border-0 shadow-sm text-md-end">
          <div class="card-body">
            <time class="badge text-bg-dark mb-2" datetime="2012">2012</time>
            <h3 class="h6 card-title mb-1">A garage in Boulder</h3>
            <p class="card-text small text-body-secondary mb-0">Two mechanics weld the first steel frame between repair jobs.</p>
          </div>
        </div>
      </div>
    </li>
    <li class="row g-0">
      <div class="col-md-6 offset-md-6 btl-alt-col btl-alt-right">
        <div class="card border-0 shadow-sm">
          <div class="card-body">
            <time class="badge text-bg-dark mb-2" datetime="2015">2015</time>
            <h3 class="h6 card-title mb-1">The Switchback ships</h3>
            <p class="card-text small text-body-secondary mb-0">400 gravel bikes, sold out in nine days through a single email list.</p>
          </div>
        </div>
      </div>
    </li>
    <li class="row g-0">
      <div class="col-md-6 btl-alt-col btl-alt-left">
        <div class="card border-0 shadow-sm text-md-end">
          <div class="card-body">
            <time class="badge text-bg-dark mb-2" datetime="2019">2019</time>
            <h3 class="h6 card-title mb-1">First flagship store</h3>
            <p class="card-text small text-body-secondary mb-0">A workshop with a shop window, test rides on the canal path.</p>
          </div>
        </div>
      </div>
    </li>
    <li class="row g-0">
      <div class="col-md-6 offset-md-6 btl-alt-col btl-alt-right">
        <div class="card border-0 shadow-sm">
          <div class="card-body">
            <time class="badge text-bg-primary mb-2" datetime="2025">2025</time>
            <h3 class="h6 card-title mb-1">Solar-powered frame factory</h3>
            <p class="card-text small text-body-secondary mb-0">90 people, 12,000 frames a year, every one still brazed by hand.</p>
          </div>
        </div>
      </div>
    </li>
  </ol>
</div>

Milestones with photos

  • border-start
  • ratio-4x3
  • object-fit-cover
  • vstack gap-4

The line is plain border-start border-2 on the ol, recolored by setting --bs-border-color on that element only. Photos sit in ratio ratio-4x3 boxes with object-fit-cover, and a small ::before dot on each li is the only custom shape.

View code
<div class="btl-ms card border-0 shadow-sm w-100">
  <div class="card-body p-4">
    <h2 class="h4 mb-4">Fifteen years of Fjordline Expeditions</h2>
    <ol class="btl-ms-list list-unstyled border-start border-2 ps-4 ms-2 mb-0 vstack gap-4">
      <li class="btl-ms-item">
        <div class="row g-3 align-items-center">
          <div class="col-sm-5">
            <div class="ratio ratio-4x3 rounded-3 overflow-hidden">
              <img src="fjord.webp" class="object-fit-cover" alt="A deep blue fjord seen from a high granite ledge" width="800" height="600" loading="lazy">
            </div>
          </div>
          <div class="col-sm-7">
            <time class="btl-ms-year" datetime="2011">2011</time>
            <h3 class="h5 mb-1">One boat, one fjord</h3>
            <p class="small text-body-secondary mb-0">The first season: six kayaks, a rented van and 140 guests on a single fjord route.</p>
          </div>
        </div>
      </li>
      <li class="btl-ms-item">
        <div class="row g-3 align-items-center">
          <div class="col-sm-5">
            <div class="ratio ratio-4x3 rounded-3 overflow-hidden">
              <img src="mountain-hiker.webp" class="object-fit-cover" alt="A hiker with a large backpack standing on a rock above a hazy mountain valley" width="800" height="600" loading="lazy">
            </div>
          </div>
          <div class="col-sm-7">
            <time class="btl-ms-year" datetime="2016">2016</time>
            <h3 class="h5 mb-1">Hut-to-hut trekking</h3>
            <p class="small text-body-secondary mb-0">Five-day guided routes join the kayak trips and double the summer bookings.</p>
          </div>
        </div>
      </li>
      <li class="btl-ms-item btl-ms-now">
        <div class="row g-3 align-items-center">
          <div class="col-sm-5">
            <div class="ratio ratio-4x3 rounded-3 overflow-hidden">
              <img src="snow-camp.webp" class="object-fit-cover" alt="Yellow tents pitched in deep snow below jagged white peaks" width="800" height="600" loading="lazy">
            </div>
          </div>
          <div class="col-sm-7">
            <time class="btl-ms-year" datetime="2026">2026</time>
            <h3 class="h5 mb-1">Winter base camps</h3>
            <p class="small text-body-secondary mb-0">The first year-round season, with heated basecamp tents and avalanche-trained guides.</p>
          </div>
        </div>
      </li>
    </ol>
  </div>
</div>

Résumé experience and education

  • row g-4
  • col-md-7
  • vstack
  • hollow dots

Two ordered lists side by side in col-md-7 and col-md-5, stacked on phones. The current role gets a filled dot and every past one a hollow dot, all drawn by one ::before rule whose color comes from a prefixed custom property.

View code
<div class="btl-cv card border-0 shadow-sm w-100">
  <div class="card-body p-4">
    <div class="d-flex align-items-center gap-3 mb-4">
      <svg class="flex-shrink-0" width="56" height="56" viewBox="0 0 56 56" aria-hidden="true"><circle cx="28" cy="28" r="28" fill="#4c3fbf"/><text x="28" y="34" text-anchor="middle" fill="#fff" font-size="19" font-weight="700" font-family="system-ui, sans-serif">MK</text></svg>
      <div>
        <h2 class="h5 mb-0">Mara Kowalczyk</h2>
        <p class="small text-body-secondary mb-0">Senior product designer, Berlin</p>
      </div>
    </div>
    <div class="row g-4">
      <div class="col-md-7">
        <h3 class="h6 fw-bold mb-3">Experience</h3>
        <ol class="btl-cv-list list-unstyled mb-0 vstack gap-3">
          <li class="btl-cv-item btl-cv-now">
            <h4 class="h6 mb-0">Lead designer, Parcelwise</h4>
            <p class="small text-body-secondary mb-1"><time datetime="2022-04">Apr 2022</time> to present</p>
            <p class="small mb-0">Runs a team of five on the courier app; cut failed deliveries by redesigning the address flow.</p>
          </li>
          <li class="btl-cv-item">
            <h4 class="h6 mb-0">Product designer, Tallyhouse</h4>
            <p class="small text-body-secondary mb-1"><time datetime="2018-09">Sep 2018</time> to <time datetime="2022-03">Mar 2022</time></p>
            <p class="small mb-0">Built the invoicing design system used by four product squads.</p>
          </li>
          <li class="btl-cv-item">
            <h4 class="h6 mb-0">UI designer, Studio Brandt</h4>
            <p class="small text-body-secondary mb-1"><time datetime="2016-02">Feb 2016</time> to <time datetime="2018-08">Aug 2018</time></p>
            <p class="small mb-0">Agency work for museums, publishers and two city festivals.</p>
          </li>
        </ol>
      </div>
      <div class="col-md-5">
        <h3 class="h6 fw-bold mb-3">Education</h3>
        <ol class="btl-cv-list list-unstyled mb-0 vstack gap-3">
          <li class="btl-cv-item">
            <h4 class="h6 mb-0">MA Interaction Design</h4>
            <p class="small text-body-secondary mb-0">Design school, Potsdam, <time datetime="2015">2015</time></p>
          </li>
          <li class="btl-cv-item">
            <h4 class="h6 mb-0">BA Communication Design</h4>
            <p class="small text-body-secondary mb-0">Academy of Fine Arts, Krakow, <time datetime="2013">2013</time></p>
          </li>
        </ol>
      </div>
    </div>
  </div>
</div>

Changelog with version badges

  • list-group
  • badge rounded-pill
  • bg-*-subtle
  • col-sm-4

A list group where every item is one release: the version badge and date sit in col-sm-4, the notes in col-sm-8. Change types use the 5.3 subtle color pairs (bg-success-subtle with text-success-emphasis) and get a fixed width so the notes line up.

View code
<div class="btl-log w-100">
  <h2 class="h5 mb-3">Quillstack CLI release notes</h2>
  <ol class="list-group shadow-sm" reversed>
    <li class="list-group-item p-4">
      <div class="row g-3">
        <div class="col-sm-4">
          <span class="badge rounded-pill text-bg-dark fs-6">v4.2.0</span>
          <span class="badge rounded-pill bg-primary-subtle text-primary-emphasis ms-1">Latest</span>
          <p class="small text-body-secondary mt-2 mb-0"><time datetime="2026-10-14">Oct 14, 2026</time></p>
        </div>
        <div class="col-sm-8">
          <h3 class="h6 mb-2">Faster cold starts</h3>
          <ul class="list-unstyled small mb-0 vstack gap-2">
            <li class="d-flex gap-2"><span class="badge bg-success-subtle text-success-emphasis btl-log-tag">Added</span><span>Parallel plugin loading, startup down from 1.4 s to 0.5 s.</span></li>
            <li class="d-flex gap-2"><span class="badge bg-warning-subtle text-warning-emphasis btl-log-tag">Fixed</span><span>Watch mode no longer misses renamed files on Windows.</span></li>
          </ul>
        </div>
      </div>
    </li>
    <li class="list-group-item p-4">
      <div class="row g-3">
        <div class="col-sm-4">
          <span class="badge rounded-pill text-bg-secondary fs-6">v4.1.3</span>
          <p class="small text-body-secondary mt-2 mb-0"><time datetime="2026-09-02">Sep 2, 2026</time></p>
        </div>
        <div class="col-sm-8">
          <h3 class="h6 mb-2">Patch release</h3>
          <ul class="list-unstyled small mb-0 vstack gap-2">
            <li class="d-flex gap-2"><span class="badge bg-warning-subtle text-warning-emphasis btl-log-tag">Fixed</span><span>Config files with a BOM parse correctly again.</span></li>
          </ul>
        </div>
      </div>
    </li>
    <li class="list-group-item p-4">
      <div class="row g-3">
        <div class="col-sm-4">
          <span class="badge rounded-pill text-bg-secondary fs-6">v4.0.0</span>
          <p class="small text-body-secondary mt-2 mb-0"><time datetime="2026-06-30">Jun 30, 2026</time></p>
        </div>
        <div class="col-sm-8">
          <h3 class="h6 mb-2">New config format</h3>
          <ul class="list-unstyled small mb-0 vstack gap-2">
            <li class="d-flex gap-2"><span class="badge bg-danger-subtle text-danger-emphasis btl-log-tag">Breaking</span><span>quill.json replaces .quillrc; run quill migrate once.</span></li>
            <li class="d-flex gap-2"><span class="badge bg-info-subtle text-info-emphasis btl-log-tag">Changed</span><span>Node 20 is now the minimum version.</span></li>
          </ul>
        </div>
      </div>
    </li>
  </ol>
</div>

Tracking, steps and roadmaps

Timelines that show where something stands: a shipment with its current step, a horizontal plan that scrolls inside its own box on phones, a quarterly roadmap and an onboarding stepper you can advance.

Order tracking with current step

  • card-header
  • aria-current="step"
  • Bootstrap Icons
  • visually-hidden

The steps are an ol inside a card body. Done steps get a filled teal circle and a solid connector, the current step carries aria-current="step" and a ring, and upcoming steps stay grey. Each step also has visually-hidden status text, so the state does not live in color alone.

View code
<div class="btl-track card border-0 shadow-sm w-100">
  <div class="card-header bg-body d-flex flex-wrap justify-content-between align-items-center gap-2 py-3">
    <div>
      <h2 class="h6 mb-0">Order #48213</h2>
      <p class="small text-body-secondary mb-0">Arrives <time datetime="2026-10-31">Friday, Oct 31</time></p>
    </div>
    <span class="badge rounded-pill btl-track-badge">Out for delivery</span>
  </div>
  <div class="card-body p-4">
    <ol class="list-unstyled mb-0">
      <li class="btl-track-step btl-track-done">
        <span class="btl-track-icon"><i class="bi bi-bag-check" aria-hidden="true"></i></span>
        <div>
          <p class="fw-semibold mb-0">Order placed <span class="visually-hidden">(completed)</span></p>
          <p class="small text-body-secondary mb-0"><time datetime="2026-10-27T09:12">Mon, Oct 27, 09:12</time></p>
        </div>
      </li>
      <li class="btl-track-step btl-track-done">
        <span class="btl-track-icon"><i class="bi bi-box-seam" aria-hidden="true"></i></span>
        <div>
          <p class="fw-semibold mb-0">Packed and shipped <span class="visually-hidden">(completed)</span></p>
          <p class="small text-body-secondary mb-0"><time datetime="2026-10-28T16:40">Tue, Oct 28, 16:40</time>, Leipzig hub</p>
        </div>
      </li>
      <li class="btl-track-step btl-track-now" aria-current="step">
        <span class="btl-track-icon"><i class="bi bi-truck" aria-hidden="true"></i></span>
        <div>
          <p class="fw-semibold mb-0">Out for delivery <span class="visually-hidden">(current step)</span></p>
          <p class="small text-body-secondary mb-0"><time datetime="2026-10-31T08:05">Today, 08:05</time>, six stops away</p>
        </div>
      </li>
      <li class="btl-track-step">
        <span class="btl-track-icon"><i class="bi bi-house-door" aria-hidden="true"></i></span>
        <div>
          <p class="fw-semibold mb-0 text-body-secondary">Delivered <span class="visually-hidden">(upcoming)</span></p>
          <p class="small text-body-secondary mb-0">Expected by 18:00</p>
        </div>
      </li>
    </ol>
  </div>
</div>

Horizontal plan that scrolls on phones

  • d-flex
  • overflow-x-auto
  • scroll-snap
  • role="region"

The ol is a d-flex row of fixed-basis steps inside an overflow-x-auto box, so on narrow screens only that box scrolls sideways, never the page. The box is focusable with tabindex="0" and labelled as a region, which lets keyboard users scroll it with the arrow keys.

View code
<div class="btl-hz card border-0 shadow-sm w-100">
  <div class="card-body p-4">
    <div class="d-flex flex-wrap justify-content-between align-items-baseline gap-2 mb-4">
      <h2 class="h5 mb-0">Kitchen renovation plan</h2>
      <span class="small text-body-secondary">Week 3 of 8</span>
    </div>
    <div class="btl-hz-scroll overflow-x-auto pb-2" tabindex="0" role="region" aria-label="Renovation steps">
      <ol class="list-unstyled d-flex mb-0">
        <li class="btl-hz-step btl-hz-done">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small text-body-secondary" datetime="2026-10-06">Oct 6</time>
          <h3 class="h6 mb-1">Survey</h3>
          <p class="small text-body-secondary mb-0">Measurements and wiring check</p>
        </li>
        <li class="btl-hz-step btl-hz-done">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small text-body-secondary" datetime="2026-10-13">Oct 13</time>
          <h3 class="h6 mb-1">Strip out</h3>
          <p class="small text-body-secondary mb-0">Old units and tiles removed</p>
        </li>
        <li class="btl-hz-step btl-hz-now" aria-current="step">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small fw-semibold btl-hz-date" datetime="2026-10-20">Oct 20</time>
          <h3 class="h6 mb-1">Plumbing</h3>
          <p class="small text-body-secondary mb-0">New supply for the island sink</p>
        </li>
        <li class="btl-hz-step">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small text-body-secondary" datetime="2026-11-03">Nov 3</time>
          <h3 class="h6 mb-1">Cabinets</h3>
          <p class="small text-body-secondary mb-0">Oak fronts, soft-close drawers</p>
        </li>
        <li class="btl-hz-step">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small text-body-secondary" datetime="2026-11-17">Nov 17</time>
          <h3 class="h6 mb-1">Worktops</h3>
          <p class="small text-body-secondary mb-0">Template, cut and fit</p>
        </li>
        <li class="btl-hz-step">
          <span class="btl-hz-dot" aria-hidden="true"></span>
          <time class="small text-body-secondary" datetime="2026-11-28">Nov 28</time>
          <h3 class="h6 mb-1">Handover</h3>
          <p class="small text-body-secondary mb-0">Snag list and final clean</p>
        </li>
      </ol>
    </div>
    <p class="small text-body-secondary mt-2 mb-0 d-md-none">Swipe the steps sideways to see the whole plan.</p>
  </div>
</div>

Product roadmap by quarter

  • row-cols-md-4
  • card h-100
  • list-group-flush
  • status badges

Quarters are columns of row-cols-1 row-cols-md-4, each a card with a flush list group of features and status badges. On phones the axis runs down the left edge; from 768px the same pseudo-elements turn into a line across the top with one dot per quarter.

View code
<div class="btl-rm w-100">
  <h2 class="h5 mb-3">Lumen Notes roadmap</h2>
  <ol class="row row-cols-1 row-cols-md-4 g-3 list-unstyled mb-0">
    <li class="col btl-rm-col btl-rm-done">
      <div class="card h-100 shadow-sm border-0">
        <div class="card-header bg-body d-flex justify-content-between align-items-center">
          <h3 class="h6 mb-0"><time datetime="2026-04">Q2 2026</time></h3>
          <span class="badge text-bg-success">Done</span>
        </div>
        <ul class="list-group list-group-flush small">
          <li class="list-group-item">Offline mode</li>
          <li class="list-group-item">Markdown export</li>
        </ul>
      </div>
    </li>
    <li class="col btl-rm-col btl-rm-done">
      <div class="card h-100 shadow-sm border-0">
        <div class="card-header bg-body d-flex justify-content-between align-items-center">
          <h3 class="h6 mb-0"><time datetime="2026-07">Q3 2026</time></h3>
          <span class="badge text-bg-success">Done</span>
        </div>
        <ul class="list-group list-group-flush small">
          <li class="list-group-item">Shared folders</li>
          <li class="list-group-item">Web clipper</li>
        </ul>
      </div>
    </li>
    <li class="col btl-rm-col btl-rm-now">
      <div class="card h-100 shadow-sm border-primary border-2">
        <div class="card-header bg-primary-subtle d-flex justify-content-between align-items-center">
          <h3 class="h6 mb-0"><time datetime="2026-10">Q4 2026</time></h3>
          <span class="badge text-bg-primary">Now</span>
        </div>
        <ul class="list-group list-group-flush small">
          <li class="list-group-item d-flex justify-content-between gap-2">Live cursors <span class="badge bg-success-subtle text-success-emphasis">Beta</span></li>
          <li class="list-group-item d-flex justify-content-between gap-2">Tables <span class="badge bg-warning-subtle text-warning-emphasis">In progress</span></li>
        </ul>
      </div>
    </li>
    <li class="col btl-rm-col">
      <div class="card h-100 shadow-sm border-0">
        <div class="card-header bg-body d-flex justify-content-between align-items-center">
          <h3 class="h6 mb-0"><time datetime="2027-01">Q1 2027</time></h3>
          <span class="badge bg-secondary-subtle text-secondary-emphasis">Planned</span>
        </div>
        <ul class="list-group list-group-flush small">
          <li class="list-group-item">Calendar view</li>
          <li class="list-group-item">Public API</li>
        </ul>
      </div>
    </li>
  </ol>
</div>

Onboarding stepper you can advance

  • aria-current="step"
  • role="status"
  • btn
  • Bootstrap Icons

Click Mark as done to advance the current step.

A vertical stepper: the script moves aria-current="step" to the next li, swaps the number for a check icon through a done class and updates a role="status" line for screen readers. The circles and connector are two short rules driven by one prefixed accent variable.

View code
<div class="btl-st card border-0 shadow-sm w-100" id="btl-st">
  <div class="card-body p-4">
    <h2 class="h5 mb-1">Launch your store</h2>
    <p class="small text-body-secondary mb-4" id="btl-st-status" role="status">Step 2 of 4: Add your first product</p>
    <ol class="list-unstyled mb-4">
      <li class="btl-st-step btl-st-done">
        <span class="btl-st-mark" aria-hidden="true"><span class="btl-st-num">1</span><i class="bi bi-check-lg"></i></span>
        <div>
          <h3 class="h6 mb-1">Create your account</h3>
          <p class="small text-body-secondary mb-0">Finished <time datetime="2026-10-28">yesterday</time></p>
        </div>
      </li>
      <li class="btl-st-step" aria-current="step">
        <span class="btl-st-mark" aria-hidden="true"><span class="btl-st-num">2</span><i class="bi bi-check-lg"></i></span>
        <div>
          <h3 class="h6 mb-1">Add your first product</h3>
          <p class="small text-body-secondary mb-0">Photos, price and stock count</p>
        </div>
      </li>
      <li class="btl-st-step">
        <span class="btl-st-mark" aria-hidden="true"><span class="btl-st-num">3</span><i class="bi bi-check-lg"></i></span>
        <div>
          <h3 class="h6 mb-1">Set up payments</h3>
          <p class="small text-body-secondary mb-0">Connect a payout account</p>
        </div>
      </li>
      <li class="btl-st-step">
        <span class="btl-st-mark" aria-hidden="true"><span class="btl-st-num">4</span><i class="bi bi-check-lg"></i></span>
        <div>
          <h3 class="h6 mb-1">Publish</h3>
          <p class="small text-body-secondary mb-0">Pick a domain and go live</p>
        </div>
      </li>
    </ol>
    <button type="button" class="btn btn-dark btl-st-btn" id="btl-st-next">Mark as done</button>
  </div>
</div>

Activity feeds, agendas and logs

Time-stamped streams: a team activity feed with initials avatars, a two-day conference agenda on the tab plugin, a commit history and a status page incident log in dark mode.

Team activity feed with avatars

  • list-group-flush
  • --bs-list-group-*
  • inline SVG avatars
  • bg-body-tertiary

A flush list group inside a card, made transparent and borderless through --bs-list-group-bg and --bs-list-group-border-width so a ::before connector can run between the initials avatars. A quoted comment uses bg-body-tertiary and two uploads use the thumbnail pattern with fixed-size images.

View code
<div class="btl-af card border-0 shadow-sm w-100">
  <div class="card-header bg-body d-flex justify-content-between align-items-center py-3">
    <h2 class="h6 mb-0">Website relaunch activity</h2>
    <a href="#" class="small link-secondary">View all</a>
  </div>
  <ol class="list-group list-group-flush btl-af-list">
    <li class="list-group-item btl-af-item">
      <svg class="btl-af-av" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#6d28d9"/><text x="20" y="25" text-anchor="middle" fill="#fff" font-size="14" font-weight="600" font-family="system-ui, sans-serif">PN</text></svg>
      <div class="btl-af-body flex-grow-1">
        <p class="small mb-1"><strong>Priya Nair</strong> moved <a href="#">Homepage hero</a> to <span class="badge bg-warning-subtle text-warning-emphasis">Review</span></p>
        <p class="small text-body-secondary mb-0"><time datetime="2026-10-29T09:41">9:41</time></p>
      </div>
    </li>
    <li class="list-group-item btl-af-item">
      <svg class="btl-af-av" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#0e7490"/><text x="20" y="25" text-anchor="middle" fill="#fff" font-size="14" font-weight="600" font-family="system-ui, sans-serif">TO</text></svg>
      <div class="btl-af-body flex-grow-1">
        <p class="small mb-2"><strong>Tomas Ortega</strong> commented on <a href="#">Pricing page</a></p>
        <p class="small bg-body-tertiary rounded-3 p-3 mb-1">Can we test the yearly toggle on the left? Most visitors never scroll to it on mobile.</p>
        <p class="small text-body-secondary mb-0"><time datetime="2026-10-29T08:15">8:15</time></p>
      </div>
    </li>
    <li class="list-group-item btl-af-item">
      <svg class="btl-af-av" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#b45309"/><text x="20" y="25" text-anchor="middle" fill="#fff" font-size="14" font-weight="600" font-family="system-ui, sans-serif">AL</text></svg>
      <div class="btl-af-body flex-grow-1">
        <p class="small mb-2"><strong>Ana Lindqvist</strong> uploaded 2 photos to <a href="#">About page</a></p>
        <div class="d-flex gap-2 mb-1">
          <img src="brick-alley.webp" class="rounded-2 object-fit-cover btl-af-thumb" alt="A narrow brick alley with fire escapes" width="800" height="600" loading="lazy">
          <img src="city-skyline.webp" class="rounded-2 object-fit-cover btl-af-thumb" alt="A city skyline at sunrise" width="800" height="600" loading="lazy">
        </div>
        <p class="small text-body-secondary mb-0"><time datetime="2026-10-28T17:02">Yesterday, 17:02</time></p>
      </div>
    </li>
    <li class="list-group-item btl-af-item">
      <svg class="btl-af-av" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="20" fill="#15803d"/><text x="20" y="25" text-anchor="middle" fill="#fff" font-size="14" font-weight="600" font-family="system-ui, sans-serif">JB</text></svg>
      <div class="btl-af-body flex-grow-1">
        <p class="small mb-1"><strong>Jonah Brooks</strong> closed <a href="#">Fix footer links</a></p>
        <p class="small text-body-secondary mb-0"><time datetime="2026-10-28T11:30">Yesterday, 11:30</time></p>
      </div>
    </li>
  </ol>
</div>

Two-day conference agenda

  • nav-pills
  • tab plugin
  • --bs-nav-pills-*
  • time datetime

Switch between the two days.

Each day is a tab pane switched by the tab plugin through data-bs-toggle="pill", with the pill colors changed only through --bs-nav-pills-link-active-bg. Inside, every session is an li with a fixed time column and a border-start track, and breaks use a dashed dot.

View code
<div class="btl-ag card border-0 shadow-sm w-100">
  <div class="card-body p-4">
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
      <h2 class="h5 mb-0">Frontline UX Summit</h2>
      <ul class="nav nav-pills btl-ag-pills" role="tablist">
        <li class="nav-item" role="presentation">
          <button class="nav-link active" id="btl-ag-d1-tab" data-bs-toggle="pill" data-bs-target="#btl-ag-d1" type="button" role="tab" aria-controls="btl-ag-d1" aria-selected="true">Thu, Mar 18</button>
        </li>
        <li class="nav-item" role="presentation">
          <button class="nav-link" id="btl-ag-d2-tab" data-bs-toggle="pill" data-bs-target="#btl-ag-d2" type="button" role="tab" aria-controls="btl-ag-d2" aria-selected="false">Fri, Mar 19</button>
        </li>
      </ul>
    </div>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btl-ag-d1" role="tabpanel" aria-labelledby="btl-ag-d1-tab" tabindex="0">
        <ol class="list-unstyled mb-0">
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-18T09:00">09:00</time>
            <div class="btl-ag-slot btl-ag-key">
              <h3 class="h6 mb-1">Opening keynote: Designing for slow networks</h3>
              <p class="small text-body-secondary mb-0">Ines Duarte <span class="badge bg-primary-subtle text-primary-emphasis ms-1">Main hall</span></p>
            </div>
          </li>
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-18T10:15">10:15</time>
            <div class="btl-ag-slot">
              <h3 class="h6 mb-1">Forms that forgive mistakes</h3>
              <p class="small text-body-secondary mb-0">Kwame Asante <span class="badge text-bg-light border ms-1">Room B</span></p>
            </div>
          </li>
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-18T11:00">11:00</time>
            <div class="btl-ag-slot btl-ag-break">
              <p class="small text-body-secondary fst-italic mb-0">Coffee break</p>
            </div>
          </li>
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-18T11:30">11:30</time>
            <div class="btl-ag-slot">
              <h3 class="h6 mb-1">Workshop: Auditing a design system</h3>
              <p class="small text-body-secondary mb-0">Lea Hoffmann <span class="badge text-bg-light border ms-1">Studio 2</span></p>
            </div>
          </li>
        </ol>
      </div>
      <div class="tab-pane fade" id="btl-ag-d2" role="tabpanel" aria-labelledby="btl-ag-d2-tab" tabindex="0">
        <ol class="list-unstyled mb-0">
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-19T09:30">09:30</time>
            <div class="btl-ag-slot btl-ag-key">
              <h3 class="h6 mb-1">Research on a shoestring</h3>
              <p class="small text-body-secondary mb-0">Ravi Menon <span class="badge bg-primary-subtle text-primary-emphasis ms-1">Main hall</span></p>
            </div>
          </li>
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-19T10:45">10:45</time>
            <div class="btl-ag-slot">
              <h3 class="h6 mb-1">Panel: Accessibility after the EAA</h3>
              <p class="small text-body-secondary mb-0">Four speakers <span class="badge text-bg-light border ms-1">Room B</span></p>
            </div>
          </li>
          <li class="btl-ag-row">
            <time class="btl-ag-time" datetime="2027-03-19T12:00">12:00</time>
            <div class="btl-ag-slot btl-ag-break">
              <p class="small text-body-secondary fst-italic mb-0">Lunch and closing drinks</p>
            </div>
          </li>
        </ol>
      </div>
    </div>
  </div>
</div>

Commit history with release tags

  • list-unstyled
  • font-monospace
  • badge
  • text-truncate

A dev-style log: the line and commit dots are pseudo-elements, merge commits get a filled dot and tagged releases a badge. Short hashes use font-monospace in a code element and long messages use text-truncate inside a flex child with min-width: 0, so they never push the card wider than the phone.

View code
<div class="btl-gh card border-0 shadow-sm w-100">
  <div class="card-header bg-body d-flex flex-wrap align-items-center gap-2 py-3">
    <i class="bi bi-clock-history" aria-hidden="true"></i>
    <h2 class="h6 mb-0 me-auto">Commits on <code class="btl-gh-branch">main</code></h2>
    <span class="small text-body-secondary">Oct 27 to 29</span>
  </div>
  <ol class="list-unstyled mb-0 px-3 py-2">
    <li class="btl-gh-item btl-gh-merge">
      <div class="btl-gh-msg flex-grow-1">
        <p class="mb-0 text-truncate fw-semibold">Merge pull request #212 from feature/search-filters</p>
        <p class="small text-body-secondary mb-0">dana.k merged <time datetime="2026-10-29T10:20">today at 10:20</time> <span class="badge bg-success-subtle text-success-emphasis ms-1">v2.4.0</span></p>
      </div>
      <code class="btl-gh-hash">9c41e7a</code>
    </li>
    <li class="btl-gh-item">
      <div class="btl-gh-msg flex-grow-1">
        <p class="mb-0 text-truncate">Add date range filter to the search panel</p>
        <p class="small text-body-secondary mb-0">rafael.m committed <time datetime="2026-10-28T18:02">yesterday</time></p>
      </div>
      <code class="btl-gh-hash">f03b2d1</code>
    </li>
    <li class="btl-gh-item">
      <div class="btl-gh-msg flex-grow-1">
        <p class="mb-0 text-truncate">Debounce filter input so the list does not re-render on every key</p>
        <p class="small text-body-secondary mb-0">rafael.m committed <time datetime="2026-10-28T15:47">yesterday</time></p>
      </div>
      <code class="btl-gh-hash">51ad09e</code>
    </li>
    <li class="btl-gh-item btl-gh-merge">
      <div class="btl-gh-msg flex-grow-1">
        <p class="mb-0 text-truncate fw-semibold">Merge pull request #209 from fix/date-parsing</p>
        <p class="small text-body-secondary mb-0">dana.k merged <time datetime="2026-10-27T12:05">Oct 27</time> <span class="badge bg-success-subtle text-success-emphasis ms-1">v2.3.2</span></p>
      </div>
      <code class="btl-gh-hash">c7e88b4</code>
    </li>
  </ol>
</div>

Status page incident log in dark mode

  • data-bs-theme="dark"
  • text-bg-*
  • bg-body-tertiary
  • --bs-border-color

Rendered under data-bs-theme="dark", so the card, body text and border colors switch without extra CSS. Updates run newest first in an ol with the reversed attribute, and each status sets a prefixed dot color while the badge text names the state.

View code
<div class="btl-in card bg-body-tertiary w-100">
  <div class="card-body p-4">
    <div class="d-flex flex-wrap align-items-center gap-2 mb-1">
      <span class="badge text-bg-success">Resolved</span>
      <h2 class="h5 mb-0">Elevated API error rates</h2>
    </div>
    <p class="small text-body-secondary mb-4">Affected: REST API, webhooks. Duration 52 minutes.</p>
    <ol class="btl-in-list list-unstyled mb-0" reversed>
      <li class="btl-in-item btl-in-ok">
        <p class="mb-1"><span class="fw-semibold">Resolved</span> <span class="text-body-secondary small">· <time datetime="2026-10-29T14:58Z">14:58 UTC</time></span></p>
        <p class="small text-body-secondary mb-0">Error rates have been normal for 30 minutes. A full report follows within five working days.</p>
      </li>
      <li class="btl-in-item btl-in-watch">
        <p class="mb-1"><span class="fw-semibold">Monitoring</span> <span class="text-body-secondary small">· <time datetime="2026-10-29T14:21Z">14:21 UTC</time></span></p>
        <p class="small text-body-secondary mb-0">The faulty config was rolled back in all regions. We are watching the error dashboards.</p>
      </li>
      <li class="btl-in-item btl-in-found">
        <p class="mb-1"><span class="fw-semibold">Identified</span> <span class="text-body-secondary small">· <time datetime="2026-10-29T14:12Z">14:12 UTC</time></span></p>
        <p class="small text-body-secondary mb-0">A load balancer change cut the connection pool for the EU region.</p>
      </li>
      <li class="btl-in-item btl-in-alert">
        <p class="mb-1"><span class="fw-semibold">Investigating</span> <span class="text-body-secondary small">· <time datetime="2026-10-29T14:06Z">14:06 UTC</time></span></p>
        <p class="small text-body-secondary mb-0">About 8% of API requests return 502 errors. Engineers are looking into it.</p>
      </li>
    </ol>
  </div>
</div>

Collapsible, filterable and themed timelines

Bootstrap JavaScript and CSS variables at work: entries that expand with the collapse plugin, a btn-group filter, a numbered list group restyled into a timeline and a scroll reveal that respects reduced motion.

Collapsible project timeline

  • collapse
  • data-bs-toggle
  • aria-expanded
  • Collapse.getOrCreateInstance

Open an entry or use Expand all.

Each entry is a card whose header button toggles its details with data-bs-toggle="collapse", and the plugin keeps aria-expanded in sync. The Expand all button calls bootstrap.Collapse.getOrCreateInstance with toggle: false and then show() or hide() on every panel; the chevron turn only animates when reduced motion is off.

View code
<div class="btl-cl w-100" id="btl-cl">
  <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
    <h2 class="h5 mb-0">Project Atlas: kickoff to launch</h2>
    <button type="button" class="btn btn-outline-dark btn-sm btl-cl-all" id="btl-cl-all">Expand all</button>
  </div>
  <ol class="btl-cl-list list-unstyled mb-0 vstack gap-2">
    <li class="btl-cl-item btl-cl-done">
      <div class="card border-0 shadow-sm">
        <h3 class="h6 mb-0">
          <button class="btn btl-cl-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#btl-cl-1" aria-expanded="true" aria-controls="btl-cl-1">
            <time class="btl-cl-date" datetime="2026-09-01">Sep 1</time>
            <span class="flex-grow-1">Kickoff workshop</span>
            <i class="bi bi-chevron-down btl-cl-chev" aria-hidden="true"></i>
          </button>
        </h3>
        <div class="collapse show btl-cl-panel" id="btl-cl-1">
          <p class="small text-body-secondary px-3 pb-3 mb-0">Goals, budget and a ten-week plan agreed with the client. Owners assigned for design, content and build.</p>
        </div>
      </div>
    </li>
    <li class="btl-cl-item btl-cl-done">
      <div class="card border-0 shadow-sm">
        <h3 class="h6 mb-0">
          <button class="btn btl-cl-toggle collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#btl-cl-2" aria-expanded="false" aria-controls="btl-cl-2">
            <time class="btl-cl-date" datetime="2026-09-22">Sep 22</time>
            <span class="flex-grow-1">Wireframes signed off</span>
            <i class="bi bi-chevron-down btl-cl-chev" aria-hidden="true"></i>
          </button>
        </h3>
        <div class="collapse btl-cl-panel" id="btl-cl-2">
          <p class="small text-body-secondary px-3 pb-3 mb-0">Fourteen templates approved after two review rounds; the checkout flow dropped from five screens to three.</p>
        </div>
      </div>
    </li>
    <li class="btl-cl-item btl-cl-now">
      <div class="card border-0 shadow-sm">
        <h3 class="h6 mb-0">
          <button class="btn btl-cl-toggle collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#btl-cl-3" aria-expanded="false" aria-controls="btl-cl-3">
            <time class="btl-cl-date" datetime="2026-10-27">Oct 27</time>
            <span class="flex-grow-1">Content migration <span class="badge bg-primary-subtle text-primary-emphasis ms-1">In progress</span></span>
            <i class="bi bi-chevron-down btl-cl-chev" aria-hidden="true"></i>
          </button>
        </h3>
        <div class="collapse btl-cl-panel" id="btl-cl-3">
          <p class="small text-body-secondary px-3 pb-3 mb-0">312 of 480 pages moved. Redirect map ready for review on Friday.</p>
        </div>
      </div>
    </li>
    <li class="btl-cl-item">
      <div class="card border-0 shadow-sm">
        <h3 class="h6 mb-0">
          <button class="btn btl-cl-toggle collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#btl-cl-4" aria-expanded="false" aria-controls="btl-cl-4">
            <time class="btl-cl-date" datetime="2026-11-16">Nov 16</time>
            <span class="flex-grow-1">Launch</span>
            <i class="bi bi-chevron-down btl-cl-chev" aria-hidden="true"></i>
          </button>
        </h3>
        <div class="collapse btl-cl-panel" id="btl-cl-4">
          <p class="small text-body-secondary px-3 pb-3 mb-0">DNS switch at 06:00, launch call at 09:00, two weeks of hypercare after go-live.</p>
        </div>
      </div>
    </li>
  </ol>
</div>

Filterable news timeline

  • btn-group
  • btn-check
  • d-none
  • aria-live

Pick a filter above the timeline.

The filter is a btn-group of btn-check radios, so it works with arrow keys and needs no state of its own. On change the script toggles d-none on items whose data-btl-type does not match and writes the visible count into a polite live region.

View code
<div class="btl-fl w-100" id="btl-fl">
  <h2 class="h5 mb-3">Harbor Labs newsroom</h2>
  <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-4">
    <div class="btn-group" role="group" aria-label="Filter news by type">
      <input type="radio" class="btn-check" name="btl-fl-type" id="btl-fl-all" value="all" autocomplete="off" checked>
      <label class="btn btn-outline-dark" for="btl-fl-all">All</label>
      <input type="radio" class="btn-check" name="btl-fl-type" id="btl-fl-release" value="release" autocomplete="off">
      <label class="btn btn-outline-dark" for="btl-fl-release">Releases</label>
      <input type="radio" class="btn-check" name="btl-fl-type" id="btl-fl-event" value="event" autocomplete="off">
      <label class="btn btn-outline-dark" for="btl-fl-event">Events</label>
      <input type="radio" class="btn-check" name="btl-fl-type" id="btl-fl-press" value="press" autocomplete="off">
      <label class="btn btn-outline-dark" for="btl-fl-press">Press</label>
    </div>
    <p class="small text-body-secondary mb-0" id="btl-fl-count" aria-live="polite">Showing 6 of 6</p>
  </div>
  <ol class="btl-fl-list list-unstyled mb-0 vstack gap-3">
    <li class="btl-fl-item btl-fl-release" data-btl-type="release">
      <p class="small text-body-secondary mb-1"><time datetime="2026-10-21">Oct 21, 2026</time> <span class="badge bg-success-subtle text-success-emphasis ms-1">Release</span></p>
      <h3 class="h6 mb-0">Harbor 3.0 adds route planning for small fleets</h3>
    </li>
    <li class="btl-fl-item btl-fl-event" data-btl-type="event">
      <p class="small text-body-secondary mb-1"><time datetime="2026-10-09">Oct 9, 2026</time> <span class="badge bg-info-subtle text-info-emphasis ms-1">Event</span></p>
      <h3 class="h6 mb-0">Live demo day in Rotterdam, 120 fleet managers attend</h3>
    </li>
    <li class="btl-fl-item btl-fl-press" data-btl-type="press">
      <p class="small text-body-secondary mb-1"><time datetime="2026-09-15">Sep 15, 2026</time> <span class="badge bg-warning-subtle text-warning-emphasis ms-1">Press</span></p>
      <h3 class="h6 mb-0">Harbor Labs opens a second office in Lisbon</h3>
    </li>
    <li class="btl-fl-item btl-fl-release" data-btl-type="release">
      <p class="small text-body-secondary mb-1"><time datetime="2026-08-04">Aug 4, 2026</time> <span class="badge bg-success-subtle text-success-emphasis ms-1">Release</span></p>
      <h3 class="h6 mb-0">Driver app gets offline delivery proofs</h3>
    </li>
    <li class="btl-fl-item btl-fl-event" data-btl-type="event">
      <p class="small text-body-secondary mb-1"><time datetime="2026-06-12">Jun 12, 2026</time> <span class="badge bg-info-subtle text-info-emphasis ms-1">Event</span></p>
      <h3 class="h6 mb-0">Webinar: cutting empty miles by a fifth</h3>
    </li>
    <li class="btl-fl-item btl-fl-press" data-btl-type="press">
      <p class="small text-body-secondary mb-1"><time datetime="2026-05-02">May 2, 2026</time> <span class="badge bg-warning-subtle text-warning-emphasis ms-1">Press</span></p>
      <h3 class="h6 mb-0">Series A round closes at 14 million euros</h3>
    </li>
  </ol>
</div>

Numbered list group, fully rethemed

  • list-group-numbered
  • --bs-list-group-*
  • --bs-badge-*
  • counter()

Bootstrap already numbers list-group-numbered items with a CSS counter and a ::before. This demo keeps that counter but turns the ::before into a numbered dot, adds the connector with ::after and rethemes the whole list through --bs-list-group-* and --bs-badge-* variables in a warm serif palette.

View code
<div class="btl-vt w-100">
  <h2 class="h4 mb-1 btl-vt-title">A year in the vines</h2>
  <p class="small btl-vt-muted mb-4">Clos des Pierres, from bud to bottle</p>
  <ol class="list-group list-group-numbered btl-vt-list">
    <li class="list-group-item">
      <div class="d-flex flex-wrap justify-content-between align-items-baseline gap-2 mb-1">
        <h3 class="h6 mb-0">Bud break</h3>
        <time class="badge btl-vt-badge" datetime="2026-04">April</time>
      </div>
      <p class="small btl-vt-muted mb-0">The first green shoots appear; a late frost is the biggest risk of the year.</p>
    </li>
    <li class="list-group-item">
      <div class="d-flex flex-wrap justify-content-between align-items-baseline gap-2 mb-1">
        <h3 class="h6 mb-0">Flowering</h3>
        <time class="badge btl-vt-badge" datetime="2026-06">June</time>
      </div>
      <p class="small btl-vt-muted mb-0">Ten days that set the size of the crop. Rain now means fewer grapes.</p>
    </li>
    <li class="list-group-item">
      <div class="d-flex flex-wrap justify-content-between align-items-baseline gap-2 mb-1">
        <h3 class="h6 mb-0">Veraison</h3>
        <time class="badge btl-vt-badge" datetime="2026-08">August</time>
      </div>
      <p class="small btl-vt-muted mb-0">Berries soften and change color; sugar climbs every week.</p>
    </li>
    <li class="list-group-item">
      <div class="d-flex flex-wrap justify-content-between align-items-baseline gap-2 mb-1">
        <h3 class="h6 mb-0">Harvest</h3>
        <time class="badge btl-vt-badge" datetime="2026-09">September</time>
      </div>
      <p class="small btl-vt-muted mb-0">Picked by hand at dawn, row by row, over three weeks.</p>
    </li>
  </ol>
</div>

Scroll reveal with IntersectionObserver

  • IntersectionObserver
  • prefers-reduced-motion
  • row g-0
  • col-sm-3

Scroll the demo into view to see the items arrive.

Bootstrap has no scroll-reveal utility, so a few lines of script add a class when each item enters the viewport. The hidden start state only applies after the script has checked prefers-reduced-motion and found IntersectionObserver, so without either the timeline simply shows.

View code
<div class="btl-sr card border-0 shadow-sm w-100">
  <div class="card-body p-4">
    <h2 class="h5 mb-4">One week at Ember Roasters</h2>
    <ol class="btl-sr-list list-unstyled mb-0" id="btl-sr">
      <li class="btl-sr-item row g-0">
        <time class="col-sm-3 btl-sr-day" datetime="2026-11-02">Monday</time>
        <div class="col-sm-9 btl-sr-body">
          <h3 class="h6 mb-1">Green coffee arrives</h3>
          <p class="small text-body-secondary mb-0">Twelve sacks from a co-op in Huila, checked for moisture on the loading dock.</p>
        </div>
      </li>
      <li class="btl-sr-item row g-0">
        <time class="col-sm-3 btl-sr-day" datetime="2026-11-03">Tuesday</time>
        <div class="col-sm-9 btl-sr-body">
          <h3 class="h6 mb-1">Sample roast and cupping</h3>
          <p class="small text-body-secondary mb-0">Three roast curves tasted blind; the panel picks the lightest.</p>
        </div>
      </li>
      <li class="btl-sr-item row g-0">
        <time class="col-sm-3 btl-sr-day" datetime="2026-11-04">Wednesday</time>
        <div class="col-sm-9 btl-sr-body">
          <h3 class="h6 mb-1">Production roast</h3>
          <p class="small text-body-secondary mb-0">Fifteen-kilo batches, logged and cooled in four minutes.</p>
        </div>
      </li>
      <li class="btl-sr-item row g-0">
        <time class="col-sm-3 btl-sr-day" datetime="2026-11-06">Friday</time>
        <div class="col-sm-9 btl-sr-body">
          <h3 class="h6 mb-1">Rested, packed, shipped</h3>
          <p class="small text-body-secondary mb-0">Two days of rest, then bags go out to cafes and subscribers.</p>
        </div>
      </li>
    </ol>
  </div>
</div>

Does Bootstrap include a timeline component?

No. The v5.3 documentation (v5.3.8 as of October 2026) lists 24 components, from Accordion to Tooltips, and a timeline isn’t among them.

What gets called a Bootstrap timeline is a custom pattern. It’s assembled from the grid, cards or a list group, utility classes and a small block of custom CSS, and it leans on existing Bootstrap components, so nothing needs installing beyond Bootstrap itself.

You can build it yourself from an ordered list, spacing utilities and CSS for the line and dots. Or you can reach for a UI kit. MDB lists a timeline among its extended components, and Themesberg Pixel documents timeline markup that uses timeline and timeline-item classes. There’s a package route too, since MDB’s bootstrap-timeline 1.0.0 is available on npm, cdnjs and jsDelivr.

Everything here targets Bootstrap 5.3. The Bootstrap team’s 4.6.2 release post set Bootstrap 4 end of life for January 1, 2023, so there’s little reason to start a new timeline on it. Bootstrap 6 reached its first alpha, 6.0.0-alpha.1, on October 8, 2026, but it’s a pre-release that requires Chrome and Edge 130, Firefox 132 or Safari 18. For now 5.3 is the version to build on.

Which method fits: grid, list group, Flexbox or a package?

A list group or cards covers a plain vertical timeline. The grid gives you a centered zig-zag, Flexbox gives you a horizontal row, and a package only earns its place when you want ready-made styling.

Each method trades markup weight against custom CSS. The freefrontend roundup of 20+ Bootstrap timelines spans vertical, left-right and horizontal layouts. Its introduction points out that Bootstrap’s grid and positioning utilities can draw the axis while the content stays in a predictable document order.

MethodOrientationCustom CSSBest for
List group or cardsVerticalLine and dots onlyChangelogs, resumes, short histories
Grid plus pseudo-elementsAlternatingLine, dots, side flipCompany history pages
Flexbox rowHorizontalLine, dots, overflowProcess steps, roadmaps
Package (MDB bootstrap-timeline)Vertical and horizontalPrebuilt stylesheetFast demos, themed sites

A list group or cards needs the least markup, because Bootstrap supplies the borders and spacing. What it doesn’t supply is a connector line, so the pseudo-element CSS is still yours to write.

The grid gives full control over which side each event lands on, through offset and order classes. It also costs the most custom CSS of the four, and the dot offsets have to match column edges.

Flexbox gets you equal-width steps with flex-fill and no plugin. It stacks into a column below a breakpoint with flex-column and flex-md-row. Long timelines overflow narrow screens, though.

With a package, horizontal, vertical, image and gradient variants come ready, and the cdnjs listing carries standard, dark, RTL and dark RTL stylesheets. You load and update one more stylesheet, and your class names follow MDB’s markup.

Packages are tempting. A list group timeline is short enough to own, and owning the CSS means nothing extra to update when Bootstrap moves on.

If the project doesn’t need the framework at all, a plain CSS timeline drops that weight too. And before copying any CodePen demo, check which Bootstrap version it loads.

How to build a vertical timeline in Bootstrap 5

Load Bootstrap 5.3 and wrap the events in an ordered list. Each item holds a time element for the date, a heading and a paragraph, and CSS draws the line and dots afterwards. The timeline needs the stylesheet only. Bootstrap’s JavaScript plays no part.

Base markup

  1. Load the Bootstrap 5.3 stylesheet from jsDelivr (cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css), using the link tag with its integrity attribute from the Bootstrap docs. Add your own stylesheet after it.
  2. Wrap the list in a container with vertical padding, for example container py-5.
  3. Give the ordered list the classes timeline, d-flex, flex-column and gap-5. The gap utility spaces the events, so no item carries a bottom margin that stretches the line past the last dot.
  4. Write each event as a list item holding a time element with a datetime value, a heading and a paragraph.
  5. Add the line and marker CSS from the next section.
<ol class="timeline d-flex flex-column gap-5">
  <li class="timeline-item">
  <time class="d-block fw-bold text-body-secondary" datetime="2024-03-11">11 March 2024</time>
  <h3 class="h5 mb-1">Company founded</h3>
  <p class="mb-0">Two people, one office, first prototype.</p>
</li>
<li class="timeline-item">
<time class="d-block fw-bold text-body-secondary" datetime="2024-09-02">2 September 2024</time>
<h3 class="h5 mb-1">First customer</h3>
<p class="mb-0">A pilot account goes live.</p>
</li>
</ol>

The time element has been Baseline widely available since April 24, 2020, according to WebDX web-features data, so the datetime attribute needs no fallback.

Older snippets color the date with text-muted. The 5.3 documentation uses text-body-secondary in its own list group examples.

Event content in cards

Swap the bare list item content for a card when an event carries more than a sentence. Bootstrap cards add a border, padding and rounded corners without extra CSS.

Put a div with the card class inside the li and a div with card-body inside that. The time element, a heading with card-title and a paragraph with card-text go in the body. If the date should sit on its own bar, use card-header instead. Either way, recalculate the dot’s top offset, because card-body padding pushes the first text line down.

How to draw the connector line and markers

Both are pseudo-elements. The line is a ::before on the list and each dot is a ::before on the list item, all absolutely positioned. Custom properties keep the offsets in sync when the gutter or dot size changes.

Line and dot with pseudo-elements

.timeline {
  --tl-gutter: 2rem;
  --tl-dot: 1rem;
  --tl-line: 2px;
  position: relative;
  margin: 0;
  padding-left: var(--tl-gutter);
  list-style: none;
}

.timeline::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--tl-dot) / 2 - var(--tl-line) / 2);
  width: var(--tl-line);
  background-color: var(--bs-border-color);
}

.timeline-item {
  position: relative;
}

.timeline-item::before {
  content: "";
  position: absolute;
  top: 0.25rem;
  left: calc(var(--tl-gutter) * -1);
  width: var(--tl-dot);
  height: var(--tl-dot);
  border-radius: 50%;
  background-color: var(--bs-primary);
  box-shadow: 0 0 0 4px var(--bs-body-bg);
}

Bootstrap uses the same technique internally. Its numbered list group generates numbers with counter-reset on the ol and a ::before pseudo-element on each li (Bootstrap list group docs, v5.3).

Hard-coded offsets are where this breaks. Change the padding or dot size without updating left, and the dot slides off the line. The calc() values above derive from three variables, so one edit moves everything.

Icon markers

Replace the dot with an icon when events fall into types. Bootstrap Icons are SVG-based, so they work as inline SVG, img elements, an SVG sprite or the icon font.

Place the icon in a span with position-absolute and the same left offset as the dot, and hide the pseudo-element dot on those items or the marker doubles up. Font Awesome works the same way (the freefrontend accordion timeline lists it as a requirement next to Bootstrap).

Alternating and horizontal timeline layouts

From the md breakpoint up, an alternating timeline centers the line and flips events between left and right. The horizontal kind is simpler to describe: a Flexbox row with the line running along the top.

Alternating timeline with the grid

Each li carries the row class, so the list stays an ordered list. Left events take col-md-6 with text-md-end, and right events take col-md-6 with offset-md-6. The line is one pseudo-element at left: 50% on the wrapper. Dots sit on the column edge that faces the line, which means position-relative on the column.

Bootstrap’s twelve-column grid system handles the side flip. The order-md classes swap columns when a spacer column sits between the content and the line.

MDB’s own basic example skips the grid. It sets each event container to 50% width, pushes right-side containers with left: 50%, and draws a 6px line at left: 50% (MDB bootstrap-timeline repository).

Both routes produce the same zig-zag. The grid route keeps spacing and breakpoints in Bootstrap classes, which is why it’s the better fit for a Bootstrap project.

Either route needs a side class on every item unless you flip with :nth-child(even). Use :nth-child when events get reordered often.

Horizontal timeline with Flexbox

Flexbox does the layout. The ol gets d-flex, each li gets flex-fill, and the line is a border-top on each item with a dot pseudo-element sitting on it.

Bootstrap’s list group docs describe the same pattern. The flex-fill class makes horizontal items equal width, and list-group-horizontal-md starts the row at the md breakpoint.

The markup doesn’t change from the vertical version, still an ordered list with a time element, a heading and a paragraph. The CSS does: line direction, dot position and item width. Wrap the row in overflow-auto when it can exceed the screen width.

Horizontal layouts fit short labels. Paragraph-length events belong in the vertical layouts.

How the timeline collapses across breakpoints

Build the timeline as one column with the line on the left, then switch to the alternating layout at the md breakpoint, which starts at 768px.

Bootstrap’s own CSS works mobile first, and the timeline should too. One min-width query then carries the whole layout change.

The Bootstrap 5.3 breakpoints documentation (v5.3.8) gives these values:

  • sm starts at 576px
  • md starts at 768px, the usual switch point for a two-sided timeline
  • lg starts at 992px
  • xl and xxl start at 1200px and 1400px
  • Max-width queries for md and below end at 767.98px (the breakpoint minus .02px)

The CSS below draws the line on the left edge and moves it to the center at 768px. Add position-relative to each column and g-0 to each row, so the two columns meet exactly at the line.

.timeline-alt {
  --tl-gutter: 2rem;
  --tl-dot: 1rem;
  --tl-line: 2px;
  position: relative;
  margin: 0;
  padding-left: var(--tl-gutter);
  list-style: none;
}

.timeline-alt::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--tl-dot) / 2 - var(--tl-line) / 2);
  width: var(--tl-line);
  background-color: var(--bs-border-color);
}

.timeline-alt .col-md-6::before {
  content: "";
  position: absolute;
  top: 0.25rem;
  left: calc(var(--tl-gutter) * -1);
  width: var(--tl-dot);
  height: var(--tl-dot);
  border-radius: 50%;
  background-color: var(--bs-primary);
  box-shadow: 0 0 0 4px var(--bs-body-bg);
}

@media (min-width: 768px) {
  .timeline-alt {
    padding-left: 0;
  }

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

  .timeline-alt .col-md-6::before {
    left: auto;
    right: calc(var(--tl-dot) / -2);
  }

  .timeline-alt .col-md-6.offset-md-6::before {
    left: calc(var(--tl-dot) / -2);
    right: auto;
  }
}

The 768px stays a literal value. Bootstrap’s docs state that CSS variables don’t work in media queries, even though the breakpoints exist as variables.

In Sass, the media-breakpoint-up(md) mixin writes the same query from the breakpoint map.

Copy-paste timelines pick their own collapse width, and the sources don’t agree. The bootstrap-timeline repository switches layouts at max-width: 600px, a value that matches no Bootstrap breakpoint. Combined with col-md columns, that rule leaves a band from 601px to 767px where the line is centered but the columns are still stacked full width. Use Bootstrap’s breakpoints for every query in the timeline.

The horizontal version follows the same idea. Below md it uses flex-column, with the line on each item’s left edge. From md up it switches to flex-md-row and moves the line to each item’s top edge, inside the same min-width query.

Stacking beats horizontal scrolling on narrow screens, because events hidden past the edge go unread.

Customizing colors, line width and spacing

Three custom properties on the list set line width, dot size and gutter. Bootstrap’s own –bs variables handle the colors, and a Sass override changes the theme before compiling.

TargetToolWhere
Line width, dot size, gutter–tl-line, –tl-dot, –tl-gutterOn the list, or on a parent to theme several timelines
Line and dot ring colors–bs-border-color, –bs-body-bgAlready read by the CSS
Dot color–bs-primary or a per-item propertyThe dot rule
Theme-wide accent$primary in Sasscustom.scss, before variables are imported
Event spacinggap-3 to gap-5On the ol

Dark mode needs no extra CSS. The timeline reads –bs-body-bg and –bs-border-color, and Bootstrap 5.3 redefines both under data-bs-theme=”dark”.

To color one dot differently, change the dot’s background-color to var(–tl-dot-color, var(–bs-primary)). Then style=”–tl-dot-color: var(–bs-success)” on a single li recolors that dot only.

For Sass, import functions first and set overrides such as $primary next. After that come variables, variables-dark, maps, mixins and root. Every Bootstrap variable carries the !default flag, so a value assigned earlier wins (Bootstrap Sass documentation).

For one or two changes, skip Sass and override Bootstrap’s default styles in your own stylesheet. Sass pays off once the colors multiply across components.

Making a Bootstrap timeline accessible

Use an ordered list for the sequence and keep the DOM order chronological. Hide purely decorative markers from assistive technology, then confirm Safari still announces the list.

The ol and li tell screen readers how many events exist and in what order. Bootstrap’s order utilities change the visual order only, since screen readers follow the markup. Inline SVG or icon-font markers get aria-hidden=”true” when the event text already says what happened. And while Bootstrap disables most of its own transitions under prefers-reduced-motion, any scroll animation you add needs the same guard.

The timeline CSS sets list-style: none, which makes Safari drop the list role for VoiceOver.

Sources disagree on the scope. A December 2025 WebKit Bugzilla report (bug 304137) says iOS VoiceOver reads such lists as plain text while macOS VoiceOver still announces them. WebKit closed it in February 2026 as a duplicate of its list-detection heuristics bug, so the behavior is intended.

Older write-ups describe both platforms as affected, including Scott O’Hara’s 2019 post on lists and Safari. Cssence’s 2022 test found the behavior still present in Safari 15.

Adding role=”list” to the ol, a small ARIA fix, covers either case. Validators flag it as redundant, which is the accepted trade-off.

WCAG 2.2 requires 4.5:1 for normal text and 3:1 for non-text elements that carry meaning (W3C), and Bootstrap’s accessibility documentation says parts of its default palette fall below both on light backgrounds.

Test the date text and any colored dots for color contrast. The plain line and dots count as decoration, while dots that encode event type need 3:1 plus a text label, which the visually-hidden helper provides.

When a Bootstrap timeline breaks down

A Bootstrap timeline breaks when it depends on another Bootstrap version, a script plugin, physical left and right values, or a list far longer than a page of events.

Every row below comes from the Bootstrap 5 migration guide, except the plugin row.

FailureWhat breaksFix
Snippet from Bootstrap 3 or 4ml- and mr- classes, text-left, float-left and data-toggle stop workingUse ms-/me-, text-start, float-start, data-bs-toggle
Dots anchored to columnsColumns lost position: relative in v5, so dots attach to a farther ancestorAdd position-relative to the column
jQuery-based horizontal pluginSlick-style sliders need jQuery, which Bootstrap 5 dropped as a dependencyRebuild the row with Flexbox
Physical left and right in RTLBootstrap 5 RTL uses logical start and end, so custom left and right do not flipUse inset-inline-start and inset-inline-end
Deprecated utilitiestext-muted is deprecated since v5.3.0, removal planned for v6.0.0Switch to text-body-secondary

Gallery roundups still list timelines built on Bootstrap 3.3.6 and on Bootstrap 4.5.2 with Slick 1.9.0 (Slider Revolution’s collection). Check the Bootstrap link tag before copying any of them.

Bootstrap 5 dropped Internet Explorer 10 and 11, and custom properties don’t run in Internet Explorer either, so cross-browser compatibility testing stops at current Chrome, Firefox, Safari and Edge.

The last limit is volume. A timeline of hundreds of events is a data view, and a plain CSS list was never built for it.

Bootstrap timeline FAQ

Is JavaScript required for a Bootstrap timeline?

No. The static version runs on CSS alone.

JavaScript only comes in for extras, such as the Collapse plugin for expandable event details, which needs Bootstrap’s JavaScript.

Can a timeline animate as the user scrolls?

Yes, with a few lines of your own code. Bootstrap ships no scroll-reveal utility, and its Scrollspy plugin (rewritten on the Intersection Observer API in v5.2.0) only highlights nav and list group items.

Observe each timeline item yourself and add a class that triggers a CSS transition.

Which content suits a timeline better than a list or table?

Anything where order carries meaning, like a company history, a project roadmap, a resume or the tracking steps of an order.

Items with no sequence read faster in a list group or table, because the line and dates add weight without adding information.

Can the timeline run in React, Bootstrap Studio or WordPress?

Yes, in all three. React takes the same markup with class changed to className, and Bootstrap Studio accepts a pasted HTML snippet in its HTML element, which is how Common Ninja’s timeline widget gets added.

Web-Boots, an HTML theme built on Bootstrap 4 (not a WordPress theme), ships a timeline block, and WordPress has plugins such as Timeline Block that add timelines to the Gutenberg editor. On a theme without Bootstrap, add Bootstrap to WordPress before pasting custom markup.

When a Bootstrap timeline outgrows plain CSS

It outgrows plain CSS when the events come from data or the list runs past one screen. Scroll animation pushes it there too.

Add scripting in a fixed order. Generate the list items from data first, collapse older events behind a show-more control next, and add scroll animation last. Each step keeps the static markup usable if the next one fails.

The trade-off is a JavaScript dependency. Generated markup vanishes when scripts fail, unless the server renders the first batch of events in HTML.

The advice was verified on October 29, 2026, when the documentation’s version list showed 5.3.x as the latest release (last update v5.3.8) and Bootstrap 6 only as an alpha, 6.0.0-alpha.1. A stable major version would likely break copied class names, as the move from 4 to 5 did.

Scroll animation is the step with its own learning curve, and CSS animations on scroll is the place to start.