Carousel

A carousel cycles through a set of slides — usually images — one at a time, with optional controls, indicator dots, and autoplay. We'll build TrailMap's "featured trail photos" carousel that appears near the top of every trail-detail page.

The Core Structure: carousel, carousel-inner, carousel-item

Every carousel needs a unique id (used by the controls and indicators to target it), an .carousel-inner wrapper, and one .carousel-item per slide — exactly one of which must start with the extra .active class:

<div id="trailPhotos" class="carousel slide">
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="ridgeline-1.jpg" class="d-block w-100" alt="Ridgeline Loop overlook">
    </div>
    <div class="carousel-item">
      <img src="ridgeline-2.jpg" class="d-block w-100" alt="Ridgeline Loop switchbacks">
    </div>
  </div>
</div>
Note: forgetting .active on exactly one .carousel-item is the single most common carousel bug — with none active, nothing renders at all; with two active, they stack on top of each other.

Prev/Next Controls

The arrow controls each target the carousel by data-bs-target and declare their direction with data-bs-slide:

<button class="carousel-control-prev" type="button"
  data-bs-target="#trailPhotos" data-bs-slide="prev">
  <span class="carousel-control-prev-icon" aria-hidden="true"></span>
  <span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button"
  data-bs-target="#trailPhotos" data-bs-slide="next">
  <span class="carousel-control-next-icon" aria-hidden="true"></span>
  <span class="visually-hidden">Next</span>
</button>

Indicators and Autoplay

.carousel-indicators renders the row of small dots at the bottom, one <button> per slide, each with data-bs-slide-to set to that slide's zero-based index:

<div class="carousel-indicators">
  <button type="button" data-bs-target="#trailPhotos"
    data-bs-slide-to="0" class="active" aria-current="true"></button>
  <button type="button" data-bs-target="#trailPhotos"
    data-bs-slide-to="1"></button>
</div>

To make the carousel cycle automatically, add data-bs-ride="carousel" on the outer .carousel element itself — it starts autoplaying as soon as the page loads (use data-bs-ride="true" instead if you only want it to start once the user has interacted with it once):

<div id="trailPhotos" class="carousel slide" data-bs-ride="carousel"> ... </div>

Captions

.carousel-caption inside a .carousel-item overlays a title and short text on top of the slide, typically bottom-aligned and centered:

<div class="carousel-item active">
  <img src="ridgeline-1.jpg" class="d-block w-100" alt="Ridgeline Loop overlook">
  <div class="carousel-caption d-none d-md-block">
    <h5>Summit Overlook</h5>
    <p>Mile 3.4, best light around sunrise.</p>
  </div>
</div>

Worked Example: TrailMap Featured Trail Photos

Putting every piece together for the Ridgeline Loop trail-detail page — three slides, controls, indicators, autoplay, and captions:

<div id="trailPhotos" class="carousel slide" data-bs-ride="carousel">

  <div class="carousel-indicators">
    <button type="button" data-bs-target="#trailPhotos" data-bs-slide-to="0" class="active"></button>
    <button type="button" data-bs-target="#trailPhotos" data-bs-slide-to="1"></button>
    <button type="button" data-bs-target="#trailPhotos" data-bs-slide-to="2"></button>
  </div>

  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="ridgeline-1.jpg" class="d-block w-100" alt="Summit overlook">
      <div class="carousel-caption d-none d-md-block">
        <h5>Summit Overlook</h5>
        <p>Mile 3.4, best light around sunrise.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="ridgeline-2.jpg" class="d-block w-100" alt="Switchback climb">
      <div class="carousel-caption d-none d-md-block">
        <h5>The Switchbacks</h5>
        <p>Mile 1.8–2.6, the steepest stretch.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="ridgeline-3.jpg" class="d-block w-100" alt="Trailhead sign">
    </div>
  </div>

  <button class="carousel-control-prev" type="button" data-bs-target="#trailPhotos" data-bs-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Previous</span>
  </button>
  <button class="carousel-control-next" type="button" data-bs-target="#trailPhotos" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>

</div>

Three .carousel-item elements, three matching indicator dots (data-bs-slide-to="0" through "2"), autoplay from data-bs-ride="carousel", and captions on the first two slides only — nothing here needed a line of custom JavaScript either.

Next: Head to Toasts — the first component in this section that actually does need a line of JavaScript to appear.