Bootstrap Animations: 17 Examples Without Extra Libraries

Bootstrap animations are a short list: fades, collapse, the carousel, spinners, placeholders and striped progress bars. In Bootstrap 5.3.8 the fade, collapse and carousel transitions run at .15s, .35s and .6s, and entrance, hover or scroll effects come from your own CSS.

Below are 17 Bootstrap animation examples you can copy, followed by a guide to retiming, reduced motion, scroll reveals and animation libraries.

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>

Transitions Bootstrap already ships

Fade, collapse, tab panes, the carousel and the accordion icon all animate out of the box, and all of them switch off when the visitor asks for reduced motion. These demos use those transitions and tune them with a few prefixed rules.

Autosave notice with .fade and .show

  • .fade
  • .show
  • role="status"
  • form-control

Click the controls to trigger the transition.

The saved notice carries the .fade class, so it sits at opacity 0 until JavaScript adds .show and Bootstrap transitions the opacity. A timer removes .show again, and the role="status" element announces the new text to screen readers.

View code
<div class="card border-0 shadow-sm ban-editor">
  <div class="card-body p-4">
    <label for="ban-title" class="form-label fw-semibold">Story title</label>
    <input type="text" class="form-control mb-3" id="ban-title" value="Six days on the Lofoten ferry">
    <label for="ban-body" class="form-label fw-semibold">Opening line</label>
    <textarea class="form-control mb-3" id="ban-body" rows="3">The ferry left Bodo at dawn, and nobody on deck said a word for the first hour.</textarea>
    <div class="d-flex flex-wrap align-items-center gap-3">
      <button type="button" class="btn btn-dark px-4" id="ban-save">Save draft</button>
      <span class="badge rounded-pill text-success-emphasis bg-success-subtle border border-success-subtle fade ban-saved" id="ban-saved" role="status"></span>
    </div>
  </div>
</div>

Order summary that expands with collapse

  • data-bs-toggle="collapse"
  • aria-expanded
  • list-group-flush

Click the controls to trigger the transition.

The collapse plugin animates the height of the item list from 0 to its natural size and keeps aria-expanded in sync on the toggle. The custom CSS only rotates the chevron, reading aria-expanded so the icon always matches the state.

View code
<div class="card border-0 shadow-sm ban-summary">
  <div class="card-body p-4">
    <div class="d-flex justify-content-between align-items-baseline mb-1">
      <h3 class="h6 mb-0">Order summary</h3>
      <span class="fw-semibold">$214.00</span>
    </div>
    <p class="text-body-secondary small mb-3">4 items, ships in 2 business days</p>
    <button class="btn btn-sm btn-outline-secondary ban-sum-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#ban-items" aria-expanded="false" aria-controls="ban-items">
      Show items <i class="bi bi-chevron-down ms-1" aria-hidden="true"></i>
    </button>
    <div class="collapse" id="ban-items">
      <ul class="list-group list-group-flush mt-3">
        <li class="list-group-item px-0 d-flex justify-content-between"><span>Waxed canvas tote</span><span>$68.00</span></li>
        <li class="list-group-item px-0 d-flex justify-content-between"><span>Merino beanie, slate</span><span>$42.00</span></li>
        <li class="list-group-item px-0 d-flex justify-content-between"><span>Trail notebook, 3 pack</span><span>$24.00</span></li>
        <li class="list-group-item px-0 d-flex justify-content-between"><span>Thermal flask 750 ml</span><span>$80.00</span></li>
      </ul>
    </div>
  </div>
</div>

Pricing toggle with fading tab panes

  • nav-pills
  • tab-pane fade
  • --bs-nav-pills-*
  • data-bs-toggle="pill"

Click the controls to trigger the transition.

Adding .fade to each .tab-pane makes the tab plugin cross-fade the panes when a pill is chosen. The pills are recolored through --bs-nav-pills-link-active-bg and the --bs-nav-link-* variables, so no Bootstrap selector is overridden.

View code
<div class="card border-0 shadow-sm ban-pricing">
  <div class="card-body p-4">
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
      <h3 class="h5 mb-0">Studio plan</h3>
      <ul class="nav nav-pills ban-pills" id="ban-bill" role="tablist">
        <li class="nav-item" role="presentation">
          <button class="nav-link active" id="ban-month-tab" data-bs-toggle="pill" data-bs-target="#ban-month" type="button" role="tab" aria-controls="ban-month" aria-selected="true">Monthly</button>
        </li>
        <li class="nav-item" role="presentation">
          <button class="nav-link" id="ban-year-tab" data-bs-toggle="pill" data-bs-target="#ban-year" type="button" role="tab" aria-controls="ban-year" aria-selected="false">Yearly</button>
        </li>
      </ul>
    </div>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="ban-month" role="tabpanel" aria-labelledby="ban-month-tab" tabindex="0">
        <p class="display-6 fw-semibold mb-1">$24<span class="fs-6 text-body-secondary fw-normal"> / month</span></p>
        <p class="text-body-secondary mb-0">Five seats, unlimited projects, cancel any time.</p>
      </div>
      <div class="tab-pane fade" id="ban-year" role="tabpanel" aria-labelledby="ban-year-tab" tabindex="0">
        <p class="display-6 fw-semibold mb-1">$240<span class="fs-6 text-body-secondary fw-normal"> / year</span></p>
        <p class="text-body-secondary mb-0">Two months free, plus priority support and SSO.</p>
      </div>
    </div>
    <button type="button" class="btn ban-cta w-100 mt-4">Start free trial</button>
  </div>
</div>

FAQ accordion with a rotating plus

  • accordion
  • --bs-accordion-*
  • accordion-flush
  • bi-plus-lg

Click the controls to trigger the transition.

The accordion keeps its built-in height transition, and the panel colors come from --bs-accordion-active-bg, --bs-accordion-active-color and --bs-accordion-btn-focus-box-shadow. The default chevron is hidden and a plus icon turns 45 degrees into a close mark when its panel opens.

View code
<div class="accordion accordion-flush ban-faq" id="ban-faq">
  <div class="accordion-item">
    <h3 class="accordion-header">
      <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#ban-faq-1" aria-expanded="true" aria-controls="ban-faq-1">
        How long does shipping take?<i class="bi bi-plus-lg ban-plus ms-auto" aria-hidden="true"></i>
      </button>
    </h3>
    <div id="ban-faq-1" class="accordion-collapse collapse show" data-bs-parent="#ban-faq">
      <div class="accordion-body">Orders leave the warehouse within one business day. Most addresses in the US get them in two to four days.</div>
    </div>
  </div>
  <div class="accordion-item">
    <h3 class="accordion-header">
      <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#ban-faq-2" aria-expanded="false" aria-controls="ban-faq-2">
        Can I return a worn item?<i class="bi bi-plus-lg ban-plus ms-auto" aria-hidden="true"></i>
      </button>
    </h3>
    <div id="ban-faq-2" class="accordion-collapse collapse" data-bs-parent="#ban-faq">
      <div class="accordion-body">Yes, within 60 days, as long as it is clean. We repair returned gear and resell it in the outlet.</div>
    </div>
  </div>
  <div class="accordion-item">
    <h3 class="accordion-header">
      <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#ban-faq-3" aria-expanded="false" aria-controls="ban-faq-3">
        Do you ship outside the US?<i class="bi bi-plus-lg ban-plus ms-auto" aria-hidden="true"></i>
      </button>
    </h3>
    <div id="ban-faq-3" class="accordion-collapse collapse" data-bs-parent="#ban-faq">
      <div class="accordion-body">We ship to Canada, the UK and the EU. Duties are calculated at checkout, so there are no surprises at the door.</div>
    </div>
  </div>
</div>

Offcanvas, toast and modal motion

Overlay components animate when they open. Bootstrap exposes the offcanvas timing as a CSS variable, and the toast and modal motion can be reshaped with a scoped class instead of a global override.

Filter drawer with a custom easing

  • offcanvas-end
  • --bs-offcanvas-transition
  • --bs-offcanvas-width
  • form-check

Open the overlay to see it move in.

Bootstrap reads the drawer animation from --bs-offcanvas-transition, so a prefixed class can swap the default 0.3s ease-in-out for a longer ease-out curve, and only when the visitor has not asked for reduced motion. The width also comes from a variable and is capped for small screens.

View code
<div class="card border-0 shadow-sm ban-shop">
  <div class="card-body p-4 d-flex flex-wrap justify-content-between align-items-center gap-3">
    <div>
      <h3 class="h6 mb-1">Rain shells</h3>
      <p class="text-body-secondary small mb-0">48 products</p>
    </div>
    <button class="btn btn-outline-dark" type="button" data-bs-toggle="offcanvas" data-bs-target="#ban-filters" aria-controls="ban-filters">
      <i class="bi bi-sliders me-2" aria-hidden="true"></i>Filters
    </button>
  </div>
</div>
<div class="offcanvas offcanvas-end ban-drawer" tabindex="-1" id="ban-filters" aria-labelledby="ban-filters-title">
  <div class="offcanvas-header border-bottom">
    <h2 class="offcanvas-title h5" id="ban-filters-title">Filters</h2>
    <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
  </div>
  <div class="offcanvas-body">
    <fieldset class="mb-4">
      <legend class="fs-6 fw-semibold">Size</legend>
      <div class="form-check"><input class="form-check-input" type="checkbox" id="ban-f-s"><label class="form-check-label" for="ban-f-s">Small</label></div>
      <div class="form-check"><input class="form-check-input" type="checkbox" id="ban-f-m" checked><label class="form-check-label" for="ban-f-m">Medium</label></div>
      <div class="form-check"><input class="form-check-input" type="checkbox" id="ban-f-l"><label class="form-check-label" for="ban-f-l">Large</label></div>
    </fieldset>
    <label for="ban-f-price" class="form-label fw-semibold">Max price: $240</label>
    <input type="range" class="form-range mb-4" id="ban-f-price" min="60" max="400" step="10" value="240">
    <button type="button" class="btn btn-dark w-100" data-bs-dismiss="offcanvas">Show 31 results</button>
  </div>
</div>

Toast that slides up as it fades in

  • toast
  • .showing
  • toast-container
  • bootstrap.Toast

Open the overlay to see it move in.

While a toast opens, Bootstrap adds the .showing class (opacity 0) and removes it when the transition time has passed. The prefixed rule pairs .showing with a small downward offset, so the toast slides up while it fades in.

View code
<div class="card border-0 shadow-sm ban-invite">
  <div class="card-body p-4">
    <h3 class="h6 mb-3">Invite a teammate</h3>
    <label for="ban-invite-mail" class="form-label small text-body-secondary">Work email</label>
    <div class="d-flex flex-wrap gap-2">
      <input type="email" class="form-control flex-grow-1 w-auto" id="ban-invite-mail" value="lena@northvale.co">
      <button type="button" class="btn btn-primary" id="ban-invite-send">Send invite</button>
    </div>
  </div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
  <div id="ban-toast" class="toast ban-toast" role="status" aria-live="polite" aria-atomic="true">
    <div class="d-flex">
      <div class="toast-body d-flex align-items-center gap-2">
        <i class="bi bi-check-circle-fill text-success" aria-hidden="true"></i>
        <span id="ban-toast-text">Invite sent.</span>
      </div>
      <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
  </div>
</div>

Loading states: spinners, skeletons, progress, counters

Spinners, placeholders and animated progress bars are Bootstrap components. A requestAnimationFrame counter rounds out the set for dashboards and landing pages.

Spinner kit: border, grow, sizes and buttons

  • spinner-border
  • spinner-grow
  • --bs-spinner-*
  • aria-busy

Click Publish to see the button loading state.

Both Bootstrap spinner types with the docs markup: role="status" plus visually hidden text, or aria-hidden inside a button that says what is happening. Larger and slower spinners come from --bs-spinner-width, --bs-spinner-height and --bs-spinner-animation-speed, and the Publish button shows the loading state for two seconds.

View code
<div class="card border-0 shadow-sm ban-spinners">
  <div class="card-body p-4">
    <div class="d-flex flex-wrap align-items-center gap-4 mb-4">
      <div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
      <div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
      <div class="spinner-border ban-spin-lg" role="status"><span class="visually-hidden">Loading...</span></div>
      <div class="spinner-grow spinner-grow-sm text-success" role="status"><span class="visually-hidden">Loading...</span></div>
      <div class="spinner-grow text-warning ban-spin-slow" role="status"><span class="visually-hidden">Loading...</span></div>
    </div>
    <div class="d-flex flex-wrap gap-2">
      <button class="btn btn-secondary" type="button" disabled>
        <span class="spinner-grow spinner-grow-sm me-2" aria-hidden="true"></span><span role="status">Syncing</span>
      </button>
      <button class="btn btn-dark ban-publish" type="button" id="ban-publish">
        <span class="spinner-border spinner-border-sm me-2 d-none" aria-hidden="true"></span><span class="ban-publish-label">Publish</span>
      </button>
    </div>
  </div>
</div>

Skeleton loading with placeholder-glow and wave

  • placeholder
  • placeholder-glow
  • placeholder-wave
  • aria-busy

Click Refresh to see the skeleton.

The real post is in the HTML, so it shows without JavaScript. Refresh swaps in a skeleton built from .placeholder blocks, with placeholder-glow on the author line and placeholder-wave on the body, then brings the post back. Bootstrap does not stop these two animations for reduced motion, so the CSS turns them off.

View code
<div class="card border-0 shadow-sm ban-feed" id="ban-feed">
  <div class="card-body p-4">
    <div class="d-flex justify-content-between align-items-center mb-3">
      <h3 class="h6 mb-0">Latest from your team</h3>
      <button type="button" class="btn btn-sm btn-outline-secondary" id="ban-refresh" aria-controls="ban-feed"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh</button>
    </div>
    <div id="ban-skel" class="d-none" aria-hidden="true">
      <div class="d-flex align-items-center gap-3 mb-3 placeholder-glow">
        <span class="placeholder rounded-circle ban-avatar"></span>
        <div class="flex-grow-1"><span class="placeholder col-6 d-block mb-2"></span><span class="placeholder placeholder-sm col-4 d-block"></span></div>
      </div>
      <span class="placeholder d-block w-100 rounded-3 mb-3 ban-skel-img"></span>
      <p class="placeholder-wave mb-0"><span class="placeholder col-12"></span> <span class="placeholder col-10"></span> <span class="placeholder col-7"></span></p>
    </div>
    <div id="ban-post">
      <div class="d-flex align-items-center gap-3 mb-3">
        <span class="ban-avatar rounded-circle d-inline-flex align-items-center justify-content-center fw-semibold">ML</span>
        <div><p class="fw-semibold mb-0">Mara Lind</p><p class="small text-body-secondary mb-0">Field photographer, 12 min ago</p></div>
      </div>
      <img src="fjord.webp" class="d-block w-100 rounded-3 mb-3 ban-post-img" alt="Deep blue fjord between steep rocky cliffs">
      <p class="mb-0">Scouted the cliff path for Thursday. Light is best before nine, and the trail is dry enough for the full crew.</p>
    </div>
  </div>
</div>

Upload queue with animated progress bars

  • progress
  • progress-bar-striped
  • progress-bar-animated
  • --bs-progress-*

Click Upload to run the third file.

Each row is a Bootstrap progress bar with role="progressbar" and aria-valuenow on the wrapper. The striped animated bar marks the active upload, Bootstrap eases each width change with its own transition, and the colors and height come from --bs-progress-bar-bg and --bs-progress-height.

View code
<div class="card border-0 shadow-sm ban-uploads">
  <div class="card-body p-4">
    <h3 class="h6 mb-3">Uploads</h3>
    <div class="mb-3">
      <div class="d-flex justify-content-between small mb-1"><span>brand-guide.pdf</span><span class="text-success fw-semibold">Done</span></div>
      <div class="progress ban-bar ban-bar-done" role="progressbar" aria-label="brand-guide.pdf upload" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 100%"></div></div>
    </div>
    <div class="mb-3">
      <div class="d-flex justify-content-between small mb-1"><span>launch-film.mp4</span><span>64%</span></div>
      <div class="progress ban-bar" role="progressbar" aria-label="launch-film.mp4 upload" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 64%"></div></div>
    </div>
    <div class="mb-4">
      <div class="d-flex justify-content-between small mb-1"><span>moodboard.fig</span><span id="ban-up-pct">Waiting</span></div>
      <div class="progress ban-bar" id="ban-up-bar" role="progressbar" aria-label="moodboard.fig upload" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 0%"></div></div>
    </div>
    <button type="button" class="btn btn-primary w-100" id="ban-up-start"><i class="bi bi-cloud-arrow-up me-2" aria-hidden="true"></i>Upload moodboard.fig</button>
  </div>
</div>

Animated stat counter with requestAnimationFrame

  • requestAnimationFrame
  • matchMedia
  • row-cols-*
  • visually-hidden

Click Replay to run the count again.

The final numbers are in the HTML and in visually hidden text, so screen readers and no-JS visitors get the real values. A requestAnimationFrame loop counts up with an ease-out curve, Replay runs it again, and with reduced motion the numbers simply stay at their final value.

View code
<section class="ban-stats px-4 py-5">
  <div class="container">
    <div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
      <h3 class="h4 mb-0">Northvale Studio in numbers</h3>
      <button type="button" class="btn btn-sm btn-outline-light" id="ban-count-replay"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Replay</button>
    </div>
    <div class="row row-cols-1 row-cols-sm-3 g-3">
      <div class="col"><div class="ban-stat"><p class="ban-stat-num mb-1"><span class="ban-count" data-ban-to="12480" aria-hidden="true">12,480</span><span class="visually-hidden">12,480</span></p><p class="mb-0 ban-stat-label">projects shipped</p></div></div>
      <div class="col"><div class="ban-stat"><p class="ban-stat-num mb-1"><span class="ban-count" data-ban-to="98.6" data-ban-dec="1" data-ban-suffix="%" aria-hidden="true">98.6%</span><span class="visually-hidden">98.6%</span></p><p class="mb-0 ban-stat-label">delivered on time</p></div></div>
      <div class="col"><div class="ban-stat"><p class="ban-stat-num mb-1"><span class="ban-count" data-ban-to="340" aria-hidden="true">340</span><span class="visually-hidden">340</span></p><p class="mb-0 ban-stat-label">active clients</p></div></div>
    </div>
  </div>
</section>

Custom motion on top of Bootstrap

Scroll reveal, staggered entrances, hover lift and press feedback are not part of Bootstrap, so these demos add short keyframes and transitions, every one of them inside a prefers-reduced-motion: no-preference block.

Scroll reveal with IntersectionObserver

  • IntersectionObserver
  • overflow-y-auto
  • list-group
  • prefers-reduced-motion

Scroll inside the panel, or click Replay.

An IntersectionObserver watches each entry inside the scrolling panel and adds ban-in when it comes into view. The hidden starting state only exists after JavaScript adds ban-armed to the panel, and only with no reduced-motion preference, so the notes are always readable without it.

View code
<div class="card border-0 shadow-sm ban-notes-card">
  <div class="card-header bg-body d-flex justify-content-between align-items-center py-3">
    <h3 class="h6 mb-0">Release notes</h3>
    <button type="button" class="btn btn-sm btn-outline-secondary" id="ban-notes-replay">Replay</button>
  </div>
  <div class="ban-notes overflow-y-auto" id="ban-notes" tabindex="0" role="region" aria-label="Release notes">
    <ul class="list-group list-group-flush">
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-success mb-2">New</span><p class="fw-semibold mb-1">Shared moodboards</p><p class="small text-body-secondary mb-0">Invite clients to comment on boards without a seat.</p></li>
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-primary mb-2">Improved</span><p class="fw-semibold mb-1">Faster exports</p><p class="small text-body-secondary mb-0">PDF exports of large decks now finish in about half the time.</p></li>
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-success mb-2">New</span><p class="fw-semibold mb-1">Color tokens panel</p><p class="small text-body-secondary mb-0">Copy any palette as CSS custom properties.</p></li>
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-secondary mb-2">Fixed</span><p class="fw-semibold mb-1">Version history gaps</p><p class="small text-body-secondary mb-0">Autosaves made offline now appear in the timeline.</p></li>
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-primary mb-2">Improved</span><p class="fw-semibold mb-1">Keyboard shortcuts</p><p class="small text-body-secondary mb-0">Press the question mark key anywhere to see them all.</p></li>
      <li class="list-group-item p-3 ban-reveal"><span class="badge text-bg-secondary mb-2">Fixed</span><p class="fw-semibold mb-1">Retina thumbnails</p><p class="small text-body-secondary mb-0">Board previews are sharp on high density screens.</p></li>
    </ul>
  </div>
</div>

Staggered notification list

  • list-group
  • @keyframes
  • animation-delay
  • CSS custom properties

Click Replay to run the entrance again.

Each row sets its position in a --ban-i custom property, and one rule turns that into an animation-delay, so the rows rise in one after another. The keyframes run only with no reduced-motion preference, and Replay restarts them by removing and re-adding the ban-play class.

View code
<div class="card border-0 ban-inbox">
  <div class="card-header bg-transparent d-flex justify-content-between align-items-center py-3">
    <h3 class="h6 mb-0">Notifications</h3>
    <button type="button" class="btn btn-sm btn-outline-light" id="ban-inbox-replay">Replay</button>
  </div>
  <ul class="list-group list-group-flush ban-stagger ban-play" id="ban-stagger">
    <li class="list-group-item ban-item d-flex gap-3" style="--ban-i: 0"><i class="bi bi-chat-left-text text-info" aria-hidden="true"></i><div><p class="mb-0">Priya commented on <strong>Homepage v3</strong></p><p class="small text-body-secondary mb-0">2 min ago</p></div></li>
    <li class="list-group-item ban-item d-flex gap-3" style="--ban-i: 1"><i class="bi bi-check2-circle text-success" aria-hidden="true"></i><div><p class="mb-0">Build 482 passed all checks</p><p class="small text-body-secondary mb-0">9 min ago</p></div></li>
    <li class="list-group-item ban-item d-flex gap-3" style="--ban-i: 2"><i class="bi bi-person-plus text-warning" aria-hidden="true"></i><div><p class="mb-0">Tomas joined <strong>Brand team</strong></p><p class="small text-body-secondary mb-0">24 min ago</p></div></li>
    <li class="list-group-item ban-item d-flex gap-3" style="--ban-i: 3"><i class="bi bi-calendar-event text-info" aria-hidden="true"></i><div><p class="mb-0">Design review moved to 3:30 PM</p><p class="small text-body-secondary mb-0">1 hr ago</p></div></li>
    <li class="list-group-item ban-item d-flex gap-3" style="--ban-i: 4"><i class="bi bi-cloud-check text-success" aria-hidden="true"></i><div><p class="mb-0">Assets synced to the shared drive</p><p class="small text-body-secondary mb-0">2 hr ago</p></div></li>
  </ul>
</div>

Hero entrance with a photo wipe

  • row
  • col-md-6
  • @keyframes
  • clip-path

Click Replay to run the entrance again.

A standard Bootstrap grid hero where the heading, text and buttons fade up in sequence while the photo uncovers with a clip-path wipe. Every keyframe sits inside the no-preference media query, so the hero is simply static with reduced motion, and Replay restarts the sequence.

View code
<section class="ban-hero ban-hero-play px-4 py-5" id="ban-hero">
  <div class="container">
    <div class="row align-items-center g-4 g-lg-5">
      <div class="col-md-6">
        <h2 class="display-6 fw-bold mb-3 ban-hero-step" style="--ban-d: 0">Gear for long days in the hills</h2>
        <p class="lead mb-4 ban-hero-step" style="--ban-d: 1">Waxed shells, wool layers and packs that are built to be repaired, not replaced.</p>
        <div class="d-flex flex-wrap gap-2 ban-hero-step" style="--ban-d: 2">
          <a href="#" class="btn btn-lg ban-hero-btn">Shop the range</a>
          <a href="#" class="btn btn-lg btn-outline-dark">Repair program</a>
        </div>
      </div>
      <div class="col-md-6">
        <img src="highland-road.webp" class="img-fluid rounded-4 ban-hero-img" alt="Narrow road winding through green highland hills below a misty ridge">
      </div>
    </div>
    <div class="text-end mt-3"><button type="button" class="btn btn-sm btn-link text-body-secondary" id="ban-hero-replay">Replay</button></div>
  </div>
</section>

Hover lift cards with icon-link-hover

  • card
  • stretched-link
  • icon-link icon-link-hover
  • --bs-box-shadow-lg

Hover a card or Tab to it.

Bootstrap has no transition utilities, so one prefixed rule lifts the card and deepens the shadow on hover and keyboard focus. The arrow nudge is Bootstrap own icon-link-hover, and stretched-link makes the whole card clickable from a single real link.

View code
<div class="row row-cols-1 row-cols-md-3 g-3 ban-trips-grid">
  <div class="col">
    <div class="card h-100 border-0 shadow-sm ban-lift">
      <img src="canal-houses.webp" class="card-img-top ban-lift-img" alt="Old canal houses with small boats moored along the water">
      <div class="card-body">
        <h3 class="h6 card-title">Canal towns by bike</h3>
        <p class="small text-body-secondary">6 days, easy, from $1,140</p>
        <a href="#" class="icon-link icon-link-hover stretched-link small fw-semibold">View trip<i class="bi bi-arrow-right" aria-hidden="true"></i></a>
      </div>
    </div>
  </div>
  <div class="col">
    <div class="card h-100 border-0 shadow-sm ban-lift">
      <img src="snow-camp.webp" class="card-img-top ban-lift-img" alt="Yellow tents pitched in snow below jagged white mountains">
      <div class="card-body">
        <h3 class="h6 card-title">Winter base camp</h3>
        <p class="small text-body-secondary">8 days, hard, from $2,350</p>
        <a href="#" class="icon-link icon-link-hover stretched-link small fw-semibold">View trip<i class="bi bi-arrow-right" aria-hidden="true"></i></a>
      </div>
    </div>
  </div>
  <div class="col">
    <div class="card h-100 border-0 shadow-sm ban-lift">
      <img src="red-rock-canyon.webp" class="card-img-top ban-lift-img" alt="Red rock canyon rim glowing orange at sunset">
      <div class="card-body">
        <h3 class="h6 card-title">Desert rim trek</h3>
        <p class="small text-body-secondary">5 days, moderate, from $980</p>
        <a href="#" class="icon-link icon-link-hover stretched-link small fw-semibold">View trip<i class="bi bi-arrow-right" aria-hidden="true"></i></a>
      </div>
    </div>
  </div>
</div>

Add to bag with press and success feedback

  • --bs-btn-*
  • badge
  • position-absolute
  • aria-live

Click Add to bag.

The button is themed with --bs-btn-* variables and scales down slightly while pressed. A click switches it to a success state with a popping check icon, bumps the bag badge, and updates a polite live region so screen reader users hear the new count.

View code
<div class="card border-0 shadow-sm ban-product">
  <div class="card-body p-4">
    <div class="d-flex justify-content-between align-items-start mb-3">
      <div>
        <h3 class="h6 mb-1">Ridge waxed jacket</h3>
        <p class="text-body-secondary small mb-0">Olive, size M</p>
      </div>
      <button type="button" class="btn btn-light position-relative ban-bag" aria-label="Shopping bag">
        <i class="bi bi-bag" aria-hidden="true"></i>
        <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill ban-bag-count" id="ban-bag-count" aria-hidden="true">0</span>
      </button>
    </div>
    <p class="fs-4 fw-semibold mb-3">$248.00</p>
    <button type="button" class="btn ban-add w-100" id="ban-add"><i class="bi bi-bag-plus me-2" aria-hidden="true"></i><span class="ban-add-label">Add to bag</span></button>
    <span class="visually-hidden" aria-live="polite" id="ban-bag-live"></span>
  </div>
</div>

What are Bootstrap animations?

The framework ships no entrance, hover or scroll effects of its own, and only a handful of components move at all. Anything sold as a Bootstrap animation beyond those comes from custom CSS or an add-on.

Every default below comes from Bootstrap 5.3.8, released August 25, 2025 (Bootstrap blog) and still the current release as of October 2026.

Search results for the topic are crowded with UI kits layered on top of the Bootstrap framework. Their animation syntax does nothing in a plain Bootstrap install.

Bootstrap Studio builds its scroll effects on the AOS library and its hover effects on Animate.css, and the docs say scroll animations are switched off on mobile devices. MDBootstrap advertises 70+ CSS-only animations, all driven by its own data-mdb-animation attributes.

Even inside a single kit the syntax moved around. MDB’s Bootstrap 4 docs use an .animated class, its older Bootstrap 5 docs use data-mdb-toggle=”animation”, and the current docs require data-mdb-animation-init.

Which Bootstrap components animate by default?

Fades, collapse, the carousel, spinners, placeholders and striped progress bars are everything that moves out of the box. All but the placeholders read their timing from a Sass variable, and the placeholder loops hard-code a 2-second cycle (Bootstrap 5.3.8 source).

Component and classWhat movesDefault timingSass variable
Fade (.fade)Opacity.15s linear$transition-fade
Collapse (.collapsing)Height, or width with .collapse-horizontal.35s ease$transition-collapse, $transition-collapse-width
Carousel (.carousel-item)Transform, or opacity with .carousel-fade.6s ease-in-out$carousel-transition-duration, $carousel-transition
Spinners (.spinner-border, .spinner-grow)Rotation, or scale and opacity.75s per cycle$spinner-animation-speed
Animated stripes (.progress-bar-animated)Stripe position1s linear infinite$progress-bar-animation-timing

These timings live in scss/_variables.scss, so a single override retimes every instance on the page.

Bootstrap’s take on skeleton screens, added in v5.1, has two animated styles. .placeholder-glow pulses opacity from .5 down to .2 and back, and .placeholder-wave slides a gradient mask across the block.

The Bootstrap carousel slides by default and crossfades once you add .carousel-fade. Text-only slides usually need .bg-body or some custom CSS to crossfade cleanly.

Padding can’t go on a .collapse element itself, so put it on an inner wrapper. Collapse and accordion share one set of transition classes in scss/_transitions.scss.

The modal reuses .fade for opacity but slides its dialog with its own $modal-transition and $modal-fade-transform. Changing $transition-fade alone won’t retime that slide.

How to change animation speed and easing in Bootstrap

Set a new value for the component’s transition variable (for example $transition-collapse or $carousel-transition-duration) before importing Bootstrap’s Sass, or overwrite the compiled CSS rule on a CDN build. Sass retimes every instance at once, while a CSS override only touches the selectors you write.

  1. Find the variable you need. $transition-fade, $transition-collapse, $carousel-transition-duration, $spinner-animation-speed and $btn-transition cover the common cases.
  2. In your own .scss file, write something like $transition-collapse: height .6s ease-in-out; above the Bootstrap import.
  3. Compile it and load the resulting CSS in place of the stock bootstrap.min.css.
  4. With no build step, write the same value into a rule that loads after Bootstrap, for example .collapsing { transition: height .6s ease-in-out; }
  5. If you’re stacking transitions on the carousel, keep transform first, as in transition: transform 2s ease, opacity .5s ease-out (Bootstrap carousel docs).

One catch. $transition-base (all .2s ease-in-out) is the general default, but buttons read $btn-transition at .15s and form controls read $input-transition. Changing the base value alone doesn’t retime them.

Overriding Bootstrap’s compiled CSS only works when your rule loads after Bootstrap’s stylesheet or carries higher specificity. Put it in the wrong place and nothing changes, and the page keeps the stock timing.

Easing matters as much as duration. Bootstrap’s own defaults pair linear with opacity, ease with height and ease-in-out with the carousel’s transform (the modal slide uses ease-out).

To audition a curve before touching Sass, the cubic-bezier editor in your browser’s DevTools shows it live. Then keep the duration short. Stretching a collapse from .35s to a full second just feels laggy.

Turning off transitions and honoring reduced motion

Set $enable-transitions to false to strip Bootstrap’s predefined transitions at build time, and leave $enable-reduced-motion on (its default) so the prefers-reduced-motion media query suppresses motion for users who ask for it. That media query covers Bootstrap’s own components only, not custom keyframes or third-party libraries.

$enable-transitions is true by default. Switch it to false and the transitions Bootstrap adds to components aren’t compiled, which takes the animated progress stripes with them. $enable-reduced-motion is also true by default and turns on the prefers-reduced-motion media query that suppresses certain animations and transitions.

There’s a third switch, $enable-smooth-scroll, also true by default. It applies scroll-behavior: smooth, except for users who ask for reduced motion.

For one component only, drop the class instead. A modal without .fade appears instantly, per Bootstrap’s modal docs.

The docs show the support widening over time. The 4.1 docs limited it to Safari on macOS and iOS, the 4.5 docs added carousel sliding to the disabled list, and the 4.6 and 5.3.8 docs add slowed spinners.

The Safari-only limit is history, since MDN lists prefers-reduced-motion as widely available across browsers since January 2020.

There’s a gap in 5.3.8, though. The placeholder loops carry no prefers-reduced-motion rule, so .placeholder-glow and .placeholder-wave keep animating for users who asked for less motion. A backport (pull request 42769, July 2026) adds the guard on Bootstrap’s main branch, but it isn’t in the 5.3.8 release.

Until a release ships the fix, add it yourself. Set animation to none on .placeholder-glow .placeholder and .placeholder-wave inside @media (prefers-reduced-motion: reduce).

Motion preferences belong to web accessibility work, not the polish pass. WCAG 2.3.3, Animation from Interactions (Level AAA, added in WCAG 2.1), asks that non-essential motion triggered by interaction can be turned off, and the W3C lists the prefers-reduced-motion query as a sufficient technique (C39).

Autoplaying carousels fall under 2.2.2 Pause, Stop, Hide (Level A). That’s why Bootstrap’s carousel docs recommend avoiding autoplay or adding a pause control.

How to add custom animations to Bootstrap elements

Write a @keyframes rule, attach it to a class with animation-duration, animation-delay and animation-fill-mode, and load the stylesheet after Bootstrap. Wrap it in a prefers-reduced-motion guard, because Bootstrap’s reduced-motion handling stops at its own components.

Writing the keyframes

A fade-up on page load takes one @keyframes block and one class. CSS keyframe rules handle it fine: start at opacity 0 with translateY(1rem), end at opacity 1 with translateY(0).

Keep animation-duration inside Bootstrap’s own range, from .15s for fades up to .6s for the carousel. Stagger siblings with animation-delay in steps of .1s, and set animation-fill-mode to both so the first frame holds during the delay and the last frame stays afterward.

Hover effects need a transition, not keyframes. Set transition on the base state and change transform or box-shadow on :hover. Most hover effects written in CSS follow that pattern.

Watch out for one conflict. Carousel items already animate transform through $carousel-transition, and the modal dialog does the same through $modal-fade-transform. Put custom keyframes on an inner wrapper, not on the managed element.

Running code when a Bootstrap transition starts or ends

Events come in pairs. The infinitive form fires when an action starts, and the past participle form fires when it completes.

On a collapse, show.bs.collapse and hide.bs.collapse fire immediately, while shown.bs.collapse and hidden.bs.collapse wait for the CSS transition to finish. The carousel works the same way: slide.bs.carousel fires when the slide method is invoked, and slid.bs.carousel fires once the slide transition has completed.

Every infinitive event accepts preventDefault(), which stops the action before it starts. A method call on a component that is mid-transition is ignored (Bootstrap JavaScript docs), so to reach slide 2 right after slide 1, call carousel.to(‘2’) inside a slid.bs.carousel listener.

jQuery can trip this up. If it’s on the window and the body has no data-bs-no-jquery attribute, the 5.3 docs say Bootstrap emits events through jQuery’s event system, so listeners must use .on or .one instead of addEventListener.

Animate.css, AOS, WOW.js and GSAP compared for Bootstrap projects

Animate.css gives you ready-made keyframe effects. AOS and WOW.js fire effects when an element scrolls into view, and GSAP covers sequenced or scroll-linked motion. WOW.js is the one I’d retire from new projects.

LibraryTriggerBootstrap-side syntaxReduced motion
Animate.css 4.1.1Class you addanimate__animated animate__fadeInBuilt in
AOSViewport entrydata-aos=”fade-up” plus AOS.init()No guard by default, use the disable option
WOW.jsViewport entryclass=”wow animate__fadeIn” plus new WOW({ animateClass: ‘animate__animated’ }).init()Only if animateClass is set to animate__animated, so the Animate.css 4.x rule applies
GSAPTimeline or ScrollTriggergsap.from(“.card”, { opacity: 0 })Your code decides

Load Bootstrap’s stylesheet first and the library’s second, so your own overrides sit last. Scripts go before the closing body tag, with AOS.init() called right after the AOS script (AOS README).

Animate.css

The Animate.css 4.1.1 source sets –animate-duration and –animate-delay to 1s, then forces a 1ms duration and a single iteration under prefers-reduced-motion.

It’s the best-known CSS animations library, and class swaps alone trigger every effect. The speed classes scale that default: animate__faster runs 500ms, animate__fast 800ms, animate__slow 2s and animate__slower 3s.

Version 4 prefixes every class, so a 3.x snippet like .animated .fadeIn becomes animate__animated animate__fadeIn. The animate.compat.css build keeps the unprefixed names for migrations.

The catalog is large and sits behind a single class prefix. The reduced-motion rule ships inside the file, so there’s nothing to add there, and the speed and delay classes cover timing without custom CSS.

It has no scroll trigger of its own. The license deserves a look too. The main-branch file header lists the Hippocratic License 2.1, where the 4.0.0 header and the 4.1.1 npm build list MIT, so check terms before shipping to a client.

AOS

AOS needs a data-aos attribute on the element, the stylesheet in the head, and AOS.init() after the script tag before the closing body tag.

The defaults are a 400ms duration, a 120px offset, ease easing and once set to false, with duration and delay capped at 3000ms in 50ms steps (AOS README).

On the plus side it takes one attribute per element, needs no custom JavaScript, and has a useClassNames option that applies Animate.css classes from the data-aos value. The downside is that disable defaults to false, so the reduced-motion guard is yours to write, and elements replay on every pass until once is true.

WOW.js

WOW.js is the weakest bet for new work, and its defaults show its age. boxClass is wow and animateClass is animated, the Animate.css 3.x name. mobile defaults to true, so reveals run on phones, and live defaults to true, so content added after init still animates.

The README offers WOW.js under GPLv3 or a paid commercial license, and an MIT-licensed fork’s README marks it temporarily deprecated in favour of AOS. That fork’s planned 3.0 would warn on animation durations with granularity finer than 50ms or longer than 3s, the same limits AOS applies.

If a legacy page already calls new WOW().init(), leave it alone. For anything new, pick AOS.

GSAP

Webflow made GSAP free for everyone on April 30, 2025, plugins included, and widened the standard license to cover commercial use (Webflow update notes).

That removes the cost objection. ScrollTrigger and the other plugins still load as separate imports that you register with gsap.registerPlugin().

Timelines and staggers come from a single control point, and ScrollTrigger handles scroll-linked motion. The cost is a second animation system with its own reduced-motion handling, and it’s overkill for a fade or a hover lift.

How to animate Bootstrap elements on scroll

Hide each element in its starting state and play its animation when it enters the viewport. AOS does that through a data-aos attribute, and the Intersection Observer API does it with a few lines of your own JavaScript.

The Intersection Observer API has been available across browsers since March 2019, per MDN, and Bootstrap’s own scrollspy has run on it since 5.2.0.

  1. Add the AOS stylesheet in the head and its script before the closing body tag.
  2. Call AOS.init({ once: true, disable: window.matchMedia(‘(prefers-reduced-motion: reduce)’).matches }) after the script.
  3. Put data-aos=”fade-up” on the Bootstrap columns or cards, and add data-aos-delay in steps of 100 to stagger siblings.
  4. To drive Animate.css instead, set useClassNames to true, initClassName to false and animatedClassName to animate__animated (the AOS README example uses the 3.x name, animated).

The native route skips the library. Give the elements a .reveal class with opacity 0 and translateY(1rem), watch them with an IntersectionObserver, add .is-visible on entry, then unobserve so each one animates once.

It’s the script-light version of CSS animations on scroll, since the observer only toggles a class and CSS does the animating. Put the hidden starting state inside @media (prefers-reduced-motion: no-preference), so users who ask for less motion never meet hidden content.

A few things bite people here. once defaults to false in AOS, so elements animate again every time they re-enter the viewport. AOS’s documented custom-animation pattern starts at opacity 0 and reveals on .aos-animate, so a script that never runs leaves the content invisible.

Phones are the other one. AOS’s disable option accepts ‘phone’, ‘tablet’ or ‘mobile’, while WOW.js runs on mobile unless you set mobile to false.

Skip scroll triggers for anything above the fold. Those elements are in view at load, so a hidden starting state only delays them.

When Bootstrap’s built-in animations are not enough

Work through the options in order: native transition, custom CSS or Animate.css, a scroll trigger, then GSAP. Stop at the first one that does the job.

  • Stay with a native transition when the component already moves and only timing or easing needs to change. Move on once the effect isn’t a component state change.
  • One or two effects, such as a fade-up or a hover lift, are plain CSS work, and a long list of keyframe effects justifies Animate.css. Move on when the trigger has to be scroll position.
  • AOS or the Intersection Observer API suits reveals tied to the viewport. Move on when elements must run in sequence.
  • GSAP is for timelines, staggers and scroll-linked motion.

The cheapest option that works is the right one.

Button feedback, hover states and focus changes are micro-interactions, and Bootstrap’s native transitions already cover them.

One or two effects cost less as custom CSS animation than as a dependency, and the reduced-motion guard stays in your own stylesheet.

Bootstrap’s JavaScript docs warn that React, Vue and Angular can mutate the same element as Bootstrap and leave dropdowns stuck open. An animation library that writes inline styles onto a Bootstrap-managed element invites the same kind of collision.

The components carry their own limits. The carousel docs say nested carousels are not supported and that carousels in general can cause usability and accessibility challenges.

How Bootstrap animations differ between versions 4, 5 and 6

Bootstrap 5 dropped jQuery and renamed the reduced-motion switch, while Bootstrap 6, still an alpha as of October 29, 2026, moves transitions into CSS tokens and native browser features. Production work belongs on 5.3.x for now.

On release status, the docs label 5.3.x as Latest and 6.0.x as Alpha, and GitHub marks v6.0.0-alpha.1, published October 8, 2026, as a pre-release.

Bootstrap 4 to 5

  • jQuery was dropped in 5.0.0, and every plugin was rewritten in plain JavaScript.
  • Data attributes got a namespace, so data-toggle became data-bs-toggle.
  • The reduced-motion switch $enable-prefers-reduced-motion-media-query became $enable-reduced-motion.
  • 5.2.0 added local CSS variables to components, so a spinner’s –bs-spinner-animation-speed changes at runtime without recompiling Sass.

Event names carry over between 4 and 5, so show.bs.collapse and slid.bs.carousel keep working. Native addEventListener replaced jQuery’s on() as the default way to listen.

Bootstrap 5 to 6

Bootstrap 6 raises the minimum browsers to Chrome and Edge 130, Firefox 132 and Safari 18, up from Chrome and Firefox 60 and Safari 12 in Bootstrap 5 (Bootstrap 6 migration guide).

A floor this high turns cross-browser compatibility into an audit before upgrading, and the guide tells teams to check their own analytics first. It also explains why v6 animates with newer CSS: collapse and the accordion use interpolate-size, and browsers without it open and close at once, with Firefox the one to watch.

  • $transition-fade and $transition-collapse become –transition-fade-duration, –transition-fade-timing and a matching collapse pair.
  • Transition mixins now emit inside a prefers-reduced-motion: no-preference query, so a browser without the feature gets no transition at all.
  • .transition-none and .animation-none switch motion off per element, and the decorative .animation-shake and .animation-pop disable themselves under reduced motion.
  • .collapsing is gone and the plugin only toggles .show, while the accordion sits on native details and summary.

The carousel is rebuilt on CSS scroll snap, with the ride option renamed autoplay and made a boolean. Modal becomes Dialog and offcanvas becomes Drawer, both on the native dialog element, and toasts, alerts, tabs, tooltips and popovers animate through CSS, so nothing depends on .fade.

On the JavaScript side, show(), hide() and toggle() return promises that resolve when the transition ends, so await collapse.show() can replace a shown.bs.collapse listener. The events still fire, the bundle is ESM-only with no window.bootstrap global, and jQuery support is gone.

Placeholders change names too. .placeholder-wave is the standard shimmer, .placeholder-pulse is the old opacity loop, and .placeholder-glow stays as an alias. The alpha also guards both animations behind prefers-reduced-motion (pull request 42768).

Bootstrap Animations FAQ

How do I make a Bootstrap modal zoom in instead of sliding down?

Set $modal-fade-transform to scale(.8) before compiling, which is the zoom-in example in Bootstrap’s modal docs. On a precompiled build, write that transform onto .modal.fade .modal-dialog in your own stylesheet.

For a panel that enters from the side, use the offcanvas component instead of bending the modal.

Can I speed up or slow down a Bootstrap spinner?

Yes. Set –bs-spinner-animation-speed on the spinner element, for example style=”–bs-spinner-animation-speed: 1.5s”, or change $spinner-animation-speed in Sass for every spinner at once.

The .spinner-border-sm and .spinner-grow-sm modifiers change size only, so the speed value carries over.

What is the difference between data-bs-interval and the slide transition duration?

data-bs-interval sets the pause between automatic slides, 5000ms by default. How long a single slide takes to move comes from $carousel-transition-duration.

Add data-bs-interval to a single .carousel-item to override the pause for that slide only.

Why do text-only slides overlap when I use .carousel-fade?

A crossfade changes opacity, so the outgoing and incoming slides are visible at the same moment. Text slides with no background let the old text show through the new one.

Give each .carousel-item a solid background from the bg utilities, matched to the page color.

How Far to Take Bootstrap Animations

Stay on the native components until a design actually calls for an entrance, hover or scroll effect.

The order that costs least is Sass variable overrides first, custom keyframes second, a scroll library last. Each step adds code to maintain, and Bootstrap’s reduced-motion handling stops applying once an effect is your own. So variables cover timing and easing, keyframes get a prefers-reduced-motion check, and AOS or GSAP only show up when scroll triggers are needed.

A library gets the effect built faster, but it brings an extra script and a manual accessibility check.

This position was verified on October 29, 2026. A stable Bootstrap 6 release would change it, because the new motion utilities and the CSS-based carousel replace several of today’s workarounds.